How the Codex Plugin Handles Job Cancellation and Turn Interruption
The /codex:cancel command follows a six-step sequence that resolves the target job, attempts a remote turn interrupt, forcibly terminates the local process, and persists the cancelled state.
The openai/codex-plugin-cc repository implements a robust cancellation workflow for Codex jobs running in Claude Code sessions. When users need to stop a running or queued task, the plugin coordinates between local process management and the shared Codex App Server to ensure clean termination. This article breaks down the internal mechanics of how job cancellation and turn interruption are handled, with direct references to the source implementation.
Resolving the Target Job with resolveCancelableJob
Before any cancellation occurs, the plugin must identify which job to terminate. The helper function resolveCancelableJob in plugins/codex/scripts/lib/job-control.mjs (lines 81-108) handles this resolution:
- Returns the active job (
queuedorrunningstatus) matching the provided criteria - Validates job IDs when explicitly supplied, confirming existence and active state
- When no ID is provided, restricts search to the current Claude session and raises an error if multiple active jobs exist
- Surfaces clear errors for ambiguous identifiers or cases where no active jobs are found
This strict resolution prevents accidental cancellation of jobs from other sessions while giving users precise control when needed.
Attempting Turn Interruption on the App Server
Once the target job is identified, the plugin attempts to interrupt any in-progress Codex turn. The function interruptAppServerTurn in plugins/codex/scripts/lib/codex.mjs (lines 960-990) manages this remote request:
// Conceptual flow based on implementation
async function interruptAppServerTurn(threadId, turnId) {
if (!threadId || !turnId) {
return { attempted: false }; // No turn to interrupt
}
const client = new CodexAppServerClient();
try {
await client.request("turn/interrupt", { threadId, turnId });
return { attempted: true, interrupted: true };
} catch (error) {
return { attempted: true, interrupted: false, error };
}
}
The function extracts threadId and turnId from the job's stored metadata. If either identifier is missing, it returns attempted: false—indicating no turn was active. Otherwise, it connects to the Codex App Server and sends the interruption request, reporting success or failure explicitly.
Forcibly Terminating the Local Process
Regardless of the turn interruption outcome, the plugin guarantees job termination by killing the local process tree:
terminateProcessTree(job.pid)is invoked unconditionally- This ensures the background task stops even if the remote turn cannot be interrupted or the App Server is unreachable
- Provides a reliable fallback for network failures or server-side issues
This dual-layer approach—graceful remote interruption plus forceful local termination—prevents hung jobs from consuming resources indefinitely.
Persisting State and Reporting Results
After process termination, the plugin completes the cancellation workflow in plugins/codex/scripts/codex-companion.mjs (lines 963-1020):
Logging
- Appends a log entry describing the turn-interrupt attempt (success or failure)
- Adds a final "Cancelled by user." line to the job's log file
State Persistence
The job record is rewritten with:
status: "cancelled"phase: "cancelled"pid: null- Timestamps:
completedAt,cancelledAt - Generic
errorMessage
The in-memory job store is synchronized via upsertJob.
JSON Output
With --json flag, the command returns structured data:
{
"jobId": "task-live",
"status": "cancelled",
"title": "Live task",
"turnInterruptAttempted": true,
"turnInterrupted": true
}
The flags turnInterruptAttempted and turnInterrupted give users clear visibility into whether the remote interruption was tried and whether it succeeded.
Usage Examples
Cancel the only active job in current session
/codex:cancel
Succeeds when exactly one active job belongs to the current Claude session. Reports full interruption status in the response.
Cancel specific job by ID
/codex:cancel task-live
Passes task-live to resolveCancelableJob for validation. The job need not belong to the current session if explicitly identified.
Handle ambiguous job selection
/codex:cancel
When multiple active jobs exist:
Error: Multiple Codex jobs are active. Pass a job id to /codex:cancel.
The error originates from resolveCancelableJob and forces explicit disambiguation.
Programmatic cancellation with JSON output
/codex:cancel task-live --json
Returns machine-parseable output for script integration or CI/CD pipelines.
Summary
resolveCancelableJobinjob-control.mjsstrictly resolves which job to cancel, with session scoping and ambiguity detectioninterruptAppServerTurnincodex.mjsattempts graceful remote turn interruption whenthreadIdandturnIdare available- Local process termination via
terminateProcessTreeruns unconditionally as a reliability fallback - State persistence and structured logging ensure auditability and proper job lifecycle management
- The JSON response surface exposes interruption attempt status for transparent debugging
Frequently Asked Questions
What happens if the Codex App Server is unreachable during cancellation?
The plugin reports turnInterruptAttempted: true, turnInterrupted: false in the JSON output and appends the error details to the job log. The local process is still terminated, so the job does not continue running.
Can I cancel a job from a different Claude session?
Yes, if you provide the explicit job ID. Session scoping only applies when no ID is supplied. With /codex:cancel <job-id>, the plugin validates the job exists and is active, regardless of which session created it.
Why does the plugin require explicit IDs when multiple jobs are active?
This prevents accidental termination of the wrong job. The resolveCancelableJob function raises a clear error prompting for disambiguation, protecting users from unintended cancellations in multi-task workflows.
What is the difference between turnInterruptAttempted and turnInterrupted?
turnInterruptAttempted indicates whether the plugin tried to contact the App Server (true when threadId and turnId were present). turnInterrupted reports whether that request succeeded. A job with attempted: false never started a Codex turn; attempted: true, interrupted: false means the turn existed but 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 →