# How the Codex Plugin Handles Process Termination and Cleanup When Cancelling Jobs

> Discover how the Codex plugin ensures reliable cleanup during job cancellation with its five-step shutdown sequence including process termination and state persistence.

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

---

**The Codex plugin orchestrates a five-step shutdown sequence—job resolution, graceful turn interruption, process tree termination, state persistence, and result reporting—to ensure reliable cleanup when cancelling jobs.**

When you trigger `/codex:cancel`, the `openai/codex-plugin-cc` repository executes a cross-platform termination pipeline that prioritizes graceful shutdowns while guaranteeing the underlying OS process is killed. This article breaks down the exact mechanism implemented in the plugin's source code.

---

## Job Resolution: Finding the Target to Cancel

The cancellation flow begins in `resolveCancelableJob` (**`plugins/codex/scripts/lib/job-control.mjs`**, lines 81-107).

This function performs two critical tasks:

- **Filters for active jobs** — Only jobs with `queued` or `running` status are considered.
- **Resolves the target** — If you provide a job reference (e.g., `task-live`), it matches against that ID. If omitted, it selects the sole active job belonging to your current Claude session.

If no matching job exists, the function throws a descriptive error: `"No active Codex jobs to cancel for this session."`

---

## Graceful Turn Interruption Before Process Killing

Before terminating the OS process, the plugin attempts to abort the running Codex turn through the app-server protocol.

The `interruptAppServerTurn` function (**`plugins/codex/scripts/lib/codex.mjs`**, lines 60-88) executes this logic:

1. Validates that `threadId` and `turnId` exist on the job record.
2. Verifies the Codex app-server is reachable.
3. Opens a client connection and sends a `turn/interrupt` request.
4. Returns a payload with two boolean flags: `attempted` and `succeeded`.

This step is **optional** — if the turn cannot be interrupted (server unreachable, IDs missing, or network failure), the plugin proceeds directly to process termination. The interrupt attempt is purely opportunistic to reduce orphaned work on the Codex backend.

---

## Cross-Platform Process Tree Termination

The `terminateProcessTree` function (**`plugins/codex/scripts/lib/process.mjs`**, lines 57-118) implements platform-specific process killing:

### Windows Implementation

```javascript
// Simplified logic from process.mjs
spawn("taskkill", ["/PID", pid, "/T", "/F"]);
// Falls back to process.kill(pid) if "process not found"

```

- Uses `taskkill /PID <pid> /T /F` to forcefully terminate the process and its children (`/T` flag).
- If `taskkill` reports the process no longer exists, falls back to Node's `process.kill(pid)`.

### POSIX Implementation

```javascript
// Simplified logic from process.mjs
try {
  process.kill(-pid, "SIGTERM"); // Negative PID = process group
} catch (err) {
  if (err.code === "ESRCH") {
    process.kill(pid, "SIGTERM"); // Retry single PID
  }
}

```

- First attempts to kill the **process group** via `kill(-pid, "SIGTERM")`.
- If the group doesn't exist (`ESRCH` error), retries with the single PID.
- Returns whether a signal was successfully delivered.

This dual-path approach ensures reliable termination regardless of how the Codex worker was spawned.

---

## State Persistence and Audit Logging

After process termination, the plugin performs three cleanup operations:

| Operation | Function | File |
|-----------|----------|------|
| Write cancellation log | `appendLogLine(job.logFile, "Cancelled by user.")` | `plugins/codex/scripts/lib/tracked-jobs.mjs` (lines 36-44) |
| Update job record | `writeJobFile` | `plugins/codex/scripts/lib/state.mjs` |
| Refresh in-memory index | `upsertJob` | `plugins/codex/scripts/lib/state.mjs` |

The job record update nullifies `status`, `phase`, and `pid`, adds a `cancelledAt` timestamp, and sets `errorMessage` to `"Cancelled by user."`. This ensures `/codex:status` immediately reflects the cancelled state.

---

## Result Payload Structure

The CLI returns a JSON payload (or human-readable equivalent) summarizing the operation:

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

```

The `turnInterruptAttempted` and `turnInterrupted` fields explicitly communicate whether the graceful turn abort succeeded, distinguishing between "process killed but turn may continue on server" versus full cleanup.

---

## Command-Line Usage Examples

Cancel the most recent job in your session:

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

```

Cancel a specific job by ID:

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

```

Programmatic cancellation from another Node script:

```javascript
import { spawnSync } from "child_process";

function cancelJob(jobId) {
  const result = spawnSync(
    "node",
    ["plugins/codex/scripts/codex-companion.mjs", "cancel", jobId, "--json"],
    { encoding: "utf8" }
  );
  if (result.status !== 0) throw new Error(result.stderr);
  return JSON.parse(result.stdout);
}

const payload = cancelJob("task-live");
console.log(`Job ${payload.jobId} was ${payload.status}`);
// Output: Job task-live was cancelled

```

---

## Key Source Files and Entry Points

| File | Responsibility |
|------|--------------|
| `plugins/codex/scripts/codex-companion.mjs` | CLI entry point; `handleCancel` at lines 63-87 |
| `plugins/codex/scripts/lib/job-control.mjs` | `resolveCancelableJob` — job lookup logic |
| `plugins/codex/scripts/lib/codex.mjs` | `interruptAppServerTurn` — app-server protocol |
| `plugins/codex/scripts/lib/process.mjs` | `terminateProcessTree` — cross-platform killing |
| `plugins/codex/scripts/lib/tracked-jobs.mjs` | Logging helpers |
| `plugins/codex/scripts/lib/state.mjs` | Persistent state read/write and index updates |

---

## Summary

- **Job resolution** filters for active jobs and validates the cancellation target via `resolveCancelableJob`.
- **Graceful interruption** attempts to abort the Codex turn through `interruptAppServerTurn` before killing the process.
- **Cross-platform termination** in `terminateProcessTree` uses `taskkill` on Windows and process-group `SIGTERM` on POSIX, with fallback strategies.
- **State cleanup** nullifies the job's PID, timestamps the cancellation, and persists to disk.
- **Explicit result reporting** distinguishes between "process killed" and "turn interrupted" so callers understand cleanup completeness.

---

## Frequently Asked Questions

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

The plugin proceeds with **process termination regardless**. The `turnInterruptAttempted` field in the response will be `true` (attempt was made) but `turnInterrupted` will be `false`. The OS process is still killed, ensuring no orphaned worker remains.

### How does the plugin handle already-dead processes?

Both Windows and POSIX implementations include fallback logic. On Windows, if `taskkill` reports "process not found", the code falls back to `process.kill(pid)`. On POSIX, if killing the process group fails with `ESRCH` (no such process), it retries with the single PID.

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

No. When no job ID is specified, `resolveCancelableJob` restricts the search to jobs belonging to **your current session**. To cancel another session's job, you must explicitly provide the job reference.

### Is the turn interrupt guaranteed to stop work on the Codex backend?

No. The turn interrupt is **best-effort**. It requires the app-server to be reachable and responsive. The plugin does not wait or retry; it immediately proceeds to process termination after the single interrupt attempt.