Skip to content

[Windows Desktop 26.715] Completed large thread is fully replayed every few seconds, causing system-wide input stutter #33786

Open

Description

@PrinnyQ

What version of Codex is running?

  • Codex Desktop package: 26.715.2305.0
  • Desktop client version in app-server traces: 26.715.21425
  • Bundled WSL Codex runtime: 0.145.0-alpha.18

What operating system are you using?

Windows 10 Pro x64, version 10.0.19045 (24 logical processors, 128 GB RAM), with the Codex app-server running through WSL2.

What is the issue?

A large thread that has already emitted task_complete and is no longer active continues to be polled by Codex Desktop with thread/turns/list.

Each request reconstructs the entire rollout (about 167 MB / 94,706 items). This repeats indefinitely while the Desktop app is open and causes:

  • delayed typing in the composer;
  • mouse-pointer and general Windows UI stutter every 1–2 seconds;
  • lag even while viewing a different project/thread;
  • occasional loss of responsiveness around session navigation.

Task switching is not required. The stutter continues while the app is simply left open.

Sanitized trace pattern:

app_server.client_name="Codex Desktop"
app_server.client_version="26.715.21425"
rpc.method="thread/turns/list"
Resuming rollout from "<redacted>.jsonl"
Resumed rollout with 94706 items

The latest turn had already ended with task_complete, but the same Desktop connection continued issuing these requests.

Steps to reproduce

  1. Have a large local thread (in this case about 167 MB and 94.7k reconstructed items).
  2. Let its current turn finish and emit task_complete.
  3. Switch to another thread/project, or leave Codex Desktop idle.
  4. Keep Codex Desktop open and move the mouse or type in Codex/another Windows app.
  5. Observe periodic whole-system input/UI stutter.
  6. Fully quit Codex Desktop: the stutter stops immediately.
  7. Use Codex CLI with Desktop closed: the system remains smooth.
  8. Reopen Desktop: the periodic stutter returns.

Measurements

Over one five-minute sample after the thread had completed:

  • 34 complete whole-rollout replays;
  • mean gap between replay completions: about 7.16 seconds;
  • cache-warmed replay duration: about 3.65–4.52 seconds each;
  • app-server sustained about 1.02 CPU cores, with about 1.87 cores observed during an earlier replay/startup sample;
  • app-server RSS: about 409 MB;
  • the settled Windows frontend used only about 0.06 CPU core in a comparison sample.

This is not memory exhaustion; the machine has 128 GB RAM.

The rollout was also copied, with matching hash, from the Windows-mounted filesystem to WSL ext4:

  • before: each full replay took about 33.7–45.5 seconds and was largely I/O-bound;
  • after: the initial resume took 6.87 seconds and later replays took 3.65–4.52 seconds;
  • however, the polling/replay loop continued, became CPU-bound, and the system-wide stutter remained.

The state database passed quick_check, the JSONL is valid, and there were no repeated rollout parse errors. Moving storage therefore improves each scan but does not fix the repeated full-history hydration.

Expected behavior

  • A completed, inactive thread should not be continuously polled.
  • thread/turns/list should use indexed/cached metadata and true pagination instead of reconstructing the complete JSONL for every request.
  • Background status/session refresh should not hydrate an entire transcript.
  • Desktop should not affect mouse or system-wide input responsiveness while idle.
  • Any unavoidable history indexing should be coalesced, cancellable, and run at low priority.

A/B isolation

  • Codex Desktop open: periodic system-wide stutter.
  • Codex Desktop fully closed: stutter disappears immediately.
  • Codex CLI alone: smooth.
  • Faster WSL-local storage: individual scans are faster, but the loop and stutter remain.

Related issues

I have intentionally omitted the rollout, databases, thread IDs, titles, local paths, prompts, and raw trace files because they contain private project data. I can provide additional sanitized counters or a synthetic large-rollout reproducer if maintainers specify the preferred format.

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
    on Jul 17, 2026
  2. fengjikui commented on Jul 19, 2026

    @fengjikui

    Adding a macOS arm64 reproduction because this now appears cross-platform and app-server-side rather than specific to WSL I/O.

    Environment:

    • macOS 15.7.4, arm64
    • Codex Desktop client 26.715.31925
    • bundled app-server / CLI 0.145.0-alpha.18

    In a sanitized 75-second sample, Desktop issued 22 thread/turns/list requests. The app-server completed 20 full reconstructions of the same 615,541,816-byte legacy rollout, producing 281,148 items each time. During the loop it was at about 101.6% CPU and 13.16 GiB RSS. Earlier in the same launch, a separate 4,246,305,774-byte legacy rollout was reconstructed 16 times (99,084 items per reconstruction, about 10-11 seconds each). The state-store backfill is already complete and these replays have no rollout parse errors.

    Current main documents the expensive branch directly: paginated histories use the ThreadStore path, while legacy thread/turns/list calls load_thread_turns_list_history; the comment says it "still replays the entire rollout on every request" until turn metadata is indexed separately:

    • Ok(thread) if thread.history_mode == ThreadHistoryMode::Paginated => {
      return self
      .paginated_thread_turns_list_response(
      thread_uuid,
      cursor,
      limit,
      sort_direction,
      items_view,
      )
      .await;
      }
      Ok(_) => {}
      Err(ThreadStoreError::InvalidRequest { message })
      if message == format!("no rollout found for thread id {thread_uuid}") => {}
      Err(ThreadStoreError::ThreadNotFound { thread_id }) if thread_id == thread_uuid => {}
      Err(ThreadStoreError::InvalidRequest { message }) => {
      return Err(invalid_request(message));
      }
      Err(ThreadStoreError::Unsupported { operation }) => {
      return Err(unsupported_thread_store_operation(operation));
      }
      Err(err) => return Err(internal_error(format!("failed to read thread: {err}"))),
      }
      let items = self
      .load_thread_turns_list_history(thread_uuid)
      .await
      .map_err(thread_read_view_error)?;
      // 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.
    • pub async fn load_rollout_items(
      path: &Path,
      ) -> std::io::Result<(Vec<RolloutItem>, Option<ThreadId>, usize)> {
      trace!("Resuming rollout from {path:?}");
      let mut items: Vec<RolloutItem> = Vec::new();
      let mut thread_id: Option<ThreadId> = None;
      let mut parse_errors = 0usize;
      let mut reader = compression::open_rollout_line_reader(path).await?;
      let mut saw_non_empty_line = false;
      while let Some(line) = reader.next_line().await? {
      if line.trim().is_empty() {
      continue;
      }
      saw_non_empty_line = true;
      let mut v: Value = match serde_json::from_str(&line) {
      Ok(v) => v,
      Err(e) => {
      warn!("failed to parse line as JSON: {line:?}, error: {e}");
      parse_errors = parse_errors.saturating_add(1);
      continue;
      }
      };
      if strip_legacy_ghost_snapshot_rollout_line(&mut v) {
      trace!("skipping legacy ghost_snapshot rollout line");
      continue;
      }
      // Parse the rollout line structure
      match serde_json::from_value::<RolloutLine>(v.clone()) {
      Ok(rollout_line) => {
      let item = rollout_line.item;
      // Use the FIRST SessionMeta encountered in the file as the canonical
      // thread id and main session information. Keep all items intact.
      if thread_id.is_none()
      && let RolloutItem::SessionMeta(session_meta_line) = &item
      {
      thread_id = Some(session_meta_line.meta.id);
      }
      items.push(item);
      }
      Err(e) => {
      if thread_id.is_none() {
      // The first SessionMeta belongs to this rollout. Later SessionMeta lines
      // can be copied from fork history, so only validate unknown history modes
      // before we have parsed the rollout's own SessionMeta.
      reject_unknown_thread_history_mode(&v)?;
      }
      trace!("failed to parse rollout line: {e}");
      parse_errors = parse_errors.saturating_add(1);
      }
      }
      }
      if !saw_non_empty_line {
      return Err(IoError::other("empty session file"));
      }
      tracing::debug!(
      "Resumed rollout with {} items, thread ID: {:?}, parse errors: {}",
      items.len(),
      thread_id,
      parse_errors,
      );
      Ok((items, thread_id, parse_errors))

    The reader streams every line, parses it into serde_json::Value, clones/deserializes it into RolloutLine, and retains all items in a Vec. This explains why repeated polling becomes both CPU- and memory-heavy at multi-GB scale. Transparent storage compression helps disk usage but does not remove the full decode/parse/rebuild on each request.

    Possible landing order: stop or deduplicate unnecessary client polling first; add app-server single-flight/caching keyed by stable rollout identity as an immediate guard; then migrate/index legacy turn metadata so pagination no longer reconstructs the full file. I have omitted thread IDs, titles, local paths, and content.

  3. zaptrem commented on Jul 23, 2026

    @zaptrem

    Same defect on macOS 26.5.2 (arm64), with a symbolized native profile pinning the hot path. I filed #34915 before finding this issue; closing that as a duplicate and consolidating the evidence here.

    Environment: Codex Desktop 26.715.72359 (build 5718), Codex Framework 150.0.7871.124, macOS 26.5.2 (25F84), Mac17,8.

    • Two still-active sessions with rollouts of 679 MB and 582 MB. Unlike the completed-thread case in the OP, these are live threads — so the replay loop runs at steady state whenever the app is open, and the full ingest restarts after every app relaunch.
    • The app-server holds ~210–228% CPU and ~2.6–2.8 GB RSS from the first minute after relaunch and does not settle (20 s samples over ~3 min; ~6 min of CPU time consumed in the first 2m46s of process lifetime).
    • sample <pid> 3 on the busy app-server — hot frames:
    codex_app_server::request_processors::thread_processor::build_thread_turns_page_response
    codex_app_server::request_processors::build_legacy_api_turns_from_rollout_items
    codex_app_server_protocol::protocol::thread_history::ThreadHistoryBuilder::handle_rollout_item
    codex_app_server_protocol::protocol::thread_history::ThreadHistoryBuilder::handle_event
    codex_protocol::dynamic_tools::normalize_dynamic_tool_specs
    codex_protocol::items::parse_hook_prompt_message
    

    i.e. the time is in thread/turns/list page construction re-parsing rollout items from the raw JSONL, matching the replay counts measured in this thread. lsof shows both rollouts held open (two fds each) continuously. An earlier profile on the previous app build additionally showed heavy serde_json deserialization under tokio fs reads.

  4. grtninja commented on Jul 24, 2026

    @grtninja

    A bounded pagination/cache design may help here:

    • treat the newest detail response as a bounded window or delta, not the complete authority;
    • union it with retained bounded client scrollback;
    • give runtime events a stable identity (call_id, otherwise a stable event key);
    • replace a stale runtime card in place when the same identity arrives again;
    • preserve ordinary messages chronologically;
    • deduplicate once, then apply one explicit retention cap.

    A likely product split is:

    1. server: indexed pagination/cursor and no full rollout reconstruction for completed inactive threads;
    2. client: bounded monotonic union keyed by stable event IDs, with stale runtime cards replaced rather than appended;
    3. lifecycle: stop background polling after task_complete unless an explicit subscription or version changes.

    That would address repeated large history hydration without trading it for missing-history regressions. This is architectural guidance, not a claim about the upstream app’s internal code path.

  5. heithmurray commented on Jul 26, 2026

    @heithmurray

    I can now reproduce this exact failure on the current unified Codex/ChatGPT Desktop build, with direct local measurements.

    Environment

    • Windows 10 x64, build 19045
    • Installed package: OpenAI.Codex_26.721.4979.0
    • Desktop client version recorded by app-server: 26.721.41059
    • Large task rollout: approximately 708 MB
    • Reconstructed item count: approximately 132,583
    • Rollout parses successfully with zero corruption and zero parse errors

    Reproduction and trace

    While working in a different task, Codex Desktop continued polling the large May task in the background. After reopening the large task, the same behavior continued:

    • thread/turns/list issued by Codex Desktop every roughly 7–8 seconds
    • each request logs Resuming rollout and reconstructs all ~132.5k items
    • 61 related log rows / 40 distinct timestamps were recorded in one five-minute window
    • measured codex.exe read rate averaged about 86 MB/s and peaked around 108 MB/s during a 15-second sample
    • an earlier 10-second sample recorded approximately 892 MB read by codex.exe

    This is not failed compaction or a corrupt rollout. The large task worked before the recent Desktop update. The current client appears to ignore the practical compaction boundary and repeatedly reconstruct the complete physical JSONL.

    We also found and eliminated two plausible local-state triggers:

    1. A stale restored localhost browser pane was surgically removed from both global-state files.
    2. A stranded queued follow-up (Run ended before the steer was accepted) later disappeared from both state files.

    Neither changed the seven-second thread/turns/list replay. The loop therefore appears to be Desktop client behavior rather than a recoverable local-state entry.

    User impact

    The impact is severe and not confined to the Codex window:

    • CPU remains heavily loaded while Desktop is open.
    • Mouse movement, keyboard input, clipboard operations, and dictation become delayed or unreliable.
    • Windows UI responsiveness degrades.
    • The app has produced repeated Application Hang events.
    • Fully closing Desktop stops the replay and restores normal responsiveness.
    • CLI use without Desktop does not produce the same behavior.

    I lost approximately three working days diagnosing and containing this regression. I also downgraded from the $200 Pro plan because I cannot justify paying that amount for a desktop client that currently prevents productive work.

    Storage clarification

    The repeated history scan creates substantial read amplification. Persistent SQLite TRACE/WAL churn is also present, but current physical-disk measurements did not show the extreme sustained write rates reported in the worst SSD-endurance cases. I am not claiming the SSD is already damaged. The confirmed defect is the continuous full-history replay, resulting CPU load, SQLite/logging churn, and system-wide input degradation.

    Requested fix

    Please prioritize:

    • stopping background polling of completed or inactive tasks;
    • true indexed/paginated thread/turns/list behavior that does not reconstruct the complete JSONL;
    • caching/coalescing task status refreshes;
    • a supported way to disable or recover a pathological Desktop task without deleting its history;
    • bounded persistent logging and WAL maintenance;
    • a regression test using a large, compacted task on Windows.

    Private paths, task contents, thread IDs, raw logs, and account details have been omitted. I can provide additional sanitized counters if maintainers specify the preferred format.

  6. heithmurray commented on Jul 26, 2026

    @heithmurray

    One additional note: I am available and willing to work directly with OpenAI/Codex maintainers on this reproduction. Please tag or reply to me here if you need specific sanitized traces, counters, A/B checks, or confirmation against a candidate build.

    I want the product fixed and am happy to help with a focused maintainer-led investigation. What I cannot continue doing is spending additional working days independently reverse-engineering and containing a Desktop release regression just to regain basic usability.

  7. heithmurray commented on Jul 26, 2026

    @heithmurray

    A further product-impact detail should be recorded: the troubleshooting experience inside Codex compounded the regression.

    The user repeatedly identified that the failure began with the Desktop update and that previously healthy large tasks were being handled differently. The assistant nevertheless spent too long exploring weaker local explanations involving GPU labels, antivirus behavior, Windows configuration, hardware, and persisted UI state. The user had to restate the same core evidence many times before the investigation traced the live thread/turns/list replay.

    The assistant also treated a stranded queued follow-up as a likely root cause and prepared a surgical state repair before verifying that removing the entry stopped the replay. The entry later disappeared, while the seven-to-eight-second full-history reconstruction continued unchanged. That disproved the proposed cause.

    This matters because the regression did not only make Desktop unusable. It also made the built-in assistant less effective at diagnosing the product failure, forcing the user to act as the investigator, repeatedly challenge premature conclusions, and spend substantially more time reaching the actual app-server trace.

    A better diagnostic path would have been immediate inspection of live app-server requests, measurement of repeated rollout reconstruction, and comparison against current upstream reports before proposing local repairs. The combination of the Desktop defect and the assistant-led troubleshooting detours materially increased the user’s time loss and reduced trust in the product.

  8. mingisrookie commented on Jul 26, 2026

    @mingisrookie

    Corroborating data point from another Windows machine (native, no WSL).

    Environment

    • Codex Desktop 26.721.4979.0, app-server codex-cli 0.146.0-alpha.3.1 (native Windows)
    • Windows 11, 24 GB RAM
    • ~/.codex/sessions: 718 rollout .jsonl files, 1.4 GB total, largest single file 39 MB

    Observation

    After ~8 hours of uptime, the app-server process (codex.exe app-server) had accumulated:

    ReadTransferCount:  30.4 GB   (sessions dir is only 1.4 GB → ~22x read amplification)
    WriteTransferCount:  5.2 GB
    CPU time:            1382 s
    Working set:         427 MB
    

    Measured via Get-CimInstance Win32_Process. The 30 GB cumulative read against a 1.4 GB corpus is consistent with rollouts being fully re-read/replayed repeatedly rather than incrementally indexed. Symptoms match: UI increasingly sluggish over the day, recovered after killing the app-server.

    Archiving old rollouts out of ~/.codex/sessions (718 → 473 files) noticeably reduced the load after restart, which also points at full-corpus rescans.

  9. rdylina commented on Aug 1, 2026

    @rdylina

    Related current-status update from macOS: the thread-history/session rehydration problem appears to have accelerated with recent Codex Desktop updates. On the current build (26.727.51351), the renderer now crashes/reloads every 2–3 minutes during active use, making the application nearly unusable. The main app process remains alive while the window blanks and reloads, with Crashpad reporting renderer failures. This may be a more severe manifestation of the repeated history rehydration described here.

  10. meza commented on Aug 1, 2026

    @meza

    Related native-Windows data point from current Desktop client 26.727.51351 and app-server 0.146.0-alpha.9.2:

    Opening an active task with an approximately 12 MiB, 4.9k-item rollout produced nine thread/turns/list requests within roughly two seconds. Every request reconstructed the complete rollout. Eleven complete reconstructions occurred across the observed task-opening sequence.

    I have not established the continuous idle polling reported in this issue. This narrower observation shows that the same expensive complete-history path can also amplify a single navigation operation at a substantially smaller history size.

    Full reproduction: #25390 (comment).

  11. louis030195 commented on Aug 5, 2026

    @louis030195

    Adding a current macOS profile with a high thread count and direct packaged-client inspection. No thread IDs, titles, prompts, paths, or rollout contents are included.

    Environment

    • ChatGPT/Codex Desktop 26.727.51351 (build 6119)
    • bundled codex-cli 0.146.0-alpha.9.2
    • macOS 26.6, Apple Silicon

    Privacy-safe profile and process evidence

    • 84,988 indexed threads: 83,131 active and 1,857 archived
    • every indexed thread is history_mode=legacy; there are 0 paginated threads
    • 84,919 rows already have a non-empty SQLite preview, so the state index itself is substantially populated
    • rollout storage is approximately 29 GiB across about 84,987 JSONL files
    • largest active rollout is about 236 MB; largest archived rollout is about 371 MB
    • during the reported slow-open state, the app-server was observed around 142% CPU and 3.4 GiB RSS; one renderer was about 3.1 GiB RSS

    The local log database also preserves a prior Desktop replay burst on the same profile. For one valid legacy thread with 9,978 reconstructed items and zero parse errors, thread/turns/list completed 4-5 full reconstructions per second for multiple consecutive seconds. A second thread was being reconstructed concurrently. This is stronger than a single slow resume: identical page requests were multiplying whole-rollout work.

    Packaged-client and current-source path

    I unpacked the installed app.asar and traced task opening. The client requests thread/resume with excludeTurns plus initialTurnsPage when supported, but legacy fallback still calls thread/read(includeTurns: true) when that page is unavailable. It also has multiple metadata/status polling paths using thread/read(includeTurns: false) and thread/turns/list.

    Current main still documents the server-side cost explicitly: legacy thread/turns/list calls load_thread_turns_list_history, then "replays the entire rollout on every request" until turn metadata is indexed separately:

    • let items = self
      .load_thread_turns_list_history(thread_uuid)
      .await
      .map_err(thread_read_view_error)?;
      // 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.
    • pub async fn load_rollout_items(
      path: &Path,
      ) -> std::io::Result<(Vec<RolloutItem>, Option<ThreadId>, usize)> {
      trace!("Resuming rollout from {path:?}");
      let mut items: Vec<RolloutItem> = Vec::new();
      let mut thread_id: Option<ThreadId> = None;
      let mut parse_errors = 0usize;
      let mut reader = compression::open_rollout_line_reader(path).await?;
      let mut saw_non_empty_line = false;
      while let Some(line) = reader.next_line().await? {
      if line.trim().is_empty() {
      continue;
      }
      saw_non_empty_line = true;
      let mut value: Value = match serde_json::from_str(&line) {
      Ok(value) => value,
      Err(e) => {
      warn!("failed to parse line as JSON: {line:?}, error: {e}");
      parse_errors = parse_errors.saturating_add(1);
      continue;
      }
      };
      if strip_legacy_ghost_snapshot_rollout_line(&mut value) {
      trace!("skipping legacy ghost_snapshot rollout line");
      continue;
      }
      if thread_id.is_none() {
      // The first SessionMeta belongs to this rollout. Later SessionMeta lines
      // can be copied from fork history, so only validate unknown history modes
      // before we have parsed the rollout's own SessionMeta.
      reject_unknown_thread_history_mode(&value)?;
      }
      let rollout_line = match serde_json::from_value::<RolloutLine>(value) {
      Ok(rollout_line) => rollout_line,
      Err(e) => {
      trace!("failed to parse rollout line: {e}");
      parse_errors = parse_errors.saturating_add(1);
      continue;
      }
      };
      let item = rollout_line.item;

    The newer paginated-history path does not help an existing profile where every thread is legacy. That explains the observed behavior: page-sized network responses still require O(total rollout bytes/items) parse and reconstruction, and concurrent/repeated callers multiply the work.

    Suggested landing order

    1. Stop polling completed/inactive threads and deduplicate identical client requests.
    2. Add app-server single-flight/coalescing for concurrent legacy history reads keyed by thread plus stable rollout identity, so one client burst cannot launch several full reconstructions.
    3. Index or safely migrate legacy turn metadata so thread/turns/list becomes true storage-level pagination rather than response-only pagination.
    4. Bound initial hydration by bytes as well as turn count, and expose a recoverable oversized-history state instead of blocking task navigation.

    The direct user symptom is approximately 20 seconds to open an unread/older task. The profile remains intact for additional privacy-safe counters or testing against a candidate build.

  12. louis030195 commented on Aug 6, 2026

    @louis030195

    codex is unusable because of this, takes 30s to go from a chat to another chat, back to claude

  13. boombx403-byte commented on Aug 18, 2026

    @boombx403-byte

    Hi @PrinnyQ, cross-platform WSL/Windows path mismatches often make healthy transcripts look broken when workspace contexts change. Codex Rescue Alpha5 explicitly separates transcript integrity from workspace path-family portability without rewriting saved paths.

    If you'd like to check the local rollout safely:

    pip install codex-rescue==0.1.0a5
    codex-rescue doctor --latest

    Please sanitize private paths before posting any output.

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 workingperformancewindows-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