A single invalid UTF-8 rollout record makes the entire thread unreadable

Open 💬 1 comment Opened Aug 3, 2026 by shtse8

What happened?

A legacy rollout JSONL containing one newline-terminated partial record with an incomplete multibyte UTF-8 sequence cannot be opened or resumed. Thread loading fails before the existing per-record JSON recovery logic can run:

failed to load thread history: stream did not contain valid UTF-8

All valid records before and after the damaged record remain present on disk, but the entire thread becomes unreadable.

Deterministic reproduction

A sanitized regression test is available on this branch:

https://github.com/shtse8/codex/compare/openai:main...shtse8:codex:fix/skip-truncated-rollout-record

The fixture writes:

  1. a valid session_meta record;
  2. a newline-terminated partial agent_message record ending with bytes e6 87 (an incomplete three-byte UTF-8 sequence);
  3. a complete valid agent_message record.

On unmodified main (1bbfb5cfada8e56280adcd397b23d0c301423894), this test fails with:

Error: Custom { kind: InvalidData, error: "stream did not contain valid UTF-8" }

Root cause

RolloutRecorder::load_rollout_items consumes RolloutLineReader::next_line(). The plain reader is backed by tokio::io::AsyncBufReadExt::lines(), which decodes UTF-8 before serde_json sees each record. Invalid UTF-8 therefore aborts the stream instead of entering the existing parse-error path that already skips malformed JSON records.

Proposed fix

Read JSONL boundaries as bytes for rollout replay, then call serde_json::from_slice per record. An invalid UTF-8 record is counted in the existing parse_errors total and skipped, while subsequent valid records continue loading. Keep RolloutLineReader::next_line() strict for callers that explicitly request strings. The implementation covers both plain and zstd-compressed readers.

The branch above includes the focused implementation and regression test. No user rollout, conversation content, or credentials are included.

Verification

  • regression test: red on unmodified main, green after the fix;
  • just test -p codex-rollout: 113 passed;
  • just fix -p codex-rollout;
  • just fmt;
  • git diff --check.

Per the invitation-only contribution policy, please treat this as a request for maintainer review of the approach and an invitation to open the prepared PR if it aligns with the intended recovery semantics.

Where are you seeing the problem?

Codex desktop app connected to a Linux remote workspace.

What version of Codex are you using?

Reproduced against upstream main at 1bbfb5cfada8e56280adcd397b23d0c301423894.

What operating system are you using?

macOS desktop client with a Linux remote workspace.

View original on GitHub ↗

1 Comment

shtse8 · 21 days ago

A second independent occurrence reproduced the same failure on 2026-08-06 with codex-cli 0.146.0 in a Linux remote workspace.

Sanitized evidence:

  • rollout size before recovery: 56,109,637 bytes / 29,251 newline-delimited records;
  • exactly one record contained invalid UTF-8: line 28,913, byte offset 55,447,447;
  • the record was newline-terminated and ended with a lone e7 byte inside an unfinished agent_message string;
  • both adjacent records were complete valid JSON;
  • seven other UTF-8-valid partial JSON records already existed and were tolerated by the current per-record JSON error path;
  • app-level read failed before recovery with stream did not contain valid UTF-8;
  • after removing only the 106-byte invalid-UTF-8 record through a hash-verified atomic repair, the full file decoded as UTF-8 and app-level thread read/pagination succeeded.

This further isolates the availability failure to UTF-8 decoding in the line-reader layer before the existing malformed-JSON recovery path. No rollout content or user transcript is included here.