#17953: fix(telegram): prevent silent message loss and duplicate messages in streaming mode
channel: discord
channel: telegram
stale
size: L
Cluster:
Telegram Message Handling Fixes
## 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
#19479: fix(telegram): skip redundant final edit in partial streaming mode
by v8hid · 2026-02-17
86.0%
#19235: fix(telegram): tool error warnings no longer overwrite streamed rep...
by gatewaybuddy · 2026-02-17
85.4%
#14977: fix(telegram): remove ack reaction after block-streamed replies
by Diaspar4u · 2026-02-12
85.4%
#17316: fix: ack reaction not removed when block streaming is enabled (Tele...
by czmathew · 2026-02-15
85.3%
#19673: fix(telegram): avoid starting streaming replies with only 1-2 words
by emanuelst · 2026-02-18
84.7%
#12936: fix(telegram): omit message_thread_id for private DM chats
by omair445 · 2026-02-09
84.6%
#18678: fix(telegram): preserve draft message when all final payloads are e...
by julianubico · 2026-02-16
84.0%
#18460: fix(telegram): send fallback when streamMode partial drops all mess...
by BinHPdev · 2026-02-16
82.7%
#20842: fix(telegram): preserve preview when only error payloads are delivered
by marcodelpin · 2026-02-19
82.4%
#19399: telegram: fix MEDIA false positives and partial final drop
by HOYALIM · 2026-02-17
82.4%