# How Codex Thread IDs Are Tracked and Resumed Across Sessions

> Discover how Codex thread IDs are tracked and resumed across sessions. Learn about the monotonic counter, JSON state file, and workspace-specific paths for seamless thread continuation using the `codex resume` command.

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

---

**Codex thread IDs are generated via a monotonic counter stored in a JSON state file that is persisted to disk using a workspace-specific hashed path, enabling seamless thread resumption across process restarts through the `codex resume` command.**

The `openai/codex-plugin-cc` repository implements a deterministic persistence mechanism that allows Codex conversations to survive editor restarts and system reboots. This system relies on unique thread identifiers that are sequentially generated and stored in workspace-isolated state files. Understanding how Codex thread IDs are tracked and resumed across sessions is essential for developers building integrations or debugging conversation continuity issues.

## Thread ID Generation and State Management

Every new conversation receives a unique identifier produced by a simple counter mechanism. The plugin maintains an in-memory **state** object containing a `nextThreadId` integer that increments atomically with each thread creation.

When the `nextThread(state, cwd, ephemeral)` function is invoked—as implemented in `tests/fake-codex-fixture.mjs` (lines 19-26)—it constructs a thread object using the pattern `id: "thr_" + state.nextThreadId++`, producing sequential identifiers like `thr_1`, `thr_2`, and so forth. This approach guarantees monotonic uniqueness for the lifetime of the repository workspace.

### Persisting Threads to Disk

Immediately after generating a new thread, the system prepends the thread record to `state.threads` and triggers `saveState(state)`. This function serializes the entire state object to a JSON file on disk, ensuring that the incremented counter and thread metadata survive process termination. As shown in the test fixture (lines 29-31), this sequence occurs automatically for non-ephemeral threads.

```javascript
// Create a new thread (internal plugin implementation)
const thread = nextThread(state, cwd, /*ephemeral=*/false);
// → { id: "thr_1", cwd: "...", name: null, ... }

// Automatically persisted via saveState(state) which writes state.json

```

## Workspace-Isolated Storage Architecture

The persistence layer uses a deterministic file path derived from the workspace root to prevent collisions between different repositories. The `resolveStateFile(cwd)` function in `plugins/codex/scripts/lib/state.mjs` (lines 46-48) constructs the storage location by combining a state root directory with a hash of the workspace's real path.

### Environment-Based Storage Locations

The plugin determines the base storage directory through the `CLAUDE_PLUGIN_DATA` environment variable, falling back to a temporary directory structure under `/tmp/.../codex-companion` when the variable is unset. The final state file path follows the pattern `<stateRoot>/<slug>-<hash>/state.json`, effectively isolating each workspace's thread history.

### State Loading and Schema Migration

When a new process starts, `loadState(cwd)`—defined in `plugins/codex/scripts/lib/state.mjs` (lines 58-74)—reads the existing [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) file if present. This function merges persisted data with a default state shape, ensuring backward compatibility when new fields are added to the schema. The loaded state includes the `nextThreadId` counter, allowing the sequence to continue uninterrupted from the previous session.

```javascript
// Later, in a new process, load the state and fetch the thread
const state = loadState(cwd);
const restored = ensureThread(state, "thr_1");
// restored === thread

```

## Resuming Threads Across Sessions

Thread resumption relies on the `ensureThread(state, threadId)` utility function, implemented in `plugins/codex/scripts/lib/state.mjs` (lines 34-40). This performs a lookup in the persisted threads array and throws an error if the specified ID cannot be found, ensuring only valid, previously persisted threads can be restored.

### CLI and Message-Based Resumption

Users can resume conversations through the `codex resume <thread-id>` CLI command or internal `"thread/resume"` messages. As implemented in `plugins/codex/scripts/lib/codex.mjs` (lines 311-350), these entry points invoke `ensureThread` to retrieve the stored thread data, then reconstruct the conversation context. When invoked without arguments, `codex resume` automatically selects `state.lastTurnStart.threadId`, defaulting to the most recent active thread.

```bash

# Resume the most recent persisted thread for the current repo

$ codex resume

# Internally invokes the "thread/resume" handler calling

# ensureThread(state, state.lastTurnStart.threadId)

```

## Summary

- **Sequential ID generation**: Thread IDs use a monotonic counter (`thr_1`, `thr_2`) stored in the plugin's state object and incremented via `nextThread()`
- **Atomic persistence**: Every thread creation triggers an immediate write to [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) via `saveState()`, capturing the updated counter and thread metadata
- **Workspace isolation**: Storage paths incorporate a hash of the workspace real path through `resolveStateFile()` to prevent cross-repository collisions
- **Environment configuration**: The `CLAUDE_PLUGIN_DATA` variable controls the base storage directory with fallback to temporary directories
- **Resumption API**: The `ensureThread()` function validates and retrieves persisted threads for both CLI (`codex resume`) and programmatic (`"thread/resume"` message) access

## Frequently Asked Questions

### How are Codex thread IDs formatted?

Thread IDs follow a prefix-plus-counter pattern defined in `tests/fake-codex-fixture.mjs`. The system concatenates `"thr_"` with the current value of `state.nextThreadId`, then increments the counter. This produces human-readable identifiers like `thr_1`, `thr_2`, and guarantees uniqueness within the workspace for the lifetime of the state file.

### Where is the thread state stored on disk?

The state file location is determined by `resolveStateFile(cwd)` in `plugins/codex/scripts/lib/state.mjs`. It resolves to `<stateRoot>/<slug>-<hash>/state.json`, where the hash is derived from the workspace path. The state root comes from the `CLAUDE_PLUGIN_DATA` environment variable or defaults to a temporary directory under `/tmp/.../codex-companion`.

### Can thread IDs collide between different repositories?

No. The storage architecture hashes the workspace's real path to create isolated directories within the state root. Even if two repositories have identical thread counters, their [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) files exist in separate hashed directories, preventing ID collisions while maintaining independent `nextThreadId` sequences.

### What happens if the state file is deleted?

If [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) is removed, `loadState()` returns a default state object with `nextThreadId` reset to 1. The next created thread will receive `thr_1`, but previous thread histories will be irretrievable unless backups exist. This behavior ensures the system remains functional even after data loss, though conversation continuity is broken.