#19479: fix(telegram): skip redundant final edit in partial streaming mode
channel: telegram
size: S
Cluster:
Telegram Streaming Enhancements
## Summary
- Problem: In draft streaming, Telegram finalization would attempt editMessageText with unchanged content.
- Why it matters: Telegram rejects same-content edits (`message is not modified`), which trigger failover cleanup/send and create duplicate visible messages plus error logs.
- What changed: Added `lastAppliedText()` tracking in `draft-stream.ts` and used it in `bot-message-dispatch.ts` to short-circuit unchanged final edits when no mutation is needed.
- Kept final edit behavior for real mutations (formatting pass, inline buttons, `linkPreview=false`) and expanded tests.
Rebased cleanly onto main — turns out merging main into a feature branch makes the bot think you're trying to smuggle 200 unrelated commits into your PR. Lesson learned. 2 clean commits now, as intended.
Replaces #17766 (auto-closed by bot after a merge commit polluted the history).
## Change Type
- [x] Bug fix
## Scope
- [x] Integrations
## Related Fix
Commit [ac2ede5](https://github.com/openclaw/openclaw/commit/ac2ede5bb) by @steipete already treats `message is not modified` API errors as success. This PR adds a complementary optimization that prevents the unnecessary API call from being attempted in the first place.
Both fixes work together:
- `ac2ede5bb`: Reactive safety net at the API error handling layer
- This PR: Proactive optimization at the logic layer to avoid the no-op API call entirely
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds a proactive optimization to skip the redundant `editMessageText` Telegram API call when the preview stream already shows the exact final text, complementing the existing reactive error-handling safety net.
The approach is sound — adding `lastSentText()` to `TelegramDraftStream` and checking it in `bot-message-dispatch.ts` before attempting the final edit. The skip conditions correctly account for formatting passes (`renderTelegramHtmlText` comparison), inline button mutations, and `linkPreview=false` mutations.
**Key issue found:**
- `lastSentText` is assigned **optimistically before the API call** (line 78 of `draft-stream.ts`). When `editMessageText` or `sendMessage` throws, `lastSentText` already reflects the attempted text, not the last successfully delivered text. This contradicts the test assertion added in `draft-stream.test.ts` (`"keeps lastSentText at the last successful value when an edit fails"`) — that test will fail against the current implementation.
- More critically, this creates a correctness gap in the dispatch optimization: if a mid-stream edit fails (e.g. network error), `lastSentText` equals the failed-attempt text. If the final text also equals that value, the final edit is skipped, leaving the Telegram message showing the last *successfully delivered* (older) content rather than the final text.
- The fix is straightforward: save the previous `lastSentText` value before the assignment, and restore it in the catch block (and in the `sendMessage` early-return path).
**Everything else looks good:**
- The `needsFormattingPass` check using `renderTelegramHtmlText` is correct and matches how `editMessageTelegram` renders text internally.
- The `hasFinalEditMutation` check for buttons and `linkPreview` correctly identifies cases requiring the final edit.
- Test coverage in `bot-message-dispatch.test.ts` is comprehensive (plain text match, formatting pass, buttons, linkPreview).
- The `lastSentText: () => string | undefined` type is slightly wider than the actual `""` initialization (always returns `string`), but this is harmless.
<h3>Confidence Score: 3/5</h3>
- Not safe to merge as-is: the new draft-stream test will fail, and the optimization has a correctness gap when a streaming edit fails mid-stream.
- The `lastSentText` variable is set before the API call (optimistic update), but the test and the dispatch optimization require it to reflect the last *confirmed* delivery. A failing streaming edit leaves `lastSentText` pointing to the undelivered text, allowing the final edit to be incorrectly skipped. The new `draft-stream.test.ts` test ("keeps lastSentText at the last successful value when an edit fails") will fail against the current implementation. Fixing it requires restoring `lastSentText` to its prior value in the catch block.
- src/telegram/draft-stream.ts — the `sendOrEditStreamMessage` function needs to restore `lastSentText` on API failure.
<sub>Last reviewed commit: 1999e63</sub>
<!-- greptile_other_comments_section -->
<sub>(4/5) You can add custom instructions or style guidelines for the agent [here](https://app.greptile.com/review/github)!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#19673: fix(telegram): avoid starting streaming replies with only 1-2 words
by emanuelst · 2026-02-18
87.0%
#17953: fix(telegram): prevent silent message loss and duplicate messages i...
by zuyan9 · 2026-02-16
86.0%
#18678: fix(telegram): preserve draft message when all final payloads are e...
by julianubico · 2026-02-16
85.7%
#22440: fix(slack): prevent duplicate final replies from draft previews
by Solvely-Colin · 2026-02-21
82.8%
#21276: fix(telegram): stabilize partial finalization and MEDIA dedupe (AI-...
by HOYALIM · 2026-02-19
82.2%
#19665: feat(telegram): native sendMessageDraft streaming (Bot API 9.3)
by edonadei · 2026-02-18
81.3%
#20842: fix(telegram): preserve preview when only error payloads are delivered
by marcodelpin · 2026-02-19
81.2%
#20818: feat(telegram): add draftMinInitialChars and initialDraftText confi...
by lingzhua77 · 2026-02-19
81.1%
#18460: fix(telegram): send fallback when streamMode partial drops all mess...
by BinHPdev · 2026-02-16
80.9%
#14977: fix(telegram): remove ack reaction after block-streamed replies
by Diaspar4u · 2026-02-12
80.5%