Skip to content

Commit 216174e

Browse files
ziggyclaude
andcommitted
fix: kill entire process group on timeout and reset to prevent orphaned MCP servers
When a session ends via SIGKILL (idle/absolute timeout or /reset), only the top-level agent-runner node process was killed. MCP servers spawned inside it by the Claude Agent SDK became orphaned — reparented to launchd and left running. Spawn agents with detached=true to create a new process group. Use process.kill(-pid, signal) in killOnTimeout and killAgent so the entire group is torn down together. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 59f9bf4 commit 216174e

2 files changed

Lines changed: 28 additions & 11 deletions

File tree

src/container-runner.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ export async function runContainerAgent(
296296
stdio: ['pipe', 'pipe', 'pipe'],
297297
env: agentEnv,
298298
cwd: groupDir,
299+
// detached=true creates a new process group with the agent as leader.
300+
// This lets us kill the entire group (agent + all MCP server children)
301+
// with process.kill(-pid, signal) rather than just the top-level node process.
302+
detached: true,
299303
});
300304

301305
onProcess(agentProcess, processName);
@@ -408,21 +412,25 @@ export async function runContainerAgent(
408412
timeoutHandled = true;
409413
timedOut = true;
410414
timeoutReason = reason;
415+
const pid = agentProcess.pid;
411416
logger.error(
412-
{ group: group.name, processName, reason },
417+
{ group: group.name, processName, reason, pid },
413418
reason === 'idle'
414-
? 'Agent idle timeout — no stdout for too long, killing process'
415-
: 'Agent absolute timeout — hard ceiling reached, killing process',
419+
? 'Agent idle timeout — no stdout for too long, killing process group'
420+
: 'Agent absolute timeout — hard ceiling reached, killing process group',
416421
);
417-
agentProcess.kill('SIGTERM');
418-
agentProcess.once('close', () => {
419-
// Process already dead, nothing more to do
420-
});
422+
// Kill the entire process group (negative PID) so MCP server children
423+
// spawned by the Claude Agent SDK are also terminated, not orphaned.
424+
try {
425+
if (pid) process.kill(-pid, 'SIGTERM');
426+
} catch {
427+
/* already dead */
428+
}
421429
setTimeout(() => {
422430
try {
423-
agentProcess.kill('SIGKILL');
431+
if (pid) process.kill(-pid, 'SIGKILL');
424432
} catch {
425-
// already dead
433+
/* already dead */
426434
}
427435
}, 15000);
428436
};

src/group-queue.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,17 @@ export class GroupQueue {
197197
const state = this.getGroup(groupJid);
198198
if (!state.process || state.process.killed) return false;
199199

200-
logger.info({ groupJid }, 'Force-killing agent process (/reset)');
201-
state.process.kill('SIGKILL');
200+
const pid = state.process.pid;
201+
logger.info(
202+
{ groupJid, pid },
203+
'Force-killing agent process group (/reset)',
204+
);
205+
// Kill the entire process group so MCP server children are also terminated.
206+
try {
207+
if (pid) process.kill(-pid, 'SIGKILL');
208+
} catch {
209+
/* already dead */
210+
}
202211
return true;
203212
}
204213

0 commit comments

Comments
 (0)