How Job Cancellation Works in Codex: Process Trees and App-Server Turns
Codex handles job cancellation through a two-step mechanism that first interrupts the shared app-server turn to stop model invocations, then terminates the process tree with SIGTERM, and finally updates the job status to "cancelled" in the job record.
When you trigger the /codex:cancel command (or the equivalent CLI node scripts/codex-companion.mjs cancel) in the openai/codex-plugin-cc repository, the system executes a coordinated shutdown that targets both the brokered process and any in-flight app-server activity. This job cancellation architecture ensures that no orphaned processes remain and that the shared app-server state stays consistent.
Identifying the Target Job with resolveCancelableJob
The cancellation flow begins by calling resolveCancelableJob() in plugins/codex/scripts/lib/job-control.mjs (lines 81–107). This function filters the job registry to find candidates with a status of queued or running.
If you provide a specific job ID, the function matches that exact record. If you omit the ID, it attempts to infer the active job associated with the current Claude session. When no active job exists—or when multiple ambiguous jobs are found—the routine throws an error: "No active Codex jobs to cancel for this session."
Upon success, the function returns an object containing the workspace root and the selected job record, which includes the critical PID needed for process termination.
Interrupting the App-Server Turn
Before killing any processes, Codex attempts to abort the in-flight turn on the shared app-server to prevent further model invocations.
The Turn-Interrupt Protocol
The cancellation routine sends a “turn-interrupt” payload defined in plugins/codex/scripts/lib/app-server-protocol.d.ts. This payload is transmitted through the app-server protocol by the sendTurnInterrupt function in plugins/codex/scripts/lib/app-server.mjs.
The payload contains two boolean flags:
turnInterruptAttempted: Indicates whether the interrupt request was sent.turnInterrupted: Confirms the app-server successfully aborted the turn.
Verification in Tests
The test suite in tests/runtime.test.mjs (lines 1770–1780) verifies that both flags are set to true following a successful interrupt, ensuring the app-server protocol contract is honored before proceeding to process termination.
Killing the Process Tree
After interrupting the app-server (or skipping this step if the app-server is not involved), Codex terminates the job’s process group. The PID stored in the job record receives a SIGTERM signal, and a helper utility—using either tree-kill or native process.kill—recursively terminates any child processes.
Once the process tree stops, the job’s JSON file is updated in plugins/codex/scripts/lib/job-control.mjs (lines 1629–1634). The status field becomes "cancelled" and the pid field is cleared to indicate the job is no longer active.
Cancellation Output and State Updates
The CLI prints a JSON response that confirms the operation’s result:
{
"status": "cancelled",
"turnInterruptAttempted": true,
"turnInterrupted": true,
"jobId": "<the-job-id>"
}
If you attempt to cancel without specifying a job ID and no active job exists for the current session, the command errors immediately during the resolveCancelableJob phase.
Practical Examples
Cancel the most recent active job for the current Claude session:
node scripts/codex-companion.mjs cancel --json
Cancel a specific job by ID:
node scripts/codex-companion.mjs cancel task-live --json
Programmatic cancellation (as implemented internally by the /codex:cancel command):
import { resolveCancelableJob } from "./lib/job-control.mjs";
import { sendTurnInterrupt } from "./lib/app-server.mjs";
async function cancelJob(cwd: string, jobId?: string) {
const { workspaceRoot, job } = await resolveCancelableJob(cwd, jobId);
await sendTurnInterrupt(workspaceRoot, job.id); // attempts app-server turn interrupt
process.kill(job.pid, "SIGTERM"); // kills the process tree
// …update job file to status "cancelled"
}
Summary
- Job resolution relies on
resolveCancelableJob()injob-control.mjsto identify active jobs by status (queuedorrunning) and extract the target PID. - App-server interruption occurs first via the turn-interrupt protocol, setting
turnInterruptAttemptedandturnInterruptedflags to prevent further model calls. - Process termination sends SIGTERM to the job’s PID and recursively kills child processes using tree-kill logic.
- State persistence updates the job file to
"cancelled"and clears the PID, with JSON output confirming the interrupt flags and final status.
Frequently Asked Questions
What happens if I cancel without specifying a job ID?
If you run the cancel command without a job ID, resolveCancelableJob() attempts to infer the active job from the current Claude session. When no active job exists, the function throws an error: "No active Codex jobs to cancel for this session." If multiple jobs could match, it similarly errors to prevent accidental cancellations.
How does Codex ensure child processes are also terminated?
After sending SIGTERM to the parent PID stored in the job record, Codex uses a helper utility (leveraging tree-kill or native process.kill) to recursively terminate the entire process tree. This ensures that any spawned subprocesses or workers are cleaned up alongside the main job process.
What is the difference between turnInterruptAttempted and turnInterrupted?
turnInterruptAttempted indicates that the cancellation routine sent the interrupt request to the app-server, while turnInterrupted confirms that the app-server successfully acknowledged and aborted the in-flight turn. The first flag tracks the request, and the second confirms the completion according to the protocol defined in app-server-protocol.d.ts.
Can I cancel a job that has already completed?
No. The resolveCancelableJob() function specifically filters for jobs with a status of queued or running. If a job has already finished, failed, or been cancelled previously, it will not appear in the cancellable set, and the command will return an error indicating no active jobs are available for the session.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →