openclaw - 💡(How to fix) Fix Persistent 409 getUpdates conflict with single bot instance — dual transport + confirmPersistedOffset race [1 comments, 2 participants]

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…
GitHub stats
openclaw/openclaw#58951Fetched 2026-04-08 02:30:50
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Timeline (top)
commented ×1subscribed ×1unsubscribed ×1

Error Message

[telegram] [diag] polling cycle error reason=getUpdates conflict inFlight=0 outcome=ok
  err=Call to 'getUpdates' failed! (409: Conflict: terminated by other getUpdates request;
  make sure that only one bot instance is running)
[telegram] getUpdates conflict: ... retrying in 30s.
[telegram] sendChatAction failed: Network request for 'sendChatAction' failed!
[telegram] sendMessage failed: Network request for 'sendMessage' failed!
[telegram] final reply failed: HttpError: Network request for 'sendMessage' failed!

Root Cause

After digging through the source, we identified three overlapping issues:

Fix Action

Fix / Workaround

2. Duplicate HTTP dispatchers (the self-sustaining loop)

resolveTelegramTransport() in extensions/telegram/src/fetch.ts creates a new undici dispatcher on every call. Two call sites create separate dispatchers:

Both dispatchers maintain TCP connections to api.telegram.org. When the polling dispatcher makes getUpdates, lingering connections from the probe dispatcher can cause Telegram to see two active sessions.

Code Example

await this.#confirmPersistedOffset(bot);  // direct getUpdates(offset, limit:1, timeout:0)
// immediately followed by:
const runner = run(bot, this.opts.runnerOptions);  // grammY runner starts its own getUpdates loop

---

[telegram] [diag] polling cycle error reason=getUpdates conflict inFlight=0 outcome=ok
  err=Call to 'getUpdates' failed! (409: Conflict: terminated by other getUpdates request;
  make sure that only one bot instance is running)
[telegram] getUpdates conflict: ... retrying in 30s.
[telegram] sendChatAction failed: Network request for 'sendChatAction' failed!
[telegram] sendMessage failed: Network request for 'sendMessage' failed!
[telegram] final reply failed: HttpError: Network request for 'sendMessage' failed!
RAW_BUFFERClick to expand / collapse

Bug Description

Single gateway instance, single Telegram bot token, zero external processes — yet getUpdates returns 409 ("terminated by other getUpdates request") on every polling cycle. The conflict is self-sustaining and never resolves.

Environment

  • OpenClaw 2026.3.28 (f9b1079)
  • Ubuntu 24.04 on GCP e2-medium
  • Single gateway process, single Telegram bot token
  • No webhooks configured
  • Confirmed via ss -tp that only one process connects to Telegram

Root Cause Analysis

After digging through the source, we identified three overlapping issues:

1. confirmPersistedOffset races with the grammY runner

In extensions/telegram/src/polling-session.ts (~line 140), #runPollingCycle() calls:

await this.#confirmPersistedOffset(bot);  // direct getUpdates(offset, limit:1, timeout:0)
// immediately followed by:
const runner = run(bot, this.opts.runnerOptions);  // grammY runner starts its own getUpdates loop

The confirmPersistedOffset call and the grammY runner's first getUpdates can overlap, triggering the initial 409.

2. Duplicate HTTP dispatchers (the self-sustaining loop)

resolveTelegramTransport() in extensions/telegram/src/fetch.ts creates a new undici dispatcher on every call. Two call sites create separate dispatchers:

  • Probe (channel.ts ~line 701): resolveTelegramTransport(opts.proxyFetch, { network: telegramCfg.network })
  • Polling (monitor.ts ~line 653): resolveTelegramTransport(proxyFetch, { network: account.config.network })

Both dispatchers maintain TCP connections to api.telegram.org. When the polling dispatcher makes getUpdates, lingering connections from the probe dispatcher can cause Telegram to see two active sessions.

Once a 409 fires, the retry loop creates new bots via #createPollingBot() which rebuilds the transport — but old TCP connections in the socket pool persist, perpetuating the conflict.

3. No per-token deduplication guard

monitorTelegramProvider() has no mechanism to prevent duplicate polling sessions for the same bot token. Hot-reload, health-monitor restart, or config watcher events can trigger startAccount twice. The waitForGracefulStop() has a 15s timeout (POLL_STOP_GRACE_MS) — if the old poller doesn't stop in time, the new one starts while the old one still holds an active getUpdates connection.

Steps to Reproduce

  1. Configure OpenClaw with a single Telegram bot via long-polling
  2. Start the gateway
  3. Observe journalctl — 409 conflicts appear within seconds and never stop
  4. Kill the gateway, wait 30+ seconds (verified zero TCP connections via ss -tp | grep 149.154)
  5. Start the gateway — 409 reappears immediately

Workaround Attempted

We patched the installed bundle:

  • Commented out await this.#confirmPersistedOffset(bot) at line 138140
  • Added a Map-based cache to resolveTelegramTransport() so probe and polling share the same dispatcher

The confirmPersistedOffset patch alone didn't fix it. The transport cache alone didn't fix it. Neither combined fully resolved it — the grammY runner itself may be creating additional connections.

Suggested Fixes

  1. Cache transport per token: resolveTelegramTransport() should return the same dispatcher for the same token/network config
  2. Remove or defer confirmPersistedOffset: The direct getUpdates call is redundant — the grammY runner handles offset tracking via its own first call
  3. Per-token mutex in monitorTelegramProvider(): Prevent concurrent polling sessions on the same token
  4. Reduce fetch.timeout below 30s to break the 30s=30s deadlock between server-side and client-side timeouts

Impact

  • Messages from users are intermittently dropped or delayed 30+ seconds
  • sendMessage, sendChatAction, editMessageText all fail when the conflict is active
  • The gateway is effectively unusable for real-time Telegram conversations

Logs

[telegram] [diag] polling cycle error reason=getUpdates conflict inFlight=0 outcome=ok
  err=Call to 'getUpdates' failed! (409: Conflict: terminated by other getUpdates request;
  make sure that only one bot instance is running)
[telegram] getUpdates conflict: ... retrying in 30s.
[telegram] sendChatAction failed: Network request for 'sendChatAction' failed!
[telegram] sendMessage failed: Network request for 'sendMessage' failed!
[telegram] final reply failed: HttpError: Network request for 'sendMessage' failed!

extent analysis

TL;DR

Implement a cache for the Telegram transport dispatcher to prevent duplicate connections and remove or defer the confirmPersistedOffset call to avoid overlapping getUpdates requests.

Guidance

  1. Implement a token-based cache for the resolveTelegramTransport function to ensure that the same dispatcher is returned for the same token and network configuration, preventing duplicate connections.
  2. Remove or defer the confirmPersistedOffset call in polling-session.ts to avoid overlapping getUpdates requests with the grammY runner.
  3. Add a per-token mutex in monitorTelegramProvider to prevent concurrent polling sessions on the same token.
  4. Review and adjust timeouts to prevent deadlocks between server-side and client-side timeouts.

Example

// Example of a simple cache for resolveTelegramTransport
const transportCache = new Map<string, Dispatcher>();

function resolveTelegramTransport(opts: any, config: any) {
  const key = `${opts.proxyFetch}-${config.network}`;
  if (transportCache.has(key)) {
    return transportCache.get(key);
  }
  const dispatcher = new Dispatcher(); // Create a new dispatcher
  transportCache.set(key, dispatcher);
  return dispatcher;
}

Notes

The provided patches and workarounds attempted to address the issue but were not fully successful, indicating that a combination of fixes may be necessary to resolve the conflict.

Recommendation

Apply the suggested fixes, starting with implementing a token-based cache for the Telegram transport dispatcher and removing or deferring the confirmPersistedOffset call, to prevent duplicate connections and overlapping getUpdates requests.

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING