# What Happens When a Codex Task Is Canceled During Execution

> Discover what happens when a Codex task is canceled. Learn how execution is interrupted, worker processes terminated, and job states updated for smooth cancellations.

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

---

**When a Codex task is canceled during execution, the plugin attempts a graceful turn interrupt via the Codex app-server, forcibly terminates the worker process, and atomically updates the persisted job record to a "cancelled" terminal state.**

The `openai/codex-plugin-cc` repository provides a Claude Code plugin that manages background Codex tasks through slash commands like `/codex:cancel`. Understanding what happens when a Codex task is canceled during execution is critical for developers managing long-running AI operations, as the process involves coordinated cleanup across the Codex app-server, local process management, and persistent state files.

## The Five-Step Cancellation Workflow

When a user issues the `/codex:cancel` slash-command (or runs `node scripts/codex-companion.mjs cancel`), the plugin executes a deterministic shutdown sequence without launching external commands. The workflow resolves the active job, requests a server-side interrupt, kills the local process, persists metadata, and returns a structured report.

## Resolving the Target Job

The cancellation logic begins in `resolveCancelableJob` (line 81 of `plugins/codex/scripts/lib/job-control.mjs`). This function loads all jobs from the workspace and filters for *active* states (`queued` or `running`).

If a `job-id` argument is provided, it matches that specific ID; otherwise, it selects the single active job belonging to the current Claude session. The function throws descriptive errors for ambiguous matches (e.g., "Multiple Codex jobs are active") or missing jobs.

Once resolved, `readStoredJob` retrieves the full JSON record from disk, providing the `threadId` and `turnId` required for the next step.

## Interrupting the Active Turn

Before killing the process, the plugin attempts a graceful shutdown via `interruptAppServerTurn` (line 18 of `plugins/codex/scripts/lib/codex.mjs`). This function sends a `POST /interrupt` request to the Codex app-server with the job's `threadId` and `turnId`.

The server responds with an object indicating whether an interrupt was **attempted** and whether it **succeeded** (`interrupt.interrupted`). The plugin logs this attempt to the job's log file via `appendLogLine`, capturing either success or failure details.

## Process Termination

Regardless of the interrupt result, the plugin guarantees cleanup by calling `terminateProcessTree` from `plugins/codex/scripts/lib/process.mjs`. This kills the operating system process identified by `job.pid`, ensuring no stray subprocesses remain active after cancellation.

## Persisting the Cancelled State

The plugin updates the persisted job record using `writeJobFile` and `upsertJob` (both in `plugins/codex/scripts/lib/state.mjs`). The updated JSON object includes:

- `status: "cancelled"`
- `phase: "cancelled"`
- `pid: null`
- `completedAt` (ISO timestamp)
- `errorMessage: "Cancelled by user."`
- `cancelledAt` (same timestamp)

This atomic update ensures that subsequent `/codex:status` or `/codex:result` calls reflect the terminal cancelled state.

## Reporting the Outcome

Finally, `renderCancelReport` (in `plugins/codex/scripts/lib/render.mjs`) generates a payload containing:

- `jobId`
- `status: "cancelled"`
- `title`
- `turnInterruptAttempted` (boolean)
- `turnInterrupted` (boolean)

The `outputCommandResult` function in `codex-companion.mjs` prints either this JSON payload (when `--json` is passed) or a human-readable formatted report.

## Implementation in handleCancel

The core orchestration logic resides in `handleCancel` (lines 63-122 of `plugins/codex/scripts/codex-companion.mjs`):

```javascript
async function handleCancel(argv) {
  // … parse args …
  const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env });
  const existing = readStoredJob(workspaceRoot, job.id) ?? {};

  // 1️⃣ Ask Codex to interrupt the current turn
  const interrupt = await interruptAppServerTurn(cwd, { threadId, turnId });
  if (interrupt.attempted) {
    appendLogLine(job.logFile,
      interrupt.interrupted
        ? `Requested Codex turn interrupt for ${turnId} on ${threadId}.`
        : `Codex turn interrupt failed${interrupt.detail ? `: ${interrupt.detail}` : "."}`);
  }

  // 2️⃣ Kill the background process
  terminateProcessTree(job.pid ?? Number.NaN);
  appendLogLine(job.logFile, "Cancelled by user.");

  // 3️⃣ Update the persisted job record
  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 });

  // 4️⃣ Return the cancellation report
  const payload = { jobId: job.id, status: "cancelled", title: job.title,
                    turnInterruptAttempted: interrupt.attempted,
                    turnInterrupted: interrupt.interrupted };
  outputCommandResult(payload, renderCancelReport(nextJob), options.json);
}

```

This implementation guarantees that the Codex server can clean up its internal state before the local process receives a SIGTERM.

## Practical Usage Examples

### Cancel the Active Session Job

When only one job is active for the current Claude session, omit the ID:

```bash
/codex:cancel

```

### Cancel a Specific Job

Provide the job ID to target a specific task:

```bash
/codex:cancel review-20241001-1234

```

### CLI Invocation

Run directly from the repository checkout:

```bash
node plugins/codex/scripts/codex-companion.mjs cancel review-20241001-1234 --json

```

### Verify Cancellation Status

After cancellation, confirm the terminal state:

```bash
/codex:status review-20241001-1234

```

The output shows `status: "cancelled"`, `phase: "cancelled"`, and `pid: null`.

## Key Source Files

- **`plugins/codex/scripts/codex-companion.mjs`** – Main entry point containing `handleCancel` and command dispatch logic.
- **`plugins/codex/scripts/lib/job-control.mjs`** – Job lookup and filtering via `resolveCancelableJob` (line 81).
- **`plugins/codex/scripts/lib/codex.mjs`** – Server communication via `interruptAppServerTurn` (line 18).
- **`plugins/codex/scripts/lib/process.mjs`** – OS process termination via `terminateProcessTree`.
- **`plugins/codex/scripts/lib/state.mjs`** – Persistence layer with `writeJobFile` and `upsertJob`.
- **`plugins/codex/scripts/lib/render.mjs`** – User-facing output formatting via `renderCancelReport`.
- **[`plugins/codex/commands/cancel.md`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/commands/cancel.md)** – Slash-command definition for `/codex:cancel`.

## Summary

- **Target Resolution:** The plugin identifies the active job via `resolveCancelableJob`, validating session ownership or explicit ID matches.
- **Graceful Interrupt:** It requests a turn abort from the Codex app-server via `interruptAppServerTurn` before killing the process.
- **Forced Termination:** `terminateProcessTree` guarantees the worker process exits regardless of the interrupt result.
- **State Persistence:** The job file is updated with `status: "cancelled"`, `phase: "cancelled"`, `pid: null`, and a `cancelledAt` timestamp.
- **Structured Reporting:** The operation returns a JSON payload indicating whether the turn interrupt succeeded alongside the final cancelled status.

## Frequently Asked Questions

### What happens if the Codex app-server is unreachable during cancellation?

If the server does not respond to the `POST /interrupt` request, `interruptAppServerTurn` returns `attempted: false` or `interrupted: false`. The plugin still proceeds to kill the local process via `terminateProcessTree` and updates the job status to cancelled, ensuring the task does not hang indefinitely.

### Can I cancel a job that belongs to a different Claude session?

Yes, if you provide the explicit `job-id` argument to `/codex:cancel`. Without the ID, `resolveCancelableJob` filters jobs to the current session environment and throws an error if multiple active jobs exist across different sessions.

### Where is the cancellation timestamp stored?

The `cancelledAt` field is written to the job JSON file alongside `completedAt` during the `writeJobFile` call in `handleCancel`. Both timestamps use the same ISO value generated by `nowIso()`.

### Does killing the process affect the Codex server-side state?

The `interruptAppServerTurn` call attempts to notify the server to abort the turn before process termination. If successful, the server cleans up its internal state; if the interrupt fails or times out, the server may eventually detect the disconnected client, but the local job record immediately reflects the cancelled status.