BigPizzaV3/CodexPlusPlus · error
Concurrent runtime change
Error message
Concurrent runtime change
What it means
Immediately before the atomic candidate write, `prepare` re-reads the target service file and requires it to still equal the `current` bytes validated a moment earlier. If it changed in between, another process (codex itself, antivirus, a sync tool) is concurrently mutating the runtime cache; proceeding would race, so Codex++ aborts with this error.
Solutions
- Ensure no other codex/desktop instance is running or updating while reconcile runs, then retry.
- Retry the operation — the race window is tiny and a rerun usually succeeds.
- Exclude the codex plugin cache directory from antivirus and file-sync tools.
- Verify reconcile is only called from the owning launcher while holding the `owner.lock` (the lock error would otherwise surface first).
Example fix
// before
match reconcile(&paths, true) { Err(e) if is_concurrent_change(&e) => { /* ignore */ } }
// after
// back off and retry with no concurrent writers
std::thread::sleep(RETRY_DELAY);
reconcile(&paths, true)?; Defensive patterns
Strategy: retry
Validate before calling
// before calling, ensure no concurrent writers: // check no other codex/desktop process is running and no pending plugin updates assert_no_codex_processes()?; // e.g. pid-file / process check
Try / catch
match reconcile(&paths, true) {
Err(e) if e.to_string().contains("Concurrent runtime change") => {
std::thread::sleep(Duration::from_millis(250));
reconcile(&paths, true)?; // single retry after quiescing writers
}
other => other?,
} Prevention
- Run only one codex/launcher instance during reconcile
- Pause antivirus and file-sync for the codex directories
- Serialize all reconcile calls through the owning launcher
- Retry with backoff; the race window is tiny
When it happens
Trigger: `reconcile(paths, true)` where `read_regular(&target) != current` at the TOCTOU re-check just before `atomic_write(&target, &candidate)` — i.e. the file changed between the earlier read and this check.
Common situations: Two Codex++/codex processes patching the same plugin cache (lock held by another instance would normally prevent this, but direct codex updates bypass the lock); antivirus quarantine/rewrite; cloud-drive sync writing during reconcile.
Related errors
- Concurrent adapter upgrade
- .codex-global-state.json changed while deleting thread
- Concurrent recovery change
- File grew beyond the size limit
- global state changed while provider sync was being written
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/9f71a069c1cc53f5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/native_browser.rs:449
let journal = Journal {
schema: 1,
original_sha: contract.service_sha.clone(),
candidate_sha: sha(&candidate),
modified_secs: modified.as_secs(),
modified_nanos: modified.subsec_nanos(),
};
// Durable original and journal precede any runtime write.
atomic_write(&journal_path, &serde_json::to_vec(&journal)?)?;
}
let (journal, original, candidate) = recovery_material(paths, key, contract)?;
if current == candidate {
return Ok(());
}
ensure!(
current == original && sha(¤t) == journal.original_sha,
"Runtime changed outside Codex++; refusing to overwrite"
);
ensure!(
read_regular(&target, MAX_SERVICE)? == current,
"Concurrent runtime change"
);
atomic_write(&target, &candidate)?;
ensure!(
read_regular(&target, MAX_SERVICE)? == candidate,
"Runtime write verification failed"
);
Ok(())
}
fn recovery_material(
paths: &BrowserPaths,
key: &str,
contract: &RuntimeContract,
) -> Result<(Journal, Vec<u8>, Vec<u8>)> {
ensure!(key_valid(key), "Invalid recovery key");
let dir = paths.state_root.join(key);View on GitHub (pinned to b1ed92e5e4)