Understanding the Thread and Turn Lifecycle in Codex

The Codex companion runtime manages conversations through a structured lifecycle where threads are created via RPC, turns emit started/completed events for each item, and state persists to disk through atomic updates.

The openai/codex-plugin-cc repository implements a robust conversation management system that tracks interactions between clients and the LLM through threads and turns. Understanding this lifecycle is essential for developers building on the Codex platform or debugging conversation state issues. The system uses JSON-RPC events to synchronize state between the runtime and UI components.

Thread Creation and Initialization

The thread/start RPC Endpoint

When a client initiates a conversation, it calls the thread/start RPC method. The runtime allocates a fresh thread ID, records metadata including the current working directory (cwd), ephemeral flag, name, and timestamps, and stores this in the persisted state.

// Client side - initiating a new thread
await codexRpc.call("thread/start", {
  cwd: process.cwd(),
  model: "gpt-5.4",
  ephemeral: false,  // false → persisted thread
});

In the broker endpoint, the runtime handles this by calling nextThread() to construct the thread object and then emits the result:

// Simplified broker endpoint logic
const thread = nextThread(state, cwd, params.ephemeral);
send({ id: msg.id, result: { thread: buildThread(thread) } });
send({ method: "thread/started", params: { thread: { id: thread.id } } });

State Initialization in state.mjs

The state.mjs module handles the underlying data structures and directory resolution. The resolveStateDir function determines the per-workspace storage location, while the thread construction logic initializes default metadata.

According to the source code in plugins/codex/scripts/lib/state.mjs (lines 29-44), the system builds the state directory structure and prepares the threads array before the broker endpoint adds new entries to state.threads.

The Turn Lifecycle

Turn Initialization with turn/started

When an operation requiring LLM interaction begins, the runtime generates a unique turn ID within the thread context and immediately emits a turn/started event. This signals the UI to display progress indicators and "thinking" states.

As implemented in the test fixture at tests/fake-codex-fixture.mjs (lines 163-174), the emission uses the send helper:

send({ 
  method: "turn/started", 
  params: { threadId, turn: buildTurn(turnId) } 
});

Item-Level Granularity

A turn consists of one or more items (e.g., prompts, tool calls, or file edits). The runtime emits item/started and item/completed events for each discrete operation, allowing fine-grained UI progress tracking:

// Item lifecycle events
send({ 
  method: "item/started",  
  params: { threadId, turnId, item: entry.started } 
});
// ... LLM processing occurs ...
send({ 
  method: "item/completed", 
  params: { threadId, turnId, item: entry.completed } 
});

Turn Completion

After all items finish processing (or an error aborts the turn), the runtime emits turn/completed with the final turn state. The thread's updatedAt field is refreshed, and the state is persisted.

The fixture at tests/fake-codex-fixture.mjs (lines 174-176) demonstrates this emission sequence immediately following the item events:

send({ 
  method: "turn/completed", 
  params: { threadId, turn: buildTurn(turnId, "completed") } 
});

State Persistence and Storage

Every mutation—whether thread creation, new turns, or status changes—flows through updateState to saveState in state.mjs. The system maintains a per-workspace "codex-companion" directory and automatically prunes jobs to retain only the 50 newest entries.

According to plugins/codex/scripts/lib/state.mjs (lines 92-104 and 124-146), the persistence logic works as follows:

// After a turn finishes
updateState(cwd, (state) => {
  state.threads.find(t => t.id === threadId).updatedAt = nowIso();
});
saveState(cwd, state);

The pruneJobs function ensures the state file does not grow indefinitely by keeping only the most recent 50 jobs.

Thread Resumption and Cleanup

Ephemeral threads (such as review-gate threads) are automatically removed from persisted state once the turn finishes and the UI no longer needs them. The ephemeral flag is stored on the thread object during creation.

For persistent threads, the runtime supports resumption via thread/resume or the task --resume-last command. The broker validates the thread ID, updates timestamps, and returns the stored thread object. If the thread cannot be found, the system raises an error message: "No previous Codex task thread was found..."

The resume flow is exercised in tests/runtime.test.mjs (lines 482-506), which verifies that the broker correctly retrieves and updates thread state.

Summary

  • Thread creation via thread/start allocates unique IDs and metadata in state.mjs, with ephemeral flags determining persistence behavior
  • Turn lifecycle emits turn/started, item/started, item/completed, and turn/completed events through the JSON-RPC protocol for real-time UI updates
  • State persistence occurs atomically through updateState → saveState, maintaining a per-workspace directory with automatic pruning of old jobs
  • Thread resumption supports recalling previous conversations via thread/resume, while ephemeral threads automatically clean up after completion

Frequently Asked Questions

What triggers a turn/started event?

A turn begins when the runtime initiates any LLM interaction, emitting the event through the JSON-RPC protocol to notify UI components. This occurs in the broker endpoint before processing items, as demonstrated in tests/fake-codex-fixture.mjs (lines 163-174).

How does Codex handle conversation persistence?

The system writes to a per-workspace state file using atomic updates via saveState in state.mjs (lines 124-146), automatically pruning to retain only the 50 most recent jobs. Every mutation flows through updateState to ensure consistency.

What is the difference between ephemeral and persistent threads?

Ephemeral threads are temporary (e.g., review-gate threads) and removed from storage after completion, while persistent threads are stored indefinitely and support resumption via thread/resume. The ephemeral flag is set during thread/start and stored in the thread metadata.

How does the turn lifecycle handle errors?

If an error aborts a turn mid-processing, the runtime still emits the appropriate turn/completed event and updates the thread state through the standard persistence flow. This ensures the UI reflects the terminal status even when operations fail, maintaining state consistency across the application.

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 →