How Codex Thread IDs Are Tracked and Resumed Across Sessions
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.
// 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 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.
// 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.
# 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 vianextThread() - Atomic persistence: Every thread creation triggers an immediate write to
state.jsonviasaveState(), 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_DATAvariable 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 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →