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.ts processes 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), not S (sleeping) — confirmed via ps -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 -TERM on these orphans had no effect — they only died to kill -9 (SIGKILL). This is consistent with the process being stuck in a tight synchronous/CPU-bound loop that starves the JS event loop, so process.on('SIGTERM', ...) handlers never get a chance to run.

Root cause (from reading server.ts)

The plugin already has anti-zombie logic:

  1. A PID_FILE at ~/.claude/channels/telegram/bot.pid — on startup, a new instance reads the previous pid and sends it SIGTERM to reclaim the Telegram getUpdates slot (only one consumer allowed per bot token).
  2. An orphan watchdog (setInterval(..., 5000).unref()) that checks process.ppid !== bootPpid or a destroyed/ended stdin, and calls shutdown() (which itself does bot.stop() + a 2s force process.exit(0) fallback).

In practice neither mechanism reliably terminates the orphan:

  • The startup SIGTERM (line ~66) is fire-and-forget wrapped in try {} catch {} with no verification the target actually died, and no escalation to SIGKILL if 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 SIGTERM to the stale PID_FILE holder at startup, verify death (e.g. poll process.kill(pid, 0) for ~1–2s) and escalate to SIGKILL if 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.ts path with ppid=1 older 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-official telegram plugin v0.0.6, bun runtime.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗