Skip to content

fix(telegram): prevent duplicate messages when preview edit times out#41662

Merged
obviyus merged 7 commits intoopenclaw:mainfrom
hougangdev:fix/telegram-duplicate-message-on-edit-timeout
Mar 10, 2026
Merged

fix(telegram): prevent duplicate messages when preview edit times out#41662
obviyus merged 7 commits intoopenclaw:mainfrom
hougangdev:fix/telegram-duplicate-message-on-edit-timeout

Conversation

@hougangdev
Copy link
Copy Markdown
Contributor

@hougangdev hougangdev commented Mar 10, 2026

Summary

Fixes the Telegram duplicate message bug that occurs when streaming preview edits time out — most commonly triggered by slow provider proxy chains like OpenRouter (via Cloudflare AI Gateway).

Root cause

When editMessageTelegram throws a post-connect network error (timeout, connection reset), the edit has likely already been processed by Telegram's server. However, the lane delivery fallback chain in tryEditPreviewMessage (lane-delivery-text-deliverer.ts:217-220) treated all edit errors the same — returning false and falling through to sendPayload, which sends a second, duplicate message.

Why OpenRouter triggers this

OpenRouter (especially routed through Cloudflare AI Gateway) adds significant latency to the LLM response path. While the LLM response itself isn't the issue, the added network hops and proxy overhead make the Telegram API calls themselves more likely to timeout. The sequence:

  1. Streaming preview message is created via sendMessage
  2. LLM streams tokens, preview is updated via editMessageText
  3. Final delivery attempts one last editMessageText to finalize the preview
  4. The edit reaches Telegram and succeeds server-side
  5. But the HTTP response times out on the client (slow proxy, network jitter)
  6. editMessageTelegram retries (default TELEGRAM_RETRY_RE matches "timeout")
  7. Retries exhaust → error propagates to tryEditPreviewMessage
  8. treatEditFailureAsDelivered is false for normal previews → returns false
  9. Fallback sendPayload fires → user sees two identical messages

The same bug can occur without OpenRouter on any high-latency or unstable connection to Telegram's API.

Fix

The editPreview callback in bot-message-dispatch.ts now classifies errors before propagating:

Error type Example Action
Pre-connect (isSafeToRetrySendError) ECONNREFUSED, ENOTFOUND Re-throw → fallback sends new message (safe — edit never reached Telegram)
Post-connect network timeout, reset, fetch failed Swallow → treat as delivered (edit likely landed, avoids duplicate)
API error 400, 500 Re-throw → fallback sends new message (Telegram rejected the edit)

Relationship to #40883

#40883 addresses the same symptom via a broader structural refactor (~500 lines across 7 files): it removes the archivedAnswerPreviews system entirely, coalesces answer segments into a single preview, and flips treatEditFailureAsDelivered to true for all final edits.

This PR is a surgical alternative (3 files, ~135 lines) that fixes the root cause at the network-error classification layer — the editPreview callback distinguishes pre-connect errors (safe to retry/fallback) from post-connect errors (edit likely landed, swallow to avoid duplicate). Both PRs prevent the duplicate; this one is narrower in scope and preserves the existing lane delivery architecture.

Related issues and PRs

Test plan

  • pnpm vitest run src/telegram/lane-delivery.test.ts — 16 tests pass
  • pnpm vitest run src/telegram/bot-message-dispatch.test.ts — 61 tests pass (2 new tests for the fix)
  • pnpm tsgo — no type errors
  • Manual: send messages through Telegram with a slow provider proxy and confirm no duplicates

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle bot added channel: telegram Channel integration: telegram size: S labels Mar 10, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR fixes a duplicate-message bug in the Telegram delivery pipeline that could occur when a slow proxy chain (e.g. OpenRouter via Cloudflare AI Gateway) caused editMessageText to time out on the client side even after Telegram had already processed the edit. Previously, all errors from editPreview propagated to the lane-delivery fallback chain, which then called sendPayload — producing a second visible message. The fix wraps the editMessageTelegram call in bot-message-dispatch.ts with a catch block that distinguishes three categories of error:

  • Pre-connect (ECONNREFUSED, ENOTFOUND, etc. via isSafeToRetrySendError) → re-thrown; fallback is safe because the edit never reached Telegram.
  • Post-connect network (timeout, reset, socket, etc. via inline regex) → swallowed; fallback is suppressed because the edit likely landed.
  • API errors (400/500 etc.) → re-thrown; fallback is safe because Telegram explicitly rejected the edit.

The change is well-reasoned and the two new integration tests in bot-message-dispatch.test.ts directly verify both code paths. The additional tests in lane-delivery.test.ts confirm the downstream fallback still operates correctly when genuine API errors do reach that layer.

Key points to review:

  • The inline /timeout|timed out|reset|closed|network|fetch failed|socket/i regex re-implements a subset of the error-classification logic already centralised in isRecoverableTelegramNetworkError in network-errors.ts. Notably, the regex only inspects err.message (not the nested cause chain), and misses error-name–based matches (AbortError, ConnectTimeoutError, etc.) already handled by the centralised function — though in practice grammY wraps these so "network" appears in the top-level message.
  • The pre-connect test asserts only expect(deliverReplies).toHaveBeenCalled() without verifying the payload content, making it slightly less specific than the timeout test which explicitly checks that the "Final answer" text was not sent via deliverReplies.

Confidence Score: 4/5

  • Safe to merge with one style suggestion — the fix is correct for the described use case and all new tests pass.
  • The root cause analysis and fix are sound, and the new tests directly verify both the "swallow timeout" and "re-throw pre-connect" paths. The main concern is that the inline regex duplicates network-error classification logic from network-errors.ts and only inspects err.message rather than the full error graph, which could theoretically miss some post-connect errors on non-grammY error shapes and allow a duplicate to slip through. In practice the risk is low because grammY always includes "network" in its HttpError wrapper messages, but using isRecoverableTelegramNetworkError would make the classification more robust and maintainable.
  • The catch block in src/telegram/bot-message-dispatch.ts (lines 499–507) deserves attention for the regex-vs-centralised-function discussion.

Last reviewed commit: 8ba0127

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: 8ba0127c23

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

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: f946659d58

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

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: e2048676fe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

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: db6b3fcfae

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@obviyus obviyus self-assigned this Mar 10, 2026
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Mar 10, 2026

Thanks for digging into this and isolating the duplicate path.

I pushed a small follow-up on your branch that keeps the narrow fix shape, but moves final edit classification into one place: pre-connect failures now fall back to a real send, ambiguous post-connect failures keep the existing preview, and message to edit not found now keeps the preview instead of sending a likely duplicate.

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: e9f132acaf

ℹ️ 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".

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: 8537211a1a

ℹ️ 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".

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: b1b607e699

ℹ️ 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".

@obviyus obviyus force-pushed the fix/telegram-duplicate-message-on-edit-timeout branch from cd036b3 to 9b17c64 Compare March 10, 2026 04:45
@obviyus obviyus force-pushed the fix/telegram-duplicate-message-on-edit-timeout branch from 9b17c64 to 2780e62 Compare March 10, 2026 04:46
@obviyus obviyus merged commit da4fec6 into openclaw:main Mar 10, 2026
10 checks passed
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Mar 10, 2026

Merged via squash.

Thanks @hougangdev!

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: 2780e62d07

ℹ️ 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".

return "preview-finalized";
}
if (finalized === "retained") {
params.retainPreviewOnCleanupByLane.answer = true;
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 Clear stale retain flag before fallback final sends

Setting retainPreviewOnCleanupByLane.answer = true here makes the retain flag sticky across later final deliveries in the same dispatch. If a subsequent answer final cannot edit the active preview and falls back to sendPayload (for example a pre-connect edit failure), deliverLaneText never resets this flag, so the cleanup path in dispatchTelegramMessage (shouldClear check) skips stream.clear() and leaves the active partial preview visible next to the fallback-sent final message. This reintroduces duplicate/stale bubbles in multi-message streams.

Useful? React with 👍 / 👎.

mukhtharcm pushed a commit to hnykda/openclaw that referenced this pull request Mar 10, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
aiwatching pushed a commit to aiwatching/openclaw that referenced this pull request Mar 10, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
dominicnunez pushed a commit to dominicnunez/openclaw that referenced this pull request Mar 11, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
Ruijie-Ysp pushed a commit to Ruijie-Ysp/clawdbot that referenced this pull request Mar 12, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
qipyle pushed a commit to qipyle/openclaw that referenced this pull request Mar 12, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
…openclaw#41662)

Merged via squash.

Prepared head SHA: 2780e62
Co-authored-by: hougangdev <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus
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 streaming=partial causes duplicate messages on edit failure

2 participants