# How the Codex Plugin Handles Job Cancellation and Turn Interruption

> Discover how the Codex plugin handles job cancellation and turn interruption. Learn about the six-step sequence for resolving, interrupting, and terminating processes.

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

---

**The `/codex:cancel` command follows a six-step sequence that resolves the target job, attempts a remote turn interrupt, forcibly terminates the local process, and persists the cancelled state.**

The **openai/codex-plugin-cc** repository implements a robust cancellation workflow for Codex jobs running in Claude Code sessions. When users need to stop a running or queued task, the plugin coordinates between local process management and the shared **Codex App Server** to ensure clean termination. This article breaks down the internal mechanics of how **job cancellation and turn interruption** are handled, with direct references to the source implementation.

---

## Resolving the Target Job with `resolveCancelableJob`

Before any cancellation occurs, the plugin must identify which job to terminate. The helper function **`resolveCancelableJob`** in `plugins/codex/scripts/lib/job-control.mjs` (lines 81-108) handles this resolution:

- Returns the active job (`queued` or `running` status) matching the provided criteria
- Validates job IDs when explicitly supplied, confirming existence and active state
- When no ID is provided, restricts search to the current Claude session and raises an error if multiple active jobs exist
- Surfaces clear errors for ambiguous identifiers or cases where no active jobs are found

This strict resolution prevents accidental cancellation of jobs from other sessions while giving users precise control when needed.

---

## Attempting Turn Interruption on the App Server

Once the target job is identified, the plugin attempts to interrupt any in-progress Codex turn. The function **`interruptAppServerTurn`** in `plugins/codex/scripts/lib/codex.mjs` (lines 960-990) manages this remote request:

```javascript
// Conceptual flow based on implementation
async function interruptAppServerTurn(threadId, turnId) {
  if (!threadId || !turnId) {
    return { attempted: false }; // No turn to interrupt
  }
  
  const client = new CodexAppServerClient();
  try {
    await client.request("turn/interrupt", { threadId, turnId });
    return { attempted: true, interrupted: true };
  } catch (error) {
    return { attempted: true, interrupted: false, error };
  }
}

```

The function extracts **`threadId`** and **`turnId`** from the job's stored metadata. If either identifier is missing, it returns `attempted: false`—indicating no turn was active. Otherwise, it connects to the **Codex App Server** and sends the interruption request, reporting success or failure explicitly.

---

## Forcibly Terminating the Local Process

Regardless of the turn interruption outcome, the plugin guarantees job termination by killing the local process tree:

- **`terminateProcessTree(job.pid)`** is invoked unconditionally
- This ensures the background task stops even if the remote turn cannot be interrupted or the App Server is unreachable
- Provides a reliable fallback for network failures or server-side issues

This dual-layer approach—graceful remote interruption plus forceful local termination—prevents hung jobs from consuming resources indefinitely.

---

## Persisting State and Reporting Results

After process termination, the plugin completes the cancellation workflow in `plugins/codex/scripts/codex-companion.mjs` (lines 963-1020):

### Logging

- Appends a log entry describing the turn-interrupt attempt (success or failure)
- Adds a final **"Cancelled by user."** line to the job's log file

### State Persistence

The job record is rewritten with:
- `status: "cancelled"`
- `phase: "cancelled"`
- `pid: null`
- Timestamps: `completedAt`, `cancelledAt`
- Generic `errorMessage`

The in-memory job store is synchronized via `upsertJob`.

### JSON Output

With `--json` flag, the command returns structured data:

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

```

The flags `turnInterruptAttempted` and `turnInterrupted` give users clear visibility into whether the remote interruption was tried and whether it succeeded.

---

## Usage Examples

### Cancel the only active job in current session

```bash
/codex:cancel

```

Succeeds when exactly one active job belongs to the current Claude session. Reports full interruption status in the response.

### Cancel specific job by ID

```bash
/codex:cancel task-live

```

Passes `task-live` to `resolveCancelableJob` for validation. The job need not belong to the current session if explicitly identified.

### Handle ambiguous job selection

```bash
/codex:cancel

```

When multiple active jobs exist:

```

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

```

The error originates from `resolveCancelableJob` and forces explicit disambiguation.

### Programmatic cancellation with JSON output

```bash
/codex:cancel task-live --json

```

Returns machine-parseable output for script integration or CI/CD pipelines.

---

## Summary

- **`resolveCancelableJob`** in `job-control.mjs` strictly resolves which job to cancel, with session scoping and ambiguity detection
- **`interruptAppServerTurn`** in `codex.mjs` attempts graceful remote turn interruption when `threadId` and `turnId` are available
- Local process termination via `terminateProcessTree` runs unconditionally as a reliability fallback
- State persistence and structured logging ensure auditability and proper job lifecycle management
- The JSON response surface exposes interruption attempt status for transparent debugging

---

## Frequently Asked Questions

### What happens if the Codex App Server is unreachable during cancellation?

The plugin reports `turnInterruptAttempted: true, turnInterrupted: false` in the JSON output and appends the error details to the job log. The local process is still terminated, so the job does not continue running.

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

Yes, if you provide the explicit job ID. Session scoping only applies when no ID is supplied. With `/codex:cancel <job-id>`, the plugin validates the job exists and is active, regardless of which session created it.

### Why does the plugin require explicit IDs when multiple jobs are active?

This prevents accidental termination of the wrong job. The `resolveCancelableJob` function raises a clear error prompting for disambiguation, protecting users from unintended cancellations in multi-task workflows.

### What is the difference between `turnInterruptAttempted` and `turnInterrupted`?

`turnInterruptAttempted` indicates whether the plugin tried to contact the App Server (true when `threadId` and `turnId` were present). `turnInterrupted` reports whether that request succeeded. A job with `attempted: false` never started a Codex turn; `attempted: true, interrupted: false` means the turn existed but interruption failed.