Split delivery pipeline into streaming/silent paths #38

Merged
weiwen merged 1 commit from sandcastle/issue-30 into main 2026-07-06 02:01:10 +08:00
Owner

Summary

Scheduled pushes were running through the same deliver_response pipeline as interactive messages, which sent a placeholder message, started a typing indicator, and streamed live edits — all inappropriate for a background scheduled push.

This PR splits the delivery pipeline into two distinct entry points sharing a common render_final tail:

  • deliver_streaming (interactive path): sends a placeholder immediately, starts a typing indicator, and streams pi snapshots to Telegram on a throttled cadence via MessageView. Renders a final authoritative pass on agent_end. Used by the Telegram message handler.
  • deliver_silent (scheduled path): passes a throwaway snapshot channel to session_manager.send_message (send failures on a dropped receiver are silently ignored, so pi.rs needed no changes), no placeholder, no typing indicator, no live edits. Renders into an empty MessageView so final pages arrive as fresh messages, not edits. Used by delivery_consumer.
  • render_final: extracted shared tail — paginates the result, edits the placeholder or sends fresh pages, attaches source buttons on the last page, or sends a plain error notice on failure.

Key decisions

  • MessageView::empty() was added as the silent-path seed so the existing reconcile logic works without modification — an empty ids vec causes every page to be sent fresh.
  • deliver_response (previously pub) is now two private functions; delivery_consumer is in the same module and needs no re-export.
  • No changes to pi.rs: the snapshot channel send already silently ignored errors on a dropped receiver, so passing a throwaway _rx is safe.

Reviewer checklist

  • Confirm deliver_silent really suppresses the typing indicator and placeholder in a live test or by reading the call sites
  • Confirm render_final correctly handles both a seeded view (streaming path) and an empty view (silent path)
  • Check that dropping _rx immediately after creation in deliver_silent does not race with send_message before the first snapshot would be sent

Closes #30

## Summary Scheduled pushes were running through the same `deliver_response` pipeline as interactive messages, which sent a placeholder message, started a typing indicator, and streamed live edits — all inappropriate for a background scheduled push. This PR splits the delivery pipeline into two distinct entry points sharing a common `render_final` tail: - **`deliver_streaming`** (interactive path): sends a placeholder immediately, starts a typing indicator, and streams `pi` snapshots to Telegram on a throttled cadence via `MessageView`. Renders a final authoritative pass on `agent_end`. Used by the Telegram message handler. - **`deliver_silent`** (scheduled path): passes a throwaway snapshot channel to `session_manager.send_message` (send failures on a dropped receiver are silently ignored, so `pi.rs` needed no changes), no placeholder, no typing indicator, no live edits. Renders into an empty `MessageView` so final pages arrive as fresh messages, not edits. Used by `delivery_consumer`. - **`render_final`**: extracted shared tail — paginates the result, edits the placeholder or sends fresh pages, attaches source buttons on the last page, or sends a plain error notice on failure. ### Key decisions - `MessageView::empty()` was added as the silent-path seed so the existing reconcile logic works without modification — an empty `ids` vec causes every page to be sent fresh. - `deliver_response` (previously `pub`) is now two private functions; `delivery_consumer` is in the same module and needs no re-export. - No changes to `pi.rs`: the snapshot channel send already silently ignored errors on a dropped receiver, so passing a throwaway `_rx` is safe. ### Reviewer checklist - [ ] Confirm `deliver_silent` really suppresses the typing indicator and placeholder in a live test or by reading the call sites - [ ] Confirm `render_final` correctly handles both a seeded view (streaming path) and an empty view (silent path) - [ ] Check that dropping `_rx` immediately after creation in `deliver_silent` does not race with `send_message` before the first snapshot would be sent Closes #30
Task completed: scheduled pushes now deliver silently (no placeholder, no
typing indicator, no live edits); interactive messages are unchanged.
PRD: issue #30.

Key decisions:
- Renamed deliver_response → deliver_streaming (interactive path, private)
  and added deliver_silent (scheduled path): awaits pi with a throwaway
  snapshot channel (receiver dropped immediately so sends fail harmlessly),
  no tokio::spawn / select! / edit loop.
- Extracted render_final as the shared tail: takes an already-awaited
  Result<AgentResponse, SessionError>, paginates → sends pages → source
  buttons on success, plain error notice on failure. Works for both paths
  via MessageView: streaming path seeds the view with a placeholder (edits
  it in place), silent path uses a new MessageView::empty() (all pages
  sent fresh via send_message).
- delivery_consumer calls deliver_silent; handle_message calls
  deliver_streaming. No enum/trait for mode — the mode is structural at
  the call site as specified.
- Added MessageView::empty() constructor and a unit test verifying it has
  no ids or rendered state.

Files changed:
- src/telegram/mod.rs: split deliver_response, add deliver_silent,
  render_final, MessageView::empty(), new test, import AgentResponse +
  SessionError
- CONTEXT.md: updated telegram/mod.rs and pi.rs architecture descriptions

Blockers/notes:
- No Rust toolchain in sandbox; CI must validate compilation and tests.
  Logic is straightforward: deliver_silent is a simple inline await with
  a dead channel, render_final is a verbatim extraction of the existing
  success/error arms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs: clarify deliver_silent paginates into multiple messages
All checks were successful
CI / check (pull_request) Successful in 2m9s
927f308100
The doc comment claimed the silent path sends 'one fresh message', but
render_final paginates long answers into several messages. Match the
wording to render_final's own doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

don’t use deliver silent. i just don’t want to send typing indicator when preparing the message. delivery_silent stops notifications

don’t use deliver silent. i just don’t want to send typing indicator when preparing the message. delivery_silent stops notifications
refactor(telegram): merge deliver_silent back into deliver_streaming
Some checks failed
CI / check (pull_request) Has been cancelled
c954e710fb
Reviewer noted deliver_silent suppressed Telegram notifications; the
only desired change for scheduled pushes was no typing indicator.

- Remove deliver_silent and MessageView::empty()
- Add typing_indicator: bool to deliver_streaming; scheduled path passes
  false, interactive path passes true
- delivery_consumer now calls deliver_streaming(false), preserving the
  placeholder send (and thus the notification) while skipping the typing
  indicator

Closes #30

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Removed deliver_silent (which skipped sending the placeholder and therefore suppressed notifications). The scheduled path now calls deliver_streaming with typing_indicator=false — it still sends a placeholder immediately (triggering the notification), streams live edits, and renders the final result, but never starts the typing indicator.

Removed deliver_silent (which skipped sending the placeholder and therefore suppressed notifications). The scheduled path now calls deliver_streaming with typing_indicator=false — it still sends a placeholder immediately (triggering the notification), streams live edits, and renders the final result, but never starts the typing indicator.
weiwen force-pushed sandcastle/issue-30 from c954e710fb
Some checks failed
CI / check (pull_request) Has been cancelled
to 6ed7aa5ccb
Some checks failed
CI / check (pull_request) Has been cancelled
2026-07-06 01:43:15 +08:00
Compare
weiwen force-pushed sandcastle/issue-30 from 6ed7aa5ccb
Some checks failed
CI / check (pull_request) Has been cancelled
to c23efdb726
All checks were successful
CI / check (pull_request) Successful in 1m22s
PR Triage — label changes-requested reviews / triage-review (pull_request) Successful in 1s
2026-07-06 01:59:11 +08:00
Compare
weiwen merged commit dac7531122 into main 2026-07-06 02:01:10 +08:00
weiwen deleted branch sandcastle/issue-30 2026-07-06 02:01:10 +08:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/evie!38
No description provided.