← Back to PRs

#17953: fix(telegram): prevent silent message loss and duplicate messages in streaming mode

by zuyan9 open 2026-02-16 10:20 View on GitHub →
channel: discord channel: telegram stale size: L
## Summary Two complementary fixes for Telegram reply delivery reliability in streaming mode. ### Fix 1: Block reply delivery tracking (fixes #15772) When block reply delivery fails (e.g., Telegram API timeout or formatting rejection), the pipeline previously marked payloads as "sent" before delivery completed. This caused `buildReplyPayloads` to blanket-drop all final payloads, silently losing messages that were never delivered to the user. **Root cause:** `onBlockReply` called `dispatcher.sendBlockReply()` (fire-and-forget), so the pipeline resolved before delivery completed. **Fix:** - Add `sendBlockReplyAsync` to `ReplyDispatcher` that returns a delivery Promise - `onBlockReply` in `dispatch-from-config.ts` now awaits actual delivery - Add `hasDeliveryFailures()` to `BlockReplyPipeline` - When failures occur, use per-payload `hasSentPayload` checks instead of blanket-dropping — failed payloads survive as final fallback - When all deliveries succeed (including coalesced), behavior is unchanged ### Fix 2: Draft stream duplicate prevention (refs #8691) When partial streaming finalization attempts `editMessageText` with content identical to the current draft, Telegram rejects it with "message is not modified". This triggers failover cleanup (delete + resend), creating duplicate messages. **Fix:** - Add `lastAppliedText()` to `TelegramDraftStream` tracking successfully applied text (not just attempted) - Skip redundant final edits when draft matches final text and no formatting/buttons/linkPreview mutation is needed - Still edits when mutations are required (buttons, formatting pass, link preview) ## Behavior Changes | Scenario | Before | After | |---|---|---| | Block reply delivery fails | Final payload dropped, message lost | Failed payloads preserved as final fallback | | Block reply delivery succeeds | Final payload dropped (correct) | Same | | All block replies fail | All messages lost | All payloads fall back to final delivery | | Draft matches final text (no mutation) | "message not modified" error → delete + resend | Skip edit, mark as delivered | | Draft matches but needs buttons/formatting | N/A | Still edits (correct) | ## Tests - **22 new test cases** across 4 files - `block-reply-delivery-fallback.test.ts` — integration tests for partial failure, full success, full failure - `draft-stream.test.ts` — `lastAppliedText` tracking on success and failure - `bot-message-dispatch.test.ts` — skip-edit and still-edit-with-buttons scenarios - All 5041 existing tests pass (1 pre-existing unrelated failure in browser profile test) ## AI Disclosure - **AI-assisted**: Built with Claude (via OpenClaw agent) - **Testing**: Full test suite run, all relevant tests passing - **Human review**: Code reviewed by human operator - **Understanding**: Both fixes address race conditions in the reply delivery pipeline — the async gap between pipeline resolution and actual Telegram API delivery Fixes #15772 Refs #8691 <!-- greptile_comment --> <h3>Greptile Summary</h3> Two complementary reliability fixes for Telegram reply delivery in streaming mode: - **Block reply delivery tracking**: `sendBlockReplyAsync` added to `ReplyDispatcher` returns a delivery Promise, enabling `onBlockReply` in the pipeline to await actual Telegram API delivery. `BlockReplyPipeline` now tracks delivery failures via `hasDeliveryFailures()`, and `buildReplyPayloads` uses per-payload `hasSentPayload` checks when failures occur instead of blanket-dropping all finals. This prevents silent message loss when partial deliveries fail. - **Draft stream duplicate prevention**: `lastAppliedText()` tracks successfully delivered text (not just attempted). When the draft already matches the final text and no formatting/buttons/linkPreview mutation is needed, the final edit is skipped, avoiding Telegram "message is not modified" errors that trigger failover delete+resend. - **Removed `onAssistantMessageStart` / `onReasoningEnd` handlers** and `forceNewMessage` integration from Telegram dispatch. This is a behavioral change: multi-turn responses will no longer be split into separate Telegram messages at assistant turn boundaries or reasoning block boundaries. - **Removed `!finalizedViaPreviewMessage` guard** from the preview-edit path — when multiple final payloads arrive, the second may overwrite the first via edit instead of being sent as a separate message (see inline comment). <h3>Confidence Score: 3/5</h3> - Core fixes are sound but the removal of the `!finalizedViaPreviewMessage` guard could cause message overwriting when multiple final payloads are delivered - The two main fixes (delivery tracking for block replies and draft stream duplicate prevention) are well-designed and correctly implemented. The async delivery Promise pattern is clean and properly handles error propagation. However, the removal of the `!finalizedViaPreviewMessage` guard introduces a potential regression where a second final payload overwrites the first via preview edit instead of being delivered as a separate message. The removal of `onAssistantMessageStart`/`onReasoningEnd` handlers is a notable behavioral change that could affect multi-turn conversation display on Telegram. Tests are comprehensive for the new behavior but the removed test for multi-final behavior is a concern. - `src/telegram/bot-message-dispatch.ts` — the missing `!finalizedViaPreviewMessage` guard at line 304 should be verified against multi-final-payload scenarios <sub>Last reviewed commit: 8e40b81</sub> <!-- greptile_other_comments_section --> <sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub> <!-- /greptile_comment -->

Most Similar PRs