# How to Handle Job Cancellation and Turn Interruption with Codex

> Learn to handle job cancellation and turn interruption with Codex CLI. Discover how to resolve active jobs and send interrupt RPC requests effectively.

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

---

**The Codex CLI executes background operations as jobs that can be cancelled via the `/codex:cancel` command, which resolves the active job and sends a `turn/interrupt` RPC request to the app-server before marking the job as cancelled.**

In the `openai/codex-plugin-cc` repository, every background operation—whether a code review, rescue task, or custom workflow—runs as a **job** inside an app-server process. Understanding how to handle job cancellation and turn interruption with Codex requires familiarity with the three-step coordination between the job store, the Codex companion script, and the app-server RPC interface.

## Understanding the Codex Job Model

Jobs persist as JSON files under the `.codex/jobs/` directory, allowing the system to track state across CLI sessions. Each job record stores critical metadata including the **thread ID** and **turn ID** required to communicate with the Codex runtime. The **session scoping** mechanism prevents accidental interference with jobs from other Claude sessions by binding each job to a specific `SESSION_ID_ENV` variable.

## The Three-Step Cancellation Flow

When you invoke `/codex:cancel`, the plugin executes a coordinated sequence to safely terminate the operation.

### Step 1: Identify the Active Job with `resolveCancelableJob()`

The `resolveCancelableJob()` function in `plugins/codex/scripts/lib/job-control.mjs` scans the repository’s job store to locate cancellable candidates. It filters for jobs with status `queued` or `running`, then matches either a supplied job ID (or unique prefix) or limits the search to the current session using the `SESSION_ID_ENV` environment variable. If no ID is provided and multiple active jobs exist, or if no jobs are found, the function fails with a descriptive error.

### Step 2: Send the Turn Interrupt via `interruptAppServerTurn()`

Once the job is identified, `interruptAppServerTurn()` in `plugins/codex/scripts/lib/codex.mjs` validates that both `threadId` and `turnId` exist on the job record and checks Codex runtime availability via `getCodexAvailability()`. If validation passes, it opens a client connection using `CodexAppServerClient.connect()` and dispatches the `turn/interrupt` RPC method with the thread and turn identifiers. The function returns an object containing `attempted`, `interrupted`, `transport`, and optional `detail` fields for UI feedback.

### Step 3: Mark the Job as Cancelled

After the interrupt request completes—whether successful or not—the cancellation flow returns a JSON-compatible result to the UI. The job's status is subsequently updated to `cancelled` by the regular job-status polling loop in `buildStatusSnapshot()`, which invokes `enrichJob()` to recalculate duration and elapsed time fields.

## Session Safety and Job Discovery

The `filterJobsForCurrentSession()` helper ensures you can only cancel jobs from your current Claude session unless explicitly providing a job ID. When resolving jobs without an explicit ID, the system uses `matchJobReference()` to handle prefix matching, while `listJobs()` enumerates all JSON files in the job store for inspection.

## Code Examples

### Cancel the Current Job

To cancel the currently running job without specifying an ID:

```javascript
// In a Claude session
await claude.run("/codex:cancel");

```

### Cancel a Specific Job by ID

To cancel a job using its full ID or unique prefix:

```javascript
await claude.run("/codex:cancel 2023-07-07-abc123");

```

### Programmatic Cancellation in Node.js

For custom tooling or testing, import the core functions directly:

```javascript
import { resolveCancelableJob } from "./plugins/codex/scripts/lib/job-control.mjs";
import { interruptAppServerTurn } from "./plugins/codex/scripts/lib/codex.mjs";

async function cancelJob(cwd, jobId) {
  const { workspaceRoot, job } = await resolveCancelableJob(cwd, jobId);
  // `job` contains threadId & turnId
  const result = await interruptAppServerTurn(workspaceRoot, {
    threadId: job.threadId,
    turnId: job.turnId,
  });
  console.log(result.interrupted
    ? `Turn ${job.turnId} interrupted successfully.`
    : `Failed to interrupt: ${result.detail}`);
}

```

## Key Implementation Files

- **`plugins/codex/scripts/lib/job-control.mjs`** – Implements `resolveCancelableJob()`, `listJobs()`, and `filterJobsForCurrentSession()` for job discovery and session filtering.
- **`plugins/codex/scripts/lib/codex.mjs`** – Contains `interruptAppServerTurn()` which manages the RPC connection to the app-server and handles the `turn/interrupt` protocol.
- **[`plugins/codex/commands/cancel.md`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/commands/cancel.md)** – Declares the `/codex:cancel` CLI command definition that forwards to the companion script.
- **`plugins/codex/scripts/codex-companion.mjs`** – Orchestrates the cancellation flow by parsing arguments and coordinating between job resolution and turn interruption.
- **`tests/runtime.test.mjs`** – Contains integration tests verifying that the cancel command sends turn-interrupt signals before terminating brokered tasks.

## Summary

- **Jobs** are stored as JSON files under `.codex/jobs/` and represent background operations running in the app-server process.
- **Cancellation** requires three steps: resolving the active job via `resolveCancelableJob()`, interrupting the turn via the `turn/interrupt` RPC, and updating the job status through the polling loop.
- **Session safety** is enforced by `SESSION_ID_ENV`, ensuring you can only cancel jobs from your current Claude session unless explicitly providing a job ID.
- **Error handling** returns structured results with `attempted` and `interrupted` booleans, allowing the UI to distinguish between transport failures and successful turn termination.

## Frequently Asked Questions

### What happens if I don't specify a job ID when cancelling?

If you omit the job ID, `resolveCancelableJob()` filters jobs to only those belonging to your current session via `SESSION_ID_ENV`. If exactly one active job exists, it proceeds; if zero or multiple jobs are found, the function returns an error requiring you to specify the ID or unique prefix.

### How does the Codex plugin prevent cancelling jobs from other user sessions?

The plugin uses the `SESSION_ID_ENV` environment variable to scope job lookups to the current Claude session. The `filterJobsForCurrentSession()` function in `job-control.mjs` automatically excludes jobs created by other sessions unless you explicitly provide a job ID that bypasses the session filter.

### What RPC method does the app-server use to interrupt running turns?

The `interruptAppServerTurn()` function calls the `turn/interrupt` RPC method on the Codex app-server, passing the `threadId` and `turnId` extracted from the job record. This signals the runtime to terminate the current turn execution immediately.

### Where are job statuses stored after cancellation?

Job statuses persist as JSON files in the `.codex/jobs/` directory. After cancellation, the regular polling mechanism in `buildStatusSnapshot()` updates the status to `cancelled` and `enrichJob()` recalculates the duration and elapsed fields to reflect the actual termination time.