# How Job Cancellation Works in Codex: Process Trees and App-Server Turns

> Understand Codex job cancellation, from interrupting app-server turns to terminating process trees. Learn how Codex ensures clean shutdowns.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: deep-dive
- Published: 2026-07-14

---

**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`](https://github.com/openai/codex-plugin-cc/blob/main/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:

```json
{
  "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:

```bash
node scripts/codex-companion.mjs cancel --json

```

Cancel a specific job by ID:

```bash
node scripts/codex-companion.mjs cancel task-live --json

```

Programmatic cancellation (as implemented internally by the `/codex:cancel` command):

```typescript
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()` in `job-control.mjs` to identify active jobs by status (`queued` or `running`) and extract the target PID.
- **App-server interruption** occurs first via the turn-interrupt protocol, setting `turnInterruptAttempted` and `turnInterrupted` flags 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`](https://github.com/openai/codex-plugin-cc/blob/main/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.