Client or integration
Codex App
Area
Installation or packaging
Summary
snapshotRolloutForRestore validates a rollout using readLatestSessionMeta, which returns the last session_meta line in the file regardless of its id. Forked/branched sessions append the parent thread's session_meta after their own, so meta.record.payload.id === entry.id fails and restore throws history_backup_rollout_postimage_mismatch.
Because preflightRestoreTargets is all-or-nothing, a handful of forked threads permanently blocks the restore of every entry in the manifest. ocx stop then exits 1 on every invocation, and ocx update aborts at the stop.status !== 0 gate in src/update/index.ts. Unlike #3008 (transient, "Codex app holds the DB busy"), this state never clears on retry — the user cannot update at all until the manifest is edited by hand.
On my machine 6 of 389 manifest entries were affected and had blocked every ocx stop / ocx update.
src/codex/history-provider.ts (v2.36.0):
// line 547
function snapshotRolloutForRestore(entry: CodexHistoryBackupEntry): RestoreRolloutSnapshot {
...
const latest = readLatestSessionMeta(entry.rolloutPath); // <-- id-agnostic
if (!latest
|| (!rolloutMatchesRestoreTuple(latest, entry, entry.modelProvider, entry.source)
&& !rolloutMatchesExpectedPostImage(latest, entry))) {
throw new CodexHistoryIntegrityError("history_backup_rollout_postimage_mismatch");
}
Both matchers require meta.record.payload.id === entry.id, so a foreign trailing session_meta can never satisfy either branch.
The id-aware helper already exists in the same file, 17 lines below the one being used:
readLatestSessionMeta — line 663 (used here)
readLatestSessionMetaForIdFromText — line 680 (not used here)
assertRestoreReadback (line 620) calls readLatestSessionMeta the same way and would hit the identical problem on the readback path.
Suggested fix: resolve the rollout's own session_meta by id in snapshotRolloutForRestore and assertRestoreReadback, e.g. a readLatestSessionMetaForId(path, entry.id) wrapper around the existing readLatestSessionMetaForIdFromText. Trailing parent-session metadata is normal for forked rollouts and is not an integrity violation.
Expected: a forked child rollout restores normally; a foreign trailing session_meta is not treated as an integrity violation.
Second, related defect: has_user_event drift on a non-relabelled row
rowMatchesExpectedPostImage (line 487) tolerates has_user_event drift only along the opencodex provider branch:
function rowMatchesExpectedPostImage(row: RestoreRowSnapshot, entry: CodexHistoryBackupEntry): boolean {
if (entry.modelProvider === "openai") {
const postHasUserEvent = hasFirstUserMessage(row.first_user_message) ? 1 : entry.hasUserEvent;
return rowMatchesRestoreTuple(row, "opencodex", entry.source, postHasUserEvent);
}
A thread captured into the manifest as openai / vscode / hasUserEvent=0 that opencodex never relabelled — provider still openai in both the DB and the rollout — but that the user then wrote in, ends up as openai / vscode / has_user_event=1. That matches neither rowMatchesRestoreTuple (drifted has_user_event) nor the post-image branch (provider is not opencodex), so preflightRestoreRows throws history_backup_postimage_mismatch — again blocking the whole manifest.
has_user_event is Codex-owned session state, not provenance opencodex writes. Treating its drift as an integrity violation looks wrong on any branch, and the restore path additionally forces it back to the manifest value.
Reproduction
- Run Codex Desktop with opencodex injected, so a
codex-history-backup-*.json manifest exists in $OPENCODEX_HOME.
- Fork/branch a thread in Codex Desktop. The child's rollout JSONL then contains two
session_meta records: its own at line 0 and the parent's at line 1, with distinct session_id/id.
- Run
ocx stop and check the exit code: it is 1, with Codex resume history could NOT be restored because the backup or restore target failed integrity checks.
- Run
ocx update: it aborts with could not stop the running proxy even though the proxy is fully stopped.
The abort message is misleading — there is no PID file, no runtime record, and no listener on the configured port. Only the history restore failed.
ocx doctor reports backup manifest or restore target failed integrity checks — manual review required and advises against re-running ocx sync, but offers no way forward.
Verified by replaying the preflight logic read-only against the manifest and ~/.codex/state_5.sqlite: all 389 entries had a matching threads row and a matching rollout_path; the 6 failures were exactly the forked rollouts, each rejected because the trailing session_meta carried the parent's id.
Workaround used: with the Codex app closed, reset the 6 affected threads rows from model_provider='opencodex' back to 'openai' (their rollout JSONLs were already native, so nothing to patch there), drop those entries from the manifest, and correct the drifted hasUserEvent on the one entry described above. ocx stop then exited 0, restored the remaining 383 entries normally, consumed the manifest, and ocx update completed.
Related: #3008 hits the same abort site in src/update/index.ts, but from a transient busy DB. The decoupling proposed there would unblock updates for this bug too, while the forked-rollout mismatch would still make the restore fail forever.
Version
2.36.0 (npm, tag latest; first hit on 2.33.0, code verified unchanged in 2.36.0)
Operating system
macOS 27.0.0 (arm64, Apple Silicon)
Provider and model
Not provider-specific.
Logs or error output
$ ocx stop
🛑 Service manager stopped (won't respawn).
⚠️ opencodex not present in Codex config. ⚠️ Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged.
$ echo $?
1
$ ocx update
opencodex v2.33.0 (installed via npm, tag latest)
⏹ Stopping the running proxy before updating...
🛑 Service manager stopped (won't respawn).
⚠️ opencodex not present in Codex config. ⚠️ Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged.
opencodex: could not stop the running proxy; aborting the update. Run 'ocx stop' and retry.
$ ocx doctor
Codex native-write coordinator
-- backup manifest or restore target failed integrity checks — manual review required
Hints
- The history manifest or its target is invalid or changed. Preserve both, inspect the
manifest/database/rollout identity, and do not repeatedly run 'ocx sync' until the
mismatch is understood. Untracked routed history is not relabeled.
Shape of an affected forked rollout JSONL (two session_meta records, distinct ids):
line 0 id=<child-uuid> session_id=<child-uuid> model_provider=openai source=vscode
line 1 id=<parent-uuid> session_id=<parent-uuid> model_provider=openai source=vscode
Environment details
- Codex CLI 0.147.0; Codex Desktop rollouts written by 0.151.0-alpha.7.1
- Node 24.16.0, bundled Bun 1.4.0
- Codex state DB:
state_5.sqlite, threads table
- Install: npm global, launchd service
Checks
Client or integration
Codex App
Area
Installation or packaging
Summary
snapshotRolloutForRestorevalidates a rollout usingreadLatestSessionMeta, which returns the lastsession_metaline in the file regardless of its id. Forked/branched sessions append the parent thread'ssession_metaafter their own, someta.record.payload.id === entry.idfails and restore throwshistory_backup_rollout_postimage_mismatch.Because
preflightRestoreTargetsis all-or-nothing, a handful of forked threads permanently blocks the restore of every entry in the manifest.ocx stopthen exits 1 on every invocation, andocx updateaborts at thestop.status !== 0gate insrc/update/index.ts. Unlike #3008 (transient, "Codex app holds the DB busy"), this state never clears on retry — the user cannot update at all until the manifest is edited by hand.On my machine 6 of 389 manifest entries were affected and had blocked every
ocx stop/ocx update.src/codex/history-provider.ts(v2.36.0):Both matchers require
meta.record.payload.id === entry.id, so a foreign trailingsession_metacan never satisfy either branch.The id-aware helper already exists in the same file, 17 lines below the one being used:
readLatestSessionMeta— line 663 (used here)readLatestSessionMetaForIdFromText— line 680 (not used here)assertRestoreReadback(line 620) callsreadLatestSessionMetathe same way and would hit the identical problem on the readback path.Suggested fix: resolve the rollout's own
session_metaby id insnapshotRolloutForRestoreandassertRestoreReadback, e.g. areadLatestSessionMetaForId(path, entry.id)wrapper around the existingreadLatestSessionMetaForIdFromText. Trailing parent-session metadata is normal for forked rollouts and is not an integrity violation.Expected: a forked child rollout restores normally; a foreign trailing
session_metais not treated as an integrity violation.Second, related defect:
has_user_eventdrift on a non-relabelled rowrowMatchesExpectedPostImage(line 487) tolerateshas_user_eventdrift only along theopencodexprovider branch:A thread captured into the manifest as
openai / vscode / hasUserEvent=0that opencodex never relabelled — provider stillopenaiin both the DB and the rollout — but that the user then wrote in, ends up asopenai / vscode / has_user_event=1. That matches neitherrowMatchesRestoreTuple(driftedhas_user_event) nor the post-image branch (provider is notopencodex), sopreflightRestoreRowsthrowshistory_backup_postimage_mismatch— again blocking the whole manifest.has_user_eventis Codex-owned session state, not provenance opencodex writes. Treating its drift as an integrity violation looks wrong on any branch, and the restore path additionally forces it back to the manifest value.Reproduction
codex-history-backup-*.jsonmanifest exists in$OPENCODEX_HOME.session_metarecords: its own at line 0 and the parent's at line 1, with distinctsession_id/id.ocx stopand check the exit code: it is1, withCodex resume history could NOT be restored because the backup or restore target failed integrity checks.ocx update: it aborts withcould not stop the running proxyeven though the proxy is fully stopped.The abort message is misleading — there is no PID file, no runtime record, and no listener on the configured port. Only the history restore failed.
ocx doctorreportsbackup manifest or restore target failed integrity checks — manual review requiredand advises against re-runningocx sync, but offers no way forward.Verified by replaying the preflight logic read-only against the manifest and
~/.codex/state_5.sqlite: all 389 entries had a matchingthreadsrow and a matchingrollout_path; the 6 failures were exactly the forked rollouts, each rejected because the trailingsession_metacarried the parent's id.Workaround used: with the Codex app closed, reset the 6 affected
threadsrows frommodel_provider='opencodex'back to'openai'(their rollout JSONLs were already native, so nothing to patch there), drop those entries from the manifest, and correct the driftedhasUserEventon the one entry described above.ocx stopthen exited 0, restored the remaining 383 entries normally, consumed the manifest, andocx updatecompleted.Related: #3008 hits the same abort site in
src/update/index.ts, but from a transient busy DB. The decoupling proposed there would unblock updates for this bug too, while the forked-rollout mismatch would still make the restore fail forever.Version
2.36.0 (npm, tag
latest; first hit on 2.33.0, code verified unchanged in 2.36.0)Operating system
macOS 27.0.0 (arm64, Apple Silicon)
Provider and model
Not provider-specific.
Logs or error output
Shape of an affected forked rollout JSONL (two
session_metarecords, distinct ids):Environment details
state_5.sqlite,threadstableChecks