codex exec on Windows: every invocation emits `failed to record rollout items: thread <uuid> not found` for the just-created session

Resolved 💬 5 comments Opened Apr 25, 2026 by Citizen3rased Closed Apr 28, 2026
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

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 on 0.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 ERROR level — or shouldn't reference a "not found" thread at all.

Source-code pointers

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_exec apply_patch deletes 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 two codex exec invocations 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.

View original on GitHub ↗

5 Comments

Citizen3rased · 4 months ago

Took a look at this — the "Limited persistence mode" hint from #18533 turns out to be a red herring for this specific symptom. EventPersistenceMode::Limited is just an event-type filter (see codex-rs/rollout/src/policy.rs); both Limited and Extended register the thread with the thread-store normally. The ThreadNotFound here 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:

  1. Exec sends Op::Shutdown (codex-rs/exec/src/lib.rs:902 → session/mod.rs:721).
  2. Dispatched to handlers::shutdown (core/src/session/handlers.rs:940).
  3. handlers.rs:963-965: live_thread.shutdown().await → LocalThreadStore::shutdown_thread → live_writer::shutdown_thread removes the recorder from the live_recorders hashmap.
  4. handlers.rs:977-981: then sess.send_event_raw(event) is called with EventMsg::ShutdownComplete.
  5. send_event_raw (session/mod.rs:1575) unconditionally calls persist_rollout_items.
  6. persist_rollout_items (session/mod.rs:2687) finds self.live_thread() still Some(...) — services.live_thread is never cleared on shutdown.
  7. live_thread.append_items → LocalThreadStore::live_recorder (thread-store/src/local/mod.rs:99-109) — hashmap miss — returns ThreadStoreError::ThreadNotFound.
  8. session/mod.rs:2691 logs it at ERROR.

The extra twist: ShutdownComplete maps to None in rollout/src/policy.rs:event_msg_persistence_mode, so RolloutRecorder::record_items would have filtered it out anyway. But the hashmap lookup in live_writer::append_items fires 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:

  1. Downgrade ThreadNotFound from error! to debug! in persist_rollout_items (~10 lines, session/mod.rs:2687-2693). Match on the error variant — ThreadStoreError::ThreadNotFound is an expected post-shutdown race, anything else stays at error!. Acts as a safety net for any post-shutdown persist, not just ShutdownComplete.
  1. Emit ShutdownComplete via deliver_event_raw instead of send_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.
  1. Clear services.live_thread on shutdown so the Some guard in persist_rollout_items short-circuits. Requires interior mutability (services is shared via Arc), so it's a bigger structural change than warranted.

My preference is (1) — it expresses the right semantic ("ThreadNotFound after 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_items after live_thread.shutdown() doesn't log at ERROR, if that'd help.

Gusarich · 4 months ago

noticing same error in many sessions too. seems to not affect anything.

Citizen3rased · 4 months ago

@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:

  • Logged at ERROR level on 100% of codex exec runs. 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 wraps codex exec, the ERROR line tripped retry logic, and every delegation produced two codex exec invocations until I filtered the line in the wrapper. Roughly 2× latency and 2× quota burn (significant on ChatGPT Plus) per delegation.
  • codex exec --full-auto is positioned for automation: CI pipelines, agent harnesses, scripted workflows. Deterministically printing an ERROR-level line on every successful run undermines that use case.
  • Skews log analytics. Anyone monitoring Codex via structured log ingestion sees a guaranteed false-positive ERROR event per exec.

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:

  • Original repro on Windows no longer emits the failed to record rollout items: thread <uuid> not found line at default log level. The session id printed in the banner is not referenced in any ERROR after the fix.
  • New regression test core/src/session/tests.rs::persist_rollout_items_after_shutdown_does_not_log_error exercises the production sequence (live_thread.shutdown(), then persist_rollout_items(ShutdownComplete)) and asserts no ERROR line for failed to record rollout items.
  • cargo fmt --check, cargo clippy -p codex-core --all-targets --no-deps -- -D warnings, and cargo test -p codex-core --lib (1510 passed, 0 failed, 4 ignored) all clean.
  • Match in persist_rollout_items is exhaustive per AGENTS.md ("avoid wildcard arms"), so any future ThreadStoreError variant surfaces as a compile error rather than silently falling into the noisy error! branch.

I read docs/contributing.md and 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) (routing ShutdownComplete through deliver_event_raw so it bypasses persistence entirely) if that's preferred.

etraut-openai contributor · 4 months ago
etraut-openai contributor · 4 months ago

Thanks for the bug report. This will be addressed in the next release.