Skip to content

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

Open

Description

@MisterRound

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

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.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    windows-osIssues related to Codex on Windows systems
    app-serverIssues involving app server protocol or interfaces
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    contextIssues related to context management (including compaction)
    on May 30, 2026
  2. 92645417d9e5c763259dbebc306e3e commented on Jun 3, 2026

    @92645417d9e5c763259dbebc306e3e

    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.

  3. changed the title [-]/goal can make Desktop threads unrecoverable by growing rollout JSONL beyond resume/list limits[/-] [+]Long threads are unrecoverable when rollout JSONL grow beyond resume/list limits[/+] on Jun 10, 2026
  4. 0xdevalias commented on Jun 17, 2026

    @0xdevalias

    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.

    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.

  5. linear commented on Jul 8, 2026

    @linear

    from fc:

    2026-07-08 recurrence from #codex-app-feedback: 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.

  6. truongvknnlthao-gif commented on Jul 21, 2026

    @truongvknnlthao-gif

    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.

  7. DataDrivenDNA commented on Aug 13, 2026

    @DataDrivenDNA

    I can reproduce the legacy thread/turns/list cost and found a blocker in the new legacy-to-paginated migration that affects exactly the oversized sessions this issue tracks.

    Sanitized field evidence

    • Windows Codex Desktop host; affected legacy rollout is approximately 1.707 GB and 111k JSONL records.
    • PC-side app-server traces for this thread contain 11 consecutive thread/turns/list requests taking 5.4-7.1 seconds each, about 65.6 seconds in aggregate.
    • The rollout has 208 compaction records. Its latest 13 compaction records exceed 16 MiB each; the newest is 18,240,241 characters.
    • No session content, local paths, credentials, or repository details are included here.

    Current main at a7b8c074b577f897111c14de3a5e127b91e2a479 confirms the legacy behavior: thread_turns_list_response_inner pages the response but replays the complete rollout on every request. The indexed/paginated path added in #37175 and #37348 is the right architectural direction.

    Migration causality test

    I copied only this rollout into an isolated CODEX_HOME and ran the published @openai/codex@0.148.0-alpha.12 command against the copy:

    codex migrate-rollouts --apply --thread <redacted> --json
    

    The command reported status: migrated, but the result changed as follows:

    Legacy snapshot Reported-success migrated copy
    Rollout bytes 1,707,430,365 1,483,129,847
    Records over 16 MiB 13 0
    Latest retained compaction 2026-08-13 13:02:01Z 2026-08-12 22:27:31Z

    So the migration silently discarded every oversized compaction and rewound the latest retained checkpoint by roughly 14.5 hours. The real rollout was not modified.

    This matches the source. read_rollout_record drops any record over MAX_ROLLOUT_LINE_BYTES by returning line: None. The existing regression test covers only an oversized function_call_output and expects it to be dropped. However, LegacyRolloutCanonicalizer otherwise preserves RolloutItem::Compacted, because it is model-context state rather than disposable UI history.

    Proposed correctness contract

    Before automatic migration is enabled for large legacy sessions:

    1. Migration must not report migrated after silently dropping an oversized compaction or other model-context-bearing record.
    2. A synthetic compaction just over 16 MiB should preserve the same latest model context before and after migration.
    3. If bounded parsing cannot safely preserve an oversized semantic record, migration should fail closed and leave the legacy rollout untouched rather than publish a lossy replacement.
    4. Once semantic parity is established, legacy sessions should be promoted to indexed history before they can win the startup/resume writer race; otherwise the hottest thread can remain on the full-replay path.
    5. A focused app-server test should show that repeated newest-page requests after migration use list_turns and do not scale with the full legacy JSONL size.

    The repository contribution policy says external PRs require an explicit maintainer invitation. If this contract matches the intended migration behavior, I am happy to prepare the focused failing test and implementation in an invited PR.

  8. 92645417d9e5c763259dbebc306e3e commented on Aug 13, 2026

    @92645417d9e5c763259dbebc306e3e

    I checked my own pre-migration backup to determine whether the 16 MiB record limit had caused silent data loss in my sessions.

    The backup was decrypted and extracted into an isolated temporary directory. It was not imported into the active Codex home. I then scanned every rollout JSONL as raw bytes, measuring complete newline-delimited records without retaining or reporting transcript contents.

    Results

    Metric Result
    Extracted backup size Approximately 28 GiB
    Rollout JSONL files 23,582
    JSONL records scanned 17,794,035
    Records larger than 16 MiB 0
    Files containing records larger than 16 MiB 0
    Largest record 9,567,929 bytes
    Largest compacted record 6,554,521 bytes
    compacted records scanned 26,891

    The largest record was a response_item/function_call_output, not a compaction record. The largest compaction was approximately 6.25 MiB, well below the 16 MiB migration limit.

    I also checked the limited /goal activity in this backup:

    Rollout thread_goal_updated records Largest record
    Goal-related rollout 1 3 228,938 bytes
    Goal-related rollout 2 100 33,049 bytes

    Only two rollout files contained thread_goal_updated events, and neither came close to the limit.

    Interpretation

    This particular backup does not appear to have been affected by the oversized-record loss described above. However, the result also confirms that checking only the migrated paginated files would not be sufficient: a successful lossy migration would already have removed every oversized source record. The meaningful check must use a pre-migration backup or snapshot.

    The current behavior still appears unsafe in the general case because the 16 MiB check is performed before the record is classified. An oversized semantic record such as compacted, turn_context, world_state, or a model-visible message can therefore be skipped through the same path as an oversized tool output.

    I agree with the proposed fail-closed contract: if an oversized record cannot be preserved, migration should report failure and leave the legacy rollout unchanged rather than publish a paginated replacement with missing semantic state.

    This report was organized with Codex CLI from locally computed size and type metadata. Local paths, storage device names, session contents, prompts, tool outputs, credentials, and repository details have been omitted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingcontextIssues related to context management (including compaction)performancesessionIssues involving session (thread) management, resuming, forking, naming, archivingwindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions