# 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 --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](https://github.com/tdlib/telegram-bot-api/issues/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-.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.