← Back to PRs

#18678: fix(telegram): preserve draft message when all final payloads are errors

by julianubico open 2026-02-16 23:46 View on GitHub →
channel: telegram stale size: XS
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