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: false or 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: true option in runAppServerTurn sets ephemeral: false
  • Resume protection: Resumed threads always use ephemeral: false to 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:

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 →