# How to Cancel a Background Codex Job and Interrupt Turns: A Complete Technical Guide

> Learn to cancel a background Codex job and interrupt turns. Understand how `/codex:cancel` terminates processes and updates job status in this technical guide.

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

---

**When you run `/codex:cancel`, the plugin first attempts to interrupt the active turn on the Codex app-server via a `turn/interrupt` request, then terminates the local process tree and updates the job status to "cancelled" in the workspace state.**

The `openai/codex-plugin-cc` repository implements a graceful shutdown sequence for long-running Codex operations. Understanding how to cancel a background Codex job reveals a sophisticated two-phase approach that prioritizes stopping remote computation before killing local processes. This article examines the actual source code to explain exactly how turns are interrupted during cancellation.

## The Cancellation Flow: Step-by-Step

The cancellation process follows a strict seven-step pipeline defined in `codex-companion.mjs` and supporting libraries. Each step ensures that both the remote Codex turn and local background process are properly terminated.

### Step 1: Parse CLI Arguments with `handleCancel`

The process begins in `codex-companion.mjs` at lines 63-69, where the `handleCancel` function resolves the working directory and optional job ID argument. This entry point validates that the user has specified either a specific job reference or allows the system to default to the only active job in the workspace.

### Step 2: Locate the Target Job via `resolveCancelableJob`

At `job-control.mjs` lines 281-295, the `resolveCancelableJob` function queries the workspace's [`jobs.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs.json) to retrieve the job metadata. This lookup returns critical identifiers including the process ID (PID), log file path, and most importantly, the `threadId` and `turnId` required for remote interruption.

### Step 3: Interrupt the Remote Turn

The core turn interruption logic resides in `codex.mjs` at lines 60-89 within the `interruptAppServerTurn` function. This implementation checks for the presence of `threadId` and `turnId`, validates Codex app-server availability, opens a `CodexAppServerClient` connection, and dispatches the `"turn/interrupt"` request to the remote server.

### Step 4: Log the Interrupt Result

Returning to `codex-companion.mjs` at lines 76-84, the plugin examines the result from `interruptAppServerTurn`. The outcome—containing `attempted`, `interrupted`, and `detail` fields—is appended to the job's log file to provide an audit trail of whether the remote turn was successfully stopped.

### Step 5: Terminate the Local Process Tree

Regardless of the remote interrupt result, the plugin proceeds to line 86-87 in `codex-companion.mjs` and executes `terminateProcessTree(job.pid ?? Number.NaN)`. This sends SIGTERM and potentially SIGKILL to the local background process, ensuring the task does not continue running locally.

### Step 6: Persist the Cancelled State

At lines 98-106, the plugin constructs an updated job record with `status: "cancelled"`, `phase: "cancelled"`, `pid: null`, and timestamps for `completedAt` and `cancelledAt`. The `writeJobFile` and `upsertJob` functions from `job-control.mjs` atomically write this state back to disk, ensuring that subsequent status checks or UI panels display the correct "cancelled" state.

### Step 7: Return Structured Output

Finally, at lines 13-21, the `outputCommandResult` function emits a JSON payload or human-readable report containing `turnInterruptAttempted` and `turnInterrupted` boolean flags. This output allows automation scripts and users to determine whether the remote turn was actually interrupted or if only the local process was terminated.

## Key Implementation Details

Several architectural decisions in the `openai/codex-plugin-cc` source code ensure reliable cancellation behavior across different job types and network conditions.

### Optional Turn Interruption

Turn interruption is **conditional** on the presence of `threadId` and `turnId` in the job metadata. Jobs that execute local scripts without an active Codex turn skip the remote interrupt step and proceed directly to process termination. This design prevents unnecessary network calls for non-Codex background tasks.

### Graceful Fallback Handling

The implementation guarantees local cancellation even when the app-server is unreachable. If `interruptAppServerTurn` returns a failure—either due to missing IDs, server unavailability, or network errors—the plugin still executes `terminateProcessTree` and marks the job as cancelled. The error details are preserved in the `detail` field of the response and logged for debugging.

### State Persistence

All status transitions are durably stored in two locations: the workspace's [`jobs.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs.json) registry and the individual job's log file. This dual-write strategy ensures that commands like `/codex:status` and VS Code UI panels reflect the definitive "cancelled" state even if the cancellation occurred while the UI was offline.

## Code Examples

### Cancel a Job via CLI

```bash

# Cancel the only active job in the current repository

codex-companion cancel

# Cancel a specific job by ID with JSON output

codex-companion cancel my-long-task --json

```

The JSON response structure includes flags indicating the success of the remote interruption:

```json
{
  "jobId": "my-long-task",
  "status": "cancelled",
  "title": "My Long Task",
  "turnInterruptAttempted": true,
  "turnInterrupted": true
}

```

### The Turn Interruption Implementation

The `interruptAppServerTurn` function in `codex.mjs` demonstrates the network handshake required to stop a remote turn:

```javascript
// plugins/codex/scripts/lib/codex.mjs
export async function interruptAppServerTurn(cwd, { threadId, turnId }) {
  if (!threadId || !turnId) return { attempted: false, interrupted: false, detail: "missing threadId or turnId" };

  const availability = getCodexAvailability(cwd);
  if (!availability.available) return { attempted: false, interrupted: false, detail: availability.detail };

  let client = null;
  try {
    client = await CodexAppServerClient.connect(cwd, { reuseExistingBroker: true });
    await client.request("turn/interrupt", { threadId, turnId });
    return { attempted: true, interrupted: true, transport: client.transport };
  } catch (error) {
    return { attempted: true, interrupted: false, transport: client?.transport ?? null, detail: error.message };
  } finally {
    await client?.close().catch(() => {});
  }
}

```

### Updating Job State After Cancellation

The following excerpt from `codex-companion.mjs` shows how the plugin finalizes the job record:

```javascript
// plugins/codex/scripts/codex-companion.mjs
const completedAt = nowIso();
const nextJob = {
  ...job,
  status: "cancelled",
  phase: "cancelled",
  pid: null,
  completedAt,
  errorMessage: "Cancelled by user."
};

writeJobFile(workspaceRoot, job.id, { ...existing, ...nextJob, cancelledAt: completedAt });
upsertJob(workspaceRoot, { id: job.id, status: "cancelled", phase: "cancelled", pid: null, errorMessage: "Cancelled by user.", completedAt });

```

## Summary

- **Two-phase shutdown**: The plugin first attempts to interrupt the remote Codex turn via `"turn/interrupt"`, then terminates the local process tree to cancel a background Codex job.
- **Defensive implementation**: Even if the app-server is unreachable or lacks turn identifiers, the local process is always killed and the job state is updated to "cancelled".
- **Persistent state**: Job cancellation status is written to both [`jobs.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs.json) and individual log files, ensuring consistent visibility across CLI and UI interfaces.
- **Structured feedback**: The `turnInterruptAttempted` and `turnInterrupted` flags in the CLI output provide clear visibility into whether the remote turn was successfully stopped.

## Frequently Asked Questions

### What happens if the Codex app-server is unreachable when I try to cancel?

The cancellation proceeds locally even if the app-server cannot be contacted. The plugin will still terminate the local process tree using `terminateProcessTree`, update the job status to "cancelled", and report the failure in the `detail` field of the output. This ensures that background jobs do not remain orphaned when network connectivity is lost.

### Can I cancel a job that doesn't have a threadId or turnId?

Yes. Jobs lacking `threadId` or `turnId`—such as locally executed scripts without an active Codex turn—skip the remote interruption phase entirely. The `interruptAppServerTurn` function returns immediately with `attempted: false`, and the plugin proceeds directly to local process termination. This allows you to cancel any background job regardless of whether it initiated a Codex turn.

### Where is the cancellation status stored?

The cancellation status is persisted in two locations within the workspace: the [`jobs.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs.json) registry file (updated via `upsertJob`) and the job-specific log file (written via `writeJobFile`). This dual persistence ensures that status commands like `/codex:status` and VS Code UI panels reflect the accurate "cancelled" state even if queried after the cancellation event.

### How can I verify that the remote turn was successfully interrupted?

Check the `turnInterrupted` boolean field in the JSON output of the `codex-companion cancel` command. If `turnInterruptAttempted` is `true` and `turnInterrupted` is `true`, the Codex app-server confirmed the turn was stopped. If `turnInterrupted` is `false` while `turnInterruptAttempted` is `true`, the `detail` field will contain the specific error message explaining why the interruption failed.