How Thread Persistence and Ephemeral Threads Work in the Codex Plugin Architecture
Thread persistence in the Codex plugin is controlled by the ephemeral parameter sent in ThreadStart requests—when set to false, threads survive beyond the current turn; when true (the default), threads are automatically deleted after completion.
The openai/codex-plugin-cc repository implements a lightweight RPC layer that communicates with the Codex app-server to manage conversation state. Every review or task run creates a thread on the server, but the plugin client—not the server—decides whether that thread persists. This article breaks down the internal mechanics of thread lifecycle management in plugins/codex/scripts/lib/codex.mjs.
Thread Creation and the Ephemeral Flag
The startThread function initiates all thread lifecycles by calling the Codex RPC endpoint thread/start. The critical parameter ephemeral is constructed by buildThreadParams, which supplies a default value of true unless explicitly overridden.
// plugins/codex/scripts/lib/codex.mjs
function buildThreadParams(cwd, options = {}) {
return {
cwd,
// ...
ephemeral: options.ephemeral ?? true // defaults to ephemeral
};
}
This default ensures that one-off operations don't leave orphaned threads on the server. The ?? operator guarantees that even an explicit undefined falls back to true.
Creating Persistent Threads with persistThread
When the CLI requires a thread that survives for later resumption, it passes persistThread: true to runAppServerTurn. The function translates this flag into ephemeral: false in the thread parameters.
// plugins/codex/scripts/lib/codex.mjs
// Inside runAppServerTurn:
const response = await startThread(client, cwd, {
// ...
ephemeral: options.persistThread ? false : true,
threadName: options.persistThread ? options.threadName : options.threadName ?? null
});
The conditional logic ensures:
persistThread: true→ephemeral: false(thread persists)persistThread: falseor absent →ephemeral: true(thread ephemeral)
The threadName parameter receives special handling for persistent threads, allowing users to assign meaningful identifiers to long-running conversations.
Resuming Persistent Threads
Resumed threads enforce ephemeral: false unconditionally. The resumeThread call in runAppServerTurn hardcodes this value to prevent accidental deletion of existing conversation history.
// plugins/codex/scripts/lib/codex.mjs (lines 1116-1118)
const response = await resumeThread(client, options.resumeThreadId, cwd, {
// ...
ephemeral: false
});
This guarantees thread integrity—a resumed thread can never be converted to ephemeral status mid-conversation, protecting against data loss.
Ephemeral Thread Behavior
When ephemeral: true (explicit or default), the Codex app-server automatically garbage-collects the thread after the turn completes. This design optimizes for:
- Code reviews where no follow-up is expected
- CI/CD integrations where transient analysis is sufficient
- Resource conservation on the app-server
The plugin does not track ephemeral threads in local state—once the turn finishes, no record remains client-side.
State Tracking and Verification
The captureTurn function constructs a TurnCaptureState object that persists job metadata and thread identifiers to disk via plugins/codex/scripts/lib/state.mjs. While the ephemeral flag itself is not stored in state, thread persistence is implicitly verified by presence checks against the server.
The test suite validates this behavior through fake server state inspection:
// tests/runtime.test.mjs (line 238)
// Verifies persistent thread creation:
assert.strictEqual(fakeState.threads[0].ephemeral, false);
This assertion confirms that persistThread: true correctly propagates to the RPC layer.
Practical Usage Examples
Start a persistent task thread
await runAppServerTurn(cwd, {
prompt: "investigate flaky test",
persistThread: true, // thread survives after turn
threadName: "Flaky-Test-Investigation"
});
// Under the hood: ephemeral: false sent to thread/start
Start an ephemeral review thread (default)
await runAppServerReview(cwd, {
delivery: "inline"
// no persistThread flag → ephemeral: true automatic
});
// Thread deleted after review completion
Resume a persistent thread
await runAppServerTurn(cwd, {
resumeThreadId: "thr_42" // previous persistent thread
// ephemeral: false enforced for resume
});
Key Source Files
| File | Purpose |
|---|---|
plugins/codex/scripts/lib/codex.mjs |
Core RPC wrapper—implements startThread, resumeThread, buildThreadParams, and ephemeral flag logic |
plugins/codex/scripts/lib/state.mjs |
Disk persistence for job state and thread metadata |
tests/runtime.test.mjs |
Validates thread persistence through fakeState.threads inspection |
plugins/codex/scripts/codex-companion.mjs |
CLI entry point invoking runAppServerTurn and runAppServerReview |
Summary
- Default behavior: All threads are ephemeral (
ephemeral: true) unless explicitly requested otherwise - Persistence trigger: The
persistThread: trueoption inrunAppServerTurnsetsephemeral: false - Resume protection: Resumed threads always use
ephemeral: falseto preserve conversation history - Server-side cleanup: The Codex app-server handles actual thread deletion based on the flag
- No client-side tracking: Ephemeral threads leave no local state; persistent threads are tracked via
TurnCaptureState
Frequently Asked Questions
How does the Codex plugin decide whether to keep a thread after a task completes?
The plugin examines the persistThread option passed to runAppServerTurn. When true, it sends ephemeral: false to the thread/start RPC; otherwise it defaults to ephemeral: true. The app-server then handles cleanup based on this flag.
Can I convert an ephemeral thread to persistent after creation?
No. The ephemeral flag is set at thread creation time via startThread or resumeThread. To persist work, you must specify persistThread: true when initiating the task, or use resumeThreadId with an already-persistent thread.
Where is thread persistence state stored in the Codex plugin?
Persistent thread metadata is written to disk via plugins/codex/scripts/lib/state.mjs as part of TurnCaptureState. The ephemeral flag itself is not stored—instead, the presence of a thread ID in state implies persistence, while its absence indicates an ephemeral thread that was cleaned up.
What happens if I resume a thread without specifying persistence options?
The resumeThread function hardcodes ephemeral: false regardless of caller options. This ensures that resuming a thread never accidentally deletes existing conversation history on the Codex app-server.
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 →