Rollout JSONL files deleted at app-server process transition: 934 of 942 threads orphaned (Windows 26.721.4979)

Open 💬 10 comments Opened Jul 27, 2026 by hummbl-dev
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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 at Program 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.sqlite contains 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_path values point at the same CODEX_HOME, so this is not a directory relocation — the files are genuinely absent
  • the database passes PRAGMA quick_check
  • config.toml is now 617 bytes containing only auto-regenerated entries whose earliest timestamp is 2026-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_HOME and 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/index untouched since 13:02:02Z so 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 launch
  • 14:45:58.919Z: [automation-project-migration] reports 24 automations migrated to two project targets
  • 14:45:59.734Z: five legacy automation project targets remapped
  • 14:46:18.858Z: package 26.721.4979.0 spawns its bundled codex.exe
  • 14:46:22.365Z: app-server identifies itself as 0.146.0-alpha.3.1
  • 14:46:28.157Z: an early fs/readFile request fails with The 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_dir and zero remove_file entries, 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 t0 Desktop logs were therefore overwritten before they could be read. One low-volume thread log survived spanning 15:00:41Z23:59:13Z, but it records nothing between ~21:3xZ and 22: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_migration is not causal. codex-rs/execpolicy/src/sandbox_migration.rs is 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_migrations row was installed at 22:02:38Z and the goals DB's at 22:02:37Z, meaning both files were created during the incident startup rather than emptied. By contrast the thread DB's migrations run incrementally from 2026-03-14 to 2026-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-migration line above reporting 24 migrated is consistent with ~25.
  • Local git activity is ruled out. CODEX_HOME's parent is a git repository, so a checkout/reset/clean was 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 after 13: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:

  1. An established Windows Codex Desktop installation with substantial local history, configuration, and automations under the default CODEX_HOME, used heavily with both the 0.145.0 CLI and the bundled 0.146.0-alpha.3.1 app-server writing to the same directory — a normal configuration when using the CLI and Desktop app together.
  2. Update to Windows package 26.721.4979.0; first launch 2026-07-26T14:45:57Z, which ran automation-project and legacy-config migrations.
  3. Normal use continued for about seven hours; threads created throughout this period later lost their rollout files.
  4. The previous app-server process logged normally until 21:53:24Z; a new one started at 22:02:38Z.
  5. From that transition onward, 934 dangling thread rows remained in state_5.sqlite with 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 ID 019fa3af-c5f8-7281-9739-a4b8b9633da7 is worth correlating with this incident and #21196.

View original on GitHub ↗

10 Comments

github-actions[bot] contributor · 1 month ago

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

  • #35610

Powered by Codex Action

hummbl-dev · 1 month ago

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.

hummbl-dev · 1 month ago

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

  • Version. 0.130.0-alpha.5 is incorrect — it appears nowhere in local state. The installed CLI is 0.145.0, and threads.cli_version for July 26 activity alternates between 0.145.0 and 0.146.0-alpha.3.1; every surviving thread row is 0.146.0-alpha.3.1. Please triage against those.
  • Automations. "More than 100 automations are gone" is withdrawn. The local automations DB holds 25 automation definitions and 385 automation runs, and all 25 definitions are still present. I conflated runs with definitions.
  • Counts, restated consistently: 943 thread rows, 934 with missing rollout files, 9 surviving rollout JSONLs.
  • Memories DB — likely not data loss. Its single _sqlx_migrations row was installed at 2026-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 from 2026-03-14 to 2026-07-22, so that file is original. Please disregard the "0 jobs / 0 outputs" bullet as evidence.
  • Configuration, more precisely: config.toml is now 617 bytes containing only auto-regenerated entries, earliest timestamp 2026-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.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 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_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. So the loss occurred between 21:53:24Z and 22:04:38Z, a window containing exactly one process transition.

All 943 rollout_path values point at the same CODEX_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_dir and zero remove_file entries, 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, with state_db later treating the path as stale — fits this incident better than a migration-deletion theory:

  • Loss materialized precisely at a 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.

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 interleave 0.145.0 and 0.146.0-alpha.3.1 within 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 ACLs pass over user-profile app-state paths. If an ACL pass runs over CODEX_HOME while 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_migrations tables, 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.

hummbl-dev · 1 month ago

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-reports 26.721.41059 — two version strings for one release. #35610 reports 26.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 at 14:45:58.919Z reporting 24 automations migrated. But threads created at 16:24Z, 20:46Z, and 21:40Z also lost their rollout files, so those files existed well after that launch. The loss window remains 21:53:24Z22: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 external checkout/reset/clean a 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 after 13: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:11Z23:59Z by the time anyone looked. The t0 logs covering the 21:53Z22:04Z loss window had already been overwritten.

One low-volume thread log survived unrotated, spanning 15:00:41Z23:59:13Z. It is the only Desktop-side record covering the incident, and it logs nothing between roughly 21:3xZ and 22: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.

hummbl-dev · 1 month ago

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:

  • 942 thread rows, 942 distinct rollout_path values
  • 934 missing, 8 surviving

942 = 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:

  • Opposite signatures. #35610 reports project task lists showing "No tasks" — an empty list. This issue reports the inverse: the lists/metadata largely survived (942 rows) while the payload files were physically deleted. #35585 makes that distinction explicit on macOS: sidebar history gone, ~/.codex/sessions rollout JSONLs still present on disk. #35610's symptom belongs to that index-loss family; this one does not.
  • Different times, different days. #35610's feedback ID 019fa3af-c5f8-... is a UUIDv7 whose timestamp decodes to 2026-07-27T13:07:13Z. This incident's loss window is 2026-07-26T21:53:24Z22:04:38Z, roughly 15 hours earlier.
  • The shared version string is weaker evidence than I claimed. I argued these must be related because both report 26.721.41059. But #35585 shows that same string as the macOS Info.plist bundle 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.72359 that 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_catalog in the app's dev DB) is in an unbuilt state — initial_build_complete = 0, watermark_updated_at NULL, last_full_reconciled_at NULL. It holds 63 rows covering 53 distinct threads, all source_kind = 'vscode', with source_created_at spanning only 2026-06-262026-07-12. Nothing after July 12, against 942 threads in state_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_candidate column, 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 have missing_candidate = 0 while 4 of them reference threads whose rollout files are missing.

Leads tested and rejected

Recording these so nobody re-runs them:

  • Service outage / auth failure (suggested by #35585's mention of an outage and "Reset and reinstall workspace"). Not supported. The unauthorized string hits in the incident window are telemetry field names (auth_retry_after_unauthorized="false") inside normal INFO rows, not failures. WARN/ERROR traffic in 21:00Z22:10Z is dominated by skills loader (72), analytics reducer (51), MCP client (24), and plugin manifest (14) — nothing filesystem-destructive.
  • Local git operation — ruled out; details in the issue body.
  • .sandbox_migration — ruled out from source.
mrlightsource-create · 1 month ago

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

  • macOS 26.5.1 (build 25F80), Apple Silicon / arm64
  • Codex/ChatGPT Desktop: 26.721.41059 (app build 5848)
  • Bundled app-server: codex-cli 0.146.0-alpha.3.1

User-visible symptom

Many older chats disappeared from the sidebar. Opening one affected pinned thread produces a blank page and this visible error:

Failed to resume chat
failed to resolve rollout path `$HOME/.codex/sessions/2026/06/16/rollout-2026-06-16T20-39-30-019ed304-d7b8-7d33-9839-d1b3db6b8b0d.jsonl`: file does not exist

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 rows
  • Rollout present at recorded path: 1,209
  • Rollout missing from both its recorded path and archived_sessions: 1,517
  • session_index.jsonl still contains the affected thread title and ID.
  • Global state still marks the affected ID as pinned.
  • The app-level thread list reports it as notLoaded.
  • Direct thread read returns thread not loaded.
  • Desktop logs show thread/resume failing with JSON-RPC error -32600 and the missing rollout path.
  • The exact JSONL was not found in sessions, archived_sessions, Trash, or Spotlight.
  • A second pinned thread also fails because its expected June 6 rollout file is absent.

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:

  • metadata/database rows survive;
  • rollout payload files disappear;
  • the UI silently loses chats or leaves them unresumable;
  • the same Desktop/app-server release is involved.

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_path rows, and provide a supported recovery/export path instead of leaving affected chats blank and unresumable.

mrlightsource-create · 1 month ago

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.sqlite were 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.jsonl exposed the user-visible loss more accurately:

  • Before recovery, 152 indexed task IDs were absent from the live thread database.
  • I recovered 92 distinct task histories from independent local sources: 78 exact JSONLs from migration/pre-import backups, 4 parent histories reconstructed from exact event streams inherited by child rollouts, and 10 visible transcripts rebuilt from a verified Markdown export.
  • After recovery, 87 indexed IDs still have neither a state row nor a rollout file: 57 ordinary tasks and 30 automation runs.
  • All 34 pinned Codex tasks are readable now, but several are readable only because of those external/reconstructed copies; the original rollout files remain absent.
  • The two headline failures from my first comment were recovered from inherited child event streams. That does not change the underlying finding that their original parent JSONLs disappeared.

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.

hummbl-dev · 1 month ago

@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?

  1. Counts of matching .jsonl.zst siblings for the missing .jsonl paths, including the two pinned examples.
  2. Whether local_thread_store_compression was effectively enabled, plus any rollout-compression worker log or lock-file evidence.
  3. App update/install time, first observed failure, and any app-server process-transition timestamps.
  4. A set-intersection table reconciling the 1,517 missing state rows, 152 index-only IDs, 92 recovered histories, and 87 unresolved indexed IDs.
  5. The redacted SQL/query script and output used for the source classification and reconciliation.

The reason for the extra compression check is that this release supports .jsonl.zst cold 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.zst files, so its 934 missing rollout paths are not compressed-sibling false positives.

mrlightsource-create · 1 month ago

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

  • Matching <code>.jsonl.zst</code> siblings for the 1,517 recorded missing <code>.jsonl</code> paths: 0 / 1,517.
  • Controller (<code>019ed304-d7b8-7d33-9839-d1b3db6b8b0d</code>): no sibling.
  • Flux (<code>019e9e81-3632-7bb1-8a45-22e75f8f5217</code>): no sibling.
  • A broader search across the current Codex home, preserved pre-import backup, July import trees, and frozen recovery snapshot found **zero <code>.jsonl.zst</code> files total*.

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:

  • Frozen <code>codex-dev.db.local_app_server_feature_enablement</code> has no compression override; its only row is <code>remote_control = 1</code>.
  • Frozen <code>logs_2.sqlite</code>: 609,969 rows; zero feature-name hits, zero <code>.jsonl.zst</code> path hits, and zero compression component/module/file hits.
  • Zero <code>rollout-compression.lock</code> files.
  • Zero <code>.rollout-compress-*</code> temporary files.
  • Zero lock files anywhere under the current/frozen session trees.
  • The preserved older configuration has no compression flag.

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:

  • M = 1,517 frozen state rows whose recorded rollout path was missing.
  • X = 152 index-only IDs at the published 15:14:31Z checkpoint.
  • R = 92 distinct recovered histories.
  • U = 87 final unresolved indexed IDs.

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:

  • M and X are disjoint by construction: M IDs still had state rows; X IDs were absent from state.
  • Five M histories were recovered from independent copies, hence M ∩ R = 5.
  • At the 152 checkpoint, 65 X histories were subsequently recovered and 87 remained unresolved.
  • The complete R decomposition is 8 + 18 + 1 + 65 = 92:
  • 8 recovered IDs were already represented in the frozen state DB;
  • 18 frozen index-only IDs were recovered before the 152 checkpoint;
  • 1 recovered ID was absent from the earlier frozen index and was added to both index and state;
  • 65 were recovered after the 152 checkpoint.
  • On the exact earlier frozen DB/index pair, index-only = 170, with 83 recovered and 87 unresolved.

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:

  • 42 exact archive JSONLs
  • 25 exact pre-import JSONLs
  • 11 exact early/staged JSONLs
  • 4 inherited child-event reconstructions
  • 10 complete visible Markdown reconstructions

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

  • early_resolved_from_frozen_index
  • recovered_new_to_frozen_index
  • late_resolved

== 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.

shleder · 9 days ago

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:

npx --yes codex-rescue doctor --all

Or list orphaned vs intact sessions:

npx --yes codex-rescue sessions --orphans

It runs completely locally without modifying your files. If you run it, the finding codes and exact orphan counts would provide valuable field validation.