Skip to content

[regression] codex exec --ephemeral resume <id> silently persists rollouts and leaks turns to subsequent non-ephemeral resume (broken since 0.113.0) #20084

@mustafa3rsan

Description

@mustafa3rsan

Summary

Since 0.113.0, codex exec --ephemeral resume <thread_id> silently ignores the --ephemeral flag on the resume code path. The CLI flag is parsed and propagated to the client-side Config, but it is never sent to the in-process app-server because ThreadResumeParams has no ephemeral field. As a result:

  • The resumed turn's events get appended to the existing rollout file on disk.
  • A subsequent non-ephemeral codex exec resume <id> reads those events back as part of the conversation history.

This breaks the user-facing semantics of --ephemeral (advertised as "Run without persisting session rollout files to disk") whenever it is combined with resume.

0.112.0 and earlier behaved correctly. Master/HEAD still has the bug.

Reproduction

Tested on macOS 24.6.0, Node 22.20.0.

  1. Run a normal exec to establish a rollout: codex exec --json --skip-git-repo-check --sandbox read-only "Remember password GRAPE22. Just say: ok". Note the printed thread_id.
  2. Find the rollout file under ~/.codex/sessions/.../rollout-*-<thread_id>.jsonl and record its size.
  3. Resume that thread with the ephemeral flag: codex exec --ephemeral --json --skip-git-repo-check --sandbox read-only resume <thread_id> "What password? Just the word.". Re-check the rollout size.
  4. Resume the thread again, this time without --ephemeral: codex exec --json --skip-git-repo-check --sandbox read-only resume <thread_id> "What was the LAST question I asked you? Quote exactly.". Inspect the answer.

Observed (rust-v0.113.0 through 0.125.0)

  • Rollout after step 1: 23 673 bytes
  • Rollout after step 3 (--ephemeral resume): 30 592 bytes (grew by 6 919)
  • Step 4 answer: "What password? Just the word." -- the ephemeral turn was visible to the non-ephemeral resume

Expected (rust-v0.112.0)

  • Rollout after step 1: 24 769 bytes
  • Rollout after step 3: 24 769 bytes (unchanged)
  • Step 4 answer: quotes the step-4 question itself, no trace of the ephemeral turn

I bisected against the published rust-v0.x.0 builds and confirmed the regression appears starting in 0.113.0 (not in 0.112.0).

Root cause

The regression was introduced in commit da3689f -- "Add in-process app server and wire up exec to use it (#14005)".

Before #14005, codex exec resumed via thread_manager.resume_thread_from_rollout(config.clone(), path, auth_manager.clone()). config.clone() carried config.ephemeral = true directly into the resumed session, so the if config.ephemeral { return None } branches in codex-rs/core/src/session/session.rs (around lines 369 and 412) suppressed both LiveThread creation and state_db initialization.

After #14005, codex exec issues a ClientRequest::ThreadResume built by thread_resume_params_from_config() in codex-rs/exec/src/lib.rs. That function reads model, model_provider, cwd, approval_policy, approvals_reviewer, sandbox, permission_profile, and a request_overrides map from config_request_overrides_from_config(), but never reads config.ephemeral. The override builder itself only forwards the active profile, nothing else.

The protocol type compounds the problem: ThreadResumeParams (in codex-rs/app-server-protocol/src/protocol/v2.rs) does not declare an ephemeral field at all, in contrast to ThreadStartParams which has pub ephemeral: Option<bool>.

Server-side, thread_resume() in codex-rs/app-server/src/codex_message_processor.rs destructures ThreadResumeParams and rebuilds the Config via config_manager.load_for_cwd(request_overrides, typesafe_overrides, history_cwd). With nothing carrying ephemeral, the resumed session's Config has ephemeral = false, the persistence branches above fall through, LiveThread is created, and the rollout grows. The CLI flag has no effect on this path.

Suggested fix outline

  1. Add pub ephemeral: Option<bool> to ThreadResumeParams, mirroring ThreadStartParams.
  2. Set it in thread_resume_params_from_config() from config.ephemeral.
  3. In the server-side thread_resume() handler, treat the new field as a typed override and apply it before config_manager.load_for_cwd(...) so the resumed Config honours it (analogous to how the start path uses ephemeral today).
  4. Add a regression test that asserts the rollout file's size is unchanged after --ephemeral resume <id> and that a subsequent non-ephemeral resume cannot see the ephemeral turn.

Happy to share more reproduction artefacts (full rollout diff, additional traces) if useful. Per CONTRIBUTING.md I'll wait for an invitation before opening a PR.

Environment

  • codex --version: reproduced on 0.113.0, 0.123.0, 0.124.0, 0.125.0; not reproduced on 0.112.0
  • macOS 24.6.0
  • Node 22.20.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    app-serverIssues involving app server protocol or interfacesbugSomething isn't workingexecIssues related to the `codex exec` subcommand

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions