telegram channel plugin: orphaned server.ts processes accumulate unbounded, CPU-busy, immune to SIGTERM
Status Closed — duplicate
Maintainer reply None cached
Activity 2 comments · opened Jul 30, 2026 · closed Aug 15, 2026
Bug report
Plugin: claude-plugins-official marketplace, telegram channel plugin, version 0.0.6
File: server.ts (bundled in plugin cache)
Summary
Orphaned bun server.ts processes from the Telegram channel plugin accumulate unbounded across Claude Code sessions, each stuck in a CPU-busy loop (not idle), eventually causing severe system-wide slowdown.
Evidence
- Found 90 concurrent
bun server.tsprocesses on a single macOS machine,PPID=1(orphaned/reparented to launchd), accumulated over ~1.5 days of normal Claude Code usage (one new orphan roughly per session that didn't close cleanly). - Process state was
R(running), notS(sleeping) — confirmed viaps -o pid,ppid,stat,etime,command, sustained CPU usage 15–40% per process. - System load average reached 541 (1-min) on what is normally a load <10 machine; swap usage climbed to 42GB/43GB; disk pressure and thrashing followed.
kill -TERMon these orphans had no effect — they only died tokill -9(SIGKILL). This is consistent with the process being stuck in a tight synchronous/CPU-bound loop that starves the JS event loop, soprocess.on('SIGTERM', ...)handlers never get a chance to run.
Root cause (from reading server.ts)
The plugin already has anti-zombie logic:
- A
PID_FILEat~/.claude/channels/telegram/bot.pid— on startup, a new instance reads the previous pid and sends itSIGTERMto reclaim the TelegramgetUpdatesslot (only one consumer allowed per bot token). - An orphan watchdog (
setInterval(..., 5000).unref()) that checksprocess.ppid !== bootPpidor a destroyed/ended stdin, and callsshutdown()(which itself doesbot.stop()+ a 2s forceprocess.exit(0)fallback).
In practice neither mechanism reliably terminates the orphan:
- The startup
SIGTERM(line ~66) is fire-and-forget wrapped intry {} catch {}with no verification the target actually died, and no escalation toSIGKILLif it doesn't. - Once a getUpdates poller is orphaned, it appears to keep hitting Telegram's API and getting
409 Conflict(since the new session holds the slot), and — if the retry path lacks proper backoff/await — spins the event loop hot enough that neither the 5s watchdog interval nor incoming signal callbacks (SIGTERM/SIGINT) get scheduled, so the "orphan detects itself and cleanly exits" design silently fails exactly in the failure mode it was built to catch.
Suggested fix
- After sending
SIGTERMto the stalePID_FILEholder at startup, verify death (e.g. pollprocess.kill(pid, 0)for ~1–2s) and escalate toSIGKILLif still alive. - Audit the Telegram
409 Conflict/ polling-error retry path in the orphaned-consumer case for a missing backoff/await that could produce a CPU-bound tight loop. - Consider a belt-and-suspenders external reaper (e.g. plugin install/launch hook that SIGKILLs any process matching this plugin's
server.tspath withppid=1older than N minutes) since in-process self-termination is unreliable once the process is already CPU-pegged.
Environment
- macOS (darwin), Claude Code CLI,
claude-plugins-officialtelegram plugin v0.0.6, bun runtime.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗