Summary
On Windows, the context-mode MCP server child process is not reaped when its client (Claude Code / claude.exe) session ends, crashes, or the machine reboots. The orphan survives and busy-loops, accumulating unbounded CPU (hundreds to thousands of CPU-seconds) while holding 170–490 MB RSS. Healthy instances sit at ~1–3 CPU-seconds, so a hung one is unmistakable. Over a multi-session workday the strays pile up, hang ctx_* calls, and contribute to system-wide memory/CPU pressure.
Root cause
The parent-death guard already exists — but it never runs on Windows.
start.mjs (v1.0.169, lines ~62–135, added for issue #862) forwards stdin-EOF → SIGTERM → SIGKILL to the server child, which is exactly what prevents the orphan. But the entire block is gated behind:
if (typeof globalThis.Bun === "undefined" && process.platform === "linux") { ... }
plus a discovered Bun binary. On Windows the MCP server runs as a bare node …\cli.bundle.mjs child with no process.stdin.on('end'|'close') exit handler and no parent-liveness (ppid) poll. When the parent goes away ungracefully, nothing tells the child to exit, so it orphans and pins a core.
(Windows also does not cascade-kill children on parent death unless the launcher uses a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, so the server needs its own guard for hard exits.)
Environment
- Windows 11
- Claude Code desktop host
2.1.209
- context-mode plugin v1.0.169
Evidence (observed orphans, all node …\context-mode\cli.bundle.mjs)
| Started |
CPU-seconds |
RSS |
| prior day |
~4,323 |
— |
| — |
~789 |
485 MB |
| — |
~778 |
299 MB |
| — |
~7,177 |
173 MB |
| — |
~628 |
184 MB |
Healthy instances at the same times: ~1–3 CPU-seconds. Killing a stray immediately restored ctx_* responsiveness each time. Every live child had a claude.exe parent (one per active session); the leak is that ended sessions' children persist.
The sibling token-savior indexer shows the same "per-session helper not reaped" shape (seen at 145 / 116 CPU-s) — likely the same class of bug.
Suggested fix
Ungate the #862 guard so it also runs on the Windows/node boot path. At minimum, at server startup:
process.stdin.on('end', () => process.exit(0));
process.stdin.on('close', () => process.exit(0));
const ppid = process.ppid;
setInterval(() => { try { process.kill(ppid, 0); } catch { process.exit(0); } }, 15000).unref();
and ensure the MCP transport's onclose/close tears the process down, with any keep-alive setInterval/reconnect loop .unref()'d and backed off. The stdin handlers are passive (they don't consume transport data) and safe to add alongside StdioServerTransport; the .unref()'d ppid poll is the belt-and-suspenders for hard exit / reboot where stdin EOF may not arrive.
Summary
On Windows, the context-mode MCP server child process is not reaped when its client (Claude Code /
claude.exe) session ends, crashes, or the machine reboots. The orphan survives and busy-loops, accumulating unbounded CPU (hundreds to thousands of CPU-seconds) while holding 170–490 MB RSS. Healthy instances sit at ~1–3 CPU-seconds, so a hung one is unmistakable. Over a multi-session workday the strays pile up, hangctx_*calls, and contribute to system-wide memory/CPU pressure.Root cause
The parent-death guard already exists — but it never runs on Windows.
start.mjs(v1.0.169, lines ~62–135, added for issue #862) forwards stdin-EOF → SIGTERM → SIGKILL to the server child, which is exactly what prevents the orphan. But the entire block is gated behind:plus a discovered Bun binary. On Windows the MCP server runs as a bare
node …\cli.bundle.mjschild with noprocess.stdin.on('end'|'close')exit handler and no parent-liveness (ppid) poll. When the parent goes away ungracefully, nothing tells the child to exit, so it orphans and pins a core.(Windows also does not cascade-kill children on parent death unless the launcher uses a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, so the server needs its own guard for hard exits.)Environment
2.1.209Evidence (observed orphans, all
node …\context-mode\cli.bundle.mjs)Healthy instances at the same times: ~1–3 CPU-seconds. Killing a stray immediately restored
ctx_*responsiveness each time. Every live child had aclaude.exeparent (one per active session); the leak is that ended sessions' children persist.The sibling
token-saviorindexer shows the same "per-session helper not reaped" shape (seen at 145 / 116 CPU-s) — likely the same class of bug.Suggested fix
Ungate the #862 guard so it also runs on the Windows/node boot path. At minimum, at server startup:
and ensure the MCP transport's
onclose/closetears the process down, with any keep-alivesetInterval/reconnect loop.unref()'d and backed off. The stdin handlers are passive (they don't consume transport data) and safe to add alongsideStdioServerTransport; the.unref()'d ppid poll is the belt-and-suspenders for hard exit / reboot where stdin EOF may not arrive.