BigPizzaV3/CodexPlusPlus · error
Concurrent recovery change
Error message
Concurrent recovery change
What it means
`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.
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`.
Example fix
// before
// restore while codex desktop is running
reconcile(&paths, false)?;
// after
// quit codex desktop, then restore with retry
for _ in 0..3 {
match reconcile(&paths, false) {
Ok(s) => { break }
Err(_) => std::thread::sleep(RETRY_DELAY),
}
} Defensive patterns
Strategy: retry
Validate before calling
// quiesce writers before a bulk restore
assert_no_codex_processes()?;
// optionally pre-check targets are still in the journaled state
for key in journaled_keys {
let cur = std::fs::read(runtime_root.join(&key).join("service.mjs"))?;
if cur != original(&key)? && cur != candidate(&key)? { bail!("drifted: {}", key); }
} Try / catch
for attempt in 0..3 {
match reconcile(&paths, false) {
Ok(status) => break,
Err(e) if e.to_string().contains("Concurrent recovery change") && attempt < 2 => {
std::thread::sleep(Duration::from_millis(250));
}
Err(e) => return Err(e),
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Concurrent adapter upgrade
- Concurrent runtime change
- File grew beyond the size limit
- bridge context is not initialized
- bridge context lock poisoned
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/a11436543d7e27e5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/native_browser.rs:525
continue; // Desktop owns cache deletion; never resurrect an obsolete runtime.
}
let (journal, original, candidate) = recovery_material(paths, &key, contract)?;
guards.extend(pin_parents(&target)?);
let current = read_regular(&target, MAX_SERVICE)?;
ensure!(
current == original || current == candidate,
"External runtime change prevents recovery"
);
if current == candidate {
let modified = UNIX_EPOCH
.checked_add(Duration::new(journal.modified_secs, journal.modified_nanos))
.context("Invalid recovery timestamp")?;
pending.push((target, modified, original, current));
}
}
// Preflight every cache before restoring any, independent of directory enumeration order.
for (target, modified, original, current) in pending {
ensure!(
read_regular(&target, MAX_SERVICE)? == current,
"Concurrent recovery change"
);
atomic_write_with_modified(&target, &original, Some(modified))?;
ensure!(
read_regular(&target, MAX_SERVICE)? == original,
"Recovery verification failed"
);
}
Ok(())
}
/// No runtime operation occurs when this feature has never been enabled.
/// Call only from the owning launcher, never from settings save or status inspection.
pub fn reconcile(paths: &BrowserPaths, enabled: bool) -> Result<BrowserStatus> {
reconcile_contract(paths, enabled, &RuntimeContract::pinned())
}
View on GitHub (pinned to b1ed92e5e4)