codex exec on Windows: every invocation emits `failed to record rollout items: thread <uuid> not found` for the just-created session
Summary
On Windows, every codex exec invocation emits an ERROR line on stderr after the answer has already streamed successfully:
ERROR codex_core::session: failed to record rollout items: thread <uuid> not found
The <uuid> is the same session id printed in the Codex banner at the top of the run, so the thread store is failing to locate a thread that this very run just created.
The stdout answer is correct, but the stderr line is scary-looking and is misinterpreted by automation wrappers (e.g. agent harnesses that parse stderr to decide whether to retry). In our case, it caused silent retries of every codex exec call, roughly doubling latency and burning extra quota.
Environment
codex-cli 0.125.0(also reproduces on0.126.0-alpha.1)- Node
v24.14.0(npm-global install:@openai/codex) - Windows 11 IoT Enterprise LTSC 2024, build
10.0.26100.8246 - Shell: Git Bash (MSYS) on Windows
- Auth: ChatGPT Plus
- Persistent state:
~/.codex/exists and is writable; ~14 rollout files written today without other obvious problems
Reproduction
Trivial — runs from any working directory:
$ codex exec --full-auto --skip-git-repo-check "what is 2+2? answer in one word"
OpenAI Codex v0.125.0 (research preview)
--------
session id: 019dc1b7-3d0f-7da2-8ac6-41f9ea486cec
--------
user
what is 2+2? answer in one word
codex
Four
2026-04-24T22:58:28.273202Z ERROR codex_core::session: failed to record rollout items: thread 019dc1b7-3d0f-7da2-8ac6-41f9ea486cec not found
tokens used
7,973
Four
The ERROR line appears on every invocation, on a freshly-created session, with the very session id from the banner.
Expected behaviour
A run that creates a session, streams an answer to stdout, and exits cleanly should not emit ERROR codex_core::session: failed to record rollout items: thread <uuid> not found for the session it just created. Either:
- the rollout-recorder should successfully find the just-created thread, or
- if the rollout write is intentionally skipped (e.g. exec runs in some "limited persistence" mode), the message should not be logged at
ERRORlevel — or shouldn't reference a "not found" thread at all.
Source-code pointers
- Error variant:
codex-rs/thread-store/src/error.rs—ThreadNotFound { thread_id }with format"thread {thread_id} not found". - Wrapping log:
codex-rs/core/src/session/mod.rs— emitsfailed to record rollout items: <inner>.
So the chain is: rollout-recorder → thread-store lookup by the active session's thread id → ThreadNotFound → wrapped and logged at ERROR by the session module.
Related issues (similar class, but different exact symptom)
- #16300 — RolloutRecorder shutdown race emits
channel closed(different inner error, same wrapping log). - #16872 — websocket shared-thread "no rollout found" (different originator).
- #16994 — Desktop automations on Windows/WSL: "no rollout found".
- #18533 —
codex_execapply_patchdeletes lose file content; references that exec starts threads in "Limited persistence mode". - #11997 — VSCode extension on Windows ChatGPT Plus: "no rollout found" on every new conversation.
The Windows + codex_exec thread-store interaction looks like an active sore spot. Filing this one because the exact error string thread <uuid> not found for a just-created session does not appear in the existing issues.
Impact
- Cosmetic noise for interactive users.
- For agent / wrapper integrations that parse stderr to detect failures, it can trigger silent retries — wasting quota (especially significant on ChatGPT Plus) and inflating wall-clock latency. We hit this in a Claude Code subagent that wraps
codex exec: a single delegation produced twocodex execinvocations because of this stderr line.
Workarounds
- Filter stderr in wrappers:
codex exec ... 2> >(grep -v 'failed to record rollout items: thread' >&2). - Don't read this for failure-detection — check stdout completion or exit code instead.
5 Comments
Took a look at this — the "Limited persistence mode" hint from #18533 turns out to be a red herring for this specific symptom.
EventPersistenceMode::Limitedis just an event-type filter (seecodex-rs/rollout/src/policy.rs); both Limited and Extended register the thread with the thread-store normally. TheThreadNotFoundhere is a use-after-shutdown race, not a persistence-mode skip. It's also not actually Windows-specific — the code path is platform-agnostic; I just happened to notice it on Windows because my wrapper parsed stderr.Trace
On every
codex exec, after the answer streams:Op::Shutdown(codex-rs/exec/src/lib.rs:902→session/mod.rs:721).handlers::shutdown(core/src/session/handlers.rs:940).handlers.rs:963-965:live_thread.shutdown().await→LocalThreadStore::shutdown_thread→live_writer::shutdown_threadremoves the recorder from thelive_recordershashmap.handlers.rs:977-981: thensess.send_event_raw(event)is called withEventMsg::ShutdownComplete.send_event_raw(session/mod.rs:1575) unconditionally callspersist_rollout_items.persist_rollout_items(session/mod.rs:2687) findsself.live_thread()stillSome(...)—services.live_threadis never cleared on shutdown.live_thread.append_items→LocalThreadStore::live_recorder(thread-store/src/local/mod.rs:99-109) — hashmap miss — returnsThreadStoreError::ThreadNotFound.session/mod.rs:2691logs it atERROR.The extra twist:
ShutdownCompletemaps toNoneinrollout/src/policy.rs:event_msg_persistence_mode, soRolloutRecorder::record_itemswould have filtered it out anyway. But the hashmap lookup inlive_writer::append_itemsfires before the filter runs, so we get a scary log line for an item that would have been dropped as a no-op.Fix scope — I'd like to land the smallest correct change
Three options, in order of diff size:
ThreadNotFoundfromerror!todebug!inpersist_rollout_items(~10 lines,session/mod.rs:2687-2693). Match on the error variant —ThreadStoreError::ThreadNotFoundis an expected post-shutdown race, anything else stays aterror!. Acts as a safety net for any post-shutdown persist, not justShutdownComplete.ShutdownCompleteviadeliver_event_rawinstead ofsend_event_raw(~5 lines,handlers.rs:977-981). Bypasses rollout persistence for a terminal event that's already in the no-persist list. Narrower, but only addresses the one emitter we know about.services.live_threadon shutdown so theSomeguard inpersist_rollout_itemsshort-circuits. Requires interior mutability (servicesis shared viaArc), so it's a bigger structural change than warranted.My preference is (1) — it expresses the right semantic ("
ThreadNotFoundafter a deliberate shutdown isn't an error") and catches any other post-shutdown emitters that show up in the future. Happy to go with (2) instead if maintainers would rather keep log levels untouched.No tests currently cover this ordering; happy to add a regression test that asserts
persist_rollout_itemsafterlive_thread.shutdown()doesn't log atERROR, if that'd help.noticing same error in many sessions too. seems to not affect anything.
@Gusarich thanks for confirming. That's useful evidence it's not platform-specific. The symptom is benign interactively, but it has measurable impact for the automation surface this command is shaped for:
codex execruns. Wrappers that scrape stderr to detect failures (a common pattern for headless agent orchestrators) treat this as a real error. In a Claude Code subagent that wrapscodex exec, the ERROR line tripped retry logic, and every delegation produced twocodex execinvocations until I filtered the line in the wrapper. Roughly 2× latency and 2× quota burn (significant on ChatGPT Plus) per delegation.codex exec --full-autois positioned for automation: CI pipelines, agent harnesses, scripted workflows. Deterministically printing an ERROR-level line on every successful run undermines that use case.It doesn't break the visible answer, but it does break the contract that ERROR-level lines mean something went wrong, and that contract is what wrappers and dashboards rely on.
---
Validation update: I went ahead and applied option (1) on a local branch so I had a working binary for my own use (the retry-on-stderr issue is what brought me here).
On that branch:
failed to record rollout items: thread <uuid> not foundline at default log level. The session id printed in the banner is not referenced in any ERROR after the fix.core/src/session/tests.rs::persist_rollout_items_after_shutdown_does_not_log_errorexercises the production sequence (live_thread.shutdown(), thenpersist_rollout_items(ShutdownComplete)) and asserts no ERROR line forfailed to record rollout items.cargo fmt --check,cargo clippy -p codex-core --all-targets --no-deps -- -D warnings, andcargo test -p codex-core --lib(1510 passed, 0 failed, 4 ignored) all clean.persist_rollout_itemsis exhaustive per AGENTS.md ("avoid wildcard arms"), so any futureThreadStoreErrorvariant surfaces as a compile error rather than silently falling into the noisyerror!branch.I read
docs/contributing.mdand saw the invitation-only policy for external PRs, so not opening one uninvited. Posting the validation here so the next person who looks at this issue doesn't have to redo it. If option (1) is the right scope and a maintainer is open to a PR, the branch is ready to push; equally happy to hand the diff off, or to rework as option (2) (routingShutdownCompletethroughdeliver_event_rawso it bypasses persistence entirely) if that's preferred.PR posted for review: https://github.com/openai/codex/pull/19630
Thanks for the bug report. This will be addressed in the next release.