Summary
Plugin claude-plugins-official/[email protected] leaks bun server.ts processes that pin a CPU core at 100% indefinitely. After 10 days uptime with multiple concurrent Claude Code sessions, I had 3 orphaned bun server.ts instances each consuming ~100% CPU, driving system load average to 84 and triggering macOS kernel_task thermal throttling.
Environment
- Claude Code: 2.1.153
- bun: 1.3.14
- macOS Darwin 25.3.0 (Apple Silicon)
- Plugin:
claude-plugins-official/telegram 0.0.6
Root cause
server.ts tries to mitigate Telegram's "one getUpdates consumer per token" rule by writing its PID to ~/.claude/channels/telegram/bot.pid and SIGTERM-ing whatever PID it finds there at startup:
try {
const stale = parseInt(readFileSync(PID_FILE, 'utf8'), 10)
if (stale > 1 && stale !== process.pid) {
process.kill(stale, 0)
process.stderr.write(`telegram channel: replacing stale poller pid=${stale}\n`)
process.kill(stale, 'SIGTERM')
}
} catch {}
writeFileSync(PID_FILE, String(process.pid))
The file is a single slot. When N concurrent Claude Code sessions each spawn their own bun server.ts, the last writer wins the PID slot. The earlier processes:
- No longer have their PID recorded, so no future session will SIGTERM them.
- Keep calling
bot.start() (long-polling getUpdates), which returns HTTP 409 Conflict because another instance now owns the slot.
- grammy's default retry behavior spins the request, pinning a CPU core at ~100%.
The .in_use/ directory under the plugin cache had 95 entries (one per session that ever loaded the plugin in 10 days) while only the most recent session's PID was tracked.
Reproduction
- Open ≥2 concurrent Claude Code sessions that both load the telegram plugin
- Wait for both
bun server.ts instances to start
- Observe: the older one pins a CPU core at 100% on
getUpdates 409 retries
- Verify with
ps -Ao pid,pcpu,args | grep "bun.*server\.ts"
Suggested fixes
Pick one (in order of robustness):
flock/exclusive lock on bot.pid — second instance detects the lock and exits cleanly with a clear message. Only one poller per host ever runs.
- Detect
409 Conflict in the getUpdates handler and process.exit(0) — current instance becomes orphan-safe; no longer relies on the next session's startup logic as the only safety net.
- Append-only PID list + SIGTERM all stale on startup — keeps current architecture, just fixes the single-slot bug.
Workaround
pkill -9 -f "bun.*server\.ts"
Next /telegram invocation respawns one cleanly.
Summary
Plugin
claude-plugins-official/[email protected]leaksbun server.tsprocesses that pin a CPU core at 100% indefinitely. After 10 days uptime with multiple concurrent Claude Code sessions, I had 3 orphanedbun server.tsinstances each consuming ~100% CPU, driving system load average to 84 and triggering macOSkernel_taskthermal throttling.Environment
claude-plugins-official/telegram0.0.6Root cause
server.tstries to mitigate Telegram's "onegetUpdatesconsumer per token" rule by writing its PID to~/.claude/channels/telegram/bot.pidandSIGTERM-ing whatever PID it finds there at startup:The file is a single slot. When N concurrent Claude Code sessions each spawn their own
bun server.ts, the last writer wins the PID slot. The earlier processes:bot.start()(long-pollinggetUpdates), which returns HTTP409 Conflictbecause another instance now owns the slot.The
.in_use/directory under the plugin cache had 95 entries (one per session that ever loaded the plugin in 10 days) while only the most recent session's PID was tracked.Reproduction
bun server.tsinstances to startgetUpdates409 retriesps -Ao pid,pcpu,args | grep "bun.*server\.ts"Suggested fixes
Pick one (in order of robustness):
flock/exclusive lock onbot.pid— second instance detects the lock and exits cleanly with a clear message. Only one poller per host ever runs.409 Conflictin thegetUpdateshandler andprocess.exit(0)— current instance becomes orphan-safe; no longer relies on the next session's startup logic as the only safety net.Workaround
pkill -9 -f "bun.*server\.ts"Next
/telegraminvocation respawns one cleanly.