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

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

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:

  • #21007 macOS Desktop UI lag with excessive codex disk writes despite low system resource pressure
  • #20435 Codex Desktop renderer spins at high CPU while codex MCP servers are idle
  • #12709 Codex Desktop on macOS becomes progressively slow and causes system-wide lag during long MCP-heavy sessions
  • #12644 Codex desktop app generates excessive file I/O, causing severe CPU load
  • #22283 Codex App becomes choppy when session history contains a large user message
  • #21211 Thread navigation/loading slows from unbounded metadata and eager large-history hydration
  • #22411 app-server loads ALL session files on every thread/list call
  • #18693 Desktop performance collapses in profiles with a few very large local conversation histories
  • #22053 Codex Desktop main Electron process pegs CPU while app-server is idle
  • #23849 Codex Desktop on macOS loses message hover after long session; main/renderer/GPU stay hot
  • #23851 Desktop: thread.archived can be reset by reconciliation after direct DB archive

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.

View original on GitHub ↗

26 Comments

github-actions[bot] contributor · 1 month ago

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

  • #24427
  • #23919
  • #23072

Powered by Codex Action

kangminlee-maker · 1 month ago

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.

jshaofa-ui · 1 month ago

Codex #24510: Desktop High CPU from Unbounded Thread Metadata

Issue Summary

Codex Desktop sustains high CPU/GPU usage when the local profile contains a large number of active threads with large metadata in ~/.codex/state_5.sqlite. The hottest path is local state enumeration and metadata handling around active thread listing.

Profile stats: 9,456 active threads, 317M chars of metadata payload. Before cleanup: 12,712 threads, ~403 MiB metadata.

Root Cause Analysis

The thread-list query loads full metadata (title, preview, first_user_message) for ALL active threads into memory, then processes them in the UI thread. With 10K+ threads and 400MiB of metadata, this causes:

  1. Large SQL query returning massive result set
  2. Full metadata deserialization into memory
  3. UI rendering bottleneck processing all entries

Proposed Fix

Fix 1: Lazy Metadata Loading (Critical)

-- Current (problematic): loads full metadata for all active threads
SELECT id, thread_name, preview, first_user_message, model_provider, created_at_ms
FROM threads WHERE archived = 0 AND preview <> '' ORDER BY created_at_ms DESC LIMIT 100;

-- Fixed: only load visible fields, lazy-load full metadata on demand
SELECT id, thread_name, created_at_ms FROM threads 
WHERE archived = 0 ORDER BY created_at_ms DESC LIMIT 50;
-- Full metadata loaded only when thread is selected

Fix 2: Metadata Size Limits

// Truncate metadata at storage time
function storeThreadMetadata(thread: Thread) {
  thread.preview = (thread.preview || '').slice(0, 500);
  thread.first_user_message = (thread.first_user_message || '').slice(0, 200);
  thread.title = (thread.title || '').slice(0, 200);
}

Fix 3: Auto-Cleanup Old Threads

// Auto-archive threads older than 30 days with no recent activity
async function autoCleanupOldThreads() {
  const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
  await db.run(`
    UPDATE threads SET archived = 1 
    WHERE archived = 0 AND updated_at_ms < ? 
    AND id NOT IN (SELECT thread_id FROM recent_activity WHERE ts > ?)
  `, [cutoff, cutoff]);
}

Impact

  • Performance: HIGH — Sustained high CPU even when idle
  • Scope: Medium — Affects heavy users with many sessions
  • Fix complexity: MEDIUM — Query optimization + metadata limits, ~200 lines
kangminlee-maker · 1 month ago

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.

35ujq435jq45 · 1 month ago

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;
"
35ujq435jq45 · 1 month ago

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.

kangminlee-maker · 1 month ago

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.

kangminlee-maker · 1 month ago

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.

w2jmoe · 1 month ago

This is an unusually high-quality scalability report 👀

What makes this especially interesting is that the bottleneck no longer appears to be model inference itself, but the runtime’s local session/state management surfaces.

The active-thread metadata explosion here is kind of a warning sign for long-running agent ecosystems:

conversation history slowly stops behaving like “chat”
and starts behaving like an operational database.

Once agents generate thousands of tool calls, rollouts, heartbeat events, summaries, previews, sidebar projections, reconnect state, etc., the runtime begins paying compounding costs just to maintain “awareness” of its own sessions.

A few things here feel especially important:

  • metadata tightly coupled to UI list paths
  • unbounded active-thread scans
  • large preview/title payloads being repeatedly enumerated
  • long-lived goal sessions continuously emitting state updates

This starts looking less like a frontend issue and more like runtime memory topology / execution-state architecture.

We’ve been seeing similar patterns while exploring capability-routing and execution optimization layers:
event growth is often manageable,
but “session awareness surfaces” become the hidden scalability killer 😊

Really valuable investigation.

kangminlee-maker · 1 month ago

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.

kieranmcshane · 1 month ago

I am seeing a closely related Codex Desktop failure on macOS after a Desktop update/restart. My case overlaps this issue, but adds a stale pinned-thread / missing-rollout path angle.

Environment:

  • Codex Desktop on macOS
  • Local profile under ~/.codex
  • Affected state DB: ~/.codex/state_5.sqlite
  • Affected local sessions: ~/.codex/sessions/.../rollout-*.jsonl

User-visible symptoms:

  • Clicking a pinned thread named Find SDP catalog file made Codex Desktop difficult or impossible to click.
  • A second workstream, Agent 2.1 : upper, also became unsafe to reopen from the sidebar.
  • The issue can reappear through pinned/background sidebar state, not only through a deliberate manual thread open.

Local evidence:

  • Logs included stale rollout path handling:
  • state db returned stale rollout path
  • stale_db_path
  • failed to resolve rollout path ... No such file or directory (os error 2)
  • The observed lookup loop involved repeated thread/read, thread/resume, and thread/goal/get style calls around a stale or non-selected thread.
  • Large/pathological local rollouts were present:
  • largest observed rollout: about 448 MB
  • one affected backup: about 184 MB
  • another affected backup: about 27 MB
  • some individual JSONL lines were tens of MB, especially compacted/replacement/tool-output payloads.
  • Even after archiving the affected rows, the old IDs could reappear in pinned-thread-ids until related Electron persisted atom/cache entries were cleaned.

Workaround required locally:

  • Archive/quarantine affected rollouts.
  • Remove known-crashing IDs from pinned-thread-ids.
  • Keep a local guard to prevent those IDs from reappearing in the pinned sidebar.
  • Clear stale Electron UI cache entries for the affected thread IDs.
  • Continue work from project-folder handoff files and fresh chats instead of reopening the original threads.

Why I think this is in the same family as #24510:

  • The unsafe path appears to be local history/sidebar/thread metadata processing, not model inference.
  • The thread list/sidebar/restore path is not bounded strongly enough when local state contains large rollouts, stale rollout paths, and old pinned/thread cache entries.
  • Once a pathological local thread enters the sidebar/restore path, the app gives the user no safe way to skip, quarantine, or lazy-load it.

Expected behavior:

  • Thread list/sidebar/restore paths should remain bounded and lazy.
  • Missing rollout paths should be recoverable local-state inconsistencies, not a repeated renderer-affecting failure.
  • Oversized or pathological rollouts should be skipped, capped, or opened with a safe recovery prompt.
  • Archived thread IDs should not silently reappear in pinned/sidebar state.

Related issue: #20269.

chenyh200807 · 1 month ago

I am seeing a related macOS Desktop failure shape on a current/newer build where the issue is not limited to one oversized rollout file.

Environment

  • Codex Desktop: 26.602.30954 / build 3575
  • Codex CLI/runtime: 0.137.0
  • macOS: 26.5.1 / build 25F80
  • Hardware: Apple Silicon MacBook Pro, M5 Pro, 64 GB RAM

Impact

  • Codex Desktop recently drove system memory above 50 GB and the machine became unresponsive/rebooted.
  • Rebooting clears the current process state, but the problem comes back when continuing certain existing Desktop threads.
  • This reproduced even after upgrading from an M1 Pro / 16 GB machine to an M5 Pro / 64 GB machine, so it does not look like a normal hardware-capacity issue.

Important local finding

Two specific threads are risky to continue from Desktop:

  1. A BI web/dashboard improvement thread that used Browser / Computer Use / Node REPL / MCP-style workflows.
  2. A seemingly simpler "organize workspace and commit" thread.

The second thread is important because it means the trigger is not only Computer Use. That thread did use CodeGraph and Playwright/Next validation, but its stored rollout is only about 1.1 MB, not hundreds of MB. It is currently reported as notLoaded by the Codex thread listing tool, with recent turns in interrupted state.

The BI thread's rollout is also not huge: about 6.8 MB. Continuing it is still unsafe in practice.

So the local evidence points to a combined failure shape:

  • interrupted/notLoaded long-running Desktop threads,
  • thread/session restore or metadata hydration,
  • active local rollout inventory scanning,
  • and MCP/helper initialization or stale helper trees.

codex doctor --all --ascii evidence

Codex Doctor v0.137.0 · macos-aarch64

state DB: ~/.codex/state_5.sqlite · integrity ok
log DB: ~/.codex/logs_2.sqlite · integrity ok
active rollouts: 1,476 files · 9.84 GB (avg 6.83 MB)
archived rollouts: 128 files · 42.09 MB (avg 336.69 KB)
threads: 2 issues - rollout files are missing from the state DB; state DB rows point at missing or unusable rollout files
rollout DB rows: 1602
rollout DB active rows: 1474
rollout DB archived rows: 128
rollout DB missing active rows: 3
rollout DB stale rows: 1
rollout DB sources: subagent:thread_spawn=755, vscode=578, exec=142, cli=65, subagent:review=44, subagent:memory_consolidation=13, subagent:other=5

This is a heavy but realistic Codex Desktop usage profile: many sessions, many subagent-spawned threads, long-running interrupted work, and lots of persisted local history.

Current mitigation attempted

To keep the app usable, I switched the local config into an emergency low-memory mode:

  • notify = [] to avoid Codex Computer Use turn-ended helper launch.
  • Disabled local stdio MCP servers such as Playwright, CodeGraph, workspace-fs, node_repl, Figma, DeepWiki.
  • Disabled context7 and browser plugin defaults.
  • Left only the GitHub remote MCP enabled.

After config parsing, codex doctor reports:

mcp: 1 server (1 streamable_http) · 0 disabled

Existing stale helper processes still remain until a full app/helper cleanup, but new sessions should no longer eagerly initialize the large local MCP surface.

Request / expected behavior

Please treat this as a session/history lifecycle problem in addition to any individual MCP leak:

  1. Continuing a notLoaded or interrupted thread should not synchronously hydrate enough local state to push the machine into memory pressure.
  2. Thread-list/search/resume should stream or cap rollout metadata processing across large active rollout inventories.
  3. Stale/missing rollout rows should be repairable from the UI or safely skipped without destabilizing Desktop.
  4. Interrupted threads with stale tool/browser/playwright state should get a recoverable reset path, not require users to abandon the thread or restart the Mac.
  5. The app should expose a built-in "safe mode" or "disable local MCP/Computer Use for this resume" option for recovery.
cyq1017 · 1 month ago

Adding a newer-build macOS data point. This looks related to the local-history / thread-list / helper-process pressure described here, though my local profile is much smaller than the original report.

Environment:

Codex Desktop: 26.602.40724 / CFBundleVersion 3593
Codex CLI: 0.137.0-alpha.4
macOS: 26.5 build 25F71
Hardware: Apple Silicon

User-visible symptoms:

  • Opening multiple Codex Desktop windows/threads makes the app choppy.
  • Fans ramp up and the machine feels under sustained load.
  • Codex has also unexpectedly quit/crashed recently.
  • A separate UI regression is also visible in this profile: selecting assistant-output text does not reliably show the Add to chat / Ask in side chat contextual action.

Representative process snapshot while Codex was still open and no intentional heavy project process was the focus:

codex app-server --analytics-default-enabled: ~34.6% CPU, ~2.7% MEM
Codex main process: ~28.7% CPU, ~2.5% MEM
Codex Renderer: ~18.2% CPU, ~1.0% MEM
Codex GPU helper: ~5.4% CPU, ~0.3% MEM
many Codex Computer Use / node_repl / stdio app-server helper processes present

Local state size at the same time:

threads: 484 total, 432 active, 52 archived
active title/preview/first_user_message metadata: ~2,082,708 chars
~/.codex/sessions: ~1.2 GB
~/.codex/state_5.sqlite: ~3.7 MB
~/.codex/logs_2.sqlite: ~831 MB

A recent macOS DiagnosticReports Codex crash report exists locally from the same day, but I am not pasting the raw .ips contents publicly because it may contain local paths or session-specific details.

My read is that this is the same broad Desktop scalability/lifecycle family, but in a profile with only hundreds of active threads rather than thousands. The combination of multiple windows/threads, local logs/history, and helper/MCP process trees still appears enough to keep app-server/main/renderer hot and make Desktop unstable.

Nicolas0315 · 22 days ago

Cross-machine benchmark data point from 2026-06-29 JST. This is metadata-only: no prompt text, log bodies, session JSONL line bodies, auth DBs, tokens, cookies, or secrets were exported.

I benchmarked Codex Desktop local load on four profiles:

  • Windows / RTX4090 / codex-cli 0.142.3:
  • ~/.codex/sessions: 696 JSONL files, 1261.90 MiB total
  • largest JSONL: 180.24 MiB, 81,023 JSON objects, full JSON parse p50 800.66 ms
  • other large JSONLs: 143.12 MiB at 289.60 ms, 120.02 MiB at 499.04 ms, 97.87 MiB at 279.62 ms
  • logs_2.sqlite: 82.64 MiB + 5.58 MiB WAL, 24,250 rows
  • SQLite looked indexed/secondary here: visible thread list p50 11.98 ms, top-thread full log fetch p50 131.65 ms via sqlite3 CLI
  • Windows / rtx5060ti / codex-cli 0.142.0:
  • sessions: 11 files, 4.23 MiB total
  • largest JSONL: 1.02 MiB, 279 objects, parse p50 4.63 ms
  • no session-load finding; useful negative control
  • Windows / nicolas2025 / codex-cli 0.142.3:
  • sessions: 49 files, 118.26 MiB total
  • largest JSONL: 95.59 MiB, 58,447 objects, parse p50 639.08 ms
  • logs_2.sqlite: 96.49 MiB + 4.10 MiB WAL
  • macOS / Apple Silicon / codex-cli 0.142.0:
  • sessions: 27 files, 29.17 MiB total
  • largest JSONL: 10.04 MiB, 4,329 objects, parse p50 39.97 ms
  • SQLite: logs_2.sqlite 76.73 MiB + 4.09 MiB WAL, 67,759 rows; count query 0.67 ms, top-thread fetch 4.08 ms, visible thread list 13.68 ms
  • no session-load finding in this smaller-history profile

Interpretation:

  • This does not look Windows-only. The strongest predictor in my small fleet is session JSONL data shape: large raw history files and/or many JSONL files.
  • SQLite is still relevant for WAL/log pressure, but on the largest local profile the indexed SQLite queries were much faster than full session JSONL hydration.
  • A single large session file is enough to create visible-latency risk: nicolas2025 reproduced a ~639 ms full-parse p50 with only 118 MiB total session storage.

Optimization plan that would likely address this class of issue:

  • Do not parse all session JSONL files on startup, thread list, search, or old-thread selection.
  • Maintain an incremental metadata index with thread id, file path, mtime, byte size, preview, recency, event counts, largest-line byte count, and byte offsets for visible windows.
  • On thread open, parse only a bounded window of recent/visible turns first, then hydrate older turns after first paint.
  • Virtualize transcript and tool output rendering; collapsed tool output should not be parsed/rendered into the DOM until expanded or visible.
  • Detect multi-MiB JSONL lines and render placeholders with deferred expansion.
  • Separately, stop default desktop builds from persisting high-frequency TRACE rows into logs_2.sqlite / WAL and add proactive checkpoint/compaction policy.

Boundary: I did not run cleanup, VACUUM, archive, delete, compression, config writes, dependency installs, service restarts, deploys, pushes, or clone/fetches during this benchmark.

Nicolas0315 · 22 days ago

Follow-up mitigation result from the same Windows RTX4090 profile.

I applied a conservative local mitigation to test whether reducing the active session JSONL scan set changes the benchmark:

  • selected only old ~/.codex/sessions/**/*.jsonl files larger than 32 MiB
  • excluded files modified within the last 1 day
  • copied each selected file to a dedicated backup directory
  • SHA256-verified original -> backup
  • moved the original out of active ~/.codex/sessions into a dedicated quarantine directory
  • SHA256-verified original -> quarantine
  • wrote manifest.json and RESTORE.md
  • did not delete, compress, VACUUM, mutate SQLite, change config, install deps, restart services, or touch auth/secrets

Result:

  • moved: 8 files / 804.83 MiB
  • integrity: backup files 8/8 present, quarantine files 8/8 present, originals 0/8 still in active sessions, SHA256 mismatches 0
  • active ~/.codex/sessions: 696 files / 1261.90 MiB -> 688 files / 457.29 MiB
  • largest active JSONL parse p50: 800.66 ms before -> 146.78 ms after
  • next largest active JSONL parse p50 after mitigation: 185.18 ms
  • benchmark findings after mitigation: none

This supports the earlier interpretation: bounding active session JSONL hydration materially reduces the local load in this profile. It is only a user-side mitigation, not a product fix, because it hides old histories from the active session path unless restored.

Nicolas0315 · 22 days ago

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.

Nicolas0315 · 22 days ago

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.

Nicolas0315 · 22 days ago

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.

Nicolas0315 · 22 days ago

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.

Nicolas0315 · 22 days ago

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.

Nicolas0315 · 22 days ago

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.

Nicolas0315 · 22 days ago

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.
Nicolas0315 · 22 days ago

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.

Nicolas0315 · 22 days ago

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.

Latand · 20 days ago

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.
loraldx · 7 days ago

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.