openclaw - ✅(Solved) Fix Telegram long-polling: 409 Conflict retries should mark transport dirty (keep-alive socket loop) [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

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#69787Fetched 2026-04-22 07:48:19
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

When getUpdates returns HTTP 409 Conflict (e.g. terminated by other getUpdates request), the polling runtime appears to retry on the same HTTP keep-alive connection. Telegram treats that connection as the "old" session and returns 409 again, causing a tight loop of conflicts.

Root Cause

Because the transport is not marked dirty, the retry reuses the same TCP socket (HTTP keep-alive). Telegram’s backend then repeatedly terminates that session → sustained 409s at low but non-zero rate (observed on the order of a few per minute after eliminating duplicate pollers).

Fix Action

Fix / Workaround

Happy to test a pre-release or patch build on our deployment if useful.

PR fix notes

PR #69873: fix(telegram): mark polling transport dirty on 409 conflict

Description (problem / solution / changelog)

Summary

Fixes #69787. When Telegram returns HTTP 409 Conflict on getUpdates (e.g. terminated by other getUpdates request), the polling runtime retried on the same HTTP keep-alive TCP socket because markDirty() was only called in the isRecoverable branch.

Telegram treats that connection as the old session and keeps terminating it — producing a sustained low-rate 409 retry loop (reporter: a few per minute after eliminating duplicate pollers).

Fix

Broaden the dirty-mark condition in extensions/telegram/src/polling-session.ts so isConflict also triggers markDirty(). This forces the next cycle to build a fresh transport (fresh TCP connection) instead of reusing the keep-alive socket that Telegram already considers closed on its side.

// Mark transport dirty on 409 conflict as well as recoverable network
// errors. (#69787)
if (isRecoverable || isConflict) {
  this.#transportState.markDirty();
}

Test

The existing test reuses the transport after a getUpdates conflict locked in the buggy behavior — it asserted createTelegramTransport was never called. That test has been updated (renamed to rebuilds the transport after a getUpdates conflict to force a fresh TCP socket) and flipped to assert the correct behavior: one fresh transport is built, the stale one is closed.

oxlint passes on both changed files. Local vitest run blocked by a pre-existing vitest config naming conflict unrelated to this change (vitest.contracts-channel-surface.config.ts).

Scope

1 production file, 1 test file. Behavior change limited to 409 conflict path.

Closes #69787.

Changed files

  • extensions/telegram/src/polling-session.test.ts (modified, +15/-4)
  • extensions/telegram/src/polling-session.ts (modified, +6/-1)

Code Example

// conceptual — align with actual variable names in source
if (isConflict || isRecoverable) {
  this.#transportState.markDirty();
}
RAW_BUFFERClick to expand / collapse

Summary

When getUpdates returns HTTP 409 Conflict (e.g. terminated by other getUpdates request), the polling runtime appears to retry on the same HTTP keep-alive connection. Telegram treats that connection as the "old" session and returns 409 again, causing a tight loop of conflicts.

Hypothesis (bundled runtime)

In the published bundle (path shape on Linux):

/usr/lib/node_modules/openclaw/dist/monitor-polling.runtime-*.js

around ~line 280, the code calls this.#transportState.markDirty() only when isRecoverable is true. It does not mark the transport dirty on 409 / conflict.

Because the transport is not marked dirty, the retry reuses the same TCP socket (HTTP keep-alive). Telegram’s backend then repeatedly terminates that session → sustained 409s at low but non-zero rate (observed on the order of a few per minute after eliminating duplicate pollers).

Suggested fix

Broaden the dirty-mark condition so conflict also forces a fresh connection on retry, e.g.:

// conceptual — align with actual variable names in source
if (isConflict || isRecoverable) {
  this.#transportState.markDirty();
}

(i.e. treat 409 like a transport reset signal, not only "recoverable" errors.)

Evidence we saw in production

  • After ensuring only one gateway/poller used the bot token (duplicate systemd units disabled), residual 409 rate dropped dramatically but did not go to zero.
  • Log lines showed very short durationMs on conflict responses, inFlight=0, and Telegram messaging consistent with "terminated by other getUpdates request".
  • Pattern consistent with same connection being retried rather than a second independent poller.

Environment

  • OpenClaw installed via npm on Linux (global openclaw package).
  • Telegram Bot API long polling.

Ask

Please confirm in source (pre-bundle) whether conflict paths skip markDirty, and if so consider marking dirty on 409 so the next poll uses a new connection.

Happy to test a pre-release or patch build on our deployment if useful.


Filed from downstream project (Lyra personal gateway); investigation credited to session checkpoint 2026-04-21.

extent analysis

TL;DR

The most likely fix is to modify the markDirty condition to include conflict errors, forcing a fresh connection on retry for HTTP 409 Conflict responses.

Guidance

  • Review the source code around line 280 in monitor-polling.runtime-*.js to confirm if the markDirty method is skipped for conflict errors.
  • Update the condition to mark the transport as dirty for both recoverable errors and conflict errors (409 status code) to prevent reusing the same TCP socket.
  • Test the updated code to verify that the retry mechanism uses a new connection after receiving a 409 Conflict response.
  • Monitor the production environment to ensure the fix reduces the sustained 409 error rate to zero.

Example

if (isConflict || isRecoverable) {
  this.#transportState.markDirty();
}

This code snippet illustrates the suggested fix, treating 409 Conflict errors as a transport reset signal.

Notes

The provided information suggests that the issue is specific to the OpenClaw library and its handling of HTTP 409 Conflict responses. The fix may not apply to other libraries or frameworks.

Recommendation

Apply the workaround by updating the markDirty condition to include conflict errors, as this is likely to resolve the issue and prevent sustained 409 error rates.

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

openclaw - ✅(Solved) Fix Telegram long-polling: 409 Conflict retries should mark transport dirty (keep-alive socket loop) [1 pull requests, 1 participants]