# What Happens When a Codex Task Is Cancelled by the Plugin: Complete Flow

> Discover what happens when a Codex task is cancelled by the plugin. Learn about job resolution, interrupt requests, process termination, state persistence, and confirmation.

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

---

**When a Codex task is cancelled by the plugin, the system resolves the active job, sends a turn-interrupt request to the shared app-server, kills the background Node process, persists the updated state to disk, and returns a JSON payload confirming the cancellation status.**

The `openai/codex-plugin-cc` repository provides a Claude Code plugin that manages Codex tasks through a command-line interface. When users invoke the **`/codex:cancel`** command, the plugin executes a precise five-step cancellation sequence defined in the companion scripts and job control modules.

## The Cancellation Flow

### Job Resolution via resolveCancelableJob()

The cancellation process begins in `plugins/codex/scripts/lib/job-control.mjs` with the **`resolveCancelableJob()`** function. This utility examines all active jobs—those with status `queued` or `running`—to identify the cancellation target.

If the user provides a specific job ID, the function matches that reference against the active list. If no ID is provided, the system filters jobs to the current Claude session using the `SESSION_ID_ENV` environment variable. The function throws descriptive errors for ambiguous, missing, or non-existent jobs, such as *"Multiple Codex jobs are active. Pass a job id to /codex:cancel."*

### Graceful Turn-Interrupt to the App-Server

Before terminating the process, the plugin attempts to gracefully halt any in-flight Claude turn by sending a **turn-interrupt** request to the shared app-server. This RPC call is implemented in `plugins/codex/scripts/lib/app-server.mjs` and ensures that the Claude session receives immediate notification of the cancellation.

As verified in `tests/runtime.test.mjs`, the turn-interrupt mechanism returns a boolean indicating success, which propagates into the final result payload.

### Process Termination and State Mutation

Once the interrupt request completes, the plugin kills the underlying Node process associated with the job, clearing `job.pid` from memory. The job record undergoes immediate mutation:

- **`status`** is set to `"cancelled"`
- **`phase`** is updated to `"cancelled"` (via `enrichJob()`)
- **`cancelledAt`** and **`completedAt`** timestamps are recorded

### Persistent State Updates

The mutated job state is written to disk through `plugins/codex/scripts/lib/state.mjs`. The **`readJobFile()`** and **`writeJobFile()`** functions handle the atomic update of the JSON job record, ensuring that subsequent `status` or `result` commands observe the cancelled state.

### JSON Result Payload

The CLI entry point in `plugins/codex/scripts/codex-companion.mjs` formats the outcome as a JSON object and prints it to stdout. This payload includes the job ID, final status, and turn-interrupt results, making the cancellation observable to downstream automation tools.

## Practical Examples

Cancel the most recent active job in the current session:

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

```

Cancel a specific job by ID:

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

```

The `resolveCancelableJob()` implementation in `job-control.mjs` handles the selection logic:

```javascript
export function resolveCancelableJob(cwd, reference, options = {}) {
  const workspaceRoot = resolveWorkspaceRoot(cwd);
  const jobs = sortJobsNewestFirst(listJobs(workspaceRoot));
  const activeJobs = jobs.filter((job) => job.status === "queued" || job.status === "running");

  if (reference) {
    const selected = matchJobReference(activeJobs, reference);
    if (!selected) {
      throw new Error(`No active job found for "${reference}".`);
    }
    return { workspaceRoot, job: selected };
  }

  const sessionScopedActiveJobs = filterJobsForCurrentSession(activeJobs, options);

  if (sessionScopedActiveJobs.length === 1) {
    return { workspaceRoot, job: sessionScopedActiveJobs[0] };
  }
  // Error handling for ambiguous or missing jobs...
}

```

After a successful cancellation, the CLI prints this JSON structure:

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

```

## Summary

- **Job resolution** occurs through `resolveCancelableJob()` in `job-control.mjs`, which filters active jobs by status and session ID.
- **Graceful interruption** is attempted via the app-server RPC before process termination.
- **Process cleanup** kills the Node PID and updates the job record with `cancelled` status and timestamps.
- **State persistence** uses `state.mjs` to atomically write the updated job to disk.
- **Observable results** are returned as JSON containing the job ID, status, and turn-interrupt flags.

## Frequently Asked Questions

### What happens if multiple Codex jobs are running when I cancel?

The plugin throws an error requiring you to specify the job ID. According to the source code in `job-control.mjs`, when multiple jobs have status `queued` or `running`, the `resolveCancelableJob()` function detects the ambiguity and prompts: *"Multiple Codex jobs are active. Pass a job id to /codex:cancel."*

### Does cancelling a job immediately stop the Claude turn?

Yes. The plugin sends a turn-interrupt request to the app-server before killing the process. As shown in the test suite and result payload, the `turnInterrupted` field returns `true` when the interrupt succeeds, ensuring any in-flight Claude operation halts gracefully.

### Where is the cancelled job state stored?

The job state is persisted in JSON files managed by `plugins/codex/scripts/lib/state.mjs`. The `writeJobFile()` function updates the record with `status: "cancelled"`, timestamps, and phase information, making the cancellation visible to future commands.

### Can I cancel a job from a different Claude session?

No. By default, the plugin scopes cancellation to the current session via `SESSION_ID_ENV`. If you need to cancel a job from another session, you must explicitly provide the job ID as an argument to the cancel command, which bypasses the session filter in `filterJobsForCurrentSession()`.