Skip to content

fix(channel): exit when parent dies instead of spinning on EPIPE - #4

Open
JefBronze wants to merge 1 commit into
non4me:mainfrom
JefBronze:fix/orphan-epipe-spin
Open

fix(channel): exit when parent dies instead of spinning on EPIPE#4
JefBronze wants to merge 1 commit into
non4me:mainfrom
JefBronze:fix/orphan-epipe-spin

Conversation

@JefBronze

Copy link
Copy Markdown

Problem

When the Claude Code session that spawned the MCP server exits, the server is orphaned with its stdio sockets closed. Every stdout/stderr write then throws EPIPE. The uncaughtException handler calls log(), which writes to the dead stderr, which throws EPIPE again, and the process spins in an infinite exception loop at 100% CPU.

Observed on macOS: three such orphans (one running 12 days, 88 CPU-hours) pinned three cores on a fanless MacBook Air, causing repeated Dark Wake Thermal Emergency sleeps and throttled charging. SIGTERM could not stop them either, because shutdown() also logs to the dead stderr.

A sample of a stuck process shows every frame in TriggerUncaughtException → handler → stderr.writeTriggerUncaughtException.

Fix

  • log() becomes a no-op once stdio is known dead and swallows write errors.
  • error handlers on stdin/stdout/stderr treat EPIPE, ECONNRESET, EIO, EBADF as parent death.
  • stdin end/close triggers gracefulShutdown, so the offline heartbeat is still written.
  • A process.ppid watchdog (same interval as heartbeat) catches the case where the parent is SIGKILLed and no EOF arrives promptly.
  • uncaughtException handler exits on stdio errors instead of re-logging them.
  • Hard process.exit(1) after 3 s if graceful shutdown hangs.

Testing

In an isolated CC2CC_BRIDGE_DIR:

  1. Piped stdin closed after 2 s → server exited within the heartbeat interval.
  2. Parent shell SIGKILLed → orphan exited within ~8 s via the ppid watchdog.

Both previously left the process alive at 100% CPU.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TxRbndHbg5wB5BQ591TZAP

When the Claude Code session that spawned the MCP server exits, the
server's stdio sockets close. Every stderr/stdout write then throws
EPIPE; the uncaughtException handler called log(), which wrote to the
dead stderr and threw again, producing an infinite exception loop at
100% CPU. Three such orphans were found pinned for days on a fanless
MacBook Air, triggering repeated thermal-emergency sleeps.

- log(): no-op once stdio is known dead; swallow write errors
- attach error handlers to stdin/stdout/stderr for EPIPE/ECONNRESET/EIO/EBADF
- shut down gracefully on stdin end/close
- poll process.ppid and shut down if reparented (parent SIGKILLed)
- uncaughtException: treat stdio errors as parent death, never re-log
- hard exit after 3s if graceful shutdown hangs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TxRbndHbg5wB5BQ591TZAP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant