Telegram channel plugin: stale-poller cleanup is POSIX-only, leaks orphan getUpdates pollers on Windows that starve other listeners

Status Open
Reported on v2.1.251
Maintainer reply None cached
Activity 0 comments · opened Aug 31, 2026

Summary

The official Telegram channel plugin (telegram@claude-plugins-official, v0.0.7) leaks an orphaned server.ts polling process per Claude Code session on Windows, because its stale-poller cleanup is POSIX-only. The orphans outlive their sessions and silently steal the bot's single getUpdates slot from any legitimate listener sharing the token (e.g., a persistent claude --channels service on a Linux host), which then loops on 409 Conflict and appears healthy while never receiving a message.

Environment

  • Claude Code 2.1.251 (also reproduced with the listener on 2.1.241)
  • Telegram plugin claude-plugins-official/telegram 0.0.7
  • Windows 11 Home (orphan side) + Ubuntu VPS (starved listener side), one bot token used on both

Root cause

server.ts guards against a stale poller holding the getUpdates slot by reading bot.pid and verifying the old PID before SIGTERM:

const cmd = execFileSync('ps', ['-p', String(stale), '-o', 'args='], { encoding: 'utf8', ... })
if (cmd.includes('server.ts')) {
  process.kill(stale, 'SIGTERM')
}

On Windows there is no ps; execFileSync throws, the surrounding try/catch swallows it, and the stale poller is never killed. Additionally, when a session exits uncleanly the poller survives as an orphan. The two together mean every Claude Code session on Windows with the plugin configured can leave a permanent background poller, and they accumulate: we found 7 orphaned bun server.ts processes spanning Aug 29–30 sessions, all long-polling the same token.

Impact observed

A persistent Telegram listener (claude --channels under systemd on a Linux VPS, same bot token) went silent for ~2 days while looking completely healthy: unit active, process tree intact, TCP connections to Telegram established, and pending_update_count always 0 (an orphan consumed each message). The listener's real state was only visible via strace on the bridge process:

telegram channel: 409 Conflict, retrying in 0s
telegram channel: polling as @<bot>
telegram channel: 409 Conflict, retrying in 0s
...

This 409 loop never surfaces in journald or --debug-file output, which made the failure look like a hung Claude session and cost us a long false trail (version rollback, debug instrumentation) before syscall tracing found it.

Repro

  1. Configure the Telegram channel on a Windows machine (~/.claude/channels/telegram/.env with a bot token).
  2. Start and close several Claude Code sessions with the plugin enabled (unclean exit makes it more likely, but accumulation happens regardless since cleanup never works).
  3. Observe multiple bun server.ts processes persisting after their sessions have ended (Get-CimInstance Win32_Process -Filter "Name='bun.exe'").
  4. Run a second listener elsewhere with the same token: it loops on 409 Conflict and receives nothing.

Suggested fixes

  • Make the stale-PID verification cross-platform (on win32, verify via wmic/Get-CimInstance/tasklist instead of ps, or store the process start time alongside the PID and compare).
  • Consider surfacing repeated 409 Conflicts to the host process as an ERROR-level event rather than only bridge stderr — a listener that can never poll currently looks indistinguishable from a healthy idle one from the host's perspective.
  • Ensure the poller exits when its host session ends (tie lifetime to the stdio pipe: exit on stdin EOF).

View original on GitHub ↗