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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6ea65246a5
commit
c9f8c0ebaf
@@ -98,6 +98,11 @@ while [ -L "$SELF" ]; do
|
||||
esac
|
||||
done
|
||||
DIR="$(cd "$(dirname "$SELF")/.." && pwd)"
|
||||
# Thread the MCP host's pid to the server's orphan watchdog (issue #1185).
|
||||
# $PPID is our parent — the host itself when it launched this script directly;
|
||||
# an already-threaded value (the npm shim sets the true host pid) wins.
|
||||
CODEGRAPH_HOST_PPID="${CODEGRAPH_HOST_PPID:-$PPID}"
|
||||
export CODEGRAPH_HOST_PPID
|
||||
# --liftoff-only: avoid the V8 turboshaft WASM Zone OOM (issues #293/#298).
|
||||
exec "$DIR/node" --liftoff-only "$DIR/lib/dist/bin/codegraph.js" "$@"
|
||||
LAUNCH
|
||||
|
||||
+8
-1
@@ -45,7 +45,14 @@ async function main() {
|
||||
// Happy path: the npm-installed optional dependency. Fall back to a download
|
||||
// when the registry didn't deliver it.
|
||||
var resolved = resolveInstalledBundle() || (await selfHealBundle());
|
||||
var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit', windowsHide: true });
|
||||
// Thread the MCP host's pid (our parent) down to the bundled server so its
|
||||
// orphan watchdog can poll the host directly. Without this, the server can
|
||||
// only watch THIS shim — and a shim killed during the server's first ~100ms
|
||||
// of startup used to leave the server orphaned forever (issue #1185). An
|
||||
// already-set value (an outer launcher) wins.
|
||||
var env = Object.assign({}, process.env);
|
||||
if (!env.CODEGRAPH_HOST_PPID) env.CODEGRAPH_HOST_PPID = String(process.ppid);
|
||||
var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit', windowsHide: true, env: env });
|
||||
if (res.error) {
|
||||
process.stderr.write('codegraph: ' + res.error.message + '\n');
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user