openclaw - 💡(How to fix) Fix Beta blocker: telegram - isolated polling lacks durable message dispatch idempotency across replay

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Telegram isolated polling has replay/recovery paths, but handleInboundMessageLike() does not persist a per-message dispatch idempotency key before handing the message to the agent, so a replayed Telegram message_id can dispatch a second agent turn after restart/recovery.

Root Cause

Affected: Telegram users during polling replay/recovery/restart windows. Severity: High / beta blocker when combined with partial streaming, because one user message can fan out into repeated agent work and visible repeated replies. Frequency: Intermittent; depends on restart/recovery timing. Consequence: duplicate agent runs, duplicate replies, extra model/API cost, confusing Telegram state.

Fix Action

Fix / Workaround

Telegram isolated polling has replay/recovery paths, but handleInboundMessageLike() does not persist a per-message dispatch idempotency key before handing the message to the agent, so a replayed Telegram message_id can dispatch a second agent turn after restart/recovery.

A replayed Telegram update should be skipped before it calls the agent runner if the same account/chat/message id was already dispatched successfully or is already in flight.

handleInboundMessageLike() currently checks in-memory/update-level skip state, then records the message in the reply-chain cache and dispatches it to processInboundMessage(). The reply-chain cache is not a dispatch idempotency ledger.

Code Example

extensions/telegram/src/bot-handlers.runtime.ts:2618 handleInboundMessageLike(event)
extensions/telegram/src/bot-handlers.runtime.ts:2620 shouldSkipUpdate(event.ctxForDedupe)
extensions/telegram/src/bot-handlers.runtime.ts:2706 recordMessageForReplyChain(event.msg, ...)
extensions/telegram/src/bot-handlers.runtime.ts:2707 processInboundMessage(...)

---

extensions/telegram/src/polling-session.ts resolves update offsets and isolated ingress state
extensions/telegram/src/bot-update-tracker.ts tracks update ids/floors
extensions/telegram/src/telegram-ingress-spool.ts persists update spool entries and processing claims

---

{
  "mode": "polling",
  "running": true,
  "connected": true,
  "webhook_url": "",
  "pending_update_count": 0
}

---

flowchart TD
  A[Telegram getUpdates update_id N] --> B[OpenClaw isolated polling/spool]
  B --> C[handleInboundMessageLike]
  C --> D[record reply-chain cache]
  D --> E[processInboundMessage / agent run]
  B --> F[restart or recovery replays update_id N]
  F --> C

  G[Missing durable guard: accountId + chatId + message_id] -. should be checked before .-> E

  classDef bad fill:#ffd6d6,stroke:#cc3333,color:#111;
  class G bad;

---

src/plugin-sdk/persistent-dedupe.ts
extensions/feishu/src/dedup.ts
extensions/nextcloud-talk/src/replay-guard.ts
extensions/zalo/src/monitor.webhook.ts
extensions/discord/src/monitor/inbound-dedupe.ts
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

Yes

Summary

Telegram isolated polling has replay/recovery paths, but handleInboundMessageLike() does not persist a per-message dispatch idempotency key before handing the message to the agent, so a replayed Telegram message_id can dispatch a second agent turn after restart/recovery.

Steps to reproduce

NOT_ENOUGH_INFO

The source-level risk is clear, but I do not have a short deterministic repro script yet. The observed local incident involved Telegram polling restarts/recovery while a DM turn was draining; this issue is filed to track the missing durable guard separately from the visible draft-stream spam in #84885.

Expected behavior

For Telegram message-like updates, OpenClaw should process a stable Telegram message identity at most once per account/chat/message id across gateway restarts, isolated-polling offset replay, stale-claim recovery, and process recreation.

A replayed Telegram update should be skipped before it calls the agent runner if the same account/chat/message id was already dispatched successfully or is already in flight.

Actual behavior

handleInboundMessageLike() currently checks in-memory/update-level skip state, then records the message in the reply-chain cache and dispatches it to processInboundMessage(). The reply-chain cache is not a dispatch idempotency ledger.

Relevant path:

extensions/telegram/src/bot-handlers.runtime.ts:2618 handleInboundMessageLike(event)
extensions/telegram/src/bot-handlers.runtime.ts:2620 shouldSkipUpdate(event.ctxForDedupe)
extensions/telegram/src/bot-handlers.runtime.ts:2706 recordMessageForReplyChain(event.msg, ...)
extensions/telegram/src/bot-handlers.runtime.ts:2707 processInboundMessage(...)

Polling/recovery paths are intentionally at-least-once around restart/claim recovery:

extensions/telegram/src/polling-session.ts resolves update offsets and isolated ingress state
extensions/telegram/src/bot-update-tracker.ts tracks update ids/floors
extensions/telegram/src/telegram-ingress-spool.ts persists update spool entries and processing claims

No durable message:${accountId}:${chatId}:${message_id} dispatch ledger is checked between successful auth/policy checks and processInboundMessage().

OpenClaw version

2026.5.19

Operating system

macOS 26 / Darwin arm64

Install method

Global OpenClaw gateway managed by launchd (ai.openclaw.gateway)

Model

OpenRouter/OpenAI GPT-5 family route in the affected session; exact final model for every dispatch is NOT_ENOUGH_INFO.

Provider / routing chain

Telegram polling -> OpenClaw gateway -> embedded agent -> OpenRouter/OpenAI route

Additional provider/model setup details

Local Telegram status after mitigation:

{
  "mode": "polling",
  "running": true,
  "connected": true,
  "webhook_url": "",
  "pending_update_count": 0
}

Webhook+polling double delivery was ruled out locally: Telegram getWebhookInfo had an empty webhook URL and no pending updates.

Logs, screenshots, and evidence

flowchart TD
  A[Telegram getUpdates update_id N] --> B[OpenClaw isolated polling/spool]
  B --> C[handleInboundMessageLike]
  C --> D[record reply-chain cache]
  D --> E[processInboundMessage / agent run]
  B --> F[restart or recovery replays update_id N]
  F --> C

  G[Missing durable guard: accountId + chatId + message_id] -. should be checked before .-> E

  classDef bad fill:#ffd6d6,stroke:#cc3333,color:#111;
  class G bad;

The existing repo already has persistent/claimable dedupe primitives used by other channels:

src/plugin-sdk/persistent-dedupe.ts
extensions/feishu/src/dedup.ts
extensions/nextcloud-talk/src/replay-guard.ts
extensions/zalo/src/monitor.webhook.ts
extensions/discord/src/monitor/inbound-dedupe.ts

Telegram should use the same kind of durable/claimable guard for message-like updates after authorization and before recordMessageForReplyChain() / processInboundMessage().

Impact and severity

Affected: Telegram users during polling replay/recovery/restart windows. Severity: High / beta blocker when combined with partial streaming, because one user message can fan out into repeated agent work and visible repeated replies. Frequency: Intermittent; depends on restart/recovery timing. Consequence: duplicate agent runs, duplicate replies, extra model/API cost, confusing Telegram state.

Additional information

Related issues, but this is a distinct missing guard:

  • #84674 covers stale .processing claims surviving gateway recreate.
  • #84158 covers lone active spooled handler timeout recovery.
  • #72176 covers cross-channel duplicate transcript delivery.
  • #84885 covers Telegram partial draft preview visible spam.

Suggested fix shape: add a Telegram message-level claimable/persistent dedupe guard keyed by accountId, chatId, and Telegram message_id; claim after auth/policy checks; commit after dispatch ownership is established; release only for retryable pre-dispatch errors.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

For Telegram message-like updates, OpenClaw should process a stable Telegram message identity at most once per account/chat/message id across gateway restarts, isolated-polling offset replay, stale-claim recovery, and process recreation.

A replayed Telegram update should be skipped before it calls the agent runner if the same account/chat/message id was already dispatched successfully or is already in flight.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING