What Happens When a Codex Task Is Canceled During Execution
When a Codex task is canceled during execution, the plugin attempts a graceful turn interrupt via the Codex app-server, forcibly terminates the worker process, and atomically updates the persisted job record to a "cancelled" terminal state.
The openai/codex-plugin-cc repository provides a Claude Code plugin that manages background Codex tasks through slash commands like /codex:cancel. Understanding what happens when a Codex task is canceled during execution is critical for developers managing long-running AI operations, as the process involves coordinated cleanup across the Codex app-server, local process management, and persistent state files.
The Five-Step Cancellation Workflow
When a user issues the /codex:cancel slash-command (or runs node scripts/codex-companion.mjs cancel), the plugin executes a deterministic shutdown sequence without launching external commands. The workflow resolves the active job, requests a server-side interrupt, kills the local process, persists metadata, and returns a structured report.
Resolving the Target Job
The cancellation logic begins in resolveCancelableJob (line 81 of plugins/codex/scripts/lib/job-control.mjs). This function loads all jobs from the workspace and filters for active states (queued or running).
If a job-id argument is provided, it matches that specific ID; otherwise, it selects the single active job belonging to the current Claude session. The function throws descriptive errors for ambiguous matches (e.g., "Multiple Codex jobs are active") or missing jobs.
Once resolved, readStoredJob retrieves the full JSON record from disk, providing the threadId and turnId required for the next step.
Interrupting the Active Turn
Before killing the process, the plugin attempts a graceful shutdown via interruptAppServerTurn (line 18 of plugins/codex/scripts/lib/codex.mjs). This function sends a POST /interrupt request to the Codex app-server with the job's threadId and turnId.
The server responds with an object indicating whether an interrupt was attempted and whether it succeeded (interrupt.interrupted). The plugin logs this attempt to the job's log file via appendLogLine, capturing either success or failure details.
Process Termination
Regardless of the interrupt result, the plugin guarantees cleanup by calling terminateProcessTree from plugins/codex/scripts/lib/process.mjs. This kills the operating system process identified by job.pid, ensuring no stray subprocesses remain active after cancellation.
Persisting the Cancelled State
The plugin updates the persisted job record using writeJobFile and upsertJob (both in plugins/codex/scripts/lib/state.mjs). The updated JSON object includes:
status: "cancelled"phase: "cancelled"pid: nullcompletedAt(ISO timestamp)errorMessage: "Cancelled by user."cancelledAt(same timestamp)
This atomic update ensures that subsequent /codex:status or /codex:result calls reflect the terminal cancelled state.
Reporting the Outcome
Finally, renderCancelReport (in plugins/codex/scripts/lib/render.mjs) generates a payload containing:
jobIdstatus: "cancelled"titleturnInterruptAttempted(boolean)turnInterrupted(boolean)
The outputCommandResult function in codex-companion.mjs prints either this JSON payload (when --json is passed) or a human-readable formatted report.
Implementation in handleCancel
The core orchestration logic resides in handleCancel (lines 63-122 of plugins/codex/scripts/codex-companion.mjs):
async function handleCancel(argv) {
// … parse args …
const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env });
const existing = readStoredJob(workspaceRoot, job.id) ?? {};
// 1️⃣ Ask Codex to interrupt the current turn
const interrupt = await interruptAppServerTurn(cwd, { threadId, turnId });
if (interrupt.attempted) {
appendLogLine(job.logFile,
interrupt.interrupted
? `Requested Codex turn interrupt for ${turnId} on ${threadId}.`
: `Codex turn interrupt failed${interrupt.detail ? `: ${interrupt.detail}` : "."}`);
}
// 2️⃣ Kill the background process
terminateProcessTree(job.pid ?? Number.NaN);
appendLogLine(job.logFile, "Cancelled by user.");
// 3️⃣ Update the persisted job record
const completedAt = nowIso();
const nextJob = { ...job, status: "cancelled", phase: "cancelled", pid: null,
completedAt, errorMessage: "Cancelled by user." };
writeJobFile(workspaceRoot, job.id, { ...existing, ...nextJob, cancelledAt: completedAt });
upsertJob(workspaceRoot, { id: job.id, status: "cancelled", phase: "cancelled",
pid: null, errorMessage: "Cancelled by user.", completedAt });
// 4️⃣ Return the cancellation report
const payload = { jobId: job.id, status: "cancelled", title: job.title,
turnInterruptAttempted: interrupt.attempted,
turnInterrupted: interrupt.interrupted };
outputCommandResult(payload, renderCancelReport(nextJob), options.json);
}
This implementation guarantees that the Codex server can clean up its internal state before the local process receives a SIGTERM.
Practical Usage Examples
Cancel the Active Session Job
When only one job is active for the current Claude session, omit the ID:
/codex:cancel
Cancel a Specific Job
Provide the job ID to target a specific task:
/codex:cancel review-20241001-1234
CLI Invocation
Run directly from the repository checkout:
node plugins/codex/scripts/codex-companion.mjs cancel review-20241001-1234 --json
Verify Cancellation Status
After cancellation, confirm the terminal state:
/codex:status review-20241001-1234
The output shows status: "cancelled", phase: "cancelled", and pid: null.
Key Source Files
plugins/codex/scripts/codex-companion.mjs– Main entry point containinghandleCanceland command dispatch logic.plugins/codex/scripts/lib/job-control.mjs– Job lookup and filtering viaresolveCancelableJob(line 81).plugins/codex/scripts/lib/codex.mjs– Server communication viainterruptAppServerTurn(line 18).plugins/codex/scripts/lib/process.mjs– OS process termination viaterminateProcessTree.plugins/codex/scripts/lib/state.mjs– Persistence layer withwriteJobFileandupsertJob.plugins/codex/scripts/lib/render.mjs– User-facing output formatting viarenderCancelReport.plugins/codex/commands/cancel.md– Slash-command definition for/codex:cancel.
Summary
- Target Resolution: The plugin identifies the active job via
resolveCancelableJob, validating session ownership or explicit ID matches. - Graceful Interrupt: It requests a turn abort from the Codex app-server via
interruptAppServerTurnbefore killing the process. - Forced Termination:
terminateProcessTreeguarantees the worker process exits regardless of the interrupt result. - State Persistence: The job file is updated with
status: "cancelled",phase: "cancelled",pid: null, and acancelledAttimestamp. - Structured Reporting: The operation returns a JSON payload indicating whether the turn interrupt succeeded alongside the final cancelled status.
Frequently Asked Questions
What happens if the Codex app-server is unreachable during cancellation?
If the server does not respond to the POST /interrupt request, interruptAppServerTurn returns attempted: false or interrupted: false. The plugin still proceeds to kill the local process via terminateProcessTree and updates the job status to cancelled, ensuring the task does not hang indefinitely.
Can I cancel a job that belongs to a different Claude session?
Yes, if you provide the explicit job-id argument to /codex:cancel. Without the ID, resolveCancelableJob filters jobs to the current session environment and throws an error if multiple active jobs exist across different sessions.
Where is the cancellation timestamp stored?
The cancelledAt field is written to the job JSON file alongside completedAt during the writeJobFile call in handleCancel. Both timestamps use the same ISO value generated by nowIso().
Does killing the process affect the Codex server-side state?
The interruptAppServerTurn call attempts to notify the server to abort the turn before process termination. If successful, the server cleans up its internal state; if the interrupt fails or times out, the server may eventually detect the disconnected client, but the local job record immediately reflects the cancelled status.
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 →