{"record":{"id":"ab072f3ded0b3f5e","repo":"BigPizzaV3/CodexPlusPlus","slug":"external-runtime-change-prevents-recovery","errorCode":null,"errorMessage":"External runtime change prevents recovery","messagePattern":"External runtime change prevents recovery","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/native_browser.rs","lineNumber":512,"sourceCode":"    for entry in fs::read_dir(&paths.state_root)? {\n        let entry = entry?;\n        let key = entry.file_name().to_string_lossy().to_string();\n        if !key_valid(&key) || keep == Some(key.as_str()) {\n            continue;\n        }\n        let dir = entry.path();\n        plain_path(&dir)?;\n        if !dir.join(\"journal.json\").exists() {\n            continue;\n        }\n        let target = paths.runtime_root.join(&key).join(SERVICE);\n        if !target.exists() {\n            continue; // Desktop owns cache deletion; never resurrect an obsolete runtime.\n        }\n        let (journal, original, candidate) = recovery_material(paths, &key, contract)?;\n        guards.extend(pin_parents(&target)?);\n        let current = read_regular(&target, MAX_SERVICE)?;\n        ensure!(\n            current == original || current == candidate,\n            \"External runtime change prevents recovery\"\n        );\n        if current == candidate {\n            let modified = UNIX_EPOCH\n                .checked_add(Duration::new(journal.modified_secs, journal.modified_nanos))\n                .context(\"Invalid recovery timestamp\")?;\n            pending.push((target, modified, original, current));\n        }\n    }\n    // Preflight every cache before restoring any, independent of directory enumeration order.\n    for (target, modified, original, current) in pending {\n        ensure!(\n            read_regular(&target, MAX_SERVICE)? == current,\n            \"Concurrent recovery change\"\n        );\n        atomic_write_with_modified(&target, &original, Some(modified))?;\n        ensure!(","sourceCodeStart":494,"sourceCodeEnd":530,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/native_browser.rs#L494-L530","documentation":"`restore_all` restores every journaled runtime back to its original (unpatched) content, but only if the current runtime file equals either the recorded original or the recorded candidate — the only two states Codex++ is responsible for. If the file holds anything else, an external actor modified the patched runtime and Codex++ cannot safely restore (it would destroy unknown changes or restore onto a foreign base), so it aborts.","triggerScenarios":"`restore_all` (from `reconcile_locked`, e.g. disabling the feature) reads a target service file whose bytes match neither `original` nor `candidate` from the journal — the runtime was edited/replaced externally while patch state existed.","commonSituations":"Codex/plugin auto-update replaced the service file between enable and disable; user or another tool patched the .mjs directly; a partial external write left the file in a mixed state.","solutions":["Reinstall/repair the codex plugin cache so the runtime returns to a known (original) state, then run reconcile(disabled) again.","Delete the obsolete runtime cache directory (desktop owns cache deletion) so restore_all skips it via the `!target.exists()` path.","Clear `state_root/<key>` journal state once the runtime is consistent, then retry.","Upgrade Codex++ if the runtime version changed so the contract/journal matches the new original."],"exampleFix":"// before\n# service.mjs was hand-patched while enabled\nreconcile(&paths, false)?;  // -> External runtime change prevents recovery\n// after\n# let codex reinstall the plugin cache first\ncodex repair-plugins\nreconcile(&paths, false)?;","handlingStrategy":"try-catch","validationCode":"// before disabling, confirm each journaled runtime is in a known state\nlet current = std::fs::read(runtime_root.join(&key).join(\"service.mjs\"))?;\nlet original = std::fs::read(state_root.join(&key).join(\"original.mjs\"))?;\nif current != original && current != candidate_bytes {\n    // external modification detected: repair/reinstall the plugin cache first\n}","typeGuard":null,"tryCatchPattern":"match reconcile(&paths, false) {\n    Err(e) if e.to_string().contains(\"External runtime change prevents recovery\") => {\n        // repair the plugin cache (codex reinstall/repair) so the runtime\n        // returns to a known state, then retry the disable\n        codex_repair_plugins()?;\n        reconcile(&paths, false)?;\n    }\n    other => other?,\n}","preventionTips":["Disable the feature before upgrading codex or the plugin cache","Never hand-edit files in the plugin cache while patching is enabled","After an external runtime change, reset state_root/<key> and re-enable","Let the desktop app own cache deletion for obsolete runtimes"],"tags":["external-modification","recovery","state-management"],"backgroundTag":"checksum-mismatch","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"}