# How Job Cancellation Works in the OpenAI Codex Plugin: Process Termination and State Management

> Discover how job cancellation works in the OpenAI Codex plugin. Learn about process termination, state management, and cancellation reporting for interrupted jobs.

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

---

**Job cancellation in the OpenAI Codex plugin is a deterministic, safety-first operation that interrupts active turns, terminates the entire process tree, updates persistent job metadata to "cancelled" status, and emits a structured report via the CLI or programmatic API.**

The `openai/codex-plugin-cc` repository implements a robust **job cancellation** system designed to terminate background Codex processes cleanly while maintaining repository state consistency. Whether invoked via the `/codex:cancel` slash command or called programmatically, the cancellation flow ensures that running jobs transition to a terminal state with full audit trails.

## Entry Point: The `/codex:cancel` Slash Command

The cancellation interface begins in **[[`plugins/codex/commands/cancel.md`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/commands/cancel.md)](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/commands/cancel.md)**, which maps the `/codex:cancel` slash command to the Node script `codex-companion.mjs` with the `cancel` sub-command. This markdown definition acts as the command-line entry point, forwarding any provided arguments—such as an optional job ID—directly to the companion script.

## The Cancellation Orchestration Flow

At **[`plugins/codex/scripts/codex-companion.mjs`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/codex-companion.mjs)** (lines 63-87), the `handleCancel` function orchestrates a four-phase deterministic flow that ensures safe process termination.

### Phase 1: Job Resolution via `resolveCancelableJob`

The `handleCancel` function first calls **[`plugins/codex/scripts/lib/job-control.mjs#resolveCancelableJob`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/job-control.mjs)** (lines 81-108) to locate the active job that should be cancelled. This function:

- Lists all jobs in the repository and filters for those with status `queued` or `running`.
- If a job ID is supplied via the `reference` argument, it matches that specific job; otherwise, it scopes the search to the current Claude session.
- Throws explicit errors for ambiguous situations, such as when multiple active jobs exist without a specific ID, or when no cancellable job is found.

### Phase 2: Turn Interruption and Process Termination

Once a job is resolved, the system prioritizes graceful cleanup over forceful termination:

1. **Turn Interrupt**: `handleCancel` attempts `interruptAppServerTurn` against the shared app-server *before* killing the underlying process, giving the server a chance to clean up any in-flight Codex turn.
2. **Process Termination**: The process tree of the job (`job.pid`) is terminated with `terminateProcessTree`, ensuring no orphaned child processes remain.

This ordering—interrupt first, terminate second—prevents data corruption in the app-server's state.

### Phase 3: State Persistence and Logging

After termination, the plugin updates persistent storage to reflect the cancellation:

- A log entry "Cancelled by user." is appended to the job’s log file.
- The job’s metadata file is rewritten with:
  - `status: "cancelled"` and `phase: "cancelled"`
  - A `completedAt` timestamp
  - `errorMessage: "Cancelled by user."`
- The updated job is upserted into the job index via `upsertJob`.

### Phase 4: Report Rendering via `renderCancelReport`

Finally, a payload containing `jobId`, `status`, `title`, `turnInterruptAttempted`, and `turnInterrupted` is emitted via **[`plugins/codex/scripts/lib/render.mjs#renderCancelReport`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/render.mjs)**. When `--json` is not supplied, this renders as a human-readable message; otherwise, it outputs structured JSON for programmatic consumption.

## Command-Line Usage Examples

Cancel a specific job by ID:

```bash

# Corresponds to /codex:cancel <job-id> in the UI

$ codex cancel task-1234
{
  "jobId": "task-1234",
  "status": "cancelled",
  "title": "Run my‑script",
  "turnInterruptAttempted": true,
  "turnInterrupted": true
}

```

Cancel the only active job for the current Claude session (session-scoped):

```bash
$ codex cancel

# If multiple active jobs exist, the command errors with:

# "Multiple Codex jobs are active. Pass a job id to /codex:cancel."

```

## Programmatic Cancellation

Developers can invoke cancellation directly from other plugins or scripts by importing the handler:

```javascript
// Programmatic use from another plugin
import { handleCancel } from "./plugins/codex/scripts/codex-companion.mjs";

await handleCancel(["cancel", "task-1234", "--json"]);

```

This approach bypasses the CLI argument parsing and executes the same deterministic flow, making it suitable for automation and testing scenarios.

## Validation and Error Handling

The cancellation system guards all error paths with explicit messages. According to the source code in `job-control.mjs`, the system validates:

- **Ambiguous targets**: When multiple jobs are `queued` or `running` and no specific ID is provided, the command fails with a clear error indicating that a job ID is required.
- **Session scope**: Jobs belonging to different Claude sessions are filtered out unless explicitly targeted by ID.
- **Missing jobs**: If the provided reference does not match any active job, `resolveCancelableJob` throws a "No cancellable job found" error.

These guards ensure that cancellation is always an intentional, unambiguous operation.

## Summary

- **Job cancellation** flows from `/codex:cancel` → `codex-companion.mjs` (`handleCancel`) → `resolveCancelableJob` → `interruptAppServerTurn` → `terminateProcessTree` → metadata update → `renderCancelReport`.
- The **turn interruption** mechanism attempts graceful cleanup before forcefully terminating the process tree via `job.pid`.
- **State persistence** updates the job metadata to `status: "cancelled"` with a `completedAt` timestamp and appends a cancellation entry to the job log.
- **Integration tests** in **[`tests/runtime.test.mjs`](https://github.com/openai/codex-plugin-cc/blob/main/tests/runtime.test.mjs)** verify the complete cancellation flow, including turn-interrupt handling and job-state consistency.

## Frequently Asked Questions

### What happens to the Codex process when I cancel a job?

The plugin first attempts `interruptAppServerTurn` to signal the shared app-server to clean up any in-flight Codex operations. Only after this interrupt attempt does it call `terminateProcessTree` on the job's PID to forcefully kill the entire process tree, ensuring no orphaned processes remain.

### Can I cancel a job without knowing its specific ID?

Yes. When invoked without a job ID, `resolveCancelableJob` scopes the search to the current Claude session and selects the sole active job automatically. However, if multiple jobs are currently `queued` or `running`, the command errors with the message "Multiple Codex jobs are active. Pass a job id to /codex:cancel."

### What is the difference between turn interruption and process termination?

**Turn interruption** (`interruptAppServerTurn`) is a soft signal sent to the app-server to abort the current Codex turn gracefully, allowing for cleanup of in-flight operations. **Process termination** (`terminateProcessTree`) is the hard kill signal sent to the Node process tree identified by `job.pid`. The plugin always attempts the soft interrupt before resorting to hard termination.

### Where does the Codex plugin store the cancellation status?

The plugin appends the literal string "Cancelled by user." to the job's log file and rewrites the job's JSON metadata file with `status: "cancelled"`, `phase: "cancelled"`, a Unix timestamp in `completedAt`, and `errorMessage: "Cancelled by user."`. This metadata is then upserted into the repository's job index via `upsertJob`.