Files
claude-telegram-live-feed/docs/telegram-plugin-duplicate-poller-bug.md

3.7 KiB

Bug: Telegram plugin spawns one poller per Claude session → silent message drops

Plugin: claude-plugins-official/external_plugins/telegram Symptom: When two or more Claude Code sessions are running concurrently, incoming Telegram messages to the bot are randomly dropped — the user sends N messages, the bot only sees a subset, and replies arrive late or not at all. No error is surfaced anywhere.

Root cause

Each Claude Code session starts its own copy of the Telegram plugin via:

bun run --cwd <plugin-path> --shell=bun --silent start

Observed on this host (two concurrent sessions):

PID 25  pts/0  claude
PID 58  pts/0  bun run .../external_plugins/telegram start
PID 487 pts/1  claude
PID 588 pts/1  bun run .../external_plugins/telegram start

Both bun processes long-poll Telegram's getUpdates against the same bot token. Per Telegram Bot API semantics and the known tdlib issue tdlib/telegram-bot-api#43, concurrent getUpdates calls against one token race: whichever call acks an update first marks it confirmed server-side, and the other poller never sees it. With N concurrent pollers, on average ~(N-1)/N of any individual poller's "view" is missing updates. Since the plugin in any given Claude session only acts on what its poller sees, messages get silently dropped from the user's perspective.

This is not a Telegram bug — Telegram's getUpdates is documented as single-consumer. It's a plugin architecture bug.

Reproduction

  1. Open two Claude Code sessions on the same machine that both load this plugin (same bot token).
  2. From a Telegram user paired to the bot, send 5 messages back-to-back.
  3. Observe: only some messages produce a reply; the rest vanish without trace.
  4. Kill one of the two bun .../telegram start processes.
  5. Resend 5 messages — all arrive and get replies.

Confirmed reproducible on this host (2026-04-06): killing PID 58 immediately fixed message delivery for the surviving session.

Why no logs

/root/.claude/plugins/data/telegram-claude-plugins-official/ is empty — the plugin emits no per-message audit log, so the drop is invisible unless you happen to compare what the user sent vs what the agent saw.

Suggested fix

Plugin needs a singleton lock per bot token. Options:

  1. File lock (flock on /tmp/telegram-plugin-<token-hash>.lock) — first plugin instance to start grabs the lock and runs the poller; subsequent instances detect the lock and instead attach to a local Unix socket / named pipe owned by the leader to receive updates fan-out. On leader exit, a follower takes over.
  2. Webhook mode instead of long polling — Telegram delivers each update exactly once to the configured URL, sidestepping the race entirely. Requires a public endpoint or tunnel, so harder to set up but more robust.
  3. Out-of-process daemon — ship the poller as a separate long-lived service (systemd unit / docker container) and have each Claude session's plugin instance act as a thin client that subscribes to it. Cleanest separation of concerns.

Option 1 is the smallest change. Option 3 is the right long-term shape, especially since multiple Claude sessions are clearly an intended use case.

Workaround until fix lands

Run a single Claude session, OR manually kill all but one bun run .../telegram start process whenever a second session starts. Add a startup hook that does this automatically.

Also worth logging

The plugin should write a per-message log line (update_id received, chat_id, acted/ignored) to plugins/data/telegram-claude-plugins-official/. Even one line per inbound update would have saved hours of guessing here.