From 7ebb2d941856173b4405f39bbb61db74e82c1e2f Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 02:30:55 -0700 Subject: [PATCH] Escalated an aborted turn to SIGKILL. Aborting a turn sent the child a polite signal and trusted it to exit. Codex holds a writer lock on the thread for as long as it lives, so a child that ignores the signal blocks every later resume of that thread with "already has an active writer". Verified against a child that ignores SIGTERM on purpose. --- sdk/typescript/src/exec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index e7120bb45e77..7f72da302020 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -40,6 +40,11 @@ export type CodexExecArgs = { approvalPolicy?: ApprovalMode; }; +// A turn that was aborted has to leave nothing behind: codex holds a writer lock on the thread +// for as long as it lives, so a child that ignores the polite signal would block every later +// resume of that thread. Escalate rather than trust it to exit. +const KILL_GRACE_MS = 5000; + const INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"; const TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts"; const CODEX_NPM_NAME = "@openai/codex"; @@ -195,6 +200,27 @@ export class CodexExec { let spawnError: unknown | null = null; child.once("error", (err) => (spawnError = err)); + let killTimer: NodeJS.Timeout | undefined; + let exited = false; + child.once("exit", () => { + exited = true; + if (killTimer) clearTimeout(killTimer); + }); + const escalateKill = () => { + if (exited || killTimer) return; + killTimer = setTimeout(() => { + if (!exited) child.kill("SIGKILL"); + }, KILL_GRACE_MS); + killTimer.unref?.(); + }; + if (args.signal) { + if (args.signal.aborted) { + escalateKill(); + } else { + args.signal.addEventListener("abort", escalateKill, { once: true }); + } + } + if (!child.stdin) { child.kill(); throw new Error("Child process has no stdin"); @@ -242,6 +268,7 @@ export class CodexExec { } } finally { rl.close(); + if (killTimer) clearTimeout(killTimer); child.removeAllListeners(); try { if (!child.killed) child.kill();