# How the Codex Plugin Implements Turn Capture and Notification

> Discover how the Codex plugin implements turn capture and notification. Learn about turn IDs, JSON-RPC events, and WebSocket routing with this detailed guide.

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

---

**The Codex plugin wraps every backend interaction in a *turn*, generates unique IDs via `nextTurnId` in `state.mjs`, emits `turn/started` and `turn/completed` JSON-RPC events, and routes notifications through `app-server-broker.mjs` to clients via WebSocket.**

The `openai/codex-plugin-cc` repository provides a reference implementation for building conversational AI plugins that require strict observability. Understanding the turn capture and notification system reveals how the plugin maintains state across asynchronous operations and notifies external consumers like VS Code panels or test harnesses in real time.

## Understanding the Turn Capture Architecture

The plugin treats every discrete unit of work—a command sent to the Codex backend, a sub-agent invocation, or a tool execution—as a **turn**. Each turn receives a unique identifier and transitions through a well-defined lifecycle that mirrors JSON-RPC request-response patterns.

### Turn State Management in state.mjs

Central to the system is `plugins/codex/scripts/lib/state.mjs`, which maintains a monotonic counter (`nextTurnId`) to generate identifiers such as `turn_1`, `turn_2`, and so forth. When a command handler initiates work, it calls `nextTurnId(state)` to obtain a fresh ID before emitting the initial event.

The module also exports `buildTurn(turnId, status, description)`, a factory function that constructs the turn payload (lines 165–174). This payload includes the turn ID, the current status (`started` or `completed`), and minimal metadata describing the operation. According to the source, `emitTurnStarted` and `emitTurnCompleted` wrap this builder and immediately invoke the `send` helper to dispatch JSON-RPC messages to the broker.

```javascript
// plugins/codex/scripts/lib/state.mjs (lines 165-174)
export function emitTurnStarted(state, threadId, description) {
  const turnId = nextTurnId(state);
  const turn = buildTurn(turnId, "started", description);
  send({ method: "turn/started", params: { threadId, turn } });
  return turnId;
}

export function emitTurnCompleted(state, threadId, turnId) {
  const turn = buildTurn(turnId, "completed");
  send({ method: "turn/completed", params: { threadId, turn } });
}

```

### Turn vs. Item Events

While turns represent top-level conversational units, **items** represent granular steps within a turn—such as individual tool calls or streaming deltas. The plugin emits parallel `item/started` and `item/completed` events that reference the parent turn ID. This hierarchical event structure allows consumers to render nested progress indicators without parsing the turn payload itself.

## The Notification Broker System

Once `state.mjs` generates a turn event, the plugin delegates routing to `plugins/codex/scripts/app-server-broker.mjs`. This broker acts as a message router between the plugin’s internal state machine and external clients connected via WebSocket.

### Message Routing via app-server-broker.mjs

The broker’s `handleMessage` function (lines 70–90) inspects incoming JSON-RPC method names. When it encounters `turn/started` or `turn/completed`, it forwards the payload to all registered listeners. The implementation maintains a Set of client connections and iterates through them, serializing the turn object to JSON before transmission.

```javascript
// plugins/codex/scripts/app-server-broker.mjs (lines 70-90)
function handleMessage(message, clients) {
  if (message.method?.startsWith('turn/')) {
    clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify(message));
      }
    });
  }
}

```

### WebSocket Delivery in app-server.mjs

Underpinning the broker is `plugins/codex/scripts/lib/app-server.mjs`, which manages the WebSocket server lifecycle and connection state. This module handles connection upgrades, heartbeat pings, and reconnection logic, ensuring that `turn/completed` notifications survive transient network interruptions. The separation of concerns—state generation in `state.mjs`, routing in `app-server-broker.mjs`, and transport in `app-server.mjs`—allows developers to swap transport layers without modifying turn logic.

## Implementation Walkthrough: Capturing a Turn

Consider a command handler defined in [`plugins/codex/commands/review.md`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/commands/review.md). To capture the entire review operation as a turn, the implementation follows this sequence:

1. **Initialize** the turn by calling `emitTurnStarted`.
2. **Execute** the Codex API call or sub-agent tasks.
3. **Emit** item events for intermediate steps.
4. **Finalize** with `emitTurnCompleted`.

```javascript
// plugins/codex/commands/review.md (conceptual handler)
import { emitTurnStarted, emitTurnCompleted } from '../scripts/lib/state.mjs';

export async function handleReview(state, threadId, prompt) {
  const turnId = emitTurnStarted(state, threadId, { command: 'review', prompt });
  
  try {
    const result = await callCodexApi(prompt);
    // Intermediate item events omitted for brevity
    emitTurnCompleted(state, threadId, turnId);
    return result;
  } catch (error) {
    // Error handling ensures turn/completed still fires
    emitTurnCompleted(state, threadId, turnId);
    throw error;
  }
}

```

This pattern ensures that even if the underlying API call throws an exception, the notification system receives a definitive `turn/completed` event, preventing UI clients from displaying indefinite loading states.

## Summary

- **Turn Identity**: The `nextTurnId` counter in `plugins/codex/scripts/lib/state.mjs` guarantees monotonic, unique identifiers for every discrete operation.
- **Event Semantics**: `turn/started` signals the beginning of work, while `turn/completed` guarantees closure, both emitted via JSON-RPC through the `send` helper.
- **Broker Architecture**: `app-server-broker.mjs` decouples state management from transport, forwarding turn events to WebSocket clients via `handleMessage`.
- **Hierarchical Observability**: Turns encapsulate lower-level `item/*` events, enabling rich, nested progress visualization without tight coupling.
- **Resilient Delivery**: The underlying `app-server.mjs` WebSocket implementation ensures notifications survive network interruptions through connection management and heartbeat mechanisms.

## Frequently Asked Questions

### What constitutes a "turn" in the Codex plugin?

A turn represents a single logical interaction between the user and the Codex backend, such as executing a review command or running a setup script. Each turn receives a unique ID (e.g., `turn_1`) and emits lifecycle events that allow external observers to track when work begins and ends, regardless of how many intermediate API calls or sub-agents are involved.

### How does the plugin generate unique turn identifiers?

The plugin maintains a monotonic counter in the global state object managed by `plugins/codex/scripts/lib/state.mjs`. The `nextTurnId(state)` function increments this counter and returns a formatted string like `turn_${counter}`, ensuring that every turn ID is unique within the current session and sortable by creation time.

### What is the difference between turn events and item events?

Turn events (`turn/started`, `turn/completed`) bookend entire operations, while item events (`item/started`, `item/completed`) track granular steps within a turn, such as individual tool invocations or streaming tokens. The item events include a reference to the parent turn ID, enabling consumers to render nested progress trees without parsing the full turn payload.

### How does the UI receive turn notifications in real time?

The UI connects to the plugin via WebSocket through the endpoint managed by `plugins/codex/scripts/lib/app-server.mjs`. When `app-server-broker.mjs` receives a turn event from `state.mjs`, it broadcasts the JSON-RPC message to all connected WebSocket clients. The UI subscribes to these messages and updates its view upon receiving `turn/completed`, ensuring synchronous state between the plugin backend and the interface.