# How the Codex Plugin Handles Process Termination and Job Cancellation

> Discover how the Codex plugin safely cancels jobs using a five-step shutdown sequence, preventing zombie processes and ensuring clean termination.

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

---

**The Codex plugin orchestrates a five-step shutdown sequence—job resolution, turn interruption, process tree termination, state persistence, and result reporting—to safely cancel running jobs without leaving zombie processes.**

This article examines how the `openai/codex-plugin-cc` repository implements robust cancellation semantics. Whether invoked via CLI or programmatically, the `/codex:cancel` command guarantees that active jobs are halted, Codex turns are interrupted when possible, and system state remains consistent.

## Resolving the Target Job

The cancellation flow begins in `plugins/codex/scripts/lib/job-control.mjs` with the `resolveCancelableJob` function. This utility inspects the persisted job list and filters for **active jobs**—those in `queued` or `running` status.

When a job reference is provided, the function matches against that identifier. If no reference is supplied, it defaults to the sole active job belonging to the current Claude session. Lookup failures throw descriptive errors such as "No active Codex jobs to cancel for this session."

```javascript
// Conceptual flow (based on source at L81-L107)
resolveCancelableJob(sessionId, jobRef?) → Job | throws

```

## Graceful Turn Interruption

Before terminating the OS process, the plugin attempts to abort the in-flight Codex turn through the app-server protocol. The `interruptAppServerTurn` function in `plugins/codex/scripts/lib/codex.mjs` handles this:

1. Verifies that `threadId` and `turnId` are present in the job record
2. Checks Codex app-server reachability
3. Opens a client connection and sends a `turn/interrupt` request

The function returns a payload indicating whether interruption was **attempted** and whether it **succeeded**. This step is optional—if the turn cannot be interrupted, the plugin proceeds directly to process termination.

## Cross-Platform Process Tree Termination

The `terminateProcessTree` function in `plugins/codex/scripts/lib/process.mjs` implements platform-specific signal delivery:

**Windows:** Executes `taskkill /PID <pid> /T /F` to forcibly terminate the process and its children. If the command reports "process not found," it falls back to `process.kill(pid)`.

**POSIX systems:** First attempts to kill the **process group** via `kill(-pid, "SIGTERM")`. If the group does not exist (`ESRCH` error), it retries killing the single PID. The return value indicates whether any signal was successfully delivered.

```javascript
// Terminal usage examples
node plugins/codex/scripts/codex-companion.mjs cancel --json
node plugins/codex/scripts/codex-companion.mjs cancel task-live --json

```

Example JSON response:

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

```

## State Persistence and Audit Logging

After process termination, the plugin updates persistent state through two operations:

- `appendLogLine(job.logFile, "Cancelled by user.")` — writes a human-readable entry to the job's log file
- `writeJobFile` — updates the job record with `status: "cancelled"`, `phase: null`, `pid: null`, `cancelledAt` timestamp, and `errorMessage: "Cancelled by user."`

The in-memory index is refreshed via `upsertJob`, ensuring that subsequent `/codex:status` calls immediately reflect the cancelled state.

## Programmatic Cancellation

For integration with other Node.js scripts, spawn the companion CLI and parse the JSON response:

```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}`);

```

## Key Source Files

| File | Responsibility |
|------|--------------|
| `plugins/codex/scripts/codex-companion.mjs` | CLI entry point; delegates to `handleCancel` |
| `plugins/codex/scripts/lib/job-control.mjs` | Job resolution logic (`resolveCancelableJob`) |
| `plugins/codex/scripts/lib/codex.mjs` | Turn interruption via `interruptAppServerTurn` |
| `plugins/codex/scripts/lib/process.mjs` | Cross-platform process termination |
| `plugins/codex/scripts/lib/tracked-jobs.mjs` | Logging helpers |
| `plugins/codex/scripts/lib/state.mjs` | Job file I/O and index management |

## Summary

- **Job resolution** filters active jobs and validates session ownership before proceeding
- **Turn interruption** attempts graceful Codex protocol shutdown when `threadId`/`turnId` are available
- **Process termination** uses platform-native commands (`taskkill` on Windows, process-group `SIGTERM` on POSIX) with fallbacks for edge cases
- **State persistence** guarantees durable cancellation records and audit trails via JSON file updates
- **Result reporting** returns structured JSON indicating both cancellation success and turn interruption status

## Frequently Asked Questions

### What happens if the Codex turn interruption fails?

The cancellation proceeds regardless. `interruptAppServerTurn` captures success or failure in its return payload, but `terminateProcessTree` always executes. This ensures the worker process stops even when the app-server is unreachable.

### How does the plugin prevent zombie processes on Linux?

By targeting the **process group** with `kill(-pid, "SIGTERM")` rather than the individual process, the plugin signals all child processes simultaneously. The `/T` flag on Windows provides equivalent behavior for `taskkill`.

### Where is the cancellation status stored?

The `writeJobFile` function in `plugins/codex/scripts/lib/state.mjs` persists the cancelled state to disk, while `appendLogLine` records a human-readable entry. The `upsertJob` call updates the in-memory index for immediate visibility.

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

No. The `resolveCancelableJob` function scopes active job lookup to the current session ID. Attempting to cancel another session's job returns "No active Codex jobs to cancel for this session."