How the Codex Plugin Handles Thread Interruption: A Technical Deep Dive

The Codex plugin stops a running turn by sending a turn/interrupt JSON-RPC request through the app-server client, which propagates to the task runner that cancels the operation and returns an "interrupted" status.

The openai/codex-plugin-cc repository implements a cooperative interruption mechanism that allows users to cancel long-running operations gracefully. When a user initiates a cancellation, the Codex plugin orchestrates a multi-step flow involving the CLI client, app-server broker, and underlying task runner to ensure resources are properly released. Understanding how the Codex plugin handles thread interruption is essential for developers building robust client integrations or troubleshooting stuck operations.

The Thread Interruption Flow

The interruption mechanism follows a coordinated sequence across four architectural layers:

  1. Client initiation – The CLI calls interruptAppServerTurn() from plugins/codex/scripts/lib/codex.mjs when a user issues a cancel command.
  2. RPC request construction – The function builds a turn/interrupt JSON-RPC message containing the threadId and turnId, sending it via the shared app-server client.
  3. Broker routing – The plugins/codex/scripts/app-server-broker.mjs detects the method name through an isMessageInterrupt check and forwards the request to the task runner.
  4. Runner execution – The task runner (demonstrated in tests/fake-codex-fixture.mjs) looks up the turn in an in-memory map, clears any pending timers, and returns a turn object with status "interrupted".
  5. Result propagation – The response flows back through the broker to the client, where interruptAppServerTurn() records whether the interrupt was attempted and succeeded, and the companion script prints user feedback.

Implementation Details by Component

Client-Side Request Creation

In plugins/codex/scripts/lib/codex.mjs, the interruptAppServerTurn() function serves as the primary entry point. It retrieves the shared app-server client and constructs the RPC request:

// Core interrupt helper (simplified)
export async function interruptAppServerTurn(cwd, { threadId, turnId }) {
  const client = await getAppServerClient(cwd);
  let attempted = false, interrupted = false, detail = undefined;
  try {
    attempted = true;
    const resp = await client.request("turn/interrupt", { threadId, turnId });
    interrupted = resp?.turn?.status === "interrupted";
  } catch (e) {
    detail = e.message;
  }
  return { attempted, interrupted, detail };
}

Broker Routing and Validation

The plugins/codex/scripts/app-server-broker.mjs acts as the intermediary, validating that the method name is turn/interrupt before forwarding. This ensures only legitimate interruption requests reach the execution layer.

Task Runner Cancellation Logic

The actual cancellation occurs in the task runner implementation. The test fixture in tests/fake-codex-fixture.mjs demonstrates the production pattern: maintaining an interruptibleTurns Map, clearing timers on receipt of the interrupt signal, and returning the appropriate status:

// Fake runner handling the interrupt (tests/fake-codex-fixture.mjs)
case "turn/interrupt": {
  const pending = interruptibleTurns.get(message.params.turnId);
  if (pending) {
    clearTimeout(pending.timer);
    interruptibleTurns.delete(message.params.turnId);
    return { turn: buildTurn(message.params.turnId, "interrupted") };
  }
  return { error: "no such turn" };
}

Practical Usage Example

When integrating the interruption capability into a CLI tool, developers use the interruptAppServerTurn() helper along with status checking:

// CLI invokes cancel → calls interruptAppServerTurn()
import { interruptAppServerTurn } from "./plugins/codex/scripts/lib/codex.mjs";

async function cancelTurn(cwd, { threadId, turnId }) {
  const interrupt = await interruptAppServerTurn(cwd, { threadId, turnId });
  if (interrupt.attempted) {
    console.log(
      interrupt.interrupted
        ? `Requested Codex turn interrupt for ${turnId} on ${threadId}.`
        : `Codex turn interrupt failed${interrupt.detail ? `: ${interrupt.detail}` : "."}`
    );
  }
}

Summary

  • The Codex plugin uses a JSON-RPC turn/interrupt message to signal cancellation across process boundaries.
  • Four core files coordinate the flow: codex.mjs (client), app-server-broker.mjs (router), fake-codex-fixture.mjs (runner), and codex-companion.mjs (UI).
  • The interruption mechanism requires both a threadId and turnId to identify the specific operation to cancel.
  • The task runner releases resources by clearing timers and removing entries from its internal tracking map.
  • Results are returned to users through the companion script, which distinguishes between successful interrupts and failures.

Frequently Asked Questions

What is a "turn" in the Codex plugin context?

A turn represents the basic unit of work executed by the Codex app-server, encompassing a single request-response cycle between the client and the AI model. Each turn receives a unique turnId within a specific threadId, allowing the system to track and manage individual operations independently.

How does the interrupt request travel from the CLI to the task runner?

The request flows through three layers: first, interruptAppServerTurn() in plugins/codex/scripts/lib/codex.mjs creates the RPC message; second, the app-server broker validates and forwards the message; third, the task runner receives the turn/interrupt method call and executes the cancellation logic.

What happens if the turn has already completed when the interrupt arrives?

If the turn is not found in the task runner's interruptibleTurns map (either because it completed or never existed), the runner returns an error response indicating "no such turn". The interruptAppServerTurn() function captures this in the detail field of its return object, allowing the CLI to report that the interruption failed.

Can thread interruption fail, and how is that communicated?

Yes, interruption can fail if the turn does not exist, the app-server is unreachable, or an exception occurs during the RPC call. The interruptAppServerTurn() function returns an object with attempted: true, interrupted: false, and a detail string containing the error message, which codex-companion.mjs renders as a user-friendly failure message.

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 →