telegram plugin: bun server.ts leaks at 100% CPU on every MCP disconnect
Plugin / version
telegram@claude-plugins-official v0.0.6 (./external_plugins/telegram in the marketplace repo).
Summary
Every time the Telegram MCP server disconnects (e.g. when the parent Claude Code session ends or the MCP transport drops), the underlying bun server.ts process survives, gets reparented to systemd, and pins one CPU core at 100% indefinitely. Multiple disconnects in one host session compound — I observed three leaked instances consuming ~3 cores before noticing the load.
Existing mitigations in server.ts (lines 645–677) do not stop it:
process.stdin.on('end' / 'close')shutdown handlers don't fire.SIGTERMis ignored —kill <pid>has no effect.- The orphan-watchdog
setInterval(...).unref()at line 671 never fires either.
Only SIGKILL reclaims the process. The fact that both the signal handler and the watchdog setInterval are unresponsive strongly suggests the Node/Bun event loop is wedged by a synchronous busy-loop after the transport dies — likely a tight retry/error loop somewhere in the post-disconnect path.
Reproducer
- Install
telegram@claude-plugins-officialand pair a bot. - Start a Claude Code session that activates the plugin.
- End the session (or trigger an MCP transport disconnect — in my case the host emitted
plugin:telegram:telegram disconnectedmid-session and the same leak occurred). ps -eo pid,ppid,pcpu,etime,cmd | grep "bun server.ts"— the orphaned process is pinned at ~100% CPU with parent PID 1 (or systemd-user).kill <pid>is ignored; onlykill -9works.
Reproduced 4× in a single host session on my machine (Arch, Linux 7.0.12, bun installed from the bun.sh script).
Suspected cause
shutdown() (line 649) hops off to bot.stop() and a setTimeout fallback to process.exit(0) — but both depend on the event loop progressing. If the disconnect path enters a synchronous loop (e.g. grammy reconnect loop, an unhandled promise that rejects synchronously in a tight cycle, or a while somewhere in the SDK transport), neither timer fires and signals queue forever.
Suggested fixes
- Find the busy-loop. Run with
bun --inspectafter a forced disconnect and capture a CPU profile from the wedged process; the hot frame will identify the offender. Most likely candidate is grammy's long-poll retry without backoff after stdin-EOF. - Add an independent kill switch that doesn't depend on the main event loop — e.g. a
Workerthread whose only job issetInterval(checkParent, 1000); if (orphaned) process.kill(process.pid, 'SIGKILL'). Workers have their own event loop, so they survive the main loop wedging. - Backstop in the wrapper. The
bun runwrapper script couldtrapon its own exit andkill -9the child PID; right now nothing enforces termination once the wrapper dies.
Impact
Silent CPU drain — easy to miss on a workstation, but on a battery-powered laptop or any always-on dev box this burns through power budget. Also makes the plugin a noisy neighbor for any concurrent work.
Workaround for affected users
ps -eo pid,pcpu,etime,cmd --sort=-pcpu | grep "bun server.ts" | grep -v grep
# kill any leaked instance (i.e. one not owned by your current claude session):
kill -9 <pid>This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗