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

  1. Close other codex/desktop instances and any sync tools, then retry `reconcile(paths, false)`.
  2. Simply retry — the race window is small and the operation is idempotent once quiescent.
  3. Exclude the codex plugin cache and state directories from antivirus/file-sync interference.
  4. 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

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


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)