How the Codex Plugin Handles Process Termination and Cleanup When Cancelling Jobs
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
queuedorrunningstatus 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:
- Validates that
threadIdandturnIdexist on the job record. - Verifies the Codex app-server is reachable.
- Opens a client connection and sends a
turn/interruptrequest. - Returns a payload with two boolean flags:
attemptedandsucceeded.
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
// 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 /Fto forcefully terminate the process and its children (/Tflag). - If
taskkillreports the process no longer exists, falls back to Node'sprocess.kill(pid).
POSIX Implementation
// 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 (
ESRCHerror), 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:
{
"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:
node plugins/codex/scripts/codex-companion.mjs cancel --json
Cancel a specific job by ID:
node plugins/codex/scripts/codex-companion.mjs cancel task-live --json
Programmatic cancellation from another Node script:
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
interruptAppServerTurnbefore killing the process. - Cross-platform termination in
terminateProcessTreeusestaskkillon Windows and process-groupSIGTERMon 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →