{"record":{"id":"1c55988f3a4f3a92","repo":"BigPizzaV3/CodexPlusPlus","slug":"unjournaled-backup-conflict","errorCode":null,"errorMessage":"Unjournaled backup conflict","messagePattern":"Unjournaled backup conflict","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/native_browser.rs","lineNumber":412,"sourceCode":"            ensure!(\n                read_regular(&target, MAX_SERVICE)? == current,\n                \"Concurrent adapter upgrade\"\n            );\n            let modified = UNIX_EPOCH\n                .checked_add(Duration::new(journal.modified_secs, journal.modified_nanos))\n                .context(\"Invalid recovery timestamp\")?;\n            atomic_write_with_modified(&target, &original, Some(modified))?;\n            current = original;\n        }\n        ensure!(\n            sha(&current) == contract.service_sha,\n            \"Runtime changed outside Codex++\"\n        );\n    }\n    {\n        let candidate = transform(&current, &control, contract)?;\n        if backup.exists() {\n            ensure!(\n                read_regular(&backup, MAX_SERVICE)? == current,\n                \"Unjournaled backup conflict\"\n            );\n        } else {\n            write_new(&backup, &current)?;\n        }\n        let candidate_path = backup_dir.join(format!(\"candidate-{}.mjs\", sha(&candidate)));\n        if candidate_path.exists() {\n            ensure!(\n                read_regular(&candidate_path, MAX_SERVICE)? == candidate,\n                \"Candidate backup conflict\"\n            );\n        } else {\n            write_new(&candidate_path, &candidate)?;\n        }\n        let modified = fs::metadata(&target)?\n            .modified()?\n            .duration_since(UNIX_EPOCH)?;","sourceCodeStart":394,"sourceCodeEnd":430,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/native_browser.rs#L394-L430","documentation":"During `prepare` (invoked from `reconcile_locked`), Codex++ journals a pristine copy of the native browser service file in `original.mjs` under the state root before patching the runtime. If a backup file already exists but has no journal entry (\"unjournaled\"), its content must match the current runtime file byte-for-byte; a mismatch means the stored backup disagrees with what is actually deployed, so Codex++ refuses to proceed rather than overwrite either. This protects against corrupt or tampered state that could make later recovery restore the wrong original.","triggerScenarios":"`reconcile(paths, true)` runs when `state_root/<key>/original.mjs` exists but `journal.json` does not (or was deleted), and `read_regular(backup) != current` — i.e. the runtime file was rewritten (e.g. by a codex upgrade) while a stale unjournaled backup remained on disk.","commonSituations":"A previous prepare crashed between writing `original.mjs` and `journal.json`, and the plugin cache changed since; the user manually deleted `journal.json` while the runtime auto-updated; disk-sync or backup-restore tools restored an old `original.mjs`.","solutions":["Delete the stale state directory `~/.codex/plugins/state/<key>/original.mjs` (the unjournaled backup) so prepare can re-snapshot the current runtime fresh.","If the current runtime file is the correct pristine version, remove the whole `<key>` state dir and rerun reconcile.","If the runtime file was modified by hand, restore it from the plugin cache (reinstall/update codex) so `current` matches the backup.","Never edit `original.mjs` manually; let Codex++ recreate it."],"exampleFix":"// before (shell)\nrm -f ~/.codex/plugins/state/<key>/journal.json   # breaks journaling invariant\n// after\n# keep journal and backup consistent; to reset state:\nrm -rf ~/.codex/plugins/state/<key> && codex reconcile --browser-enabled","handlingStrategy":"validation","validationCode":"let backup = state_root.join(key).join(\"original.mjs\");\nlet journal = state_root.join(key).join(\"journal.json\");\nif backup.exists() && !journal.exists() {\n    let saved = std::fs::read(&backup)?;\n    let current = std::fs::read(runtime_root.join(key).join(\"service.mjs\"))?;\n    if saved != current { /* delete stale backup or reinstall runtime before reconcile */ }\n}","typeGuard":null,"tryCatchPattern":"match reconcile(&paths, true) {\n    Err(e) if e.to_string().contains(\"Unjournaled backup conflict\") => {\n        // reset the per-key state dir and retry\n        std::fs::remove_dir_all(state_root.join(&key))?;\n        reconcile(&paths, true)?;\n    }\n    other => other?,\n}","preventionTips":["Never delete journal.json while leaving original.mjs in place","Reset the whole state_root/<key> directory instead of individual files","Avoid running codex plugin updates concurrently with reconcile","Keep CODEX_HOME on a local, non-synced filesystem"],"tags":["file-conflict","state-management","native-browser"],"backgroundTag":"file-already-exists","analyzedSha":"b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6","analyzedAt":"2026-09-19T23:35:21.129Z","contentChangedAt":"2026-09-19T23:35:21.129Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}