How to Handle Job Cancellation and Turn Interruption with Codex

The Codex CLI executes background operations as jobs that can be cancelled via the /codex:cancel command, which resolves the active job and sends a turn/interrupt RPC request to the app-server before marking the job as cancelled.

In the openai/codex-plugin-cc repository, every background operation—whether a code review, rescue task, or custom workflow—runs as a job inside an app-server process. Understanding how to handle job cancellation and turn interruption with Codex requires familiarity with the three-step coordination between the job store, the Codex companion script, and the app-server RPC interface.

Understanding the Codex Job Model

Jobs persist as JSON files under the .codex/jobs/ directory, allowing the system to track state across CLI sessions. Each job record stores critical metadata including the thread ID and turn ID required to communicate with the Codex runtime. The session scoping mechanism prevents accidental interference with jobs from other Claude sessions by binding each job to a specific SESSION_ID_ENV variable.

The Three-Step Cancellation Flow

When you invoke /codex:cancel, the plugin executes a coordinated sequence to safely terminate the operation.

Step 1: Identify the Active Job with resolveCancelableJob()

The resolveCancelableJob() function in plugins/codex/scripts/lib/job-control.mjs scans the repository’s job store to locate cancellable candidates. It filters for jobs with status queued or running, then matches either a supplied job ID (or unique prefix) or limits the search to the current session using the SESSION_ID_ENV environment variable. If no ID is provided and multiple active jobs exist, or if no jobs are found, the function fails with a descriptive error.

Step 2: Send the Turn Interrupt via interruptAppServerTurn()

Once the job is identified, interruptAppServerTurn() in plugins/codex/scripts/lib/codex.mjs validates that both threadId and turnId exist on the job record and checks Codex runtime availability via getCodexAvailability(). If validation passes, it opens a client connection using CodexAppServerClient.connect() and dispatches the turn/interrupt RPC method with the thread and turn identifiers. The function returns an object containing attempted, interrupted, transport, and optional detail fields for UI feedback.

Step 3: Mark the Job as Cancelled

After the interrupt request completes—whether successful or not—the cancellation flow returns a JSON-compatible result to the UI. The job's status is subsequently updated to cancelled by the regular job-status polling loop in buildStatusSnapshot(), which invokes enrichJob() to recalculate duration and elapsed time fields.

Session Safety and Job Discovery

The filterJobsForCurrentSession() helper ensures you can only cancel jobs from your current Claude session unless explicitly providing a job ID. When resolving jobs without an explicit ID, the system uses matchJobReference() to handle prefix matching, while listJobs() enumerates all JSON files in the job store for inspection.

Code Examples

Cancel the Current Job

To cancel the currently running job without specifying an ID:

// In a Claude session
await claude.run("/codex:cancel");

Cancel a Specific Job by ID

To cancel a job using its full ID or unique prefix:

await claude.run("/codex:cancel 2023-07-07-abc123");

Programmatic Cancellation in Node.js

For custom tooling or testing, import the core functions directly:

import { resolveCancelableJob } from "./plugins/codex/scripts/lib/job-control.mjs";
import { interruptAppServerTurn } from "./plugins/codex/scripts/lib/codex.mjs";

async function cancelJob(cwd, jobId) {
  const { workspaceRoot, job } = await resolveCancelableJob(cwd, jobId);
  // `job` contains threadId & turnId
  const result = await interruptAppServerTurn(workspaceRoot, {
    threadId: job.threadId,
    turnId: job.turnId,
  });
  console.log(result.interrupted
    ? `Turn ${job.turnId} interrupted successfully.`
    : `Failed to interrupt: ${result.detail}`);
}

Key Implementation Files

  • plugins/codex/scripts/lib/job-control.mjs – Implements resolveCancelableJob(), listJobs(), and filterJobsForCurrentSession() for job discovery and session filtering.
  • plugins/codex/scripts/lib/codex.mjs – Contains interruptAppServerTurn() which manages the RPC connection to the app-server and handles the turn/interrupt protocol.
  • plugins/codex/commands/cancel.md – Declares the /codex:cancel CLI command definition that forwards to the companion script.
  • plugins/codex/scripts/codex-companion.mjs – Orchestrates the cancellation flow by parsing arguments and coordinating between job resolution and turn interruption.
  • tests/runtime.test.mjs – Contains integration tests verifying that the cancel command sends turn-interrupt signals before terminating brokered tasks.

Summary

  • Jobs are stored as JSON files under .codex/jobs/ and represent background operations running in the app-server process.
  • Cancellation requires three steps: resolving the active job via resolveCancelableJob(), interrupting the turn via the turn/interrupt RPC, and updating the job status through the polling loop.
  • Session safety is enforced by SESSION_ID_ENV, ensuring you can only cancel jobs from your current Claude session unless explicitly providing a job ID.
  • Error handling returns structured results with attempted and interrupted booleans, allowing the UI to distinguish between transport failures and successful turn termination.

Frequently Asked Questions

What happens if I don't specify a job ID when cancelling?

If you omit the job ID, resolveCancelableJob() filters jobs to only those belonging to your current session via SESSION_ID_ENV. If exactly one active job exists, it proceeds; if zero or multiple jobs are found, the function returns an error requiring you to specify the ID or unique prefix.

How does the Codex plugin prevent cancelling jobs from other user sessions?

The plugin uses the SESSION_ID_ENV environment variable to scope job lookups to the current Claude session. The filterJobsForCurrentSession() function in job-control.mjs automatically excludes jobs created by other sessions unless you explicitly provide a job ID that bypasses the session filter.

What RPC method does the app-server use to interrupt running turns?

The interruptAppServerTurn() function calls the turn/interrupt RPC method on the Codex app-server, passing the threadId and turnId extracted from the job record. This signals the runtime to terminate the current turn execution immediately.

Where are job statuses stored after cancellation?

Job statuses persist as JSON files in the .codex/jobs/ directory. After cancellation, the regular polling mechanism in buildStatusSnapshot() updates the status to cancelled and enrichJob() recalculates the duration and elapsed fields to reflect the actual termination time.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →