[Windows] Desktop startup MoAppHang from unbounded duplicated thread metadata after automation creates large first-user prompts
Summary
Codex Desktop on Windows became effectively unable to start after a long-running local automation workflow created many Codex threads whose first user messages grew into hundreds of thousands of characters.
The local SQLite databases were structurally healthy. The evidence strongly implicates valid but extremely oversized thread metadata as the cause:
titlepreviewfirst_user_message
For many affected rows, all three fields contained the same full first-user prompt.
The resulting state_5.sqlite grew to roughly 524 MB, with the threads table accounting for about 519 MB. Codex Desktop then repeatedly hung during startup with Windows Event ID 1002 / WER MoAppHang.
Renaming the existing %USERPROFILE%\.codex and allowing Desktop to create a fresh profile immediately restored startup, strongly isolating the failure to local Codex state.
This appears closely related to #21154, #21211, #29007, #32371, #32588, and #35604, but this report adds a reproducible Windows case, a concrete automation trigger, detailed measurements, and a validated recovery path.
---
Environment
Observed affected environment:
- Windows x64
- Codex Desktop package:
26.818.5229.0 - Desktop executable/runtime version reported by WER:
151.0.7922.170 - Failure type:
MoAppHang - Windows Event ID:
1002
The issue appeared abruptly after the same installation had worked normally earlier.
Repair/reset of the Windows app did not resolve the original state-dependent startup hang.
---
User-visible behavior
Typical sequence:
- Start Codex Desktop.
- Initial UI appears.
- Within seconds the window stops responding.
- Windows reports the application as hung.
- Repeated launches behave the same way.
- Occasionally the app remains usable for only a few seconds before freezing.
Windows Error Reporting repeatedly recorded:
EventName: MoAppHang
Application: OpenAI.Codex
Event ID: 1002
There was no evidence that this was simply a network-loading failure.
---
Isolation test
A reversible A/B test isolated the problem to the existing Codex profile:
%USERPROFILE%\.codex
was renamed and Desktop was allowed to create a fresh profile.
Result:
- old profile: reproducible startup hang
- fresh profile: Desktop started normally and reached login
No existing data was deleted.
This excluded the installation itself as the primary cause.
---
Database integrity
Read-only inspection of the affected profile found no physical SQLite corruption.
Checks included:
PRAGMA integrity_check / quick_check: OK
foreign-key checks: OK
JSON / JSONL parsing: OK
WAL structure: no truncation evidence
All mapped session JSONL files were syntactically valid.
This was therefore a valid-data / pathological-metadata failure, not database corruption.
---
Size of the affected local state
Affected profile:
threads: 1,218
active threads: 676
archived threads: 542
state_5.sqlite: ~524 MB
threads table: ~519 MB
logs_2.sqlite: ~1.0 GB
sessions: ~2.5 GB
The log database was large and may have amplified I/O cost, but evidence did not identify it as the source of the failure.
---
Oversized metadata
The strongest signal was the threads table.
Across the 1,218 rows:
sum(length(title)): ~147.6 million characters
sum(length(first_user_message)): ~147.6 million characters
sum(length(preview)): ~147.6 million characters
For 1,179 threads, the three fields were identical:
title == first_user_message == preview
Distribution by first-user-message size:
| First user message | Threads |
|---|---:|
| <= 10 KiB | 254 |
| 10-64 KiB | 526 |
| 64-256 KiB | 224 |
| > 256 KiB | 214 |
Additional observations:
- 438 threads had first messages larger than 64 KiB.
- 214 exceeded 256 KiB.
- The largest affected metadata field was approximately 660,000 characters.
- For a single extreme thread, that same text was effectively stored three times as list/display metadata.
The large values were not random or corrupted text.
For almost all affected rows, first_user_message matched the actual first event_msg/user_message in the session JSONL.
---
Trigger in this incident
The main producer was a custom local automation workflow using the Codex app-server.
Its controller loop repeatedly created new threads.
A bug in that workflow caused a growing state snapshot / transcript to be embedded directly into the first user message of every new controller thread.
The approximate chain was:
autonomous controller
-> growing state snapshot / transcript
-> new controller thread
-> snapshot inserted inline as first user prompt
-> Codex indexes the complete prompt
-> title = first_user_message = preview
-> threads table grows rapidly
-> startup/thread-list path becomes pathological
-> Windows Desktop MoAppHang
The problem became much worse during two automation runs that generated roughly 150 new controller threads.
Their first-user prompts grew to approximately:
530,000 - 661,000 characters
New controller sessions were being created roughly every 1-2 minutes.
This custom automation was clearly defective because it allowed state to grow without a bound.
However, Codex Desktop also appears insufficiently defensive because a valid large prompt can become unbounded UI/list metadata and be duplicated across multiple hot-path fields.
---
Why this appears to be a Codex robustness issue too
The automation should not have generated these prompts, but a local client should ideally remain recoverable when it encounters them.
The problematic invariant was:
unbounded conversation content
->
unbounded display/list metadata
->
duplicated across title / preview / first_user_message
->
appears to reach / be materialized by thread-list / startup / navigation paths
A single large prompt should not be able to poison the entire Desktop profile.
This is consistent with the broader failure mode already discussed in related issues such as #29007 and #32371.
There is also evidence that this class is not unique to the custom automation.
After recovery, unrelated internal review/subagent threads still produced metadata in the approximately 46-54 KiB range, although this was far below the size required to reproduce the incident.
That appears related to #32588.
---
User-side mitigation applied
The autonomous workflow was changed so that complete history is no longer inserted into the initial controller prompt.
New design:
full context/history
-> external append-only files / archives
new controller first prompt
-> run ID
-> current task
-> bounded state summary
-> paths/references to full context
Hard limits were added:
controller first prompt:
target < 4 KiB
hard reject at >= 10 KiB
state snapshot:
hard reject at >= 128 KiB
Raw transcripts, previous snapshots, and tool outputs are no longer recursively embedded.
The system fails closed if a controller prompt exceeds the limit.
Regression tests also verify that prompt size does not increase with accumulated history.
---
Post-fix real-world canary
A real app-server canary was run through the same automation path.
Observed:
maximum controller prompt: 2,910 bytes
consecutive controller prompts: 2,910 -> 2,910 bytes
growth: 0 bytes
maximum state snapshot: 79,460 bytes
No transcript or raw-output content appeared inline.
New automation-generated thread metadata remained approximately 2.9 KiB.
After the test:
SQLite quick_check: OK
Event ID 1002: 0
MoAppHang: 0
Desktop restart: successful
This strongly supports the identified trigger chain.
---
Recovery observations
I did not restore the affected state_5.sqlite.
Instead, I created a fresh profile and restored persistent configuration separately from conversation history.
Configuration/workspace recovery deliberately excluded:
state_5.sqlite*
logs_2.sqlite*
thread_history databases
session_index
WAL / SHM
caches
temporary state
old plugin caches
Only selected user conversations were later restored from original JSONL session files.
Important app-server behavior observed during recovery
A copied legacy rollout JSONL was initially not obvious in the Desktop list.
However:
thread/read(threadId)successfully read the restored rollout.- Codex subsequently had a valid row for the thread in
state_5.sqlite. - Exact
thread/listqueries using:
searchTermsourceKindscwd- pagination/cursor
useStateDbOnly=true
successfully found the thread.
- Desktop could then navigate to and open it.
thread/resumewas not required.- No new turn was created.
A previous thread/list(limit=50) check had incorrectly suggested that the thread was invisible simply because it was not on the first result page.
This may be useful when diagnosing apparently missing legacy threads.
---
Final recovery validation
A selected set of recent user threads was restored after the automation fix.
Final result:
threads restored: 28
JSONL files restored: 29
SHA-256 matches: 29 / 29
temporary files: 0
No old SQLite database, index, or thread mapping was copied.
Post-recovery:
state_5.sqlite quick_check: OK
foreign-key errors: 0
restored thread rows: 28 / 28
maximum restored metadata: ~23 KiB
Event ID 1002: 0
Desktop representative open tests: PASS
The Desktop remained responsive.
---
Recovery-tool mistakes that were NOT Codex bugs
For clarity, two failures encountered while building the local recovery tooling were caused by my recovery scripts rather than Codex:
1. Bad file descriptor
The initial recovery helper used an incorrect file-descriptor lifecycle around temporary-file / flush / fsync handling on Windows.
It failed before committing the first restored session.
The transaction was fail-closed and left the Codex profile unchanged.
2. Overly strict "entire .codex must be static" assumption
The first helper assumed the entire profile must remain byte-for-byte static while validating a restore.
This was invalid when other Codex processes were active.
The recovery logic was changed to verify only the target file, target thread row, and attributable metadata.
These are included here only to distinguish local recovery-tool errors from the Codex failure itself.
---
Expected behavior
I would expect Codex to enforce an invariant similar to:
full conversation content belongs in rollout/session history
thread-list/display metadata must always remain bounded
In particular:
titleshould have a strict maximum display length.previewshould have a strict maximum preview length.first_user_messageshould either:
- be bounded, or
- not be selected/materialized on startup/list hot paths.
- A large valid prompt should not be duplicated unboundedly into all three fields.
- Startup/thread reconciliation should stream/page rows rather than eagerly materializing an arbitrarily large metadata table.
- Existing poisoned profiles should degrade gracefully instead of making Desktop unusable.
---
Suggested fixes
Metadata generation
Apply hard bounds when generating/upserting thread metadata.
For example, conceptually:
title: short display label
preview: bounded preview
first_user_message: bounded projection
The raw rollout JSONL should remain the source of truth for complete content.
Thread-list/startup paths
Avoid eager loading of all large text fields.
Prefer:
- paging/keyset iteration;
- selecting only fields actually required by the current UI;
- lazy retrieval of full content;
- bounded projections.
Repair path
A supported local repair/doctor workflow would be valuable.
For example, detect:
oversized thread metadata
metadata DB unexpectedly large
title == preview == full first prompt
and offer to rebuild bounded metadata from session history without deleting the underlying conversations.
Internal automatic threads
Completed internal review/subagent/automation threads should ideally either:
- use bounded metadata;
- be excluded from normal user thread-list hot paths; or
- be automatically archived when their lifecycle ends.
---
Minimal reproduction
A simplified reproduction should not require the original automation system.
Conceptually:
- Start a Codex thread through an app-server/CLI/VS Code-origin path.
- Make the first user message several hundred KiB long.
- Do not assign an explicit short title.
- Repeat enough times to produce many such threads.
- Inspect
state_5.sqlite. - Check whether:
title == first_user_message == preview
with each field containing the full prompt.
- Restart Codex Desktop and observe thread-list/startup performance.
I have not intentionally reproduced the full hang again after recovery because I did not want to re-poison a healthy profile.
---
Privacy / artifacts
I am intentionally not attaching:
- the original SQLite databases;
- raw session JSONL;
- logs containing conversation content;
- local workspace paths;
- thread IDs;
- authentication/plugin secrets;
- raw prompts.
Those artifacts contain private local information.
I can provide additional sanitized aggregate statistics, schemas, or narrowly redacted diagnostics if maintainers need them.
---
Related issues
Likely related:
- #21154
- #21211
- #24510
- #29007
- #32371
- #32588
- #35604
This may ultimately be a duplicate of the same underlying unbounded-metadata family, but I wanted to provide the Windows MoAppHang reproduction, the automation trigger chain, and the validated recovery experience.
2 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I took a closer look at #39966. I think the two issues are related, but they don't look like the same failure mode.
#39966 is mainly about high subagent concurrency together with OAuth/re-authentication loops, renderer hangs, and black-screen/GPU failures.
What I saw in #40255 was different:
MoAppHang/ Event ID 1002 during Desktop startup;state_5.sqlitegrew to ~524 MB, with thethreadstable alone at ~519 MB;title == preview == first_user_message;After bounding new automation-generated first prompts to ~2.9 KiB and rebuilding from a clean local state, Desktop started normally again and I saw no new Event ID 1002 /
MoAppHang.So there may be a broader shared problem around Desktop robustness with large local state, but #40255 seems to be a more specific case involving unbounded duplicated thread metadata in
state_5.sqlite.