Skip to content

fix(telegram): dedupe replayed message dispatches#85208

Merged
joshavant merged 3 commits into
mainfrom
fix/telegram-dispatch-dedupe-84886
May 22, 2026
Merged

fix(telegram): dedupe replayed message dispatches#85208
joshavant merged 3 commits into
mainfrom
fix/telegram-dispatch-dedupe-84886

Conversation

@joshavant

@joshavant joshavant commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: Telegram isolated-ingress replays can deliver the same logical Telegram message under a different update_id, bypassing update-level dedupe and dispatching the message again.
  • Solution: Add account-scoped dispatch dedupe keyed by Telegram chat.id + message_id, committed only after the message reaches the real dispatch boundary.
  • What changed: Telegram handler paths now claim, release, merge, and commit dispatch dedupe keys across direct messages, inbound debounce, text fragments, media groups, and canceled debounce buffers.
  • What did NOT change (scope boundary): Existing Telegram update dedupe remains in place; this does not alter Telegram auth, routing, or visible reply policy.

Motivation

  • Fixes duplicate Telegram dispatch/reply risk from replayed ingress updates after offset or restart edge cases.

Change Type (select all)

  • Bug fix
  • Refactor required for the fix

Scope (select all touched areas)

  • Integrations
  • API / contracts

Linked Issue/PR

Real behavior proof

Behavior addressed: A replayed Telegram update for the same logical chat.id/message_id should not dispatch the message to the model a second time.

Real environment tested: AWS Crabbox, provider aws, run run_8d15e6bb87de, lease cbx_33900860a05e, live Telegram telegram credential from Convex, isolated Telegram ingress spool, blocking mock OpenAI-compatible backend.

Exact steps or command run after this patch: Built the patched checkout on AWS Crabbox, leased a Convex telegram credential, started OpenClaw gateway with Telegram isolated ingress and a blocking mock OpenAI backend, sent one real Telegram group message to the SUT bot, waited until the first model request reached the backend and stayed pending, crash-restarted the gateway against the same state directory, wrote the same Telegram message object back into the isolated ingress spool with a new synthetic update_id, then checked dispatch dedupe logs and mock model request count.

Evidence after fix:

{
  "status": "pass",
  "proofId": "84886-pending-mpgg7ika",
  "groupId": "-5140433623",
  "requestMessageId": 11701,
  "replayUpdateId": 1779425915916,
  "requestsAtFirstDispatch": 1,
  "requestsAfterRestartReplay": 1,
  "firstRequestConnectionClosed": true,
  "skipLogSeen": true
}

Observed result after fix: The original live Telegram message reached the model dispatch boundary once and was still pending when the gateway was killed. After restart, the replayed spool update logged the dispatch-dedupe skip and did not create a second mock model request.

What was not tested: A visible second Telegram reply was not required as the pass condition; the proof targeted the dispatch boundary directly by counting model requests.

Before evidence: On current main, the same replay shape was observed to reach the dispatch boundary twice for the same Telegram chat.id/message_id under different update deliveries.

Root Cause

  • Root cause: Telegram update-level dedupe uses update_id when available, but update_id is a delivery cursor rather than stable message identity. Replays of the same logical Telegram message can arrive with different update IDs.
  • Missing detection / guardrail: There was no durable message-level dispatch claim keyed by Telegram chat.id/message_id.
  • Contributing context: Isolated ingress spool replay and offset persistence/restart behavior can make update-level dedupe insufficient.

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
  • Target test or file:
    • extensions/telegram/src/message-dispatch-dedupe.test.ts
    • extensions/telegram/src/bot.create-telegram-bot.test.ts
    • src/auto-reply/inbound.test.ts
  • Scenario the test should lock in: Same Telegram message identity replayed after handler recreation is skipped, while canceled debounce claims are released and can be processed later.
  • Why this is the smallest reliable guardrail: It covers the stable identity key, persistence behavior, handler recreation, and the cancellation path that can otherwise strand claims.
  • Existing test that already covers this (if any): Existing update-offset tests cover update cursor behavior, but not message-level replay dispatch dedupe.

User-visible / Behavior Changes

Replayed Telegram messages with the same chat.id/message_id no longer trigger duplicate model dispatches/replies within the dispatch dedupe TTL.

Diagram

Before:
Telegram replay -> new update_id -> update dedupe misses -> duplicate dispatch

After:
Telegram replay -> same chat.id/message_id -> dispatch dedupe skips -> no duplicate dispatch

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
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Linux on AWS Crabbox
  • Runtime/container: Node 24.15.0 remote Crabbox checkout
  • Model/provider: mock OpenAI-compatible /v1/responses
  • Integration/channel: Telegram live bot credential from Convex telegram pool
  • Relevant config (redacted): Telegram group allowlist, isolated ingress enabled, mock OpenAI base URL on loopback

Steps

  1. Start OpenClaw gateway with Telegram isolated ingress and mock OpenAI.
  2. Send one real Telegram message to the SUT bot.
  3. Wait for the original message to reach dispatch and create one pending mock model request.
  4. Kill/restart the gateway against the same state directory before the original dispatch completes.
  5. Write the same Telegram message into the isolated ingress spool with a new update_id.
  6. Assert the gateway logs dispatch dedupe skip and mock model request count remains unchanged.

Expected

  • First message reaches dispatch once before the crash-style restart.
  • Replayed same chat.id/message_id is skipped before a second model dispatch after restart.

Actual

  • First message produced one pending mock model request.
  • Replay after restart left model request count unchanged at one and logged the dispatch dedupe skip.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets

Human Verification (required)

  • Verified scenarios:
    • Focused Vitest coverage for dispatch dedupe, Telegram bot handler replay, message processor boundary, and inbound debounce cancellation.
    • pnpm check:changed.
    • $autoreview until clean.
    • Live AWS Crabbox Telegram pending-dispatch restart replay proof.
  • Edge cases checked:
    • Account isolation.
    • Invalid message IDs.
    • Durable persistence across guard recreation.
    • Releasing claims when debounce cancellation drops buffered messages.
    • Releasing claims on pre-dispatch drops/errors.
  • What you did not verify:
    • Replay behavior after the configured dispatch dedupe TTL expires.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: A legitimate re-delivery after the dedupe TTL can dispatch again.
    • Mitigation: TTL keeps state bounded and covers the restart/offset replay window this bug involves.
  • Risk: A claim could be stranded if a buffered message is canceled before dispatch.
    • Mitigation: Debouncer now reports canceled items and Telegram releases dispatch claims for canceled buffers.

@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram size: L maintainer Maintainer-authored PR labels May 22, 2026
@clawsweeper

clawsweeper Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

Workflow note: Future ClawSweeper reviews update this same comment in place.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

Summary
The PR adds Telegram account-scoped message dispatch dedupe by chat/message identity, commits the dedupe key at dispatch start, threads release/merge handling through buffers, adds an inbound debounce cancellation hook, and adds focused tests plus a changelog entry.

Reproducibility: yes. for source-level reproduction: current main has only update-level Telegram dedupe before dispatch, and the linked beta-blocker identifies the replay path where the same Telegram chat/message identity can dispatch again under a new update delivery. I did not rerun live Telegram proof in this read-only review, but the PR body reports a live before/after Crabbox replay proof.

PR rating
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Summary: The PR has strong live proof and a focused tested implementation, with merge readiness mainly gated by maintainer acceptance of Telegram idempotency semantics.

Rank-up moves:

  • none
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Real behavior proof
Sufficient (live_output): The PR body includes after-fix live AWS Crabbox proof with a real Telegram message, isolated spool replay, blocking mock model backend, unchanged model request count, and a dedupe skip log.

Risk before merge

  • The PR changes Telegram message delivery/idempotency state, so maintainers should explicitly accept the seven-day chat/message-id suppression semantics before merge.
  • Because the guard persists local dispatch state across restarts, an incorrect release/commit path could suppress a legitimate retry or allow a duplicate dispatch; the focused tests and live proof reduce but do not eliminate that merge risk.

Maintainer options:

  1. Land after maintainer semantics review (recommended)
    Accept the current dispatch-start guard if maintainers agree that a seven-day account/chat/message-id ledger is the intended Telegram replay behavior.
  2. Tune the guard policy before merge
    If the TTL, release behavior, or account scoping needs different semantics, request that narrow adjustment while preserving the dispatch-start regression coverage.
  3. Hold for a replacement fix
    If maintainers reject durable message-id dedupe for Telegram, pause or close this PR only after linking the alternate fix path for the open beta-blocker.

Next step before merge
The latest head has no discrete blocking review finding from this pass, but the protected maintainer label and Telegram dispatch semantics require explicit maintainer review before merge.

Security
Cleared: No concrete security or supply-chain regression was found; the diff adds local JSON dedupe state and tests without new dependencies, permissions, secrets handling, downloads, or command execution paths.

Review details

Best possible solution:

Land the Telegram-owned durable dispatch guard after maintainer review confirms the persistence, release, and TTL semantics for replay recovery.

Do we have a high-confidence way to reproduce the issue?

Yes for source-level reproduction: current main has only update-level Telegram dedupe before dispatch, and the linked beta-blocker identifies the replay path where the same Telegram chat/message identity can dispatch again under a new update delivery. I did not rerun live Telegram proof in this read-only review, but the PR body reports a live before/after Crabbox replay proof.

Is this the best way to solve the issue?

Yes, subject to maintainer approval: a Telegram-owned persistent claimable guard committed at dispatch start is the narrow maintainable fix for this replay bug. The latest head appears to address the earlier post-dispatch commit finding with a dispatch-start hook and pending-dispatch regression coverage.

Label changes:

  • add rating: 🐚 platinum hermit: Current PR rating is 🐚 platinum hermit because proof is 🦞 diamond lobster, patch quality is 🐚 platinum hermit, and The PR has strong live proof and a focused tested implementation, with merge readiness mainly gated by maintainer acceptance of Telegram idempotency semantics.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes after-fix live AWS Crabbox proof with a real Telegram message, isolated spool replay, blocking mock model backend, unchanged model request count, and a dedupe skip log.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P1: The linked Telegram beta-blocker can duplicate agent dispatches and visible replies during restart or replay windows.
  • merge-risk: 🚨 message-delivery: The PR changes when Telegram message-like updates are skipped versus delivered to the agent dispatch boundary.
  • merge-risk: 🚨 session-state: The new durable dedupe ledger persists Telegram message identity state across handler and gateway recreation.
  • rating: 🐚 platinum hermit: Current PR rating is 🐚 platinum hermit because proof is 🦞 diamond lobster, patch quality is 🐚 platinum hermit, and The PR has strong live proof and a focused tested implementation, with merge readiness mainly gated by maintainer acceptance of Telegram idempotency semantics.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes after-fix live AWS Crabbox proof with a real Telegram message, isolated spool replay, blocking mock model backend, unchanged model request count, and a dedupe skip log.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix live AWS Crabbox proof with a real Telegram message, isolated spool replay, blocking mock model backend, unchanged model request count, and a dedupe skip log.

Acceptance criteria:

  • Review the latest head f3180691806e8265bd0604bbd26bf1f4e0447343 against the prior ClawSweeper finding to confirm dispatch-start persistence is accepted.
  • Verify maintainer-accessible Crabbox proof run_8d15e6bb87de / proofId 84886-pending-mpgg7ika if needed before landing.
  • Run the focused Telegram and inbound debounce tests through the repository-approved Testbox/Crabbox path before merge.

What I checked:

  • Protected PR state: Live GitHub issue search/API context shows this PR is open, unmerged, and labeled maintainer, so cleanup must keep it open for explicit maintainer handling.
  • Linked beta blocker remains open: The linked issue is open, labeled beta-blocker/P1, and describes the missing durable Telegram account/chat/message-id dispatch ledger between authorization and dispatch.
  • Current main lacks message-level dispatch dedupe: Current main still checks update-level dedupe and then records the reply-chain message before calling processInboundMessage, with no durable chat/message-id guard. (extensions/telegram/src/bot-handlers.runtime.ts:2618, 55cfe00a3a40)
  • Latest PR head commits at dispatch start: At the latest PR head, processMessageWithReplyChain passes an onDispatchStart lifecycle callback that commits dispatch dedupe keys before the Telegram dispatch call runs. (extensions/telegram/src/bot-handlers.runtime.ts:1141, f3180691806e)
  • Dispatch lifecycle boundary: The latest PR head invokes onDispatchStart after Telegram context creation and before dispatchTelegramMessage, and returns false when no dispatch context exists so pre-dispatch claims can be released. (extensions/telegram/src/bot-message.ts:179, f3180691806e)
  • Pending-dispatch regression coverage: The latest PR head adds a handler-recreation test where the first Telegram dispatch remains pending and a replayed same chat/message id is skipped before the first dispatch resolves. (extensions/telegram/src/bot.create-telegram-bot.test.ts:1730, f3180691806e)

Likely related people:

  • vincentkoc: Authored recent Telegram replay/update-watermark changes and the shared claimable dedupe helper that this PR builds on. (role: feature-history owner; confidence: high; commits: 7c7125594867, 54eaf85ea279, 028434a00ffd; files: extensions/telegram/src/bot.create-telegram-bot.test.ts, src/plugin-sdk/persistent-dedupe.ts)
  • joshavant: Has prior merged Telegram routing/test stabilization and isolated polling watchdog work, and is assigned on the linked beta-blocker report. (role: recent adjacent contributor; confidence: high; commits: 68bc6effc04a, 5020a027f107, f3180691806e; files: extensions/telegram/src/bot-handlers.runtime.ts, extensions/telegram/src/bot.create-telegram-bot.test.ts, extensions/telegram/src/polling-session.ts)
  • steipete: Local blame on the current shallow main snapshot attributes the present Telegram handler dispatch path and adjacent shared helper files to Peter Steinberger's current-main commit. (role: current-main area contributor; confidence: medium; commits: 8aabb79a8307; files: extensions/telegram/src/bot-handlers.runtime.ts, src/auto-reply/inbound-debounce.ts, src/plugin-sdk/persistent-dedupe.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 55cfe00a3a40.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels May 22, 2026
@clawsweeper

clawsweeper Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

✨ Hatched: 🌱 uncommon Frosted Clawlet

Hatch command

Comment @clawsweeper hatch when this PR is hatchable.

Hatchability rules:

  • Merged PRs are hatchable.
  • Open PRs are hatchable when they are status: 👀 ready for maintainer look, status: 🚀 automerge armed, or labeled clawsweeper:automerge.
  • Closed unmerged PRs are hatchable only when one of those hatchable labels is still present in the durable record.

Rarity: 🌱 uncommon.
Trait: sniffs out flaky tests.
Image traits: location green-check meadow; accessory rollback rope; palette pearl, teal, and neon green; mood mischievous; pose leaning over a miniature review desk; shell soft velvet shell; lighting golden review-room light; background quiet workflow signs.
Share on X: post this hatch
Copy: My PR egg hatched a 🌱 uncommon Frosted Clawlet in ClawSweeper.

What is this egg doing here?
  • Eggs appear after the PR passes real-behavior proof. It is here for vibes, not verdicts: it does not change labels, ratings, merge decisions, or automation.
  • The shell reacts to review momentum: open follow-up work warms it up, re-review makes it wobble, and a clean final review lets it hatch.
  • Hatchability usually comes from sufficient real-behavior proof, no blocking P0/P1/P2 findings, no security attention needed, and clean correctness. A merged PR is already final, so merge makes the egg hatchable independently.
  • The hatch is seeded from this repository and PR number, so the same PR keeps the same creature; the reviewed head SHA can only change safe visual details.
  • Rarity is just collectible sparkle: 🥚 common, 🌱 uncommon, 💎 rare, ✨ glimmer, and 🌈 legendary.

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels May 22, 2026
@joshavant joshavant merged commit b010852 into main May 22, 2026
123 of 127 checks passed
@joshavant joshavant deleted the fix/telegram-dispatch-dedupe-84886 branch May 22, 2026 05:14
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 24, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 24, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 24, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
galiniliev pushed a commit to galiniliev/openclaw that referenced this pull request May 25, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 26, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 26, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
SebTardif pushed a commit to SebTardif/openclaw that referenced this pull request May 26, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
SYU8384 pushed a commit to SYU8384/openclaw that referenced this pull request Jun 3, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
* Fix Telegram dispatch replay dedupe

* Add changelog for Telegram dispatch dedupe

* Persist Telegram replay dedupe at dispatch start
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram maintainer Maintainer-authored PR merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: L status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Beta blocker: telegram - isolated polling lacks durable message dispatch idempotency across replay

1 participant