feat: parent heartbeat for MCP server orphan prevention (#992)

* feat: add parent heartbeat to MCP server to prevent orphaned processes

MCP server now monitors its parent process every 30s. When the parent
dies (ppid changes to 1 on Unix), the server self-exits to prevent
orphaned node processes that accumulate over time.

- Checks ppid every 30s after server start
- Compares against initial ppid (handles reparenting)
- Timer uses unref() to not keep process alive artificially
- Unix-only (ppid=1 detection doesn't apply on Windows)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* fix: make cleanup() synchronous for consistent shutdown behavior

cleanup() only does synchronous work (clearInterval + process.exit),
so remove async to avoid inconsistent behavior when called from
setInterval callback vs signal handler vs awaited context.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
michelhelsdingen
2026-02-16 06:31:23 +01:00
committed by GitHub
parent 51719d23a4
commit cd31eaf572
+31 -2
View File
@@ -307,8 +307,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
});
// Cleanup function
async function cleanup() {
// Parent heartbeat: self-exit when parent dies (ppid=1 on Unix means orphaned)
// Prevents orphaned MCP server processes when Claude Code exits unexpectedly
const HEARTBEAT_INTERVAL_MS = 30_000;
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
function startParentHeartbeat() {
// ppid-based orphan detection only works on Unix
if (process.platform === 'win32') return;
const initialPpid = process.ppid;
heartbeatTimer = setInterval(() => {
if (process.ppid === 1 || process.ppid !== initialPpid) {
logger.info('SYSTEM', 'Parent process died, self-exiting to prevent orphan', {
initialPpid,
currentPpid: process.ppid
});
cleanup();
}
}, HEARTBEAT_INTERVAL_MS);
// Don't let the heartbeat timer keep the process alive
if (heartbeatTimer.unref) heartbeatTimer.unref();
}
// Cleanup function — synchronous to ensure consistent behavior whether called
// from signal handlers, heartbeat interval, or awaited in async context
function cleanup() {
if (heartbeatTimer) clearInterval(heartbeatTimer);
logger.info('SYSTEM', 'MCP server shutting down');
process.exit(0);
}
@@ -324,6 +350,9 @@ async function main() {
await server.connect(transport);
logger.info('SYSTEM', 'Claude-mem search server started');
// Start parent heartbeat to detect orphaned MCP servers
startParentHeartbeat();
// Check Worker availability in background
setTimeout(async () => {
const workerAvailable = await verifyWorkerConnection();