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

> Discover how the Codex plugin handles thread interruption by sending a turn interrupt JSON-RPC request which cancels the operation and returns an interrupted status. Learn more here.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: deep-dive
- Published: 2026-07-16

---

**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:

```javascript
// 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:

```javascript
// 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:

```javascript
// 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.