From 0704a0b05d05fca632647a98ccbfb2ac495cb949 Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Thu, 19 Mar 2026 22:57:27 +0100 Subject: [PATCH 1/2] fix: prevent orphaned processes via stdin/ppid/onclose lifecycle guards Adds 4 missing lifecycle guards to main() that caused orphaned node processes to accumulate (~27GB RAM from 41 orphans on a 32GB machine): - stdin end/close listeners: detect client exit via pipe closure - server.onclose: handle graceful MCP protocol disconnect - PPID polling (5s, unref'd): cross-platform parent death detection - SIGHUP handler: terminal close on Unix + Windows console close --- src/index.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/index.ts b/src/index.ts index 7a8e1e2..8f1b403 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1612,9 +1612,32 @@ async function main() { } } + // Parent death guard — catches SIGKILL, crashes, terminal close on ALL platforms. + // process.kill(pid, 0) throws ESRCH when the process no longer exists. + const parentPid = process.ppid; + if (parentPid > 1) { + const parentGuard = setInterval(() => { + try { + process.kill(parentPid, 0); + } catch { + process.exit(0); + } + }, 5_000); + parentGuard.unref(); + } + const transport = new StdioServerTransport(); await server.connect(transport); + // Detect stdin pipe closure — the primary signal that the MCP client is gone. + // StdioServerTransport only listens for 'data'/'error', never 'end'. + process.stdin.on('end', () => process.exit(0)); + process.stdin.on('close', () => process.exit(0)); + + // Handle graceful MCP protocol-level disconnect. + // Fires after SDK internal cleanup when transport.close() is called. + server.onclose = () => process.exit(0); + if (process.env.CODEBASE_CONTEXT_DEBUG) console.error('[DEBUG] Server ready'); await refreshKnownRootsFromClient(); @@ -1651,6 +1674,10 @@ async function main() { stopAllWatchers(); process.exit(0); }); + process.once('SIGHUP', () => { + stopAllWatchers(); + process.exit(0); + }); } // Export server components for programmatic use From 588ce013cac520b278af49adf4905d0241e8d0ed Mon Sep 17 00:00:00 2001 From: PatrickSys Date: Thu, 19 Mar 2026 23:08:19 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20filte?= =?UTF-8?q?r=20ESRCH,=20hoist=20cleanup=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PPID poll: only exit on ESRCH (process gone), ignore EPERM (process alive but different UID — setuid, Linux security modules, etc.) - Hoist stopAllWatchers + exit/signal listeners to before stdin/onclose handlers, closing the race window where process.exit() could fire during initProject() before the cleanup listener was registered --- src/index.ts | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8f1b403..390fbca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1619,8 +1619,11 @@ async function main() { const parentGuard = setInterval(() => { try { process.kill(parentPid, 0); - } catch { - process.exit(0); + } catch (err: unknown) { + // ESRCH = process gone → exit. EPERM = process alive, different UID → ignore. + if ((err as NodeJS.ErrnoException).code === 'ESRCH') { + process.exit(0); + } } }, 5_000); parentGuard.unref(); @@ -1629,6 +1632,28 @@ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); + // Register cleanup before any handler that calls process.exit(), so the + // exit listener is always in place when stdin/onclose/signals fire. + const stopAllWatchers = () => { + for (const project of getAllProjects()) { + project.stopWatcher?.(); + } + }; + + process.once('exit', stopAllWatchers); + process.once('SIGINT', () => { + stopAllWatchers(); + process.exit(0); + }); + process.once('SIGTERM', () => { + stopAllWatchers(); + process.exit(0); + }); + process.once('SIGHUP', () => { + stopAllWatchers(); + process.exit(0); + }); + // Detect stdin pipe closure — the primary signal that the MCP client is gone. // StdioServerTransport only listens for 'data'/'error', never 'end'. process.stdin.on('end', () => process.exit(0)); @@ -1657,27 +1682,6 @@ async function main() { /* best-effort */ } }); - - // Cleanup all watchers on exit - const stopAllWatchers = () => { - for (const project of getAllProjects()) { - project.stopWatcher?.(); - } - }; - - process.once('exit', stopAllWatchers); - process.once('SIGINT', () => { - stopAllWatchers(); - process.exit(0); - }); - process.once('SIGTERM', () => { - stopAllWatchers(); - process.exit(0); - }); - process.once('SIGHUP', () => { - stopAllWatchers(); - process.exit(0); - }); } // Export server components for programmatic use