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

  1. Inspect which PIDs remain in the child's process group (ps -o pid,pgid,stat) and kill them manually
  2. Ensure the launched command doesn't call setsid/setpgid to escape its group
  3. 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

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


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/47fa5fdc18cac0e8. Report an issue: GitHub.