#18678: fix(telegram): preserve draft message when all final payloads are errors
channel: telegram
stale
size: XS
Cluster:
Telegram Streaming Enhancements
When all final payloads carry isError (e.g., tool failures) and none finalize the preview, the finally block unconditionally called draftStream.clear() which deleted the Telegram message containing
user-visible streamed content.
Add a !draftStream?.messageId() guard so the draft is only deleted when no Telegram message was ever created. If a preview message exists, it is preserved — the error payloads are delivered as separate
messages via deliverReplies().
Closes #18244
Summary
- Problem: When a tool fails during an agent response in Telegram, the finally block in dispatchTelegramMessage calls draftStream.clear() which deletes the preview message containing the agent's streamed
conversational text. The user sees their response vanish, replaced only by a tool error (or nothing at all).
- Why it matters: Users lose the agent's actual reply and must ask the agent to resend, breaking conversational flow.
- What changed: Added a !draftStream?.messageId() guard to the finally block so the draft is only deleted when no Telegram message was ever sent. If a preview message exists, it is preserved.
- What did NOT change (scope boundary): The deliver callback logic (which already has !finalizedViaPreviewMessage and !payload.isError guards) is untouched. Tool error formatting, buildEmbeddedRunPayloads,
and deliverReplies are all unchanged.
Change Type (select all)
- Bug fix
- Feature
- Refactor
- Docs
- Security hardening
- Chore/infra
Scope (select all touched areas)
- Gateway / orchestration
- Skills / tool execution
- Auth / tokens
- Memory / storage
- Integrations
- API / contracts
- UI / DX
- CI/CD / infra
Linked Issue/PR
- Closes #18244
- Related #8688, #12595
User-visible / Behavior Changes
- Agent's conversational text now remains visible in Telegram when a tool error occurs during the response.
- Tool errors appear as separate follow-up messages instead of overwriting the agent's reply.
- No config changes required.
Security Impact (required)
- New permissions/capabilities? No
- Secrets/tokens handling changed? No
- New/changed network calls? No
- Command/tool execution surface changed? No
- Data access scope changed? No
Repro + Verification
Environment
- OS: macOS 26.3
- Runtime/container: Node v25.6.1
- Model/provider: Any
- Integration/channel: Telegram (polling mode)
- Relevant config: streamMode: "partial" (default)
Steps
1. Start a conversation with the agent in Telegram
2. Send a message that triggers the agent to use a tool that will fail (e.g., exec running an invalid command like alias birdcli)
3. Agent begins streaming a conversational response, then executes tools
4. One or more tools fail
Expected
- Agent's conversational text stays visible in its message
- Tool error appears as a separate follow-up message
Actual (before fix)
- Agent's conversational text is deleted from Telegram
- User sees only the tool error or nothing at all
Evidence
- Trace/log snippets — Confirmed via console.error instrumentation that the finally block calls draftStream.clear() when finalizedViaPreviewMessage is false and a preview message with content exists. After
the fix, the preview message is preserved and the error is delivered separately via deliverReplies().
Human Verification (required)
- Verified scenarios: Tool error during streamed response (exec failure), multiple tool calls where one fails (8 tool calls, first fails), agent response displayed then overwritten by error
- Edge cases checked: No tool errors (normal finalization still works), empty response with no streamed content (draft still cleaned up correctly), all-error payloads with no agent text
- What I did not verify: Webhook mode (tested polling only), block streaming mode (tested partial only), forceNewMessage interaction with this fix
Compatibility / Migration
- Backward compatible? Yes
- Config/env changes? No
- Migration needed? No
Failure Recovery (if this breaks)
- How to disable/revert this change quickly: Revert the single-line condition back to if (!finalizedViaPreviewMessage)
- Files/config to restore: src/telegram/bot-message-dispatch.ts
- Known bad symptoms reviewers should watch for: Orphaned draft preview messages that never get cleaned up (would appear as stale partial-text messages in Telegram chats)
Risks and Mitigations
- Risk: Orphaned draft messages if the agent streams partial text but the entire dispatch fails before any final payload is delivered (e.g., unhandled exception). The preview message would remain with
partial text instead of being deleted.
- Mitigation: This is a better UX outcome than the current behavior (deleting the message entirely). The partial text gives the user context about what the agent was doing. The draftStream.stop() call
still runs unconditionally to clean up timers.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Added a guard condition `!draftStream?.messageId()` to prevent deleting Telegram preview messages that contain streamed conversational text when tool errors occur.
- **Previous behavior**: When all final payloads had `isError=true` and none finalized the preview, the `finally` block unconditionally called `draftStream.clear()`, deleting the Telegram message containing the agent's streamed response
- **Fix**: Draft messages are now only deleted when no Telegram message was ever created (`messageId()` returns `undefined`), preserving user-visible streamed content when tool failures occur
- **Error delivery**: Tool errors are delivered as separate follow-up messages via `deliverReplies()` instead of overwriting the agent's response
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk
- The change is a single-line defensive guard that prevents unwanted message deletion. The logic is sound: `draftStream?.messageId()` returns `undefined` when no message was created, and returns a number when a message exists. The fix preserves existing streamed content while maintaining cleanup for cases where no message was sent. The PR author thoroughly documented the issue, tested multiple scenarios, and the change aligns with the existing error-handling pattern already present in the `deliver` callback (lines 308-315)
- No files require special attention
<sub>Last reviewed commit: 529f01d</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#20842: fix(telegram): preserve preview when only error payloads are delivered
by marcodelpin · 2026-02-19
89.6%
#19479: fix(telegram): skip redundant final edit in partial streaming mode
by v8hid · 2026-02-17
85.7%
#19399: telegram: fix MEDIA false positives and partial final drop
by HOYALIM · 2026-02-17
84.5%
#19673: fix(telegram): avoid starting streaming replies with only 1-2 words
by emanuelst · 2026-02-18
84.5%
#19235: fix(telegram): tool error warnings no longer overwrite streamed rep...
by gatewaybuddy · 2026-02-17
84.2%
#17953: fix(telegram): prevent silent message loss and duplicate messages i...
by zuyan9 · 2026-02-16
84.0%
#19375: telegram: align tool-error summaries
by NorthyIE · 2026-02-17
81.4%
#18460: fix(telegram): send fallback when streamMode partial drops all mess...
by BinHPdev · 2026-02-16
80.9%
#22440: fix(slack): prevent duplicate final replies from draft previews
by Solvely-Colin · 2026-02-21
80.5%
#18764: fix(telegram): preserve streamed draft when tool error occurs
by stakeswky · 2026-02-17
80.4%