Rollout JSONL files deleted at app-server process transition: 934 of 942 threads orphaned (Windows 26.721.4979)
What version of Codex is running?
Authoritative Desktop startup logs and the installed package identify:
- Windows package:
OpenAI.Codex_26.721.4979.0_x64__2p2nqsd0c76g0(confirmed atProgram Files\WindowsApps\...) - app self-reported version:
26.721.41059 - bundled app-server:
0.146.0-alpha.3.1 - CLI on PATH:
0.145.0
26.721.4979.0 (package) and 26.721.41059 (self-reported) are two version strings for the same release. Note that #35610 reports 26.721.41059, i.e. the same release.
Correction: an earlier revision of this report named 0.130.0-alpha.5. That string appears nowhere in local state and was wrong.
What subscription do you have?
Paid ChatGPT account
Which model were you using?
Not model-specific
What platform is your computer?
Windows 11 x64
What issue are you seeing?
Nearly all accumulated local Codex state was lost on 2026-07-26. Thread metadata largely survived in SQLite, but the rollout files referenced by those rows are absent from disk.
Observed after the incident:
state_5.sqlitecontains 942 thread rows (as captured in the preserved snapshot; the live DB has grown since with normal use)- 934 of 942 rollout paths referenced by those rows are missing; only 8 rollout JSONL files survive. All 942 paths are distinct, so these are file counts, not row-vs-file artifacts
- all 942
rollout_pathvalues point at the sameCODEX_HOME, so this is not a directory relocation — the files are genuinely absent - the database passes
PRAGMA quick_check config.tomlis now 617 bytes containing only auto-regenerated entries whose earliest timestamp is2026-07-26T22:24:10Z(all post-incident), with no user-authored model, approval, or MCP settings- no corresponding files were found in the Recycle Bin or File History
- the separate skills directory was not deleted, and the SQLite databases survived
- scope is an open question. An earlier revision of this report asserted the loss was selective to
CODEX_HOMEand not broader profile corruption. That was inference, not observation, and it is now withdrawn as a conclusion: 147 of 147 tracked files in a git repository rooted at the parent directory are also missing from the working tree, including whole directories. I could not date that deletion (USN journal window too short, no File History, empty Recycle Bin,.git/indexuntouched since13:02:02Zso no git operation caused it). It may be the same event or an unrelated earlier one. Flagging it because if the two are the same event, the mechanism below cannot be the explanation
Update launch (14:45Z) is distinct from the data loss (~22:00Z)
The first surviving Desktop startup log records:
2026-07-26T14:45:57.445Z: app launch14:45:58.919Z:[automation-project-migration]reports 24 automations migrated to two project targets14:45:59.734Z: five legacy automation project targets remapped14:46:18.858Z: package26.721.4979.0spawns its bundledcodex.exe14:46:22.365Z: app-server identifies itself as0.146.0-alpha.3.114:46:28.157Z: an earlyfs/readFilerequest fails withThe system cannot find the path specified. (os error 3)— the log does not name the path, so this is not evidence that the request targeted a rollout
An earlier revision of this report inferred from the above that filesystem state was already missing at first post-update initialization. That inference was wrong. Threads created at 16:24Z, 20:46Z, and 21:40Z — hours after this launch — also lost their rollout files, so those files existed after 14:46Z and were lost later.
The loss is bounded to an ~11-minute window
logs_2.sqlite survived and retains 225,476 rows from 2026-07-17 onward, covering the incident. All times UTC:
| Time | Event |
|---|---|
| 21:40:27 | Last thread created whose rollout file is missing |
| 21:53:24 | Last log line from the previous app-server process |
| 22:02:37–38 | Goals and memories DBs created (fresh migration rows) |
| 22:02:38 | New app-server startup — TUI terminal probe, Initialized |
| 22:02:39 | Update check recorded |
| 22:02:59 | .sandbox_migration marker written |
| 22:04:38 | First thread created whose rollout file survives |
The cutoff is clean: every thread created at or before 21:40:27Z lost its rollout file; every thread from 22:04:38Z onward retained it. The loss therefore occurred between 21:53:24Z and 22:04:38Z, a window containing exactly one app-server process transition — roughly seven hours after the update launch.
The loss left no log trace, and the Desktop logs for the window were destroyed by rotation
- Across all 225,476 app-server telemetry rows there are zero
remove_dirand zeroremove_fileentries, and no cleanup or migration entries anywhere in the startup window. ~934 user files became unreachable with no audit record. - Codex Desktop's high-volume logs rotate through five 10 MB files, giving roughly 50 minutes of retention. The incident-window
t0Desktop logs were therefore overwritten before they could be read. One low-volume thread log survived spanning15:00:41Z–23:59:13Z, but it records nothing between ~21:3xZ and22:04:26Z. For a data-loss class of bug this retention is too short to diagnose the incident; longer retention or a separate durable audit log would help materially.
Ruled out
.sandbox_migrationis not causal.codex-rs/execpolicy/src/sandbox_migration.rsis scoped to removing legacy allow rules from the sandbox policy file and atomically writing the marker. It touches no sessions, rollouts, or databases. The marker is temporal correlation only.- Memories/goals DBs are not evidence of loss. The memories DB's single
_sqlx_migrationsrow was installed at22:02:38Zand the goals DB's at22:02:37Z, meaning both files were created during the incident startup rather than emptied. By contrast the thread DB's migrations run incrementally from2026-03-14to2026-07-22, so that file is original. - "More than 100 automations gone" is withdrawn. The local automations DB holds 25 automation definitions and 385 automation runs, and all 25 definitions are present. Runs were conflated with definitions; the
automation-project-migrationline above reporting 24 migrated is consistent with ~25. - Local git activity is ruled out.
CODEX_HOME's parent is a git repository, so acheckout/reset/cleanwas a plausible external cause. It does not hold: no rollout path was ever tracked in any commit, the reflog shows no operation on 2026-07-26 after13:02Z, and every git command in the Desktop logs during the window is read-only (ls-files --others,status,merge-base,config --get).
Consistency with the mechanism proposed in #21196
@qex-xep's analysis in #21196 — rollout writers holding files open after unlink (NLINK=0), continuing to append to an unreachable fd, with state_db later treating the path as stale — fits this incident, conditional on the scope question above. It explains files Codex held open; it cannot explain deletion of files Codex never opened. On the evidence available for the rollout files specifically:
- Loss materialized precisely at an app-server process transition, not at an arbitrary point.
- Metadata survived intact while payloads vanished, with a sharp timestamp boundary.
- No delete syscall was ever logged, consistent with files being unlinked while held open rather than removed by an explicit cleanup pass.
The surviving Desktop logs do not show the windowsSandbox/setupStart / codex-windows-sandbox-setup.exe sequence that #21196 correlates with, so #21196 is a matching end-state and plausible related path, not a confirmed identical trigger.
Possibly relevant trigger: two Codex processes were sharing a single CODEX_HOME — the ordinary result of using the CLI in a terminal alongside the Desktop app, not an unusual setup.** July 26 thread rows interleave 0.145.0 and 0.146.0-alpha.3.1 within minutes of each other, so a stable CLI and the bundled alpha app-server were both writing to the same default directory. Note this concurrency cannot be sufficient on its own, or it would fire constantly for everyone running both. The rarer ingredient is the app-server process transition at 22:02:38Z. If one process's writer unlinks or rotates rollout files another process holds open across such a restart, that would reproduce this signature — and testing it would not require reproducing a destructive update.
What steps can reproduce the bug?
I do not have a disposable second installation with the pre-incident state, so I have not attempted to reproduce this destructively.
The observed sequence was:
- An established Windows Codex Desktop installation with substantial local history, configuration, and automations under the default
CODEX_HOME, used heavily with both the0.145.0CLI and the bundled0.146.0-alpha.3.1app-server writing to the same directory — a normal configuration when using the CLI and Desktop app together. - Update to Windows package
26.721.4979.0; first launch2026-07-26T14:45:57Z, which ran automation-project and legacy-config migrations. - Normal use continued for about seven hours; threads created throughout this period later lost their rollout files.
- The previous app-server process logged normally until
21:53:24Z; a new one started at22:02:38Z. - From that transition onward, 934 dangling thread rows remained in
state_5.sqlitewith their rollout JSONLs absent.
The most promising reproduction path is not the update itself but the concurrency case: run two Codex builds against one CODEX_HOME and restart one while the other holds rollout writers open.
What is the expected behavior?
An update, startup migration, sandbox setup, or recovery operation must preserve sessions, memories, automations, configuration, and other user state. Before any destructive or non-atomic transformation it should:
- create and verify a recoverable backup
- validate record/file counts and referential integrity
- retain or restore the original state on failure
- never leave hundreds of database references pointing to missing rollout files
- clearly report a failed migration/recovery instead of silently presenting a reset state
- log the path count and reason whenever rollout data is removed or orphaned, at INFO or above
- retain Desktop logs long enough to diagnose a data-loss event
Additional information
The current state is preserved byte-for-byte: consistent SQLite backups (WALs checkpointed, all four DBs return quick_check: ok), a 10,477-file SHA-256 manifest that verifies clean on spot-check, and — added after discovering the rotation problem — a copy of all surviving Desktop logs with its own manifest.
A redacted diagnostics bundle can be posted publicly on request: incident-window telemetry rows, _sqlx_migrations tables, row counts, and rollout-path basenames, with no transcript content, credentials, or project paths.
Related:
- #21196 — same core Windows signature (metadata survives, rollout payloads missing), plus a proposed patch adding writer locks and link-count checks. Larger data point here: 942 rows / 934 missing vs. 91 / 90. Happy to have this folded in there if maintainers prefer.
- #35610 — filed minutes before this report, also Windows, also release
26.721.41059, reporting project tasks/chats disappearing after an update and stating it was the second occurrence. Different granularity and no forensics, but same release, so plausibly the same root cause rather than a separate issue. Feedback ID019fa3af-c5f8-7281-9739-a4b8b9633da7is worth correlating with this incident and #21196.
10 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Research update (2026-07-27): GitHub suggested #35610, filed minutes before this report. That reporter is also on Windows and Desktop 26.721.41059, says project tasks/chats disappeared after an update, and states this is the second occurrence. This makes the 26.721 release family a live regression cluster rather than a single-machine anomaly. Separately, #21196 contains a cross-platform durability reproduction where rollout pathnames were unlinked while the live app-server retained open file descriptors and continued writing; SQLite metadata then outlived the missing JSONL path. A proposed patch added writer locks, link-count checks, recovery metadata, and protections against dropping the only thread row. Current upstream recorder source does not appear to contain the proposed st_nlink/unlinked-file guard. This is a plausible explanation for the metadata-survives/payload-disappears signature, but the process that unlinked this machine's rollout tree remains unproven. Please correlate #35610 feedback ID 019fa3af-c5f8-7281-9739-a4b8b9633da7 with this incident and #21196.
Correction and additional forensics
Two numbers in my original report were wrong. I've also since analyzed a telemetry database that survived the incident, which narrows the loss to an ~11-minute window and points at a mechanism already proposed in #21196.
Corrections
0.130.0-alpha.5is incorrect — it appears nowhere in local state. The installed CLI is0.145.0, andthreads.cli_versionfor July 26 activity alternates between0.145.0and0.146.0-alpha.3.1; every surviving thread row is0.146.0-alpha.3.1. Please triage against those._sqlx_migrationsrow was installed at2026-07-26T22:02:38Z, i.e. the file was created during the incident startup rather than emptied. Same for the goals DB (22:02:37Z). By contrast the thread DB's migrations run incrementally from2026-03-14to2026-07-22, so that file is original. Please disregard the "0 jobs / 0 outputs" bullet as evidence.config.tomlis now 617 bytes containing only auto-regenerated entries, earliest timestamp2026-07-26T22:24:10Z(all post-incident), with no user-authored model, approval, or MCP settings.The loss is bounded to ~11 minutes
logs_2.sqlitesurvived and retains 225,476 rows from2026-07-17onward, covering the incident. All times UTC:| Time | Event |
|---|---|
| 21:40:27 | Last thread created whose rollout file is missing |
| 21:53:24 | Last log line from the previous Codex process |
| 22:02:37–38 | Goals and memories DBs created (fresh migration rows) |
| 22:02:38 | New process startup — TUI terminal probe, app-server
Initialized|| 22:02:39 | Update check recorded |
| 22:02:59 |
.sandbox_migrationmarker written || 22:04:38 | First thread created whose rollout file survives |
The cutoff is clean: every thread created at or before
21:40:27Zlost its rollout file; every thread from22:04:38Zonward retained it. So the loss occurred between21:53:24Zand22:04:38Z, a window containing exactly one process transition.All 943
rollout_pathvalues point at the sameCODEX_HOME, so this is not a directory relocation — the files are genuinely absent.The deletion left no log trace
Across all 225,476 rows there are zero
remove_dirand zeroremove_fileentries, and no cleanup or migration entries anywhere in the startup window. Roughly 934 user files became unreachable with no audit record. I'd suggest that's independently worth fixing: any path that removes or orphans rollout data should log count and reason at INFO or above, otherwise incidents like this are undiagnosable after the fact.Consistency with the mechanism proposed in #21196
@qex-xep's analysis in #21196 — rollout writers holding files open after unlink (
NLINK=0), continuing to append to an unreachable fd, withstate_dblater treating the path as stale — fits this incident better than a migration-deletion theory:One environmental detail that may be a trigger and wasn't in my original report: two builds were sharing a single
CODEX_HOME. July 26 thread rows interleave0.145.0and0.146.0-alpha.3.1within minutes of each other, so a stable CLI and an alpha build were both writing to the same default directory. If one build's writer unlinks or rotates rollout files another build holds open, that would reproduce this signature. That seems worth testing directly, and would not require reproducing a destructive update.Also possibly relevant from #21196: its
read-acl-only mode: applying read ACLspass over user-profile app-state paths. If an ACL pass runs overCODEX_HOMEwhile rollout writers hold handles, Windows delete-pending semantics could produce the same outcome.Diagnostics
The preserved snapshot verifies clean on spot-check (10,477-file SHA-256 manifest plus consistent SQLite backups with checkpointed WALs). I can attach a redacted bundle here with no private channel needed: incident-window log rows,
_sqlx_migrationstables, row counts, and rollout-path basenames — no transcript content, no credentials, no project paths. Say the word and I'll post it.Re the duplicate flag on #35610: that report is a different granularity (project tasks disappearing, Codex App
26.721.41059, no forensics) and I don't think it's the same issue, though it may share a root cause. #21196 is the close match, and if maintainers prefer, I'm happy to have this closed and folded in there as a second data point.Follow-up: three corrections to my own comment above, and one new evidence problem
I've since read the Codex Desktop logs, which I had not examined when I posted the comment above. Corrections to that comment:
1. #35610 is probably the same issue, not a different one. I said I didn't think it was. That was wrong. The installed Windows package here is
OpenAI.Codex_26.721.4979.0_x64__2p2nqsd0c76g0, and the app self-reports26.721.41059— two version strings for one release. #35610 reports26.721.41059, i.e. the same release. Different symptom granularity and no forensics there, but same-release data loss on Windows within minutes of this report is more likely one root cause than two.2. The update launch and the data loss are ~7 hours apart. Desktop logs place the first post-update launch at
2026-07-26T14:45:57.445Z, with an[automation-project-migration]step at14:45:58.919Zreporting 24 automations migrated. But threads created at16:24Z,20:46Z, and21:40Zalso lost their rollout files, so those files existed well after that launch. The loss window remains21:53:24Z–22:04:38Z. The update is not the proximate cause; an app-server process transition is where the loss materializes.3. Local git activity is ruled out. Worth stating because
CODEX_HOME's parent here is a git repository, which makes an externalcheckout/reset/cleana reasonable alternative explanation that a maintainer would otherwise have to consider. It does not hold: no rollout path was ever tracked in any commit, the reflog shows no git operation on 2026-07-26 after13:02Z, and every git command in the Desktop logs during the window is read-only (ls-files --others,status,merge-base,config --get).New: Desktop log rotation destroyed the incident-window logs
This is a separate defect worth fixing on its own.
Codex Desktop's high-volume logs rotate through five 10 MB files. On this machine that is roughly 50 minutes of retention — the five files for the relevant process covered only
23:11Z–23:59Zby the time anyone looked. Thet0logs covering the21:53Z–22:04Zloss window had already been overwritten.One low-volume thread log survived unrotated, spanning
15:00:41Z–23:59:13Z. It is the only Desktop-side record covering the incident, and it logs nothing between roughly21:3xZ and22:04:26Z — the loss window is a gap in the only surviving Desktop log.For a data-loss bug class this retention is too short to diagnose an incident that is noticed even an hour later. Either longer retention, size-based rotation with more generations, or a small separate durable audit log for destructive filesystem operations would make these reports actionable instead of speculative.
I have now copied all surviving Desktop logs into the preservation snapshot with their own SHA-256 manifest, so no further rotation can destroy what remains. Happy to provide redacted excerpts from any window you name.
The issue body has been updated with the full corrected picture, including the authoritative version identification from the Desktop logs.
Re: the duplicate flag on #35610 — assessment, plus a correction to my own numbers
My "restated counts" were wrong; the original figures were right
In my first comment I "corrected" the counts to 943 rows / 934 missing / 9 surviving and described the original 942 / 934 / 8 as internally inconsistent. That was my error. Against the preserved snapshot — the actual evidence — the figures are:
rollout_pathvalues942 = 934 + 8, and every path is distinct, so these are true file counts. The original report was correct and consistent. My higher numbers came from querying the live database, which has kept growing with normal use since the snapshot. The issue body now uses the snapshot figures.
Worth flagging for anyone else doing this kind of forensics: query the frozen copy, not the live directory. I also briefly thought the local thread catalog was shrinking in real time; that was a set-vs-row-count artifact on a composite primary key, not data loss.
#35610 is probably not a duplicate of this issue
I said the opposite in my previous comment. Having now looked at the flagged issues properly, that was an overcorrection and I want to settle it with the actual reasoning rather than keep moving:
~/.codex/sessionsrollout JSONLs still present on disk. #35610's symptom belongs to that index-loss family; this one does not.019fa3af-c5f8-...is a UUIDv7 whose timestamp decodes to2026-07-27T13:07:13Z. This incident's loss window is2026-07-26T21:53:24Z–22:04:38Z, roughly 15 hours earlier.26.721.41059. But #35585 shows that same string as the macOSInfo.plistbundle version. It's a cross-platform build identifier, not a Windows-release correlate, so it doesn't establish a shared root cause.The bot's suggestion on #35610 itself — #35585 and #34782 — looks like the better cluster for that report. #34782 (closed) was a Windows↔WSL path-resolution regression on
26.715.72359that also produced missing sidebar history while files remained.So: I don't think this should be closed as a duplicate of #35610. It may still be worth folding into #21196, which is the only report matching this signature (metadata survives, payloads physically absent).
One genuine link at the catalog layer
The one thing that does connect this incident to the "empty list" family: the local thread catalog here (
local_thread_catalogin the app's dev DB) is in an unbuilt state —initial_build_complete = 0,watermark_updated_atNULL,last_full_reconciled_atNULL. It holds 63 rows covering 53 distinct threads, allsource_kind = 'vscode', withsource_created_atspanning only2026-06-26→2026-07-12. Nothing after July 12, against 942 threads instate_5.sqlite.So the catalog that backs the project/task UI never fully reconciled on this machine either. That's a plausible shared surface with #35610 without being the same defect. Also relevant to #21196's proposed fix: the catalog schema already carries a
missing_candidatecolumn, so the "payload may be gone" case is anticipated in the data model — worth checking whether it is actually maintained, since here all 63 rows havemissing_candidate = 0while 4 of them reference threads whose rollout files are missing.Leads tested and rejected
Recording these so nobody re-runs them:
unauthorizedstring hits in the incident window are telemetry field names (auth_retry_after_unauthorized="false") inside normal INFO rows, not failures. WARN/ERROR traffic in21:00Z–22:10Zis dominated by skills loader (72), analytics reducer (51), MCP client (24), and plugin manifest (14) — nothing filesystem-destructive..sandbox_migration— ruled out from source.macOS corroboration: same release, 1,517 orphaned threads
I am seeing the same missing-rollout end state on macOS with the same self-reported Desktop release as this issue.
Environment
26.721.41059(app build 5848)codex-cli 0.146.0-alpha.3.1User-visible symptom
Many older chats disappeared from the sidebar. Opening one affected pinned thread produces a blank page and this visible error:
The affected thread ID is
019ed304-d7b8-7d33-9839-d1b3db6b8b0d. Its title/index metadata survives, but its transcript does not.Local diagnostics
state_5.sqlite: 2,726 thread rowsarchived_sessions: 1,517session_index.jsonlstill contains the affected thread title and ID.notLoaded.thread not loaded.thread/resumefailing with JSON-RPC error-32600and the missing rollout path.sessions,archived_sessions, Trash, or Spotlight.The missing set is strongly date-clustered: almost all recorded rollouts from June 18 through July 14 are absent, July 15 is partially absent, while recorded rollouts from July 16 onward are present. This suggests a bulk retention/deletion event rather than isolated file corruption.
Why this matters for #35619
This is the same core signature as the Windows report:
The macOS reproduction means the underlying problem may not be Windows-sandbox-specific.
Please add durable auditing for rollout deletion/unlink, preserve verified backups before cleanup/migration, detect dangling
rollout_pathrows, and provide a supported recovery/export path instead of leaving affected chats blank and unresumable.macOS recovery/audit update: corrected user-task count
Correction to my earlier “1,517 orphaned threads” headline: the 1,517 missing rollout paths in
state_5.sqlitewere 1,510 internal subagent rollouts and 7 user-source rows. That raw count should not be read as 1,517 missing sidebar chats.A separate reconciliation against
session_index.jsonlexposed the user-visible loss more accurately:I also found no usable copy in Trash, local Time Machine snapshots, deleted-but-open file handles, or the July migration’s session-backup tree (that tree preserved directories but contained zero files).
So the corrected severity is not “1,517 user chats lost,” but it is still a confirmed cross-platform data-loss event on the same Desktop release: dozens of ordinary macOS task payloads remain missing while title/index metadata survives.
@mrlightsource-create Thanks—this materially strengthens the evidence for a cross-platform metadata-survives/payload-disappears failure mode. Your correction is important: we’ll treat 1,517 as a raw missing-path count, not as 1,517 missing user chats.
Before folding the macOS numbers into the issue body, could you provide the following from a frozen pre-recovery snapshot?
.jsonl.zstsiblings for the missing.jsonlpaths, including the two pinned examples.local_thread_store_compressionwas effectively enabled, plus any rollout-compression worker log or lock-file evidence.The reason for the extra compression check is that this release supports
.jsonl.zstcold rollouts, while the release tag appears not to include the later per-thread writer-ownership protections in PRs #34986 and #35031. That makes a cross-process cleanup/archive/compression race plausible across both operating systems, but it is not yet enough to claim an identical trigger.For comparison, our preserved Windows snapshot contains zero
.jsonl.zstfiles, so its 934 missing rollout paths are not compressed-sibling false positives.macOS frozen-snapshot follow-up
Thanks. I ran the requested checks against the preserved pre/bulk-recovery snapshot and the matching Desktop/app-server logs.
One checkpoint correction up front: 152 was a real live-query count at 2026-07-28T15:14:31Z, after a small targeted recovery pass and immediately before the bulk pass. The earlier frozen database/index pair itself yields 170 index-only IDs. I reconcile both below rather than treating them as one atomic snapshot.
1. Compressed siblings
Thus the macOS missing-path set is not a compressed-sibling false positive.
2. Compression feature / worker evidence
The installed bundled binary contains the implementation strings (<code>local_thread_store_compression</code>, <code>rollout-compression.lock</code>, <code>.rollout-compress-</code>, and <code>.jsonl.zst</code>), but its current feature report is:
~~~text
local_thread_store_compression under development false
~~~
Additional negatives:
Caveat: the incident-time <code>config.toml</code> was not preserved, and the current one was regenerated after the incident. Therefore I cannot prove that no session/remote override ever existed. The convergent filesystem, database, feature-list, and log evidence strongly indicates that the compression worker did not run, but I am treating that as a strong negative finding rather than absolute proof.
3. Update and process timeline
All timestamps UTC:
| Time | Evidence |
|---|---|
| 2026-07-25 19:09:01 | App-server PID 983 spawned, version <code>0.146.0-alpha.3</code>; telemetry begins at 19:09:17. |
| 2026-07-26 20:09:05 | Last telemetry row from PID 983. |
| 2026-07-26 20:10:18 | Desktop launches an app session later identifying itself as release <code>26.721.30844</code>. |
| 2026-07-26 20:10:19 | App-server PID 781 spawned, version <code>0.146.0-alpha.3</code>; telemetry begins at 20:10:42. |
| 2026-07-26 20:10:41 | Sparkle reports <code>update_available</code>, current build 5813. |
| 2026-07-27 03:17:41 | First observed missing-rollout failure, for Flux, while that old <code>26.721.30844</code> Desktop session/PID 781 was still active. |
| 2026-07-27 08:28:58 | Last telemetry row from PID 781. |
| 2026-07-27 08:28:59 | Sparkle reports <code>install_started</code>, current build 5813. |
| 2026-07-27 08:29:03 | New Desktop process launches; it spawns app-server PID 83080, version <code>0.146.0-alpha.3.1</code>. This brief process logs from 08:29:06–08:29:07. |
| 2026-07-27 14:56:29 | Next sustained Desktop launch; logs explicitly identify release <code>26.721.41059</code>, with app-server PID 875 / <code>0.146.0-alpha.3.1</code> logging from 14:56:53. |
The first observed failure therefore predates the Sparkle install transition by about 5 h 11 m. This rules out attributing the initial observed loss to the newly launched 26.721.41059 process. It does not identify when the paths were unlinked, and it does not rule out a race involving the older running Desktop process, another concurrent Codex process, or update staging. I do not have a logged unlink event.
4. Set reconciliation
Definitions:
Requested checkpoint table:
| Set | M (1,517) | X (152) | R (92) | U (87) |
|---|---:|---:|---:|---:|
| M (1,517) | 1,517 | 0 | 5 | 0 |
| X (152) | 0 | 152 | 65 | 87 |
| R (92) | 5 | 65 | 92 | 0 |
| U (87) | 0 | 87 | 0 | 87 |
Why the counts do not sum linearly:
Source-classification correction: my previous phrase “7 user-source rows” was too broad. The exact frozen classification is 1,510 internal/subagent, 3 source=exec, and 4 source=unknown. Among those seven non-subagent rows, only 2 explicitly have <code>thread_source=user</code>; the other 5 have NULL <code>thread_source</code>.
Recovery sources remain:
Total: 92.
5. Redacted scripts and outputs
These emit IDs only as short SHA-256 set digests and named pinned examples. They omit titles, transcript text, working directories, credentials, and project paths.
<details>
<summary>Python reconciliation script</summary>
~~~python
#!/usr/bin/env python3
"""Redacted set reconciliation for openai/codex#35619.
The script emits counts only. It intentionally omits task titles, transcript text,
working directories, and full rollout paths.
"""
from __future__ import annotations
import csv
import hashlib
import json
import re
import sqlite3
import sys
from pathlib import Path
UUID_RE = re.compile(
r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"
)
def ids_from_db(path: Path) -> set[str]:
with sqlite3.connect(path) as db:
return {row[0] for row in db.execute("SELECT id FROM threads")}
def sources_from_db(path: Path) -> dict[str, tuple[str, str | None]]:
with sqlite3.connect(path) as db:
return {
row[0]: (row[1], row[2])
for row in db.execute("SELECT id, source, thread_source FROM threads")
}
def ids_from_index(path: Path) -> set[str]:
result: set[str] = set()
with path.open(encoding="utf-8") as handle:
for line in handle:
if line.strip():
result.add(json.loads(line)["id"])
return result
def ids_from_tree(path: Path) -> set[str]:
result: set[str] = set()
if not path.exists():
return result
for item in path.rglob("*.jsonl"):
match = UUID_RE.search(item.name)
if match:
result.add(match.group(1))
return result
def category(source: str) -> str:
if source.startswith('{"subagent"'):
return "internal/subagent"
return f"non-subagent/source={source}"
def digest(values: set[str]) -> str:
payload = "\n".join(sorted(values)).encode()
return hashlib.sha256(payload).hexdigest()[:16]
def main() -> int:
if len(sys.argv) != 2:
print(f"usage: {Path(sys.argv[0]).name} SNAPSHOT_ROOT", file=sys.stderr)
return 64
root = Path(sys.argv[1])
pre_db = root / "databases/state_5.sqlite"
post_db = root / "post-recovery/databases/state_5.sqlite"
index_path = root / "metadata/session_index.jsonl"
post_index_path = root / "post-recovery/metadata/session_index.jsonl"
missing_path = root / "manifests/missing-rollouts.tsv"
unresolved_path = root / "manifests/final-unresolved-index-20260728.json"
s0 = ids_from_db(pre_db)
s1 = ids_from_db(post_db)
index_ids = ids_from_index(index_path)
post_index_ids = ids_from_index(post_index_path)
index_only = index_ids - s0
missing_ids: set[str] = set()
missing_rollout_paths: dict[str, Path] = {}
with missing_path.open(encoding="utf-8", newline="") as handle:
for row in csv.DictReader(handle, delimiter="\t"):
missing_ids.add(row["thread_id"])
missing_rollout_paths[row["thread_id"]] = Path(row["rollout_path"])
unresolved_doc = json.loads(unresolved_path.read_text(encoding="utf-8"))
unresolved = {row["id"] for row in unresolved_doc["absentDetails"]}
# The pre-import stage contains 38 candidates, of which 13 were already
# represented by live indexed copies and were quarantined as duplicates.
preimport = ids_from_tree(root / "staged-exact-preimport")
preimport_duplicates = ids_from_tree(
root / "quarantined-preimport-live-duplicates"
)
recovery_sources = {
"archive_exact": ids_from_tree(root / "staged-exact-archive-complete"),
"preimport_exact": preimport - preimport_duplicates,
"early_exact": ids_from_tree(root / "staged-rollouts"),
"inherited_events": ids_from_tree(root / "staged-reconstructed"),
"markdown_visible": ids_from_tree(root / "staged-markdown-recovery"),
}
recovered = set().union(*recovery_sources.values())
source_by_id = sources_from_db(pre_db)
missing_categories: dict[str, int] = {}
non_subagent_thread_source: dict[str, int] = {}
for thread_id in missing_ids:
source, thread_source = source_by_id[thread_id]
key = category(source)
missing_categories[key] = missing_categories.get(key, 0) + 1
if key.startswith("non-subagent"):
thread_source_key = thread_source or "NULL"
non_subagent_thread_source[thread_source_key] = (
non_subagent_thread_source.get(thread_source_key, 0) + 1
)
zst_matches = {
thread_id
for thread_id, rollout in missing_rollout_paths.items()
if Path(str(rollout) + ".zst").is_file()
}
named = {
"Controller": "019ed304-d7b8-7d33-9839-d1b3db6b8b0d",
"Flux": "019e9e81-3632-7bb1-8a45-22e75f8f5217",
}
sets = {
"M_missing_state_paths": missing_ids,
"X_index_only_pre": index_only,
"R_recovered": recovered,
"U_unresolved_post": unresolved,
}
print("BASE COUNTS")
print(f"pre_state_rows={len(s0)}")
print(f"post_state_rows={len(s1)}")
print(f"frozen_unique_index_ids={len(index_ids)}")
print(f"post_unique_index_ids={len(post_index_ids)}")
for name, values in sets.items():
print(f"{name}={len(values)} sha256_16={digest(values)}")
print("\nMISSING-PATH SOURCE CLASSIFICATION")
for key in sorted(missing_categories):
print(f"{key}={missing_categories[key]}")
for key in sorted(non_subagent_thread_source):
print(
f"non-subagent/thread_source={key}="
f"{non_subagent_thread_source[key]}"
)
print("\nRECOVERY SOURCE COUNTS")
for key, values in recovery_sources.items():
print(f"{key}={len(values)}")
print("\nPAIRWISE INTERSECTIONS")
names = list(sets)
for i, left in enumerate(names):
for right in names[i + 1 :]:
print(f"{left} & {right}={len(sets[left] & sets[right])}")
print("\nRECOVERY RECONCILIATION")
print(f"recovered_already_in_pre_state={len(recovered & s0)}")
print(f"recovered_added_to_post_state={len(recovered & (s1 - s0))}")
print(f"recovered_missing_from_post_state={len(recovered - s1)}")
print(f"index_only_resolved={len(index_only - unresolved)}")
print(f"index_only_still_unresolved={len(index_only & unresolved)}")
print(f"unresolved_not_pre_index_only={len(unresolved - index_only)}")
# The published 152 count came from a contemporaneous live query at
# 2026-07-28T15:14:31Z, after a small targeted recovery pass but before the
# bulk pass. The frozen DB/index pair is earlier and yields 170. Reconcile
# the two checkpoints without treating them as one atomic snapshot.
published_checkpoint_index_only = 152
late_resolved = published_checkpoint_index_only - len(unresolved)
early_resolved_from_frozen_index = len(index_only & recovered) - late_resolved
recovered_new_to_frozen_index = len((post_index_ids - index_ids) & recovered)
recovered_already_in_frozen_state = len(recovered & s0)
print("\nPUBLISHED 152 CHECKPOINT RECONCILIATION")
print(f"frozen_index_only={len(index_only)}")
print(f"resolved_before_published_checkpoint={early_resolved_from_frozen_index}")
print(f"published_checkpoint_index_only={published_checkpoint_index_only}")
print(f"resolved_after_published_checkpoint={late_resolved}")
print(f"final_unresolved={len(unresolved)}")
print(f"recovery_absent_from_frozen_index={recovered_new_to_frozen_index}")
print(
"recovered_total_decomposition="
f"{recovered_already_in_frozen_state}+"
f"{early_resolved_from_frozen_index}+"
f"{recovered_new_to_frozen_index}+{late_resolved}="
f"{recovered_already_in_frozen_state + early_resolved_from_frozen_index + recovered_new_to_frozen_index + late_resolved}"
)
print("\nREQUESTED CHECKPOINT INTERSECTION TABLE")
print("set M(1517) X(152) R(92) U(87)")
print(f"M(1517) 1517 0 {len(missing_ids & recovered)} 0")
print(f"X(152) 0 152 {late_resolved} {len(unresolved)}")
print(f"R(92) {len(missing_ids & recovered)} {late_resolved} 92 0")
print(f"U(87) 0 {len(unresolved)} 0 87")
print("\nZSTD SIBLING CHECK")
print(f"matching_jsonl_zst_siblings={len(zst_matches)}/{len(missing_ids)}")
for label, thread_id in named.items():
print(
f"{label.lower()}_in_missing_set={thread_id in missing_ids} "
f"zst_sibling_present={thread_id in zst_matches}"
)
invariants = {
"missing_manifest_is_subset_of_pre_state": missing_ids <= s0,
"unresolved_is_subset_of_pre_index_only": unresolved <= index_only,
"recovered_and_unresolved_are_disjoint": recovered.isdisjoint(unresolved),
"recovered_is_subset_of_post_state": recovered <= s1,
"published_checkpoint_partitions": (
late_resolved + len(unresolved) == published_checkpoint_index_only
),
"recovered_decomposition": (
recovered_already_in_frozen_state
== len(recovered)
),
}
print("\nINVARIANTS")
for key, value in invariants.items():
print(f"{key}={str(value).lower()}")
return 0 if all(invariants.values()) else 2
if __name__ == "__main__":
raise SystemExit(main())
~~~
</details>
<details>
<summary>Reconciliation output</summary>
~~~text
BASE COUNTS
pre_state_rows=2752
post_state_rows=2836
frozen_unique_index_ids=2122
post_unique_index_ids=2123
M_missing_state_paths=1517 sha256_16=0b86269f397d7813
X_index_only_pre=170 sha256_16=fd2804e5fd91b672
R_recovered=92 sha256_16=717a158194b4843f
U_unresolved_post=87 sha256_16=bc0daff1db44dfb3
MISSING-PATH SOURCE CLASSIFICATION
internal/subagent=1510
non-subagent/source=exec=3
non-subagent/source=unknown=4
non-subagent/thread_source=NULL=5
non-subagent/thread_source=user=2
RECOVERY SOURCE COUNTS
archive_exact=42
preimport_exact=25
early_exact=11
inherited_events=4
markdown_visible=10
PAIRWISE INTERSECTIONS
M_missing_state_paths & X_index_only_pre=0
M_missing_state_paths & R_recovered=5
M_missing_state_paths & U_unresolved_post=0
X_index_only_pre & R_recovered=83
X_index_only_pre & U_unresolved_post=87
R_recovered & U_unresolved_post=0
RECOVERY RECONCILIATION
recovered_already_in_pre_state=8
recovered_added_to_post_state=84
recovered_missing_from_post_state=0
index_only_resolved=83
index_only_still_unresolved=87
unresolved_not_pre_index_only=0
PUBLISHED 152 CHECKPOINT RECONCILIATION
frozen_index_only=170
resolved_before_published_checkpoint=18
published_checkpoint_index_only=152
resolved_after_published_checkpoint=65
final_unresolved=87
recovery_absent_from_frozen_index=1
recovered_total_decomposition=8+18+1+65=92
REQUESTED CHECKPOINT INTERSECTION TABLE
set M(1517) X(152) R(92) U(87)
M(1517) 1517 0 5 0
X(152) 0 152 65 87
R(92) 5 65 92 0
U(87) 0 87 0 87
ZSTD SIBLING CHECK
matching_jsonl_zst_siblings=0/1517
controller_in_missing_set=True zst_sibling_present=False
flux_in_missing_set=True zst_sibling_present=False
INVARIANTS
missing_manifest_is_subset_of_pre_state=true
unresolved_is_subset_of_pre_index_only=true
recovered_and_unresolved_are_disjoint=true
recovered_is_subset_of_post_state=true
published_checkpoint_partitions=true
recovered_decomposition=true
~~~
</details>
<details>
<summary>Telemetry SQL and output</summary>
~~~sql
.headers on
.mode column
-- Run against the frozen logs_2.sqlite copy.
SELECT
COUNT(*) AS total_rows,
SUM(instr(lower(COALESCE(feedback_log_body, '')),
'local_thread_store_compression') > 0) AS feature_name_hits,
SUM(instr(lower(COALESCE(feedback_log_body, '')),
'.jsonl.zst') > 0) AS zst_path_hits,
SUM(instr(lower(COALESCE(target, '') || ' ' ||
COALESCE(module_path, '') || ' ' ||
COALESCE(file, '')),
'compress') > 0) AS compression_component_hits
FROM logs;
-- Relevant app-server transitions. The first pair brackets an ordinary app
-- relaunch; the latter pair brackets the later Sparkle install transition.
SELECT
CASE process_uuid
WHEN 'pid:983:9f6453cb-912c-44c8-8373-44e3aac9988e' THEN 'prior_app_run'
WHEN 'pid:781:b510c90b-d68b-464a-9403-b22b4d4369a3' THEN 'old_release_30844'
WHEN 'pid:83080:1d30c935-2397-4d1c-8e10-8efb0581b6ab' THEN 'post_install_brief'
WHEN 'pid:875:9ee7acdd-1cdc-4f35-b54e-42aeb2049ee1' THEN 'new_release_41059'
END AS process_label,
COUNT(*) AS rows,
strftime('%Y-%m-%dT%H:%M:%SZ', MIN(ts), 'unixepoch') AS first_utc,
strftime('%Y-%m-%dT%H:%M:%SZ', MAX(ts), 'unixepoch') AS last_utc
FROM logs
WHERE process_uuid IN (
'pid:983:9f6453cb-912c-44c8-8373-44e3aac9988e',
'pid:781:b510c90b-d68b-464a-9403-b22b4d4369a3',
'pid:83080:1d30c935-2397-4d1c-8e10-8efb0581b6ab',
'pid:875:9ee7acdd-1cdc-4f35-b54e-42aeb2049ee1'
)
GROUP BY process_uuid
ORDER BY MIN(ts);
~~~
~~~text
total_rows feature_name_hits zst_path_hits compression_component_hits
---------- ----------------- ------------- --------------------------
609969 0 0 0
process_label rows first_utc last_utc
------------------ ------ -------------------- --------------------
prior_app_run 101522 2026-07-25T19:09:17Z 2026-07-26T20:09:05Z
old_release_30844 69035 2026-07-26T20:10:42Z 2026-07-27T08:28:58Z
post_install_brief 46 2026-07-27T08:29:06Z 2026-07-27T08:29:07Z
new_release_41059 2256 2026-07-27T14:56:53Z 2026-07-27T16:46:24Z
~~~
</details>
<details>
<summary>App feature-override SQL and output</summary>
~~~sql
.headers on
.mode column
-- Run against the frozen codex-dev.db copy.
SELECT feature_name, enabled, updated_at
FROM local_app_server_feature_enablement
ORDER BY feature_name;
~~~
~~~text
feature_name enabled updated_at
-------------- ------- -------------
remote_control 1 1779093154297
~~~
</details>
Bottom line: the macOS paths have no zstd siblings and there is no evidence that a compression worker ran. A cleanup/archive/compression race remains mechanically plausible because the writer-ownership protections were absent, but the local evidence does not establish compression as this machine's trigger. The earliest observed failure occurred under the older Desktop release before the logged install transition.
This mass-orphaning scenario (SQLite state DB retaining hundreds of thread entries whose backing rollout files were deleted or unlinked during process restart) is specifically modeled by Codex Rescue's inventory discovery engine.
You can inspect and reconcile the on-disk session inventory against the state DB using the read-only Alpha6 release:
Or list orphaned vs intact sessions:
It runs completely locally without modifying your files. If you run it, the finding codes and exact orphan counts would provide valuable field validation.