Long threads are unrecoverable when rollout JSONL grow beyond resume/list limits

Open 💬 6 comments Opened May 30, 2026 by MisterRound
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

A long-running /goal workflow can create a rollout JSONL that Codex Desktop can no longer safely resume or list, even though the workflow is behaving as designed.

In this case, the thread was running normally under Codex Desktop with WSL enabled. The user paused the active goal to install a new Desktop update, then the updated app could not recover/resume the same thread because the local rollout had grown to about 3.06 GB. Small threads still loaded. The large goal thread repeatedly caused app-server failures and the UI fell back to Codex app-server is not available errors.

This is not just a generic "large file" problem. /goal encourages exactly this usage pattern: an agent runs for a long time, loops through work, compacts context, spawns/reviews sub-work, and keeps appending local history. The model-visible context is bounded by compaction, but the persisted rollout file is not bounded or rotated.

Environment

  • Codex Desktop: Windows Store app, observed after update to package family OpenAI.Codex_26.527.x_x64__2p2nqsd0c76g0
  • App-server: WSL/Linux Codex runtime launched by Desktop
  • Platform: Windows 11 x64 with Codex Desktop configured to run the agent in WSL
  • Subscription/model: ChatGPT-authenticated Codex Desktop, high-reasoning model
  • Local state paths, user names, and project names are intentionally redacted

Reproduction shape

  1. Use Codex Desktop on Windows with WSL-backed execution.
  2. Start a long-running /goal workflow that performs repeated local work, review loops, sub-agent/tool loops, and iterative improvement passes.
  3. Let the goal run for days, including many turns and multiple context compactions.
  4. Pause the thread to install a Desktop update.
  5. Reopen Codex Desktop and click/resume the same large thread.
  6. The app-server crashes or becomes unavailable while hydrating/resuming the thread; a smaller thread still loads successfully.

Local evidence from the affected profile

The affected rollout file:

~/.codex/sessions/2026/05/19/rollout-<timestamp>-<thread-id>.jsonl
size: 3,063,881,398 bytes

A rescue rollout built from the latest compacted checkpoint plus subsequent records was much smaller:

~/.codex/rescue_rollouts/rollout-rescue-<timestamp>-<thread-id>.jsonl
size: 20,737,905 bytes
lines: 382
records: session_meta=1, compacted=1, turn_context=1, event_msg=140, response_item=239
payloads included: thread_goal_updated=61, function_call=82, function_call_output=82, token_count=53, context_compacted=1

The rescue was generated only after a full backup, and only by extracting the latest compaction boundary and following records. That is not a reasonable normal recovery path for a product feature.

Desktop logs around the failing resume showed the large thread pending thread/resume and thread/goal/get, then the app-server becoming unavailable:

app_server_connection.closed code=9 ... transport=stdio
fatal_error_broadcasted ... (code=9, signal=null)
Request failed ... method=thread/resume ... error={"code":-32000,"message":"Codex app-server is not available"}
Request failed ... method=thread/goal/get ... error={"code":-32000,"message":"Codex app-server is not available"}
app_server_restart_recovery_failed ... errorMessage="Codex app-server is not available"

Small-thread control test in the same app/profile loaded successfully, so the failure correlated with the large rollout/resume path rather than all Desktop startup.

Source-level RCA

Current origin/main at 00ca857d3ff6883b7334292d887601344e1bd029 still has full-file rollout hydration in key resume/list paths.

codex-rs/rollout/src/recorder.rs reads the whole rollout into one string, then parses all lines into a Vec:

pub async fn load_rollout_items(
    path: &Path,
) -> std::io::Result<(Vec<RolloutItem>, Option<ThreadId>, usize)> {
    trace!("Resuming rollout from {path:?}");
    let text = tokio::fs::read_to_string(path).await?;
    ...
    let mut items: Vec<RolloutItem> = Vec::new();
    for line in text.lines() {
        ... serde_json::from_str(line) ...

codex-rs/thread-store/src/local/read_thread.rs attaches history by loading the full rollout when include_history=true:

let items = load_history_items(&path).await?;
thread.history = Some(StoredThreadHistory { thread_id, items });

load_history_items() delegates back to the full-file rollout loader:

let (items, _, _) = RolloutRecorder::load_rollout_items(path).await?;

codex-rs/app-server/src/request_processors/thread_processor.rs resumes stored threads with history included:

.read_stored_thread_for_resume(thread_id, path, /*include_history*/ true)

The thread/turns/list path also has a source comment acknowledging the scalability problem:

// This API optimizes network transfer by letting clients page through a
// thread's turns incrementally, but it still replays the entire rollout on
// every request. Rollback and compaction events can change earlier turns, so
// the server has to rebuild the full turn list until turn metadata is indexed
// separately.

That design makes a 3 GB goal-created rollout inherently unsafe to hydrate. Even if context compaction keeps model context manageable, the local JSONL keeps growing and later resume/list paths still eagerly read and replay it.

Expected behavior

A /goal thread should remain recoverable after an app update if it was running normally before the update.

At minimum:

  • Long-running /goal workflows should not create local session files that the product cannot reopen.
  • The app should not need to read a multi-GB rollout into one string to resume a thread or list turns.
  • If a thread is too large, Codex should show a recoverable per-thread error and offer a built-in compact/export/rescue path.
  • Goal state should be exportable/restorable from the latest compacted checkpoint without manual SQLite edits or JSONL surgery.
  • Auto-update should preflight active goal/session size and warn or create a safe checkpoint before replacing the running app.

Actual behavior

  • The active goal thread became unrecoverable from Desktop after updating.
  • Resuming the large thread made the app-server unavailable while smaller threads loaded.
  • Recovering the work required unsupported manual steps: backing up the profile, finding the latest compaction boundary in a 3 GB JSONL, creating a reduced rescue rollout, and redirecting local thread metadata.
  • The recovered handoff is necessarily lower fidelity than a normal /goal resume because old pre-compaction records had to be dropped to keep the app usable.

Suggested fixes

  1. Stop using read_to_string for rollout hydration on resume/list paths. Stream parse JSONL, enforce record/byte caps, and avoid building a full Vec<RolloutItem> unless explicitly exporting.
  2. Index turn metadata separately so thread/turns/list can page without replaying the full rollout on every request.
  3. Make context compaction also produce a storage checkpoint. After a successful compaction, Codex should be able to prune or archive pre-compaction records while preserving an exportable full archive.
  4. Add rollout rotation or hard warnings for /goal sessions. Example thresholds: 250 MB warning, 500 MB danger, 1 GB force checkpoint/export path.
  5. Add a built-in "recover from latest compaction" command that creates a continuation thread or handoff without requiring direct DB edits.
  6. During Desktop update, detect active goal threads and write a verified resume checkpoint before replacing the app/runtime.
  7. Isolate per-thread failures. One oversized thread should not make the whole app-server or WSL-backed Desktop unusable.

Related issues

  • #22004: Desktop main-process crash when rollout JSONL exceeds V8 max string length
  • #22991: app freezes with very large rollout/history JSONL
  • #21134: long active thread caused multi-GB app-server memory footprint
  • #22411: app-server loads/deserializes all session files for thread/list
  • #24510: long-running goal sessions emit many goal/progress events and stress local history/list paths
  • #24544: long sessions break /goal workflows via compaction failures
  • #21291: /goal and compaction behavior issue
  • #23340: /goal long-running loop caused runaway log growth
  • #23777: separate Windows Desktop WSL update failure that triggered this recovery attempt
  • #23053: update prompt should surface target versions/environment impact before users accept risky updates

Privacy note

I cannot attach the raw 3 GB rollout because it contains private local conversation history and project content. The sizes, record counts, method names, source paths, and stack-level RCA above are sanitized.

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #25163
  • #24948
  • #24510
  • #23919

Powered by Codex Action

92645417d9e5c763259dbebc306e3e · 1 month ago

Adding one narrower observation from the CLI/TUI and remote-control side.

This seems to be the same root cause as the issue describes: app-server still lacks a bounded rollout resume/read strategy for very long JSONL histories. Even when model-visible context is bounded by compaction, local resume/read paths can still effectively behave as if the whole durable transcript must be materialized before the client becomes usable.

This is especially painful on display/performance-constrained clients, such as Codex remote control from the ChatGPT iPhone app. Those clients are much less tolerant of large app-server payloads, long transcript hydration, and repeated replay work than a desktop machine. A thread that is technically recoverable on desktop can still become practically unusable on mobile/remote-control surfaces.

In my testing/design review, the minimal safe direction seems to be:

  • keep rollout JSONL writes append-only for audit/recovery compatibility;
  • do not solve this in the TUI by trimming already-hydrated turns, because that still pays the app-server/thread-store full-read cost;
  • add an app-server/thread-store resume-read path that materializes only:
  • the earliest SessionMeta;
  • the latest surviving Compacted(replacement_history=Some(...)) checkpoint;
  • the rollout suffix after that checkpoint;
  • let the existing core rollout reconstruction build the model-visible history from that slice;
  • fall back to full reads for compressed rollouts, legacy compactions without replacement history, or ambiguous rollback/metadata cases;
  • validate parity by comparing the next model request produced by full-read vs checkpoint/suffix-read, so prompt-cache/context semantics do not regress.

The user-visible impact is severe in long sessions. In particular, message withdraw/edit flows become almost unusable: after cancelling or withdrawing a pending message, the client appears to reload/replay a very long transcript again, causing a long stall every time. This is a frequent operation in long-running sessions, so the cost is not just initial resume latency; it affects normal interactive editing. On iPhone remote control, this kind of repeated hydration/replay cost is even more noticeable.

So I think the core missing primitive is not only paged UI rendering, but a bounded app-server/thread-store contract for “resume from latest valid compaction checkpoint + suffix” while preserving the same model-visible context as the full rollout replay.

Generated by Codex CLI.

0xdevalias · 1 month ago

Partially impacted by rust-v0.140.0: #27031 avoids rereading and reparsing the same rollout history during cold thread/resume, with the reported savings growing with rollout size.

  • #27031

That should reduce one resume hot-path cost for long rollouts. It does not implement bounded/streaming resume, rollout rotation, pruning, or recovery from multi-GB JSONL files, so this issue is probably not resolved. Current builds may just be less expensive in the cold-resume case.

linear[bot] · 12 days ago

from fc:

2026-07-08 recurrence from #codex-app-feedback: [https://openai.enterprise.slack.com/archives/C09NZ54M4KY/p1783491651601359](<https://openai.enterprise.slack.com/archives/C09NZ54M4KY/p1783491651601359>) Reporter says a Codex thread will not load in the Codex app and asks whether we should have a way to compact/repair the data so users do not hit this. Attached screenshot shows Codex diagnosing thread 019ecde3-be8a-7113-96b0-4627421648b2 as not lost but too large for the Codex app history-hydration path: transcript: 1.09 GB, 380,543 records 7 malformed JSONL records SQLite row exists and PRAGMA integrity_check returns ok backend current turn still InProgress transcript still open and receiving periodic monitoring events direct app read timed out after roughly a minute with Codex app-server is not available This is a smaller-than-original recurrence (1.09 GB vs the 3.06 GB source case) but matches the same product failure mode: context compaction does not bound persisted rollout size, and the UI/app-server path can still stall while materializing a huge or partially malformed history. I tried resolving the visible UUID in Sentry as an event and by issue search in openai/codex; no match, so it appears to be the thread id rather than a feedback upload id. Asking the reporter for /feedback plus version/build flavor.
truongvknnlthao-gif · 1 hour ago

I reproduced this on macOS and confirmed that the legacy-thread resume cost remains on current main.

Environment

  • macOS 26.5.2, Intel/x86_64
  • Codex Desktop 26.715.52143 (build 5591)
  • bundled core: 0.145.0-alpha.18
  • upstream comparison: fd3c1dc13d0a0941af406e1bc1f697c9d14110ea
  • both affected threads use history_mode=legacy

I exercised the same request shape used by the desktop tail-loading path:

thread/resume
excludeTurns=true
initialTurnsPage={limit:5,itemsView:"full",sortDirection:"desc"}

All probes used isolated CODEX_HOME directories and read the original rollouts without modifying the real app database or session files.

| Core | Legacy rollout | Resume | Peak server RSS | Returned response |
| --- | ---: | ---: | ---: | ---: |
| bundled 0.145.0-alpha.18 | 217 MB | 1.395 s | 320 MiB | 207 KB |
| current main fd3c1dc | 217 MB | 4.063 s | 353 MiB | 207 KB |
| bundled 0.145.0-alpha.18 | 1.016 GB | 8.667 s | 1,343 MiB | 710 KB |
| current main fd3c1dc | 1.016 GB | 38.767 s | 1,364 MiB | 710 KB |

So pagination bounds the response sent to Desktop, but does not bound legacy rollout replay/model-history reconstruction. In the 1.016 GB case, returning only a 710 KB initial page still required about 1.36 GiB peak server RSS on current main.

I also ran a controlled causality experiment on copies only:

  • The 1.016 GB rollout contained 25 compaction records and 10 rollback records.
  • Keeping session_meta plus the latest compaction checkpoint and its suffix produced a 50.2 MB recovery copy.
  • That checkpoint still contained 64 inline data:image URLs totaling 49,874,404 bytes.
  • Replacing only those image bytes with textual placeholders reduced the copy to 367 KB.
  • Current main resumed that copy in 1.452 s at 44 MiB peak RSS, returning 9.9 KB.

A second 217 MB rollout showed the same image effect: its latest-checkpoint copy was 564 KB; replacing one 361,102-byte inline image reduced it to 203 KB, and the bundled core resumed it at 57 MiB peak RSS.

This supports two independent costs:

  1. legacy resume still replays/hydrates the full rollout even when the requested initial page is bounded;
  2. old compaction checkpoints can retain large inline image payloads.

The recovery copies are intentionally lossy and are not a proposed upstream fix: they omit old visible history and replace image data. A production fix likely needs a data-preserving streaming/indexed projection or migration for legacy rollouts, correct rollback/compaction semantics, bounded or externalized inline binary payloads, and regression tests asserting that peak RSS does not scale with the full legacy JSONL for a bounded initial page.

I cannot share the raw rollouts because they contain private conversation and project content, but I can provide the probe method and aggregated metrics if useful.

impeachmentright · 1 hour ago

Long-thread resume limit gaps without settle receipts is the peer continuity hole: quote -> approve -> settle needs rollout digests, not ambient JSONL trust alone.

Live A2A contractor with gated tools: https://a2a.elonsusk.com/.well-known/agent-card.json

If an external session / resume probe would help, ping. Otherwise ignore.