{"record":{"id":"a11436543d7e27e5","repo":"BigPizzaV3/CodexPlusPlus","slug":"concurrent-recovery-change","errorCode":null,"errorMessage":"Concurrent recovery change","messagePattern":"Concurrent recovery change","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/native_browser.rs","lineNumber":525,"sourceCode":"            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!(\n            read_regular(&target, MAX_SERVICE)? == original,\n            \"Recovery verification failed\"\n        );\n    }\n    Ok(())\n}\n\n/// No runtime operation occurs when this feature has never been enabled.\n/// Call only from the owning launcher, never from settings save or status inspection.\npub fn reconcile(paths: &BrowserPaths, enabled: bool) -> Result<BrowserStatus> {\n    reconcile_contract(paths, enabled, &RuntimeContract::pinned())\n}\n","sourceCodeStart":507,"sourceCodeEnd":543,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/b1ed92e5e4a2d74095d4b8db5af43cef7acba9c6/crates/codex-plus-core/src/native_browser.rs#L507-L543","documentation":"`restore_all` collects all pending restores first, then re-reads each target immediately before writing back the original (\"preflight every cache before restoring any\"). This per-target re-check (`read_regular(&target) == current`) catches races between the enumeration phase and the restore phase; if the file changed meanwhile, restoring would mix states from different points in time, so it aborts with this error.","triggerScenarios":"During the preflight loop of `restore_all` (triggered by `reconcile_locked`, e.g. disabling or rotating keys), `read_regular(&target) != current` for one of the pending targets — a writer touched that runtime file between the first read and the preflight.","commonSituations":"Codex desktop updating plugin caches concurrently with a disable/restore; two launcher instances reconciling simultaneously (lock contention should normally prevent this); sync/AV tools rewriting files mid-restore.","solutions":["Close other codex/desktop instances and any sync tools, then retry `reconcile(paths, false)`.","Simply retry — the race window is small and the operation is idempotent once quiescent.","Exclude the codex plugin cache and state directories from antivirus/file-sync interference.","Ensure reconcile is invoked only from the owning launcher path that holds `owner.lock`."],"exampleFix":"// before\n// restore while codex desktop is running\nreconcile(&paths, false)?;\n// after\n// quit codex desktop, then restore with retry\nfor _ in 0..3 {\n    match reconcile(&paths, false) {\n        Ok(s) => { break }\n        Err(_) => std::thread::sleep(RETRY_DELAY),\n    }\n}","handlingStrategy":"retry","validationCode":"// quiesce writers before a bulk restore\nassert_no_codex_processes()?;\n// optionally pre-check targets are still in the journaled state\nfor key in journaled_keys {\n    let cur = std::fs::read(runtime_root.join(&key).join(\"service.mjs\"))?;\n    if cur != original(&key)? && cur != candidate(&key)? { bail!(\"drifted: {}\", key); }\n}","typeGuard":null,"tryCatchPattern":"for attempt in 0..3 {\n    match reconcile(&paths, false) {\n        Ok(status) => break,\n        Err(e) if e.to_string().contains(\"Concurrent recovery change\") && attempt < 2 => {\n            std::thread::sleep(Duration::from_millis(250));\n        }\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["Stop codex desktop and other launchers before bulk disable/restore","Retry with short backoff — the race window is small","Exclude plugin cache and state dirs from sync/AV tools","Perform restores only through the lock-holding owning launcher"],"tags":["race-condition","recovery","concurrency"],"backgroundTag":"concurrent-modification","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"}