Skip to content

fix(telegram): delete orphaned preview message after fallback send#39455

Closed
eveiljuice wants to merge 3 commits intoopenclaw:mainfrom
eveiljuice:fix/telegram-lane-orphaned-preview
Closed

fix(telegram): delete orphaned preview message after fallback send#39455
eveiljuice wants to merge 3 commits intoopenclaw:mainfrom
eveiljuice:fix/telegram-lane-orphaned-preview

Conversation

@eveiljuice
Copy link
Copy Markdown

@eveiljuice eveiljuice commented Mar 8, 2026

Problem

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 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.

  • Cleanup is only performed when the fallback send succeeds (delivered === true). When the send fails, the preview is kept so the user still sees something.
  • This differs from the archived-preview path in consumeArchivedAnswerPreviewForFinal which 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 before stopDraftLane(), delete after successful sendPayload()

Related Issues

Closes #38365
Related: #37702, #38434, #33308, #33453


Split from #38644 per @steipete request (triage call).

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
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR fixes a duplicate-message bug in the Telegram answer lane by capturing the in-flight preview messageId before stopDraftLane() is called and deleting that orphaned preview after a successful fallback sendPayload(). The deletion logic itself is well-reasoned and consistent with the consumeArchivedAnswerPreviewForFinal strategy.

However, the PR converts lane-delivery.ts from a re-export barrel into a full, standalone implementation (merging lane-delivery-text-deliverer.ts and lane-delivery-state.ts in-line). During that copy, the isMessageNotModifiedError guard was inadvertently omitted from tryEditPreviewMessage. This guard was present in the original lane-delivery-text-deliverer.ts and treated Telegram 400 "MESSAGE_NOT_MODIFIED" responses as a successful, already-delivered edit rather than falling back to sendPayload — exactly the fallback path that causes duplicate messages. Its absence introduces a regression that is directly contradictory to the PR's goal.

Key issues:

  • Critical regression: tryEditPreviewMessage no longer handles Telegram's "message is not modified" error (400 Bad Request) as a delivered state. Without this, any edit where the text is unchanged will fall back to sendPayload, triggering the very duplicate-message behavior this PR is trying to eliminate.
  • The existing test "treats 'message is not modified' preview edit errors as delivered" (lane-delivery.test.ts:149) imports from ./lane-delivery.js (the new implementation) and will fail against the current code.
  • No new test was added to cover the new orphaned-preview-deletion path introduced in this PR.

Confidence Score: 1/5

  • Not safe to merge — the consolidation accidentally drops a critical error-handling branch that guards against the same duplicate-message bug this PR is intended to fix.
  • The new orphaned-preview deletion logic is correct and addresses the described issue, but copying the implementation into lane-delivery.ts silently dropped the isMessageNotModifiedError guard from tryEditPreviewMessage. This guard was the only protection against Telegram 400 "message is not modified" responses re-triggering a fallback send, which is precisely the duplicate-message scenario this PR targets. The omission will cause an existing test to fail and creates a regression in the common case where a preview edit is a no-op.
  • src/telegram/lane-delivery.ts — the tryEditPreviewMessage catch block (around line 218) is missing the isMessageNotModifiedError check present in the original lane-delivery-text-deliverer.ts.

Last reviewed commit: 6ba1f99

Comment on lines +218 to +230
} 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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@eveiljuice
Copy link
Copy Markdown
Author

Addressed review feedback:

Restored isMessageNotModifiedError guard in tryEditPreviewMessage (commit 92a0d45).

The guard was inadvertently dropped during the consolidation of lane-delivery-text-deliverer.ts into lane-delivery.ts. Without it, Telegram 400 "Bad Request: message is not modified" responses fell through to the generic error handler and triggered a fallback sendPayload() — producing the exact duplicate-message behavior this PR is meant to fix.

Per Telegram Bot API docs, editMessageText returns 400 when text/markup are identical to the current message. This is expected during streaming when preview text hasn't changed between flushes and must be treated as delivered.

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.
@eveiljuice eveiljuice force-pushed the fix/telegram-lane-orphaned-preview branch from 3d8cfb7 to 5ee1ebb Compare March 8, 2026 19:10
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +470 to +473
if (delivered && typeof previewMessageIdAfterStop === "number") {
try {
await params.deletePreviewMessage(previewMessageIdAfterStop);
} catch (err) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

vincentkoc pushed a commit that referenced this pull request Mar 12, 2026
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.
@eveiljuice eveiljuice closed this by deleting the head repository Mar 21, 2026
dustin-olenslager pushed a commit to dustin-olenslager/ironclaw-supreme that referenced this pull request Mar 24, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram DM streaming can duplicate final message after preview stream

1 participant