# How the Job Cancellation Mechanism Works at the Process Level in OpenAI Codex

> Discover how OpenAI Codex cancels background jobs at the process level. Learn about turn-interrupt messages, force-killing process trees, and updating job status.

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

---

**The job cancellation mechanism terminates Codex background jobs by first sending a turn-interrupt message to the app-server, then force-killing the operating-system process tree via `terminateProcessTree`, and finally updating the in-memory job store to reflect the cancelled status.**

The `openai/codex-plugin-cc` repository implements a robust process-level cancellation system that safely shuts down running Codex sessions across Windows and POSIX platforms. When a user issues a `/codex:cancel` command (or uses the CLI equivalent), the plugin orchestrates a coordinated shutdown sequence that cleans up remote broker resources before terminating the local process group. This article examines the complete cancellation flow from the JavaScript entry point through the OS-specific termination logic implemented in `process.mjs`.

## The Five-Step Cancellation Pipeline

The cancellation sequence follows a strict order to ensure resources are released before processes are killed:

1. **Locate the Job**: The cancel command queries the in-memory job store (`state.jobs`) in `tracked-jobs.mjs` to retrieve the active job’s PID.
2. **Interrupt the Turn**: Before killing the process, the system sends a **turn-interrupt** message to the shared app-server so remote brokers can clean up their resources.
3. **Terminate the Process Tree**: The `terminateProcessTree(pid, options)` function in `process.mjs` handles OS-specific kill logic—using `taskkill` on Windows or `SIGTERM` to the process group on POSIX.
4. **Update Job State**: The job status is set to `"cancelled"` and the PID is cleared in the job store.
5. **Return Result**: The CLI prints a JSON payload indicating the cancellation status and whether the turn-interrupt succeeded.

## Step-by-Step Process Flow

When a user invokes the cancel command via CLI or API, the system executes the following logic defined in `codex-companion.mjs` (lines 1060–1070).

### 1. Job Lookup and Validation

The cancel handler first validates that the target job exists and is currently running:

```javascript
const job = state.jobs.find(j => j.id === jobId);

```

If the job is not found or already finished, `job-control.mjs` throws an error and halts the operation.

### 2. Turn Interruption

Before process termination, the handler contacts the shared **app-server** (via `app-server.mjs`) to request interruption of the current Codex "turn." This ensures the remote broker stops accepting new work for that job ID. The payload includes `turnInterruptAttempted: true` to signal that cleanup was requested.

### 3. Process Tree Termination

The core termination logic resides in `process.mjs` (lines 57–118). The `terminateProcessTree(job.pid)` function implements platform-specific strategies:

- **POSIX Platforms**: Sends `SIGTERM` to the entire process group using `process.kill(-pid, "SIGTERM")`. If the process group does not exist, it falls back to killing the individual PID.
- **Windows**: Executes `taskkill /PID <pid> /T /F` to forcefully terminate the process tree. If `taskkill` is unavailable, it falls back to Node’s `process.kill(pid)`.

The function returns an object describing whether the kill was **attempted**, **delivered**, and which **method** succeeded (`process-group`, `process`, or `taskkill`).

### 4. State Mutation

After the kill attempt, the job record is updated in-place:

```javascript
job.status = "cancelled";
job.pid = null;

```

This mutation occurs in `job-control.mjs` within the `"cancelled"` state transition branch, ensuring subsequent status queries report the job as terminated.

### 5. JSON Result Payload

The CLI returns a structured JSON object indicating the operation outcome:

```json
{
  "jobId": "task-live",
  "status": "cancelled",
  "turnInterruptAttempted": true,
  "turnInterrupted": true
}

```

## Platform-Specific Termination Logic

The `terminateProcessTree` function in `process.mjs` abstracts OS differences to provide a unified Node.js API.

### POSIX Process Groups

On Linux and macOS, the function leverages Unix process groups by negating the PID:

```javascript
process.kill(-pid, "SIGTERM");

```

This signals the entire process group, ensuring child processes spawned by the Codex job are also terminated. If this fails (e.g., the process group leader already exited), the code catches the error and retries with the single PID.

### Windows Taskkill

On Windows, the function spawns `taskkill` with the `/T` flag (terminate tree) and `/F` flag (force):

```bash
taskkill /PID <pid> /T /F

```

This is more reliable than Node’s built-in `process.kill` for terminating complex process hierarchies on Windows, though the implementation falls back to the native method if `taskkill` is not found in the system PATH.

## Practical Code Examples

### Cancel via CLI

To cancel a specific job and receive JSON output:

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

```

### Programmatic Cancellation

Import the job state and termination logic to cancel jobs from within Node.js:

```javascript
import { state } from "./scripts/lib/tracked-jobs.mjs";
import { terminateProcessTree } from "./scripts/lib/process.mjs";

async function cancelJob(jobId) {
  const job = state.jobs.find(j => j.id === jobId);
  if (!job || !job.pid) throw new Error("No running job found");

  // Notify the app-server that we are interrupting the turn
  await appServer.sendTurnInterrupt(jobId);

  const result = terminateProcessTree(job.pid);
  job.status = "cancelled";
  job.pid = null;
  return result;
}

```

### Direct Process Tree Termination

Use `terminateProcessTree` directly for low-level process management:

```javascript
import { terminateProcessTree } from "./plugins/codex/scripts/lib/process.mjs";

const pid = 12345;
const outcome = terminateProcessTree(pid);
console.log(outcome);
// { attempted: true, delivered: true, method: "process-group", result: … }

```

## Key Source Files

Understanding the cancellation mechanism requires familiarity with these modules:

- **`process.mjs`**: Implements `terminateProcessTree` with Windows and POSIX termination strategies (lines 57–118).
- **`codex-companion.mjs`**: CLI entry point that parses the `cancel` sub-command and orchestrates turn-interrupt and process kill logic (lines 1060–1070).
- **`job-control.mjs`**: Contains higher-level job-state transitions, including the `"cancelled"` branch that mutates the job record.
- **`tracked-jobs.mjs`**: Holds the in-memory job registry (`state.jobs`) that the cancel logic queries and updates.
- **`app-server.mjs`**: Provides the RPC interface for sending turn-interrupt messages before process termination.

## Summary

- The cancellation flow begins with a job lookup in `state.jobs` to retrieve the target PID.
- A turn-interrupt message is sent to the app-server to clean up remote broker resources before local termination.
- `terminateProcessTree` in `process.mjs` handles OS-specific killing: `taskkill /T /F` on Windows and `SIGTERM` to the process group (`-pid`) on POSIX.
- After termination, the job status is set to `"cancelled"` and the PID is nulled in the job store.
- The mechanism works consistently across platforms and integrates cleanly with Codex’s brokered execution model.

## Frequently Asked Questions

### How does the Codex plugin ensure child processes are killed on Linux?

According to the `openai/codex-plugin-cc` source code, the plugin sends `SIGTERM` to the entire process group by calling `process.kill(-pid, "SIGTERM")` in `process.mjs`. The negative PID signals the process group leader, which propagates the signal to all child processes spawned by the Codex job.

### What happens if the `taskkill` command is not available on Windows?

The `terminateProcessTree` function in `process.mjs` implements a fallback chain. If spawning `taskkill` fails (e.g., command not found), the code catches the error and falls back to Node.js’s built-in `process.kill(pid)` method to terminate the single process.

### Why does the cancellation mechanism send a turn-interrupt before killing the process?

The turn-interrupt message (sent via `app-server.mjs`) notifies remote brokers that the job is ending, allowing them to release resources and stop accepting new work for that session. This prevents race conditions where the broker might assign new tasks to a job that is about to be force-terminated at the OS level.

### Where is the job status updated after cancellation?

The job state mutation occurs in `job-control.mjs`, specifically within the logic branch that handles the `"cancelled"` status transition. This module updates the job object’s `status` property to `"cancelled"` and sets `job.pid` to `null`, ensuring the in-memory state reflects the termination immediately.