How Job Cancellation Works in the OpenAI Codex Plugin: Process Termination and State Management
Job cancellation in the OpenAI Codex plugin is a deterministic, safety-first operation that interrupts active turns, terminates the entire process tree, updates persistent job metadata to "cancelled" status, and emits a structured report via the CLI or programmatic API.
The openai/codex-plugin-cc repository implements a robust job cancellation system designed to terminate background Codex processes cleanly while maintaining repository state consistency. Whether invoked via the /codex:cancel slash command or called programmatically, the cancellation flow ensures that running jobs transition to a terminal state with full audit trails.
Entry Point: The /codex:cancel Slash Command
The cancellation interface begins in [plugins/codex/commands/cancel.md](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/commands/cancel.md), which maps the /codex:cancel slash command to the Node script codex-companion.mjs with the cancel sub-command. This markdown definition acts as the command-line entry point, forwarding any provided arguments—such as an optional job ID—directly to the companion script.
The Cancellation Orchestration Flow
At plugins/codex/scripts/codex-companion.mjs (lines 63-87), the handleCancel function orchestrates a four-phase deterministic flow that ensures safe process termination.
Phase 1: Job Resolution via resolveCancelableJob
The handleCancel function first calls plugins/codex/scripts/lib/job-control.mjs#resolveCancelableJob (lines 81-108) to locate the active job that should be cancelled. This function:
- Lists all jobs in the repository and filters for those with status
queuedorrunning. - If a job ID is supplied via the
referenceargument, it matches that specific job; otherwise, it scopes the search to the current Claude session. - Throws explicit errors for ambiguous situations, such as when multiple active jobs exist without a specific ID, or when no cancellable job is found.
Phase 2: Turn Interruption and Process Termination
Once a job is resolved, the system prioritizes graceful cleanup over forceful termination:
- Turn Interrupt:
handleCancelattemptsinterruptAppServerTurnagainst the shared app-server before killing the underlying process, giving the server a chance to clean up any in-flight Codex turn. - Process Termination: The process tree of the job (
job.pid) is terminated withterminateProcessTree, ensuring no orphaned child processes remain.
This ordering—interrupt first, terminate second—prevents data corruption in the app-server's state.
Phase 3: State Persistence and Logging
After termination, the plugin updates persistent storage to reflect the cancellation:
- A log entry "Cancelled by user." is appended to the job’s log file.
- The job’s metadata file is rewritten with:
status: "cancelled"andphase: "cancelled"- A
completedAttimestamp errorMessage: "Cancelled by user."
- The updated job is upserted into the job index via
upsertJob.
Phase 4: Report Rendering via renderCancelReport
Finally, a payload containing jobId, status, title, turnInterruptAttempted, and turnInterrupted is emitted via plugins/codex/scripts/lib/render.mjs#renderCancelReport. When --json is not supplied, this renders as a human-readable message; otherwise, it outputs structured JSON for programmatic consumption.
Command-Line Usage Examples
Cancel a specific job by ID:
# Corresponds to /codex:cancel <job-id> in the UI
$ codex cancel task-1234
{
"jobId": "task-1234",
"status": "cancelled",
"title": "Run my‑script",
"turnInterruptAttempted": true,
"turnInterrupted": true
}
Cancel the only active job for the current Claude session (session-scoped):
$ codex cancel
# If multiple active jobs exist, the command errors with:
# "Multiple Codex jobs are active. Pass a job id to /codex:cancel."
Programmatic Cancellation
Developers can invoke cancellation directly from other plugins or scripts by importing the handler:
// Programmatic use from another plugin
import { handleCancel } from "./plugins/codex/scripts/codex-companion.mjs";
await handleCancel(["cancel", "task-1234", "--json"]);
This approach bypasses the CLI argument parsing and executes the same deterministic flow, making it suitable for automation and testing scenarios.
Validation and Error Handling
The cancellation system guards all error paths with explicit messages. According to the source code in job-control.mjs, the system validates:
- Ambiguous targets: When multiple jobs are
queuedorrunningand no specific ID is provided, the command fails with a clear error indicating that a job ID is required. - Session scope: Jobs belonging to different Claude sessions are filtered out unless explicitly targeted by ID.
- Missing jobs: If the provided reference does not match any active job,
resolveCancelableJobthrows a "No cancellable job found" error.
These guards ensure that cancellation is always an intentional, unambiguous operation.
Summary
- Job cancellation flows from
/codex:cancel→codex-companion.mjs(handleCancel) →resolveCancelableJob→interruptAppServerTurn→terminateProcessTree→ metadata update →renderCancelReport. - The turn interruption mechanism attempts graceful cleanup before forcefully terminating the process tree via
job.pid. - State persistence updates the job metadata to
status: "cancelled"with acompletedAttimestamp and appends a cancellation entry to the job log. - Integration tests in
tests/runtime.test.mjsverify the complete cancellation flow, including turn-interrupt handling and job-state consistency.
Frequently Asked Questions
What happens to the Codex process when I cancel a job?
The plugin first attempts interruptAppServerTurn to signal the shared app-server to clean up any in-flight Codex operations. Only after this interrupt attempt does it call terminateProcessTree on the job's PID to forcefully kill the entire process tree, ensuring no orphaned processes remain.
Can I cancel a job without knowing its specific ID?
Yes. When invoked without a job ID, resolveCancelableJob scopes the search to the current Claude session and selects the sole active job automatically. However, if multiple jobs are currently queued or running, the command errors with the message "Multiple Codex jobs are active. Pass a job id to /codex:cancel."
What is the difference between turn interruption and process termination?
Turn interruption (interruptAppServerTurn) is a soft signal sent to the app-server to abort the current Codex turn gracefully, allowing for cleanup of in-flight operations. Process termination (terminateProcessTree) is the hard kill signal sent to the Node process tree identified by job.pid. The plugin always attempts the soft interrupt before resorting to hard termination.
Where does the Codex plugin store the cancellation status?
The plugin appends the literal string "Cancelled by user." to the job's log file and rewrites the job's JSON metadata file with status: "cancelled", phase: "cancelled", a Unix timestamp in completedAt, and errorMessage: "Cancelled by user.". This metadata is then upserted into the repository's job index via upsertJob.
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 →