How to Cancel a Background Codex Job and Interrupt Turns: A Complete Technical Guide
When you run /codex:cancel, the plugin first attempts to interrupt the active turn on the Codex app-server via a turn/interrupt request, then terminates the local process tree and updates the job status to "cancelled" in the workspace state.
The openai/codex-plugin-cc repository implements a graceful shutdown sequence for long-running Codex operations. Understanding how to cancel a background Codex job reveals a sophisticated two-phase approach that prioritizes stopping remote computation before killing local processes. This article examines the actual source code to explain exactly how turns are interrupted during cancellation.
The Cancellation Flow: Step-by-Step
The cancellation process follows a strict seven-step pipeline defined in codex-companion.mjs and supporting libraries. Each step ensures that both the remote Codex turn and local background process are properly terminated.
Step 1: Parse CLI Arguments with handleCancel
The process begins in codex-companion.mjs at lines 63-69, where the handleCancel function resolves the working directory and optional job ID argument. This entry point validates that the user has specified either a specific job reference or allows the system to default to the only active job in the workspace.
Step 2: Locate the Target Job via resolveCancelableJob
At job-control.mjs lines 281-295, the resolveCancelableJob function queries the workspace's jobs.json to retrieve the job metadata. This lookup returns critical identifiers including the process ID (PID), log file path, and most importantly, the threadId and turnId required for remote interruption.
Step 3: Interrupt the Remote Turn
The core turn interruption logic resides in codex.mjs at lines 60-89 within the interruptAppServerTurn function. This implementation checks for the presence of threadId and turnId, validates Codex app-server availability, opens a CodexAppServerClient connection, and dispatches the "turn/interrupt" request to the remote server.
Step 4: Log the Interrupt Result
Returning to codex-companion.mjs at lines 76-84, the plugin examines the result from interruptAppServerTurn. The outcome—containing attempted, interrupted, and detail fields—is appended to the job's log file to provide an audit trail of whether the remote turn was successfully stopped.
Step 5: Terminate the Local Process Tree
Regardless of the remote interrupt result, the plugin proceeds to line 86-87 in codex-companion.mjs and executes terminateProcessTree(job.pid ?? Number.NaN). This sends SIGTERM and potentially SIGKILL to the local background process, ensuring the task does not continue running locally.
Step 6: Persist the Cancelled State
At lines 98-106, the plugin constructs an updated job record with status: "cancelled", phase: "cancelled", pid: null, and timestamps for completedAt and cancelledAt. The writeJobFile and upsertJob functions from job-control.mjs atomically write this state back to disk, ensuring that subsequent status checks or UI panels display the correct "cancelled" state.
Step 7: Return Structured Output
Finally, at lines 13-21, the outputCommandResult function emits a JSON payload or human-readable report containing turnInterruptAttempted and turnInterrupted boolean flags. This output allows automation scripts and users to determine whether the remote turn was actually interrupted or if only the local process was terminated.
Key Implementation Details
Several architectural decisions in the openai/codex-plugin-cc source code ensure reliable cancellation behavior across different job types and network conditions.
Optional Turn Interruption
Turn interruption is conditional on the presence of threadId and turnId in the job metadata. Jobs that execute local scripts without an active Codex turn skip the remote interrupt step and proceed directly to process termination. This design prevents unnecessary network calls for non-Codex background tasks.
Graceful Fallback Handling
The implementation guarantees local cancellation even when the app-server is unreachable. If interruptAppServerTurn returns a failure—either due to missing IDs, server unavailability, or network errors—the plugin still executes terminateProcessTree and marks the job as cancelled. The error details are preserved in the detail field of the response and logged for debugging.
State Persistence
All status transitions are durably stored in two locations: the workspace's jobs.json registry and the individual job's log file. This dual-write strategy ensures that commands like /codex:status and VS Code UI panels reflect the definitive "cancelled" state even if the cancellation occurred while the UI was offline.
Code Examples
Cancel a Job via CLI
# Cancel the only active job in the current repository
codex-companion cancel
# Cancel a specific job by ID with JSON output
codex-companion cancel my-long-task --json
The JSON response structure includes flags indicating the success of the remote interruption:
{
"jobId": "my-long-task",
"status": "cancelled",
"title": "My Long Task",
"turnInterruptAttempted": true,
"turnInterrupted": true
}
The Turn Interruption Implementation
The interruptAppServerTurn function in codex.mjs demonstrates the network handshake required to stop a remote turn:
// plugins/codex/scripts/lib/codex.mjs
export async function interruptAppServerTurn(cwd, { threadId, turnId }) {
if (!threadId || !turnId) return { attempted: false, interrupted: false, detail: "missing threadId or turnId" };
const availability = getCodexAvailability(cwd);
if (!availability.available) return { attempted: false, interrupted: false, detail: availability.detail };
let client = null;
try {
client = await CodexAppServerClient.connect(cwd, { reuseExistingBroker: true });
await client.request("turn/interrupt", { threadId, turnId });
return { attempted: true, interrupted: true, transport: client.transport };
} catch (error) {
return { attempted: true, interrupted: false, transport: client?.transport ?? null, detail: error.message };
} finally {
await client?.close().catch(() => {});
}
}
Updating Job State After Cancellation
The following excerpt from codex-companion.mjs shows how the plugin finalizes the job record:
// plugins/codex/scripts/codex-companion.mjs
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 });
Summary
- Two-phase shutdown: The plugin first attempts to interrupt the remote Codex turn via
"turn/interrupt", then terminates the local process tree to cancel a background Codex job. - Defensive implementation: Even if the app-server is unreachable or lacks turn identifiers, the local process is always killed and the job state is updated to "cancelled".
- Persistent state: Job cancellation status is written to both
jobs.jsonand individual log files, ensuring consistent visibility across CLI and UI interfaces. - Structured feedback: The
turnInterruptAttemptedandturnInterruptedflags in the CLI output provide clear visibility into whether the remote turn was successfully stopped.
Frequently Asked Questions
What happens if the Codex app-server is unreachable when I try to cancel?
The cancellation proceeds locally even if the app-server cannot be contacted. The plugin will still terminate the local process tree using terminateProcessTree, update the job status to "cancelled", and report the failure in the detail field of the output. This ensures that background jobs do not remain orphaned when network connectivity is lost.
Can I cancel a job that doesn't have a threadId or turnId?
Yes. Jobs lacking threadId or turnId—such as locally executed scripts without an active Codex turn—skip the remote interruption phase entirely. The interruptAppServerTurn function returns immediately with attempted: false, and the plugin proceeds directly to local process termination. This allows you to cancel any background job regardless of whether it initiated a Codex turn.
Where is the cancellation status stored?
The cancellation status is persisted in two locations within the workspace: the jobs.json registry file (updated via upsertJob) and the job-specific log file (written via writeJobFile). This dual persistence ensures that status commands like /codex:status and VS Code UI panels reflect the accurate "cancelled" state even if queried after the cancellation event.
How can I verify that the remote turn was successfully interrupted?
Check the turnInterrupted boolean field in the JSON output of the codex-companion cancel command. If turnInterruptAttempted is true and turnInterrupted is true, the Codex app-server confirmed the turn was stopped. If turnInterrupted is false while turnInterruptAttempted is true, the detail field will contain the specific error message explaining why the interruption failed.
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 →