Skip to content

Codex Desktop high CPU from unbounded active thread metadata and local history/list processing #24510

Open

Description

@kangminlee-maker

Codex Desktop app-server high CPU from unbounded active thread metadata / thread-list path

Summary

Codex Desktop can sustain high CPU/GPU usage when the local profile contains a
large number of active threads with large title / preview /
first_user_message metadata in ~/.codex/state_5.sqlite.

This appears to be a thread-list / sidebar / local-history scalability issue.
The usage pattern is high-volume, but it is within expected Codex workflows:
long local sessions, repeated agent/review/automation runs, and many project
threads.

The hottest path we observed was not model inference. It was local state
enumeration and metadata handling around active thread listing.

Environment

  • Codex Desktop: 26.519.41501
  • Platform: macOS 26.4.1
  • Hardware: Apple Silicon
  • Local state DB: ~/.codex/state_5.sqlite
  • Local sessions: ~/.codex/sessions/.../rollout-*.jsonl

Observed Symptoms

  • Codex Desktop / codex app-server keeps using CPU even when no intentional
    user task is running.
  • Desktop renderer/GPU can also become warm depending on where the bottleneck
    surfaces.
  • Restarting Codex may temporarily change the active PID, but the expensive
    local-state path returns.
  • The problem correlates with large active local thread metadata and repeated
    thread-list/sidebar/reconnect activity, not with a single running model call.

Local Diagnostics

Current redacted profile statistics after one manual cleanup pass:

active threads: 9,456
active metadata payload chars: 317,838,640
archived threads: 3,264
archived metadata payload chars: 328,436,582

Before that cleanup, the profile had roughly:

active threads: 12,712
active metadata payload chars: ~403 MiB
one repeated review/agent group: 3,256 rows, ~201 MiB

The slow active thread-list query shape observed locally:

SELECT ...
FROM threads
WHERE archived = 0
  AND preview <> ''
  AND model_provider IN (?)
  AND created_at_ms < ?
ORDER BY created_at_ms DESC
LIMIT ?

Query plan:

SEARCH threads USING INDEX idx_threads_archived (archived=?)
USE TEMP B-TREE FOR ORDER BY

So the query starts from archived=0, scans a very large active set, and then
sorts for the sidebar/list path. The cost is amplified because selected columns
include large text metadata used as title/preview/first-user-message.

Long-running Goal Session Evidence

One long-running goal session also showed that active local sessions can keep
the history/logging path warm for a long time.

Redacted local session:

thread id: <redacted local thread id>
created: 2026-05-25 22:26:26 KST
still active in state DB at inspection time
cwd: /Users/.../<project>
rollout JSONL size: ~38.7 MB and still growing
state DB tokens_used: 511,046,356
state DB has_user_event: 0

Rollout JSONL event counts at inspection time:

lines: 22,546
function_call: 4,879
function_call_output: 4,875
thread_goal_updated: 3,998
token_count: 3,459
reasoning: 1,894
message: 1,061
agent_message: 1,002
custom_tool_call: 388
custom_tool_call_output: 388
patch_apply_end: 374
context_compacted: 28

Tool-call distribution:

exec_command: 3,408
write_stdin: 1,271
update_plan: 196
get_goal: 3
create_goal: 1

The thread_goal_updated events were emitted very frequently:

thread_goal_updated count: 3,998
median interval: 6.148 seconds
p95 interval: 33.085 seconds
p99 interval: 54.032 seconds
largest observed gap: 5,015.697 seconds

Near the end of the sample, the rollout was still appending events such as:

write_stdin call
thread_goal_updated status=active tokens=13,127,963 seconds=34,696
function_call_output
token_count

So this was not just a large historical file. The active goal session was still
emitting progress/heartbeat-like events and polling a running command.

logs_2.sqlite also showed a short retained window for this same thread:

time window: 2026-05-26 09:24:47-09:27:22 KST
rows: 923
estimated bytes: 10,259,493
largest row: 781,177 bytes
top target: codex_client::transport|TRACE
top target bytes: 9,016,481 across 12 rows
process: codex resume <same thread id>

In a 15 second observation window, the rollout file continued to grow by 2,627
bytes. The log table appeared to be subject to retention/pruning, so row counts
can decrease while the max timestamp continues advancing.

Important confounder: this specific session also still had a real child task
running under the Codex resume process:

codex resume <same redacted thread id>
child: python scripts/<project-audit>.py --skip-fetch --force
child CPU at inspection: ~100%
cwd: /Users/.../<project>

That means this particular long-goal sample should not be described as an
entirely idle Codex session. It does show, however, that very long active goals
can keep Codex's rollout/log/session-sync surfaces hot, and it can be hard for a
user to tell from the Desktop UI which old goal/session is still driving local
work.

Why This Looks Like A Product Scalability Bug

The trigger is heavy local usage, but the usage itself is not exotic for Codex:

  • users can create thousands of threads
  • some threads can be very long
  • agent/review/automation flows can create many repeated sessions
  • long-running goals can stay active for many hours and continue writing local
    progress/log events
  • sidebar, resume, reconnect, and archive reconciliation should have bounded
    cost regardless of total local history size

The app appears to treat local conversation history more like directly coupled
UI metadata than like a large local product database. The list/sidebar path
should probably operate on bounded metadata projections and lazy-load full
history only when a specific thread is opened.

Related Issues

These seem adjacent or overlapping:

This report may be the same family as #21211 / #22411, but the local evidence
here points specifically at active state_5.sqlite metadata and query/index
shape.

Workaround / Mitigation Script

I also prepared a local mitigation script that may help other affected users
diagnose and temporarily reduce the active-list load.

It does not patch Codex itself. It is a user-side workaround.

Script gist:

https://gist.github.com/kangminlee-maker/f933aade364a02b176d899e5c8a2486d

The script:

  • reports active thread metadata size from ~/.codex/state_5.sqlite
  • detects repeated high-volume active thread groups dynamically
  • keeps one real active representative thread per group
  • archives older sibling threads
  • writes archive_groups.jsonl so archived siblings can be searched/restored
  • can create optional helper SQLite indexes for the observed thread-list query
  • can copy/move old archived rollout JSONL files to cold storage
  • defaults to dry-run and backs up state_5.sqlite before applying changes

Safety notes:

  • Do not use this for SQLite corruption or migration checksum mismatch issues.
  • Run the report/dry-run first.
  • Close Codex before applying changes.
  • Keep the generated backup.
  • Direct SQLite edits are unsupported and may interact with Codex reconciliation.

Example dry-run:

python3 scripts/codex_state_batch.py batch --policy config/sample-policy.json

Example apply after closing Codex:

python3 scripts/codex_state_batch.py batch --policy config/sample-policy.json --apply

Dry-run result on my affected profile:

candidate archive threads: 1,000
candidate metadata payload chars: 140,384,001
dynamic groups detected: 10
representative manifest entries planned: 5
cold archive candidates: 2,447 rollout JSONL files, 813,736,111 bytes

The mitigation is attached as a small script package. It is meant as diagnostic
evidence and a temporary local workaround, not as an official fix.

Expected Behavior

Codex Desktop should keep thread-list/sidebar/reconnect paths bounded:

  • list views should read small metadata projections only
  • very large title / preview / first_user_message fields should be capped
    or summarized
  • thread history should be hydrated lazily
  • active sidebar queries should use indexes that satisfy both filtering and
    ordering
  • archive state should remain stable across reconciliation
  • heavy local profiles should not keep app-server / renderer / GPU hot while
    idle

Actual Behavior

Large active local metadata causes expensive repeated local processing. Depending
on the profile and app state, the visible bottleneck can appear in SQLite,
app-server CPU, Electron main/IPC, renderer, or GPU, but the common source is the
unbounded local-history/list path.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    app-serverIssues involving app server protocol or interfaces
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on May 26, 2026
  2. kangminlee-maker commented on May 26, 2026

    @kangminlee-maker
    Author

    Thanks for the duplicate suggestions. I reviewed #24427, #23919, and #23072.

    I agree they are related to the same broader Desktop performance family, especially around many local sessions, long histories, and high CPU. I am going to leave this issue open for now because the distinguishing evidence here is the local state/list path:

    • concrete state_5.sqlite active metadata size measurements
    • the observed active thread-list query shape and SQLite query plan
    • logs_2.sqlite TRACE/log growth evidence
    • long-running goal-session rollout stats showing frequent thread_goal_updated events
    • a temporary mitigation script/gist that archives repeated high-volume active groups and adds helper indexes

    If maintainers prefer one canonical tracking issue, I am happy for this to be closed as a duplicate, but I wanted to preserve the state DB/query-plan evidence because it may help isolate the product-side fix.

  3. jshaofa-ui commented on May 26, 2026

    @jshaofa-ui
  4. kangminlee-maker commented on May 26, 2026

    @kangminlee-maker
    Author

    Thanks for the summary and proposed fixes. I agree with the general direction: bounded list metadata, lazy loading, and limiting stored preview/title size would likely help.

    One small clarification so the evidence stays precise:

    • What I directly observed is the local state_5.sqlite query shape, its query plan, and the large active metadata payload that the list path has to scan/sort.
    • I did not directly prove that this exact work happens on the UI thread. The renderer/GPU was also hot in some samples, but the UI-thread part is still an inference.
    • The local schema I observed uses fields such as title, preview, and first_user_message; I did not see a thread_name column in this local threads table.
    • Auto-cleanup could help, but I think it should be user-controlled or policy-based. Automatically archiving active history by age alone may surprise users who intentionally keep long-running work visible.

    So I would phrase the product fix as: keep the thread list bounded by using small indexed projections, cap or summarize large metadata fields, lazy-load full history/large metadata only when opening a specific thread, and make any cleanup/archival behavior explicit and reversible.

  5. 35ujq435jq45 commented on May 26, 2026

    @35ujq435jq45

    Only by setting additional indices, I got fewer freezes:

    Make a backup

    set -euo pipefail
    
    backup_dir="$HOME/.codex/backups/state-index-war-$(date +%Y%m%d-%H%M%S)"
    db="$HOME/.codex/state_5.sqlite"
    
    mkdir -p "$backup_dir"
    
    sqlite3 "$db" ".backup '$backup_dir/state_5.sqlite'"
    
    for suffix in -wal -shm; do
      if [ -e "$db$suffix" ]; then
        cp -p "$db$suffix" "$backup_dir/state_5.sqlite$suffix"
      fi
    done
    
    sqlite3 -readonly "$backup_dir/state_5.sqlite" "pragma integrity_check;"
    
    printf 'BACKUP_DIR=%s\n' "$backup_dir"
    ls -l "$backup_dir"
    

    Add indices

    set -euo pipefail
    
    db="$HOME/.codex/state_5.sqlite"
    
    sqlite3 "$db" "
    PRAGMA busy_timeout=30000;
    CREATE INDEX IF NOT EXISTS idx_threads_archived_created_at_ms_id
    ON threads(archived, created_at_ms DESC, id DESC);
    CREATE INDEX IF NOT EXISTS idx_threads_archived_updated_at_ms_id
    ON threads(archived, updated_at_ms DESC, id DESC);
    ANALYZE threads;
    "
    
    sqlite3 -readonly "$db" "pragma integrity_check;"
    ls -l "$db" "$db-wal" "$db-shm" 2>/dev/null || true
    sqlite3 -readonly -header -csv ~/.codex/state_5.sqlite "
    select name, sql
    from sqlite_master
    where type='index'
      and name in (
        'idx_threads_archived_created_at_ms_id',
        'idx_threads_archived_updated_at_ms_id'
      );
    "
    sqlite3 -readonly ~/.codex/state_5.sqlite "
    explain query plan
    select id,title,preview,first_user_message
    from threads
    where archived=0
      and preview<>''
      and model_provider in ('openai')
      and created_at_ms < 9999999999999
    order by created_at_ms desc, id desc
    limit 50;
    "
    sqlite3 -readonly ~/.codex/state_5.sqlite "
    explain query plan
    select id,title,preview,first_user_message
    from threads
    where archived=0
      and preview<>''
      and model_provider in ('openai')
      and updated_at_ms < 9999999999999
    order by updated_at_ms desc, id desc
    limit 50;
    "
    
  6. 35ujq435jq45 commented on May 26, 2026

    @35ujq435jq45

    Thanks for digging into this. I think this issue is very relevant to the slow thread/list behavior I am seeing in a related Codex Desktop freeze report.

    I checked the affected remote Codex profile, and it lines up closely with the query-shape concern described here:

    • ~/.codex/state_5.sqlite: ~26 MB plus WAL
    • Total threads: 2,436
    • Active threads: 2,428
    • Archived threads: 8
    • Active metadata across title, preview, and first_user_message: 19,859,906 characters
    • ~/.codex/sessions: ~1.5 GB
    • ~/.codex/logs_2.sqlite: ~605 MB

    The remote thread/list query plan also matches the inefficient shape called out in this issue:

    SEARCH threads USING INDEX idx_threads_archived (archived=?)
    USE TEMP B-TREE FOR ORDER BY
    

    That makes me think this issue likely explains a substantial share of the 10-25 second thread/list calls captured in the freeze investigation.

    There may still be a distinct Desktop/remote-SSH failure mode layered on top of it, though. In the related captures, the user-visible freeze tends to appear around remote SSH app-server reconnect/recovery. The logs show app_server_connection.closed code=1006, app-server proxy termination, reconnect/recovery, and then clustered long thread/list / thread/resume calls across multiple large remote conversations.

    So my current read is: this issue may be the underlying scalability problem, while the remote SSH reconnect path can amplify it into a visible Codex Desktop freeze.

  7. kangminlee-maker commented on May 27, 2026

    @kangminlee-maker
    Author

    Update after the latest Codex Desktop update:

    I updated/restarted Codex Desktop and rechecked this issue on:

    • Codex Desktop: 26.519.81530 / build 3178
    • Public release: rust-v0.134.0

    My read is that this release contains adjacent changes, but not a direct fix for the bottleneck described here. The changelog includes work around local conversation history search, thread content search, tracing/analytics, websocket/session behavior, and compaction. I did not find an explicit change for the active thread-list / state_5.sqlite metadata scan/sort path.

    The issue still appears reproducible in my local profile after restart:

    • state_5.sqlite currently has 8,657 active threads
    • active title + preview + first_user_message payload is still about 202,756,007 characters
    • archived side has 4,291 threads and about 468,871,981 characters
    • the updated_at active-list query path still uses a temp B-tree sort in my DB:
    SEARCH threads USING INDEX idx_threads_active_preview_provider_created_ms (archived=? AND model_provider=?)
    USE TEMP B-TREE FOR ORDER BY
    

    One caveat: my local DB still has helper indexes from the temporary mitigation, so any improvement on the created_at path should not be treated as evidence that the product fix landed. The updated_at path above is why I think this issue remains valid after the update.

  8. kangminlee-maker commented on May 27, 2026

    @kangminlee-maker
    Author

    A note on the additional-index workaround from @35ujq435jq45: I think this is a useful tactical mitigation and it matches what I saw locally.

    Composite indexes that include archived plus the list sort key can reduce or avoid the expensive active-list scan/sort path, especially when the query can satisfy ORDER BY ... DESC, id DESC from the index. In my local mitigation, helper indexes around the active preview/list path made the created_at query much faster.

    I would still treat this as a workaround rather than the full product fix:

    • the index has to match the actual query shape, including predicates such as model_provider, preview <> '', workspace/cwd filters, and whether the list is ordered by created_at_ms or updated_at_ms
    • if the index does not match the active query shape, SQLite can still fall back to USE TEMP B-TREE FOR ORDER BY
    • indexes reduce scan/sort cost, but they do not bound the payload size; a list query that selects large title, preview, and first_user_message fields can still move a lot of data
    • the product fix likely still needs a small indexed list projection, lazy loading of large metadata/history only when a thread is opened, size bounds or summaries for preview fields, and any cleanup/archive behavior should be explicit and reversible

    So I think the index workaround is valuable evidence and a good short-term mitigation for affected users, but the underlying scalability issue should still be fixed in the app/server query and metadata model.

  9. w2jmoe commented on May 27, 2026

    @w2jmoe
  10. kangminlee-maker commented on May 27, 2026

    @kangminlee-maker
    Author

    One design thought after the discussion above:

    Keep the full history, but do not make the session list carry its weight. Add a buffer between history and UI, and split "active" into more than one state.

    The current behavior seems to make archived=0 do too much work. "Not archived" can mean:

    • running
    • recently used
    • pinned
    • long-running goal
    • generated/sub-session
    • old but resumable
    • preserved but not worth keeping in the hot sidebar path

    Those are very different states, but they all appear to end up in the same active list surface.

    I think a durable fix would be to keep the current local history/resume behavior, but put a small bounded list layer in front of it:

    • full session/event history remains preserved
    • resume/open can still load the full state
    • sidebar/history list reads a small indexed summary
    • large previews, first messages, tool/rollout details, and goal state are loaded only after opening a thread
    • long-running or generated sessions can be grouped behind a small representative item

    Indexes are a useful workaround for the current query shape, but they do not fully solve the design issue. They make the heavy path faster; they do not make the path lighter.

    The main point is: the sidebar should show a bounded view of the local history, not become the place where the growing local history is repeatedly paid for.

  11. 5 remaining items

  12. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Follow-up correction on the user-side mitigation above:

    After moving the 8 large JSONL files out of active ~/.codex/sessions, the user observed that even recent sessions were no longer visible in the Codex UI. I restored all 8 files back to their original active session paths and SHA256-verified the restored files.

    Post-restore checks:

    • restored files: 8/8
    • SHA256 mismatches: 0
    • active ~/.codex/sessions: back to 696 JSONL files / about 1262 MiB
    • latest 20 state_5.sqlite thread rows all had rollout_path files present after restore

    So the quarantine experiment is useful as evidence that active JSONL scan-set size affects load, but it is not a safe user-side workaround for normal use because Codex Desktop appears to depend on those files being present for session visibility/navigation. The product-side fix still needs to be lazy loading / indexing / virtualization rather than asking users to move session files.

  13. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Follow-up after testing a safer user-side mitigation:

    Manual filesystem quarantine is not a safe default workaround. I moved 8 old large JSONL files out of active ~/.codex/sessions only after copying and SHA256-verifying them, and it reduced the benchmark cost, but the user observed that even recent sessions disappeared from the Desktop UI. I restored all 8 files to their original paths and verified SHA256 matches. So removing/moving active JSONL files can preserve bytes but still break usability.

    Safer local mitigation used afterward:

    • Used official codex archive <thread-id> for local threads older than 7 days.
    • Took an online backup of state_5.sqlite before the batch and wrote a restore manifest/runbook.
    • Archived 85 remaining old active threads via the CLI; 85 succeeded, 0 failed.
    • Post-state: 39 active local threads, 660 archived, 0 old active threads.
    • codex_app.list_threads(limit=50) returned the recent local 7-day window again.
    • Active session JSONL dropped from about 1262 MiB to 275.13 MiB.
    • Largest active JSONL parse p50 dropped from 800.66 ms to 145.93 ms.
    • Benchmark findings after archive: none.

    Conclusion: official archiving is a much better user-side mitigation than moving JSONL files, but the product-side fix is still needed: thread list/startup/resume should not require parsing large active JSONL files, and transcript hydration should be bounded/lazy.

  14. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Correction / additional finding:

    After running codex doctor --json, I found that the earlier direct SQLite archive experiment left a parity warning:

    • active DB rows: 41
    • archived DB rows: 660
    • warning: 499 archived rows still had rollout_path under active ~/.codex/sessions/...
    • mismatched files existed: 499 files / 177.99 MiB

    So direct DB updates are not a safe mitigation, even if recent threads appear again and data bytes are preserved. They can leave archived DB rows whose rollout files are still in the active sessions tree.

    Current safer plan:

    • Do not manually move JSONL files.
    • Do not run bulk repair while Codex Desktop/app-server is active.
    • Use official CLI only, with Codex closed, and start with a 5-thread pilot: codex unarchive <id> then codex archive <id> so Codex itself normalizes state and file placement.
    • Verify with codex doctor --json, active thread list, and the benchmark before any all-thread repair.

    I also updated the metadata-only benchmark script to include logs_2.sqlite write-rate sampling. Current latest sample still shows high-frequency log persistence:

    • logs_write_sample: 10s, 1,969 rows, 196.86 rows/s
    • top target: TRACE log
    • app-server CPU sample during that window: codex.exe app-server peaked at about 130% of one core

    Conclusion: session JSONL archive reduces the largest parse bottleneck, but there are two remaining product-side issues: archive/file parity robustness and high-rate TRACE/DEBUG log persistence.

  15. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Follow-up on the local visibility regression after history-load mitigation:

    • Host: Windows 11 Pro 10.0.26200, Codex CLI 0.142.3, Desktop app-server 0.142.0.
    • User-visible symptom: after old-thread archiving and mitigation testing, the Desktop thread list can appear empty, including sessions from the last 7 days.
    • Data-loss check: recent local rollouts still exist. state_5.sqlite has 42 non-archived rows with existing files under ~/.codex/sessions; 41 files have mtimes within the last 7 days, totaling about 106 MiB.
    • Residual parity warning: codex doctor --json still reports 499 archived DB rows whose rollout files remain under active sessions rather than archived_sessions.
    • Suspicious metadata state: every row in threads has has_user_event=0, including all non-archived recent rows, while first_user_message, preview, and title are populated for all 42 active rows.
    • Metadata-only JSONL scan: the 42 recent active JSONL files all contain user-like event types/roles. No message bodies, prompt text, or log bodies were exported.
    • Interpretation: this looks like an index/metadata regression rather than missing JSONL data. If the Desktop list filters by has_user_event or cached interactive-thread state, the UI can show no recent sessions even though the active rollout files are present.
    • Local safety boundary: I am not hand-editing state_5.sqlite while Desktop/app-server are running. Any local repair is gated on closing Codex, copying state_5.sqlite, state_5.sqlite-wal, and state_5.sqlite-shm, then applying a small pilot and re-running codex doctor.

    This is adjacent to the original load issue: user-side archiving can reduce JSONL parse cost, but the product needs a reliable metadata/index repair path so the list does not regress into an empty or stale state.

  16. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Additional local mitigation details after the visibility regression:

    • The recent rollout JSONL files still exist, but the local threads.has_user_event flag is 0 for every row. A metadata-only scan of recent active JSONL files found user-like event types/roles in all recent active files checked; no message bodies or prompt text were exported.
    • I added a local stopped-Codex repair harness that can run two separate pilot repairs:
      • set has_user_event=1 only for recent non-archived rows whose JSONL structure contains user-like event types/roles;
      • repair archive parity by using official codex unarchive / codex archive commands for rows whose DB archived flag and rollout file location disagree.
    • Both repairs default to dry-run, refuse to apply while Desktop/app-server/node_repl are running, create restore instructions and local backups before mutation, and keep 10 newest backup directories by default.
    • Read-only external review returned conditional GO with these preconditions: close Codex completely, dry-run first, apply only a 5-row pilot, verify codex doctor, restart Desktop, confirm recent sessions are visible, then proceed further only if the pilot is healthy.

    This reinforces the product-side need for a safe, built-in state/index repair path in addition to lazy/bounded history loading. User-side archiving can reduce parse load, but stale metadata flags can make the Desktop list look empty even when rollout files are present.

  17. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Follow-up from the same Windows Codex Desktop profile (2026-06-29 03:38 JST): recent sessions now appear to be hidden by thread metadata, not missing files.

    Read-only health check:

    • threads: total 702, active 42, archived 660
    • recent active within 7 days: 39
    • recent active rollout files present: 39 / 39, total 103.23 MiB
    • recent_active_has_user_event: 0 / 39
    • recent_active_metadata_nonempty: 39 / 39 (first_user_message / preview / title populated)
    • bounded JSONL structural scan: 20 / 20 recent active files contain user-like event markers, but all 20 have DB has_user_event=0
    • archive parity warning remains: 499 archived DB rows still point under active ~/.codex/sessions

    Interpretation: this looks like a state/index metadata regression. The recent session JSONL files still exist, but if Desktop filters/list-builds by has_user_event, the UI can look empty even for sessions newer than 7 days.

    Local mitigation prepared but not applied live: a stopped-Codex repair pilot that backs up state_5.sqlite + WAL/SHM, verifies SQLite integrity, then updates only threads.has_user_event=1 for recent active rows whose JSONL structural scan confirms user events. I am not mutating the DB while Codex Desktop/app-server is running.

  18. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Follow-up local mitigation from the same Windows Codex Desktop profile (2026-06-29 03:53 JST): disabled MCP config did not stop already-running helper processes until I explicitly cleaned up the old app-server children.

    Setup:

    • codex mcp list: chrome-devtools, playwright, filesystem, codebase-memory-mcp disabled in config
    • However the running Desktop app-server still had helper processes for those disabled servers
    • I used a scoped cleanup that only selected processes under the Codex app-server process tree and only matching disabled MCP server command patterns
    • Read-only second review returned GO before apply

    Apply result:

    • stopped 42 Codex app-server child processes
    • breakdown: chrome-devtools-mcp 12, playwright-mcp 12, filesystem-mcp 12, codebase-memory-mcp 6
    • rollback is restart Codex Desktop

    Benchmark comparison, before -> after cleanup:

    • Codex-related process_count_end: 102 -> 58 (-44, -43.1%)
    • disabled helper process kinds: all -> 0
    • total CPU sample: 28.1% -> 25.5% of one core
    • logs_2.sqlite write sample: 6.38 rows/s -> 0 rows/s
    • visible thread list p50: 11.15 ms -> 10.45 ms
    • immediate dry-run after cleanup selected 0 processes, so these disabled helpers did not immediately respawn

    This suggests two related product improvements:

    1. when MCP servers are disabled/reloaded, app-server should terminate already-running helper processes for disabled servers;
    2. Desktop startup/app-server should not eagerly start disabled MCP helpers or retain them after config changes.
  19. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Follow-up after the disabled MCP helper cleanup: I also measured the remaining plugin helper footprint in the same Windows Desktop session.

    After disabled MCP helpers were removed, Codex app-server still had 21 plugin-server-* child processes. Reading only process metadata/current directories showed:

    • data-analytics: 7 processes
    • creative-production: 7 processes
    • codex-security: 7 processes

    These were enabled plugin helpers and not needed for the current performance investigation, so after a read-only second review and an explicit dry-run selecting exactly 21 processes, I stopped only those three plugin groups. Rollback is restarting Codex Desktop.

    Benchmark comparison before any helper cleanup -> after disabled MCP + optional plugin helper cleanup:

    • Codex-related process_count_end: 102 -> 42 (-60, -58.8%)
    • disabled MCP helper kinds: all -> 0
    • plugin-server-bundle/cjs/mjs: 7/7/7 -> 0/0/0
    • total CPU sample: 28.1% -> 20.5% of one core
    • logs_2.sqlite write sample: 6.38 rows/s -> 0 rows/s

    Feature tradeoff: stopping enabled plugin helpers may make those plugin-backed tools unavailable until respawn/restart. Product-side improvement would be to lazy-start plugin helpers on first actual use, and terminate/reclaim idle helpers after a timeout, instead of keeping one process per server helper alive in the default Desktop session.

  20. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Additional local cleanup from the same Windows Desktop session: after disabled MCP and optional plugin helpers were stopped, six node_repl.exe processes remained directly under the Codex app-server. They were not needed for this performance investigation, so after read-only review and confirming no in-flight MCP tool call, I stopped all six together.

    Result:

    • node_repl: 7 -> 0 in the benchmarked process kind counts
    • process_count_end after plugin cleanup -> after node_repl cleanup: 42 -> 32 (-10, -23.8%)
    • overall comparison before helper cleanup -> after all runtime cleanup: 102 -> 32 (-70, -68.6%)
    • app-server remained running; no immediate respawn after 30s
    • rollback is Codex Desktop restart

    Tradeoff: browser/chrome/node-repl-backed tools may be unavailable until respawn/restart. Product-side improvement: avoid keeping an idle node_repl pool alive across sessions, or reclaim idle node_repl workers after a timeout.

  21. Latand commented on Jun 30, 2026

    @Latand

    Adding a Linux Desktop data point from 2026-06-30. I am running the official Codex Desktop bundle through the community Linux wrapper, so treat the platform layer with that caveat. The failure shape looks very close to the local-history / thread-list / hydration path described here.

    Environment:

    Codex Desktop: 26.623.42026
    Electron: 42.1.0
    Codex CLI: 0.142.4
    OS: Arch Linux x86_64, kernel 7.0.12-arch1-1
    Launch: X11 path with --x11 / --ozone-platform=x11
    

    User-visible symptom:

    • The Codex Desktop window became unclickable while a turn was still visibly progressing.
    • The composer stayed in a spinner / working state.
    • A soft renderer reload did not recover it.
    • Killing the main Desktop process and relaunching restored the UI.

    Trigger pattern:

    1. In a local thread, I asked Codex to find a previous production deploy conversation.
    2. The agent used thread/conversation search with a query like prod deploy.
    3. The search result pointed at older long-running deploy/session threads.
    4. Around the same time, the renderer became unresponsive and Electron logged unresponsive-window telemetry.
    5. After relaunch, the app resumed the affected thread and opened/read the large matched deploy thread, again sending a large serialized thread/turn payload to the renderer.

    Representative process snapshot shortly after relaunch, while the app was usable again but still hot:

    Codex main Electron: ~63% CPU, ~5.2% MEM
    Electron GPU helper: ~34% CPU
    Codex app-server: ~10% CPU
    Electron renderer: ~41% CPU, ~2.6% MEM
    secondary renderer: ~4% CPU
    

    Local profile sizes from the same machine:

    ~/.codex/sessions: 11G
    ~/.codex/logs_2.sqlite: 761M
    ~/.codex/state_5.sqlite: 35M
    

    Log signals observed locally:

    thread_stream_view_activity_changed ... resumeState=needs_resume
    maybe_resume_started ... routePath=/
    response_routed ... method=thread/resume
    maybe_resume_success ... markedStreaming=true
    response_routed ... method=turn/start
    Sending server response ... response={"contentItems":[{"type":"inputText","text":"{\"schemaVersion\":1,\"thread\":...\"turns\":[...]"}]}
    Conversation state not found ... rendererWindowAppearance=avatarOverlay
    Received item/started for unknown conversation ... rendererWindowAppearance=avatarOverlay
    

    My read is that search/open of old local threads can push an oversized serialized thread payload through the app-server -> Electron renderer path. In this profile it manifested as a renderer/main/GPU hot loop and an unclickable Desktop window. This is adjacent to the unbounded local-history/list processing described in this issue, with thread search as the user-level trigger.

    Expected behavior:

    • Thread search should return bounded metadata/projections.
    • Opening a search hit should hydrate visible/recent turn windows first.
    • Large matched histories should have lazy transcript hydration and virtualized rendering.
    • The renderer should stay responsive enough to cancel/close/recover even when a matched thread has a huge rollout/history.
  22. loraldx commented on Jul 13, 2026

    @loraldx

    Adding a sanitized Windows data point at a smaller scale than the original report.

    Environment:

    • Codex Desktop 26.707.8168.0
    • Windows 11 x64
    • WSL2 workspaces

    Local state summary:

    • 511 unarchived threads
    • 93 archived threads
    • 92 open thread/subagent spawn relationships
    • 512 active session JSONL files totaling approximately 1.10 GB
    • archived session data totaling approximately 347 MB
    • the currently opened thread was only approximately 1.4 MB

    The renderer and app UI remained measurably active while idle. Because the currently opened thread was modest in size, the slowdown does not appear attributable only to rendering one exceptionally large transcript. The aggregate active-thread index, reconciliation work, spawn-edge state, or eager local-history hydration may also contribute.

    This suggests the scalability threshold can become visible at hundreds of active threads combined with subagent relationships, not only at several thousand threads or one unusually large conversation.

    Useful mitigations at the product level could include bounded sidebar projections, lazy hydration of inactive thread metadata, incremental reconciliation, and first-class bulk archive/cleanup controls.

    Thread titles, identifiers, prompts, local paths, account information, and raw session files have been omitted.

  23. Appiah1907 commented on Aug 2, 2026

    @Appiah1907

    Windows Codex Desktop corroboration (sanitized)

    Environment: Windows desktop app package 26.727.6591.0; Ultra; multi-agent enabled with a cap of 8.

    Observed on an affected profile:

    • After manually archiving a prior set of stale child threads, re-enabling multi-agent mode produced 19 new unarchived subagent threads without a user-requested parallel workload.
    • Current local state: 389 total threads, 49 unarchived threads, 332 spawn edges still marked open.
    • Current single Codex Desktop process tree: 34 Codex-related processes and about 2.73 GB private memory. One app-server retains 16 node_repl helper children, alongside sandbox/app-server descendants.
    • The desktop remains visibly choppy/flickering. Windows event logs showed no app hang, crash, or graphics-driver-reset event in the affected interval.

    This reproduces the active-thread/local-history scalability symptoms in this issue and adds a lifecycle angle: internal helper/approval/harness flows appear able to create retained child-thread and helper-process state too. Please ensure terminal reconciliation covers ordinary subagents and internal helper flows, and that active-list/sidebar work excludes completed children promptly.

    No local paths, transcripts, thread IDs, or configuration contents are included here. Redacted diagnostics are available if maintainers request them.

  24. Appiah1907 commented on Aug 2, 2026

    @Appiah1907

    Additional Windows Desktop finding (sanitized)

    The current behavior makes sustained professional development work nearly impossible: the machine becomes visibly choppy while Codex has no intended code task running, and the user cannot reliably continue or reopen work threads.

    New observations after controlled cleanup/restart:

    • 332 spawn edges remain marked open even after their child threads were archived. The active orchestration index is therefore not reconciling terminal child state.
    • With only one open subagent, the desktop still runs a GPU process and renderer at measurable CPU, holds roughly 1.4 GB private memory across its Codex process tree, and visibly stutters.
    • A legacy completion notify hook repeatedly reappears in the local configuration after app restarts despite prior removal. The observed hook had previously failed with Windows error 206 (overlong command line/file name). This may indicate configuration reconciliation or another writer restoring stale state.
    • No scheduled Codex job and no large current disk write explain the ongoing UI load.

    This is not a request to disable multi-agent workflows. The expected professional behavior is: terminal children and spawn edges are reconciled automatically; internal helper/review flows are not retained as active work; hook configuration remains stable; and the desktop renderer remains quiet when no task is running.

    Please treat this as a reliability/productivity issue for the Windows desktop client, not only a performance optimization.

  25. Dylan997S commented on Aug 17, 2026

    @Dylan997S

    macOS SSH data point: opening one large remote transcript can pin the renderer at 89% CPU

    This is a reproducible remote-thread variant of the unbounded history/hydration problem.

    Environment

    • ChatGPT/Codex Desktop: 26.810.52044 (build 6662)
    • Bundled Codex: 0.148.0-alpha.9
    • macOS 26.5 (25F71), Apple Silicon
    • Linux SSH remote Codex: 0.147.0

    Reproduction and measurements

    The SSH connection and remote app-server were healthy (connected), and direct SSH plus codex --version completed normally. Clicking an existing remote thread caused the app to remain on the loading view indefinitely.

    At the time of the freeze:

    • primary renderer: 88.7% CPU continuously;
    • other renderers: approximately 0%;
    • an earlier reproduction on another SSH project held the renderer near 46-48% for several minutes;
    • restarting Desktop restored the UI only until the same thread was selected again.

    The affected active remote rollouts were:

    Rollout size Indexed tokens_used
    9.18 MiB 45,312,492
    10.99 MiB 56,690,653
    12.96 MiB 21,268,902

    This was not an SSH latency failure. The renderer loop started specifically when opening the large transcript. Archiving those three threads in the remote state_5.sqlite and moving the rollouts to archived_sessions made the project usable again; smaller active threads remained available.

    There was also repeated git hash-object --stdin-paths wait_failed activity while the affected project was selected, but repairing the repository refs alone did not prevent the freeze. The transcript size/hydration path remained a separate trigger.

    Expected behavior

    • Fetch bounded thread metadata first, then page/virtualize turns and tool output.
    • Do not serialize or render the entire remote rollout synchronously.
    • Apply output-size limits before sending app-server thread payloads to Electron.
    • Keep navigation and cancellation responsive even when one thread cannot hydrate.
    • Offer a safe recovery action such as “open recent turns only” or “skip restoring last thread.”

    Related: #35096 (renderer freezes while restoring SSH remote threads). This data point adds macOS, direct per-thread size measurements, and a successful archive-based isolation test.

    Disclosure: this report was drafted with Codex assistance from sanitized process, SQLite metadata, and file-size measurements. No transcript content or private paths are included.

  26. idle-paladin commented on Sep 3, 2026

    @idle-paladin

    Summary

    I investigated a reproducible Codex Desktop problem where the Windows UI became
    unresponsive or nearly unresponsive around prompt submission, turn completion,
    thread updates, and subsequent window interaction.

    This was reproduced on Windows. The Markdown-related finding below is
    independently supported by the trace-confirmed macOS investigation in #35604,
    while the user-visible freeze pattern is also similar to #33524.

    In my tested environment, the freezes were eliminated after applying three
    targeted changes to a copy of Codex Desktop. The immediate improvement was most
    obvious after removing full Markdown AST parsing from display-only title and
    preview processing. Two additional changes removed major amplification paths:
    re-reading append-only rollout JSONL from byte zero and reprocessing every
    unchanged thread on each observation update.

    This is a report of a locally verified result, not a claim that every reported
    Windows freeze has the same cause. The exact code structure must be confirmed
    again in current releases.

    Tested environment

    • Windows 10
    • Codex Desktop 26.810.7004.0
    • Long local task history with large previews and rollout files
    • The modifications were applied only to a separate copy of the application
    • No conversation or rollout contents were required to identify the problem

    Observed symptoms before the changes

    • The UI stalled around prompt submission and turn completion.
    • The app sometimes approached the Windows Not Responding state.
    • Window movement and focus changes could become temporarily unresponsive after
      a turn.
    • On some occurrences, the stall also disrupted normal IME behavior.
    • The problem became more visible with large or numerous existing tasks.

    Finding 1: full Markdown AST parsing in a display-only plain-text path

    A main-process path used for thread names, preview-derived display titles, and
    desktop notification text converted the complete input to a Markdown AST merely
    to produce short display text. Large previews therefore entered a relatively
    expensive parser even though full Markdown rendering was unnecessary.

    The message-body renderer was a separate path and was not changed.

    Local workaround

    For these display-only strings, I replaced full Markdown AST construction with
    lightweight whitespace normalization.

    function makeDisplayText(input):
        text = convert input to string
        text = replace consecutive whitespace with one space
        return trim(text)
    

    An upstream implementation could alternatively use a bounded lightweight
    plain-text extractor. The important properties are:

    • Do not build a full Markdown AST for an unbounded preview merely to create a
      short title or notification.
    • Bound or truncate the input before any expensive display-only processing.
    • Do not change the normal Markdown renderer used for assistant message bodies.

    Result

    This change alone produced an immediate and dramatic responsiveness improvement
    in the tested environment.

    Finding 2: append-only rollout JSONL was repeatedly scanned from byte zero

    The rollout aggregation path could reopen local rollout JSONL files and parse
    their complete historical contents again. The files observed during the test
    were append-only: new events, including compaction-related events, were appended
    as new lines rather than rewriting previous lines.

    One measured cold scan covered 69 files totaling approximately 1.133 GiB and
    took approximately 92.5 seconds in the tested environment. This path may not be
    the direct cause of every prompt-submission stall, but it is an independent and
    very large amplification path during history aggregation or resume work.

    Local workaround

    I retained per-file aggregation state and resumed parsing at the first unread
    byte.

    cache key:
        stable file identity
    
    cached value:
        parsed byte offset
        completed-turn aggregate state
        in-progress-turn state
    
    function updateAggregate(file):
        metadata = stat(file)
        previous = cache[file identity]
    
        if no previous state:
            start from byte 0
        else if file identity changed or file size < previous offset:
            discard cached state and start from byte 0
        else:
            resume from previous offset
    
        parse only complete JSONL lines
        do not commit an incomplete final line
        update aggregate state and parsed offset atomically
    

    Additional safeguards used by the local workaround:

    • Serialize concurrent aggregation of the same file.
    • Invalidate on file replacement or truncation.
    • Leave a non-newline-terminated final JSON record unread until it is complete.
    • Bound the cache size; the local test used a maximum of 256 files.
    • Fall back to a full scan whenever the append-only assumption cannot be
      established safely.

    A production implementation should use whatever stable file-generation or
    append-only invariant the app-server owns instead of depending on version-
    specific minified code.

    Finding 3: unchanged thread observations were fully converted and emitted

    The thread observation/catalog path received a page of threads and repeatedly
    converted all entries, regenerated display titles, and notified listeners even
    when only zero or one thread had changed.

    In one measurement:

    • Input: 100 thread entries
    • Combined preview input: approximately 7,370,347 characters
    • Display-title/Markdown helper invocations: approximately 200

    This multiplied the cost in Finding 1.

    Local workaround

    I cached the version of each observed thread and emitted only changed catalog
    entries.

    observation version:
        thread id + updatedAt + status
    
    catalog-entry cache key:
        thread id + updatedAt + display name
    
    function processObservation(threads):
        changedEntries = []
    
        for each thread:
            version = makeVersion(thread)
    
            if version is unchanged:
                continue
    
            entry = reuse or build cached catalog entry
            changedEntries.append(entry)
            remember version and entry
    
        notify listeners only when changes exist
    
    receiver:
        merge changed entries into the existing map
        preserve explicit removal and full-resync semantics
    

    The local workaround bounded these caches to 512 entries and cleared them on
    dispose.

    Result

    For the measured case, downstream work fell from 100 threads and approximately
    7.37 million preview characters to either no notification or one changed entry
    containing 41 characters. Calls to the expensive display-text path fell from
    approximately 200 to approximately 2.

    Combined result

    With all three changes applied:

    • Prompt submission and turn completion no longer caused the previously
      reproducible UI freezes in this environment.
    • Long-history task interaction became subjectively smooth.
    • Normal assistant-message Markdown rendering continued to work.
    • Task history remained available after restarting the modified copy.

    During validation, a separate unmodified official instance was once left
    running without a visible window. It continued processing the same local task
    history and made the machine appear to freeze even though the modified instance
    was idle. After all unmodified and older test instances were closed and only the
    final modified copy was running, the residual stalls disappeared. Anyone
    testing this should verify that only one top-level Codex Desktop instance is
    active.

    Agent-readable investigation and workaround guidance

    The following section is intended to let another affected user give this report
    to a local coding agent. It deliberately describes semantic targets rather than
    release-specific minified identifiers.

    Safety requirement

    • Work only on a separate copy of the currently installed application.
    • Preserve untouched backups of app.asar and its corresponding
      app.asar.unpacked directory.
    • Do not copy an old release's app.asar over a newer release.
    • Do not assume bundle names or minified function names remain stable.
    • Preserve native unpack rules when repacking ASAR content.
    • Stop if a semantic target cannot be identified uniquely.
    • Keep the official installation unchanged so the modification can be rolled
      back by simply closing and deleting the test copy.
    • Do not run the official and modified copies simultaneously during testing.

    Semantic targets

    Patch 1:
    Locate the display-title, preview-title, and notification-text normalization
    path that parses the complete input as Markdown. Replace only this display-only
    path with bounded lightweight plain-text normalization. Do not modify the
    message-body Markdown renderer.
    
    Patch 2:
    Locate rollout-history aggregation that opens append-only JSONL files at offset
    zero on repeated invocations. Add a bounded per-file incremental cache with
    identity, parsed offset, aggregate state, incomplete-line handling, truncation
    and replacement invalidation, and same-file concurrency serialization.
    
    Patch 3:
    Locate the thread-observation/catalog listener that maps and emits the whole
    thread page on every update. Cache semantic thread versions, convert only
    changed threads, emit deltas, and merge them on the receiving side while
    preserving deletion and full-resync behavior.
    

    Validation checklist

    1. Start the copied application with an isolated web profile.
    2. Confirm that only one top-level Codex Desktop process is running.
    3. Verify login and task-list display.
    4. Create a new task and submit several prompts.
    5. Open and interact with a long existing task.
    6. Verify prompt submission, turn completion, window movement, task switching,
      search, and notifications.
    7. Verify normal message-body Markdown, including code blocks and links.
    8. Continue through an automatic compaction and verify subsequent turns.
    9. Restart the copied application and verify history consistency.
    10. Confirm that unchanged thread observations produce no downstream catalog
      work and repeated rollout aggregation reads only newly appended bytes.
    11. Roll back immediately if titles, deletion handling, task history, or JSONL
      aggregation becomes inconsistent.

    Notes for maintainers

    The release-specific local patch was made against minified packaged JavaScript,
    so names observed in that build are intentionally omitted here. The semantic
    boundaries and measured amplification are more useful for locating the original
    source:

    1. display-only Markdown-to-plain-text conversion;
    2. rollout JSONL aggregation and resume state;
    3. thread observation, catalog conversion, and listener notification.
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 workingperformancesessionIssues involving session (thread) management, resuming, forking, naming, archiving

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions