{"record":{"id":"47fa5fdc18cac0e8","repo":"Yeachan-Heo/oh-my-codex","slug":"detached-child-process-group-did-not-disappear-aft","errorCode":null,"errorMessage":"detached child process group did not disappear after forced termination","messagePattern":"detached child process group did not disappear after forced termination","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/cli/index.ts","lineNumber":7157,"sourceCode":"    if (escalation) clearTimeout(escalation);\n    if (process.platform !== \"win32\" && child.pid) {\n      const waitForGroupExit = async (deadline: number): Promise<boolean> => {\n        while (Date.now() < deadline) {\n          try {\n            process.kill(-child.pid!, 0);\n            await new Promise((resolveWait) => setTimeout(resolveWait, 20));\n          } catch {\n            return true;\n          }\n        }\n        return false;\n      };\n      let groupGone = await waitForGroupExit(Date.now() + 5_000);\n      if (!groupGone) {\n        signalChildTree(\"SIGKILL\", true);\n        groupGone = await waitForGroupExit(Date.now() + 5_000);\n      }\n      if (!groupGone) throw new Error(\"detached child process group did not disappear after forced termination\");\n    }\n    const signalExitCodes: Partial<Record<NodeJS.Signals, number>> = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143, SIGKILL: 137 };\n    process.exitCode = externalInterrupt\n      ? signalExitCodes[externalInterrupt] ?? 1\n      : outcome.code ?? (outcome.signal ? signalExitCodes[outcome.signal] ?? 1 : 1);\n    traceDetachedLeaderPhase(\"post-launch-start\");\n    await postLaunch(\n      payload.cwd,\n      payload.sessionId,\n      binding,\n      payload.codexHomeOverride,\n      payload.preLaunchOptions.enableNotifyFallbackAuthority,\n      payload.projectLocalCodexHomeForCleanup,\n      async () => {\n        if (payload.readyPath) writeDetachedLeaderReport(payload.readyPath, {\n          version: 1, kind: \"terminal\", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,\n          paneId: pane, leaderPid: process.pid, finalized: true,\n          ...(typeof process.exitCode === \"number\" ? { exitStatus: process.exitCode } : {}),","sourceCodeStart":7139,"sourceCodeEnd":7175,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/cli/index.ts#L7139-L7175","documentation":"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.","triggerScenarios":"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.","commonSituations":"The launched codex command spawns grandchildren in a separate group, NFS/processes stuck on I/O, or PID namespace/container quirks preventing group signals.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await teardown(); } catch (e) {\n  if (/did not disappear after forced termination/.test((e as Error).message)) {\n    await killStragglersByPgid(child.pid); // manual sweep, then continue\n  }\n}","preventionTips":["Ensure launched commands don't call setsid","Watch for D-state processes on NFS","Periodically reap orphaned children in long-lived sessions"],"tags":["process-management","signals","cleanup"],"backgroundTag":"child-process-did-not-exit","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}