An MCP host that kills the launcher chain within the server's first ~100ms while keeping the stdio pipes open (config probe, cancelled request, startup timeout; Rust hosts that kill a child without dropping its stdio handles) left the server orphaned: it booted already reparented to init, so the PPID watchdog's "ppid changed" baseline was captured as 1 and could never fire, and stdin never EOF'd. The process lingered — idle, ~30MB — until the host itself exited, accumulating one per abandoned launch (the pile-up reported in #1185). Reproduced on released 1.2.0/macOS: SIGKILL the launcher at +50ms → permanent orphan; at +150ms the old late baseline had already run and reaped it. Three-part fix: - Capture process.ppid at the earliest line of the CLI entry (early-ppid.ts) and use it as every watchdog baseline, shrinking the blind window to the few ms before our first JS runs. - Thread the real host pid down the bundled path: the npm shim and the standalone sh launcher set CODEGRAPH_HOST_PPID (an outer launcher's value wins), so the watchdog polls the host directly. Previously only the --liftoff-only relaunch set it, leaving the entire npm/standalone install base with hostPpid=null. - Never-initialized backstop (startup-handshake.ts): a serve --mcp that receives no MCP traffic for CODEGRAPH_STARTUP_HANDSHAKE_TIMEOUT_MS (default 15min, 0 disables) shuts down — the catch-all for a kill landing in the residual pre-JS window. Disarmed on the first byte, so a quiet-but-live session is never touched. Also scrub CODEGRAPH_HOST_PPID from the detached daemon's env — it has no host, and a stale pid must not leak into anything it spawns. Validated end-to-end on the built bundle: the +50ms early-kill orphan is now reaped while the host still holds the pipes open, and all six normal lifecycle paths (clean close, SIGTERM/SIGKILL child, host exit/SIGKILL, fd-holding adversarial host) stay clean. New coverage in startup-handshake.test.ts, mcp-startup-orphan.test.ts, and npm-shim.test.ts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 lines
1.4 KiB
TypeScript
26 lines
1.4 KiB
TypeScript
/**
|
|
* Parent-pid baseline captured as early as possible in process life (#1185).
|
|
*
|
|
* The PPID watchdog's POSIX signal is "`process.ppid` CHANGED since startup" —
|
|
* but a launcher killed within the first ~100ms of our boot (an MCP host's
|
|
* config probe, an instant user cancel, an initialize-timeout teardown) can
|
|
* reparent this process to init BEFORE the serve/proxy code captured its
|
|
* baseline. The baseline then reads `1`, never diverges, and the watchdog is
|
|
* permanently blind — the orphaned-server accumulation reported in #1185.
|
|
* Reproduced on macOS: SIGKILL the launcher 50ms after spawn while the host
|
|
* holds the stdio pipes open, and the server survived indefinitely; at 150ms
|
|
* the old capture had already run and the watchdog reaped it.
|
|
*
|
|
* The CLI entry imports this module before anything else, so the capture runs
|
|
* within the first few ms of JS execution — the earliest a Node process can
|
|
* observe its parent. A kill landing in the remaining pre-JS window (process
|
|
* spawn → first require) still captures `1`; that residual case is covered by
|
|
* the startup-handshake timeout (see ./startup-handshake.ts), which reaps a
|
|
* server that never receives any MCP traffic.
|
|
*
|
|
* Library consumers don't load the CLI entry; for them the capture runs at
|
|
* first import of the MCP layer — no worse than the previous per-call-site
|
|
* capture, and identical once the module cache warms.
|
|
*/
|
|
export const EARLY_PPID: number = process.ppid;
|