thread-store: a single BOM-prefixed rollout .jsonl fails the entire thread read and blocks message creation
Summary
A single rollout .jsonl whose first bytes are a UTF-8 BOM (EF BB BF) causes thread-store to error out, and this blocks new message/turn creation entirely — not just for that one thread. The store appears to scan rollouts and hard-fails on the first malformed file instead of skipping/isolating it.
Environment
- Codex Desktop 0.140-alpha, Windows 11
~/.codex/sessions/.../rollout-*.jsonl
Error
failed to read thread: thread-store internal error: failed to read thread \\?\C:\Users\<user>\.codex\sessions\2026\06\03\rollout-...-<id>.jsonl: rollout at ... does not start with session metadata
Root cause
The file's first line is valid session_meta JSON, but a 3-byte UTF-8 BOM was prepended (by an external tool that re-saved the file). The metadata check fails because the line doesn't start with {. Raw first bytes: EF BB BF 7B 22 74 ... (<BOM>{"t...).
Impact
One corrupt/BOM rollout makes the whole thread-store read fail, so the user can no longer send any message in Codex Desktop until the offending file is located and fixed.
Expected
- Tolerate a leading UTF-8 BOM when parsing rollout metadata, or
- Skip/quarantine an unreadable rollout and continue, instead of failing the entire store and blocking message creation.
Workaround
Strip the 3-byte BOM from the offending .jsonl (so the first byte becomes 0x7B).
3 Comments
Adding a small root-cause note in case it helps triage.
This looks like two separable failure modes that may be worth testing independently:
read_session_meta_line/ rollout metadata parsing should probably accept a UTF-8 BOM before the first JSON object, because external editors on Windows can add it without otherwise changing the JSONL payload.A focused regression test could create two rollout files: one normal file and one whose first bytes are
EF BB BF { ...session_meta... }. The expected result would be either successful parsing of the BOM-prefixed file, or at minimum a partial inventory with an error attached only to that rollout, not a global thread-store failure.This seems especially important because the state DB already has stale/missing-rollout tolerance paths; a BOM-prefixed but otherwise valid rollout feels like the same class of recoverable local-state problem.
Took a closer look at the Rust core for this — sharing a root-cause confirmation, the full set of affected read paths, and a minimal fix with a regression test, in case it helps triage.
Root cause (confirmed)
Every rollout consumer parses lines with
serde_json::from_strafterstr::trim(), e.g.codex-rs/rollout/src/list.rs→read_head_for_summary:str::trim()follows the UnicodeWhite_Spaceproperty, and U+FEFF (the BOM) is notWhite_Space— sotrim()leaves it in place,serde_jsonrejects the leading\u{FEFF}("expected value at line 1 column 1"), and thesession_metarecord is silently dropped fromhead.read_session_meta_linethen sees a non-meta record first and returns exactly the reported "does not start with session metadata".Not limited to the summary path
All rollout reads funnel through
compression::open_rollout_line_reader, so the same BOM also breaks:list.rs::read_head_for_summary/read_session_meta_line— the reported failurelist.rs(saw_session_metanever set)search.rs(session-meta line skipped)recorder.rs::load_rollout_items— the resume path, which doesserde_json::from_str(&line)directly; a BOM there bumpsparse_errorsand loses thethread_id.Minimal fix (failure mode #1 only)
Strip a single leading BOM once, at the reader boundary (
RolloutLineReader::next_line), so all four consumers benefit from one change and the on-disk file is left untouched:Applied to the first emitted line only (a BOM can only appear at byte 0), tracked with a
seen_first_lineflag on the reader.Regression test
Mirrors the existing
write_rollouthelper, prepends the three BOM bytes (EF BB BF) to the file, then asserts bothRolloutRecorder::load_rollout_itemsandread_session_meta_linerecover the thread id. I verified both directions locally:load_rollout_itemsreturnsthread_id = Noneandread_session_meta_lineerrors with "does not start with session metadata".On failure mode #2
@SylvainWinning's point that one unreadable rollout shouldn't abort the whole inventory is well taken — that's the more robust long-term behavior. But it's a separate, larger change (quarantine / partial-inventory semantics), so I'd keep it out of this fix to stay small and reviewable, and follow up separately.
I have the patch + test ready against
main. Since contributions are invitation-only, I haven't opened a PR — but if the team would welcome one for failure mode #1, I'm happy to submit it.The BOM-prefixed rollout is a clean fsck/discovery case for
codex-rescue: one locally malformed session should be isolated and reported, not make the whole inventory unusable. I’m field-testing exactly that fail-closed behavior.If you still have the affected file or can reproduce it in a disposable Codex home, could you run:
I’m interested in whether Rescue discovers the rest of the session store, how it classifies the BOM-prefixed rollout, and whether it leaves the file byte-for-byte untouched. No automatic “strip BOM” repair is implied here; a conservative diagnostic is the goal.
Please share only sanitized output + versions/exit codes. No raw rollouts/SQLite, prompts, credentials, or private paths. Repo: https://github.com/shleder/codex-rescue