fix(telegram): delete orphaned preview message after fallback send#39455
fix(telegram): delete orphaned preview message after fallback send#39455eveiljuice wants to merge 3 commits intoopenclaw:mainfrom
Conversation
When the answer lane in lane-delivery.ts fails to finalize a preview via edit and falls back to sendPayload(), the previously-flushed preview message was left visible. The user sees both the preview and the final message — the classic 'message appears twice, one disappears' behavior. The fallback send path now captures the preview message ID before stopping the draft lane and deletes it after successful delivery. Closes #38365 Related: #37702, #38434, #33308, #33453
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ba1f992a1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| params.markDelivered(); | ||
| return true; | ||
| } catch (err) { | ||
| if (args.treatEditFailureAsDelivered) { |
There was a problem hiding this comment.
Treat MESSAGE_NOT_MODIFIED edit errors as delivered
The tryEditPreviewMessage catch path now falls back to sendPayload for every edit error except the stop-created case, but Telegram frequently returns 400: ... message is not modified when the final content matches the preview. Losing the explicit MESSAGE_NOT_MODIFIED handling here turns a successful finalization into a fallback send, which can reintroduce duplicate visible messages (existing preview + newly sent final) and regresses behavior already expected by src/telegram/lane-delivery.test.ts (treats 'message is not modified' preview edit errors as delivered).
Useful? React with 👍 / 👎.
Greptile SummaryThis PR fixes a duplicate-message bug in the Telegram answer lane by capturing the in-flight preview However, the PR converts Key issues:
Confidence Score: 1/5
Last reviewed commit: 6ba1f99 |
| } catch (err) { | ||
| if (args.treatEditFailureAsDelivered) { | ||
| params.log( | ||
| `telegram: ${args.laneName} preview ${args.context} edit failed after stop-created flush; treating as delivered (${String(err)})`, | ||
| ); | ||
| params.markDelivered(); | ||
| return true; | ||
| } | ||
| params.log( | ||
| `telegram: ${args.laneName} preview ${args.context} edit failed; falling back to standard send (${String(err)})`, | ||
| ); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Missing isMessageNotModifiedError handling — regression from original implementation
The lane-delivery-text-deliverer.ts file (which this new file replaces as the source of truth) has a special guard in this same catch block for Telegram's "message is not modified" 400 error:
// lane-delivery-text-deliverer.ts, lines 202-210
} catch (err) {
if (isMessageNotModifiedError(err)) {
params.log(
`telegram: ${args.laneName} preview ${args.context} edit returned "message is not modified"; treating as delivered`,
);
params.markDelivered();
return true;
}
if (args.treatEditFailureAsDelivered) {
// ...
}The new lane-delivery.ts copy omits this check entirely. As a result, a Telegram API 400 "MESSAGE_NOT_MODIFIED" response will now fall through to the standard sendPayload fallback, producing the very duplicate-message outcome this PR is meant to fix.
The existing test at lane-delivery.test.ts:149 — "treats 'message is not modified' preview edit errors as delivered" — specifically covers this scenario. Since lane-delivery.test.ts imports from ./lane-delivery.js, that test will fail against the new implementation.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/lane-delivery.ts
Line: 218-230
Comment:
**Missing `isMessageNotModifiedError` handling — regression from original implementation**
The `lane-delivery-text-deliverer.ts` file (which this new file replaces as the source of truth) has a special guard in this same `catch` block for Telegram's "message is not modified" 400 error:
```ts
// lane-delivery-text-deliverer.ts, lines 202-210
} catch (err) {
if (isMessageNotModifiedError(err)) {
params.log(
`telegram: ${args.laneName} preview ${args.context} edit returned "message is not modified"; treating as delivered`,
);
params.markDelivered();
return true;
}
if (args.treatEditFailureAsDelivered) {
// ...
}
```
The new `lane-delivery.ts` copy omits this check entirely. As a result, a Telegram API 400 "MESSAGE_NOT_MODIFIED" response will now fall through to the standard `sendPayload` fallback, producing the very duplicate-message outcome this PR is meant to fix.
The existing test at `lane-delivery.test.ts:149` — _"treats 'message is not modified' preview edit errors as delivered"_ — specifically covers this scenario. Since `lane-delivery.test.ts` imports from `./lane-delivery.js`, that test will fail against the new implementation.
How can I resolve this? If you propose a fix, please make it concise.The guard was inadvertently dropped when lane-delivery-text-deliverer.ts was consolidated into lane-delivery.ts. Without it, Telegram 400 "message is not modified" responses fall through to the generic error handler and trigger a fallback sendPayload(), producing the exact duplicate-message behavior this PR is meant to fix. Telegram Bot API returns HTTP 400 with description "Bad Request: message is not modified" when editMessageText is called but the text/markup are identical to the current message content. This is expected during streaming when the preview text has not changed between flushes and must be treated as a successful delivery (the message is already correct). Ref: https://core.telegram.org/bots/api#editmessagetext Addresses review feedback from greptile-apps.
|
Addressed review feedback: Restored The guard was inadvertently dropped during the consolidation of Per Telegram Bot API docs, |
Add 5 test cases for the orphaned preview deletion path in fallback final send: - deletes orphaned preview after successful fallback send - preserves preview when fallback send fails - no-op when no preview message exists - logs error but does not throw when cleanup fails - handles preview created by stop() during fallback Addresses Greptile review feedback on #39455.
3d8cfb7 to
5ee1ebb
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ee1ebb325
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (delivered && typeof previewMessageIdAfterStop === "number") { | ||
| try { | ||
| await params.deletePreviewMessage(previewMessageIdAfterStop); | ||
| } catch (err) { |
There was a problem hiding this comment.
Reset stream state after fallback preview deletion
After deletePreviewMessage succeeds in the final fallback path, the lane stream still retains the deleted preview messageId, so later assistant-message boundaries can archive that stale id in rotateAnswerLaneForNewAssistantMessage (src/telegram/bot-message-dispatch.ts) when hasStreamedMessage is true. The next final then consumes this archived id first, attempts to edit a message that was already deleted, and is forced into fallback send even when a live preview exists, reintroducing duplicate/fallback behavior across multi-message runs (for example, tool-call boundaries after a media final).
Useful? React with 👍 / 👎.
Add 5 test cases for the orphaned preview deletion path in fallback final send: - deletes orphaned preview after successful fallback send - preserves preview when fallback send fails - no-op when no preview message exists - logs error but does not throw when cleanup fails - handles preview created by stop() during fallback Addresses Greptile review feedback on #39455.
Add 5 test cases for the orphaned preview deletion path in fallback final send: - deletes orphaned preview after successful fallback send - preserves preview when fallback send fails - no-op when no preview message exists - logs error but does not throw when cleanup fails - handles preview created by stop() during fallback Addresses Greptile review feedback on openclaw#39455.
Problem
When the answer lane in
lane-delivery.tsfails to finalize a preview via edit and falls back tosendPayload(), the previously-flushed preview message is left visible. The user sees both the preview and the final message — the classic "message appears twice, one disappears" behavior (#38365).Solution
The fallback send path now captures the preview message ID before stopping the draft lane and deletes it after successful delivery.
delivered === true). When the send fails, the preview is kept so the user still sees something.consumeArchivedAnswerPreviewForFinalwhich may delete even on send failure — that path deals with boundary-rotated previews that are stale by definition.Files Changed
src/telegram/lane-delivery.ts— capture preview ID beforestopDraftLane(), delete after successfulsendPayload()Related Issues
Closes #38365
Related: #37702, #38434, #33308, #33453
Split from #38644 per @steipete request (triage call).