Yeachan-Heo/oh-my-codex · error · Error
detached child process group did not disappear after forced
Error message
detached child process group did not disappear after forced termination
What it means
After terminating the detached child, the leader waits up to 5s for the process group to disappear, escalates to SIGKILL, and waits again; if the group still exists, this error is thrown.
Source
Thrown at src/cli/index.ts:7157
if (escalation) clearTimeout(escalation);
if (process.platform !== "win32" && child.pid) {
const waitForGroupExit = async (deadline: number): Promise<boolean> => {
while (Date.now() < deadline) {
try {
process.kill(-child.pid!, 0);
await new Promise((resolveWait) => setTimeout(resolveWait, 20));
} catch {
return true;
}
}
return false;
};
let groupGone = await waitForGroupExit(Date.now() + 5_000);
if (!groupGone) {
signalChildTree("SIGKILL", true);
groupGone = await waitForGroupExit(Date.now() + 5_000);
}
if (!groupGone) throw new Error("detached child process group did not disappear after forced termination");
}
const signalExitCodes: Partial<Record<NodeJS.Signals, number>> = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143, SIGKILL: 137 };
process.exitCode = externalInterrupt
? signalExitCodes[externalInterrupt] ?? 1
: outcome.code ?? (outcome.signal ? signalExitCodes[outcome.signal] ?? 1 : 1);
traceDetachedLeaderPhase("post-launch-start");
await postLaunch(
payload.cwd,
payload.sessionId,
binding,
payload.codexHomeOverride,
payload.preLaunchOptions.enableNotifyFallbackAuthority,
payload.projectLocalCodexHomeForCleanup,
async () => {
if (payload.readyPath) writeDetachedLeaderReport(payload.readyPath, {
version: 1, kind: "terminal", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
paneId: pane, leaderPid: process.pid, finalized: true,
...(typeof process.exitCode === "number" ? { exitStatus: process.exitCode } : {}),View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Inspect which PIDs remain in the child's process group (ps -o pid,pgid,stat) and kill them manually
- Ensure the launched command doesn't call setsid/setpgid to escape its group
- Check for stuck D-state processes or container runtime restrictions on group signals
Defensive patterns
Strategy: try-catch
Try / catch
try { await teardown(); } catch (e) {
if (/did not disappear after forced termination/.test((e as Error).message)) {
await killStragglersByPgid(child.pid); // manual sweep, then continue
}
} Prevention
- Ensure launched commands don't call setsid
- Watch for D-state processes on NFS
- Periodically reap orphaned children in long-lived sessions
When it happens
Trigger: waitForGroupExit returns false after both the graceful and SIGKILL rounds — e.g. children in uninterruptible sleep (D state), a fork-bombing grandchild, or kill(0) permission issues on the group.
Common situations: The launched codex command spawns grandchildren in a separate group, NFS/processes stuck on I/O, or PID namespace/container quirks preventing group signals.
Related errors
- preLaunch setup finalization failed
- detached leader interrupted before release: ${interrupted}
- bound session retained close failed: ${closeEvidence.error?.
- detached post-launch lifecycle cleanup failed
- Native hook transaction staged deletion cleanup claim failed
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/47fa5fdc18cac0e8.
Report an issue: GitHub.