Telegram plugin: stale-poller cleanup silently fails on Windows (ps -o unsupported), leaving orphaned getUpdates consumers

Status Open
Maintainer reply None cached
Activity 0 comments · opened Sep 24, 2026

Summary

The telegram@claude-plugins-official plugin tries to kill a stale getUpdates poller on startup, but the check it uses to verify the target is a server.ts process calls ps -p <pid> -o args=. On Windows that call throws, the error is swallowed by an empty catch {}, and the SIGTERM is never sent.

Result: every Claude Code session leaves a live poller behind. Telegram allows exactly one getUpdates consumer per bot token, so the instances contend and inbound messages stop being delivered.

Environment

  • Windows 11 Pro N, 10.0.26200
  • Claude Code v2.1.280
  • Plugin telegram@claude-plugins-official v0.0.7 (same code path in v0.0.6)
  • Bun 1.3.x, Git for Windows on PATH

Root cause

server.ts, lines 58 to 77. The comment states the intent exactly:

// Telegram allows exactly one getUpdates consumer per token. If a previous
// session crashed (SIGKILL, terminal closed) its server.ts grandchild can
// survive as an orphan and hold the slot forever, so every new session sees
// 409 Conflict. Kill any stale holder before we start polling.

The guard:

const cmd = execFileSync('ps', ['-p', String(stale), '-o', 'args='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
if (cmd.includes('server.ts')) {
  process.stderr.write(`telegram channel: replacing stale poller pid=${stale}\n`)
  process.kill(stale, 'SIGTERM')
}

Two independent failures on Windows:

  1. Git Bash ps does not support -o. The call exits non-zero with ps: unknown option -- o, execFileSync throws, and the enclosing try { } catch {} discards it. process.kill(stale, 'SIGTERM') is never reached.
  2. Even if the flag were accepted, Git Bash ps prints only PID/PPID/PGID/WINPID/TTY/UID/STIME/COMMAND and does not include full argv, so cmd.includes('server.ts') would not match either.

Reproduced directly:

$ ps -p 1380 -o args=
ps: unknown option -- o
$ echo $?
1

Observed impact

With four Claude Code sessions open (three interactive, one scheduled task), four bun.exe server.ts pollers ran concurrently. Orphans outlived their parent session by more than two hours: a poller started at 08:17 by a scheduled task that finished minutes later was still running and still polling at 10:42.

Inbound Telegram messages were not delivered to the session started with claude --channels plugin:telegram@claude-plugins-official, even though bot.pid named that session's poller. Killing the competing pollers restored delivery immediately.

Related: ~/.claude/channels/telegram/bot.pid records the most recent process to start, not the process that currently holds the poll. After the cleanup above it pointed at a PID that was already dead, so it is not a reliable indicator of the active consumer.

Suggested fix

Branch the verification per platform instead of assuming a POSIX ps. On win32 the equivalent identity check is available from tasklist or, more precisely:

Get-CimInstance Win32_Process -Filter "ProcessId=$pid" | Select-Object CommandLine

Alternatively drop the argv check and verify identity from state the plugin already controls (for example a start-time or marker written next to bot.pid), which avoids shelling out entirely.

Either way, the empty catch {} around the whole block hides the failure. Writing the caught error to stderr, as the module already does elsewhere, would have made this visible immediately rather than presenting as "the bot stopped answering".

View original on GitHub ↗