app-server loads ALL session files on every thread/list call — causes high CPU, slow startup, and background token waste #22411
Description
Summary
The codex app-server loads and deserializes every session file in full on every thread/list request. After a few months of normal use, this becomes catastrophically slow and burns through API tokens invisibly in the background.
Environment
- Extension:
openai.chatgptv26.422.71525 (VS Code Remote) - OS: Linux (Ubuntu), VS Code Remote SSH
- Sessions accumulated: ~34 files, 624 MB total
What happens
Every time the Codex sidebar opens (or VS Code reconnects to the extension host), the app-server receives a thread/list JSON-RPC call. In response it reads and fully deserializes every .jsonl session file from ~/.codex/sessions/. I can see this clearly in the SQLite debug log:
Resuming rollout from "~/.codex/sessions/2026/03/22/rollout-...jsonl"
Resumed rollout with 9116 items, thread ID: ...
Resuming rollout from "~/.codex/sessions/2026/04/01/rollout-...jsonl"
Resumed rollout with 13819 items, thread ID: ...
This repeats for every session file on startup.
Observed symptoms
codex app-serverprocess running at 23–26% CPU continuously — even when idle- Constant writes to
logs_2.sqliteandstate_5.sqlite(WAL file growing to 4MB+) - VS Code extension crashes with
Error: Channel has been closedafter 15–60 minutes — the socket to the app-server drops under load - Extension host restarts (
exthost1→exthost2→exthost3...) — 5 restarts in one session - API tokens consumed in the background with no user interaction — 75% of daily limit burned in ~50 minutes
- Slow startup that gets worse the more sessions you accumulate
Root cause
The thread/list handler appears to eagerly load full rollout data for every session to build the list, instead of reading only the metadata needed for display (title, date, model).
Fix
Load only metadata (first/last message, timestamp, thread ID) for the session list. Deserialize the full rollout only when a specific thread is opened by the user. This is standard lazy-loading — the list view doesn't need 13,000 items of conversation history per entry.
Workaround
Manually delete old session files from ~/.codex/sessions/. Keeping only 2–3 recent sessions brought CPU from ~25% to near 0% and eliminated the crashes.
Impact
This is a silent token-burning bug. Users see their daily API limit disappear with no visible activity, and have no way to know why. On limited plans or tight budgets, this is a serious problem.
Activity
arkh-node commentedon May 13, 2026
Additional environment detail:
The affected setup is the official VS Code extension:
openai.chatgptCodex – OpenAI’s coding agent26.42.71525A bounded check of current
openai/codexmainsuggests that one inspectedthread/listpath is already metadata/head-only. So this may be specific to the extension/app-server version above, triggered by the SSH remote extension setup, or caused by another app-server path used during sidebar refresh/reconnect.The practical workaround that removed the high CPU behavior was archiving old
~/.codex/sessionsfiles on the remote server and keeping only a few recent sessions.I’m happy to provide sanitized file counts/sizes and logs after I rest.
kangminlee-maker commentedon May 26, 2026
I opened a separate issue with additional local evidence that may overlap with this all-session/list loading path: #24510. It includes state_5.sqlite active metadata size, the observed thread-list query/index plan, logs_2.sqlite TRACE/log evidence, long-running goal-session rollout stats, and a temporary mitigation script gist.
rnul commentedon Jul 14, 2026
I can confirm this issue on a current Codex Desktop → Remote SSH → Linux app-server → Android remote-control setup, with a much larger session population and a clean before/after remediation.
Environment
26.707.616080.144.10.142.3codex_chatgpt_android_remote(API v2)Reproduction and timing
The Linux state store had:
source='exec' AND has_user_event=0state_5.sqliteDuring one Android connection attempt, app-server logs recorded five consecutive
thread/listrequests on new remote-control connections. They took 34s, 36s, 35s, 39s, and 41s before Android retried. Every resumed rollout reportedparse errors: 0.A direct app-server protocol comparison on the same profile showed:
thread/list: 45.77s first run, 39.17s second runthread/listwithuseStateDbOnly: true: 0.173sThe generated protocol schema explicitly describes omitted/false
useStateDbOnlyas scanning JSONL rollouts to repair metadata. The Android request omitted this optimization.Controlled remediation
I made a consistent SQLite backup and manifest, then reversibly archived only inactive rows matching:
Rollouts updated during the preceding hour were excluded to avoid touching active automation.
Results:
sessionstreePRAGMA quick_checkImmediately afterward, the Android app connected and loaded all conversations successfully. No app-server restart, Linux reboot, account change, or network change was needed.
Why automatic maintenance appears necessary
Even after cleanup, 46 no-user exec sessions had been created or updated within the preceding hour. This profile will regress without retention/archival.
Expected behavior would be one or more of:
execthreads are automatically archived or excluded from interactive thread discovery.thread/listuses the state-DB-only path and reconciles rollouts asynchronously.I have not attached raw logs, rollout contents, account identifiers, hostnames, or local project paths, but can provide additional sanitized aggregate diagnostics if useful.
MikeeI commentedon Aug 3, 2026
Hi, thanks for documenting this.
I noticed one current downstream detail that may be relevant to the same rollout-scan root cause:
5157493,latest_thread_cwdcallstokio::fs::read_to_stringon the complete rollout and scans its lines in reverse to recover the latestTurnContextworking directory.resolve_resume_thread_id,codex exec resume --lastperforms that read serially for each candidate returned bythread/listuntil the CWD matches.latest_thread_cwdbefore checkingargs.all, soresume --last --allstill reads the first candidate rollout completely even though no CWD filtering is required.This appears to be a separate client-side full-file pass after
thread/list. Improving the app-server listing and repair path alone may therefore leavecodex exec resume --lastreopening candidate rollout files to recover metadata that could potentially come from the state DB or thread projection.I have not measured the incremental latency of this second pass. The evidence above establishes only the current control flow and the fact that its work scales with the number and size of candidates inspected.
Would it make sense for
exec resumeto short-circuit--allbefore callinglatest_thread_cwdand otherwise consume an authoritative latest-CWD metadata projection instead of reopening each candidate rollout?I checked all relevant issues, comments, pull requests, discussions, and release notes; this evidence is not already reported.
I am only reporting this finding and am not proposing a pull request unless a maintainer invites one.
Disclosure
Investigated thoroughly with GPT-5.6 Sol (runtime-default reasoning effort), using Oh My Pi as the agent framework.
This report is not generic or unreviewed AI-generated output. Its claims were checked against the cited evidence, and it includes the relevant detail intended to help maintainers resolve the issue.
If reports like this are not useful to the project, please let me know and I will refrain from submitting similar ones. My intent is to help without wasting maintainer time or energy or discouraging their work.
Thank you for your work.
arkh-node commentedon Aug 3, 2026
Author of this issue here.
I see #31305 (
[thread-store] let stores own derived metadata) has been open since early July. If that work is paused, or if its scope is deliberately narrower than this issue, I'd rather take a small piece off your hands than duplicate anything.The part I have in mind is the one @MikeeI described above, which looks separable from the app-server listing path:
latest_thread_cwdreads a whole rollout and scans it in reverse,resolve_resume_thread_iddoes that serially per candidate, andresume --last --allstill pays for the first candidate even though no CWD filtering is needed.Two things I'd want to be honest about before anyone spends time on this. I can't reproduce the scale reported in this thread — my own profile is small, so the evidence above is rnul's and MikeeI's, not mine. And the change I'm describing is control-flow you can read and cover with a test, not something I could validate under real load.
I'd only start if a maintainer says it's useful and not already in flight. Otherwise I'm happy to leave it be.
justinTM commentedon Sep 3, 2026
This is a separate payload mitigation, not a fix for #22411's scan-and-repair bottleneck. Fork-only implementation proof: justinTM#1
The short version: this fixes one measured sidebar payload amplifier, not the entire cold-open problem.
thread/listpreviews are bounded to 1,024 Unicode scalar values.thread/read, history, names, and server-side search remain intact.2026-09-03T20:03:15Z1,623-row snapshot, the projected preview JSON falls from 3.72 MB to 0.97 MB raw and from 0.86 MB to 0.22 MB after gzip, about 74 to 75 percent less.This should reduce deep or repeated sidebar pagination over a hotspot or remote SSH connection. It does not reduce scan-and-repair, coalesce duplicate requests, prioritize a selected read, bound history hydration, or prove native Desktop cold opens are now fast. I left the
useStateDbOnlydefault unchanged because its fallback and stale-index semantics need a separate compatibility decision.The preview behavior is also directly related to #21211.
Diagnostic evidence, throughput control, and caveats
codex-cli 0.144.1), Desktop navigation was usually acknowledged in 4 to 11 seconds; one one-turn read invocation against the large diagnostic task returned no response for more than 70 seconds. That was not a renderer-ready measurement on currentmain.thread/listrequest events. The trace did not prove whether those events were distinct pages or overlapping traversals, nor whether the selected read waited behind them.2026-09-03T20:03:15Z: 1,623 active previews, 3,611,401 characters total, 521 over 1,024, maximum 77,294. No private text was emitted during measurement.thread/read, resume, stored metadata, state-DB selection, or scan-and-repair.8f31b64c7f9ef67d8f966bff5ddf9e08eafe0b4d; upstreammainhad advanced to32c303c197e437cb13d444389d651fbaae02a6edafter validation, so it is not presented as a current-upstream rebase.