How the Codex Plugin Handles Process Termination and Job Cancellation
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."
// 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:
- Verifies that
threadIdandturnIdare present in the job record - Checks Codex app-server reachability
- Opens a client connection and sends a
turn/interruptrequest
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.
// 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:
{
"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 filewriteJobFile— updates the job record withstatus: "cancelled",phase: null,pid: null,cancelledAttimestamp, anderrorMessage: "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:
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/turnIdare available - Process termination uses platform-native commands (
taskkillon Windows, process-groupSIGTERMon 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."
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 →