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.
- 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.
- Find the rollout file under
~/.codex/sessions/.../rollout-*-<thread_id>.jsonl and record its size.
- 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.
- 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
- Add
pub ephemeral: Option<bool> to ThreadResumeParams, mirroring ThreadStartParams.
- Set it in
thread_resume_params_from_config() from config.ephemeral.
- 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).
- 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
Summary
Since 0.113.0,
codex exec --ephemeral resume <thread_id>silently ignores the--ephemeralflag 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: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.
codex exec --json --skip-git-repo-check --sandbox read-only "Remember password GRAPE22. Just say: ok". Note the printed thread_id.~/.codex/sessions/.../rollout-*-<thread_id>.jsonland record its size.codex exec --ephemeral --json --skip-git-repo-check --sandbox read-only resume <thread_id> "What password? Just the word.". Re-check the rollout size.--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)
--ephemeral resume): 30 592 bytes (grew by 6 919)Expected (rust-v0.112.0)
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 execresumed viathread_manager.resume_thread_from_rollout(config.clone(), path, auth_manager.clone()).config.clone()carriedconfig.ephemeral = truedirectly into the resumed session, so theif config.ephemeral { return None }branches incodex-rs/core/src/session/session.rs(around lines 369 and 412) suppressed both LiveThread creation and state_db initialization.After #14005,
codex execissues a ClientRequest::ThreadResume built bythread_resume_params_from_config()incodex-rs/exec/src/lib.rs. That function reads model, model_provider, cwd, approval_policy, approvals_reviewer, sandbox, permission_profile, and a request_overrides map fromconfig_request_overrides_from_config(), but never readsconfig.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 anephemeralfield at all, in contrast to ThreadStartParams which haspub ephemeral: Option<bool>.Server-side,
thread_resume()incodex-rs/app-server/src/codex_message_processor.rsdestructures ThreadResumeParams and rebuilds the Config viaconfig_manager.load_for_cwd(request_overrides, typesafe_overrides, history_cwd). With nothing carrying ephemeral, the resumed session's Config hasephemeral = 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
pub ephemeral: Option<bool>to ThreadResumeParams, mirroring ThreadStartParams.thread_resume_params_from_config()fromconfig.ephemeral.thread_resume()handler, treat the new field as a typed override and apply it beforeconfig_manager.load_for_cwd(...)so the resumed Config honours it (analogous to how the start path uses ephemeral today).--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