# Understanding the Thread and Turn Lifecycle in Codex

> Explore the thread and turn lifecycle in Codex. Learn how threads are created via RPC, turns emit events, and state persists through atomic updates for robust conversation management.

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

---

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

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

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

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

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

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

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