BigPizzaV3/CodexPlusPlus · error
External runtime change prevents recovery
Error message
External runtime change prevents recovery
What it means
`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.
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.
Example fix
// before # service.mjs was hand-patched while enabled reconcile(&paths, false)?; // -> External runtime change prevents recovery // after # let codex reinstall the plugin cache first codex repair-plugins reconcile(&paths, false)?;
Defensive patterns
Strategy: try-catch
Validate before calling
// before disabling, confirm each journaled runtime is in a known state
let current = std::fs::read(runtime_root.join(&key).join("service.mjs"))?;
let original = std::fs::read(state_root.join(&key).join("original.mjs"))?;
if current != original && current != candidate_bytes {
// external modification detected: repair/reinstall the plugin cache first
} Try / catch
match reconcile(&paths, false) {
Err(e) if e.to_string().contains("External runtime change prevents recovery") => {
// repair the plugin cache (codex reinstall/repair) so the runtime
// returns to a known state, then retry the disable
codex_repair_plugins()?;
reconcile(&paths, false)?;
}
other => other?,
} Prevention
- 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
When it happens
Trigger: `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.
Common situations: 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.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Runtime changed outside Codex++; refusing to overwrite
- Candidate backup conflict
- .codex-global-state.json changed while deleting thread
- Concurrent recovery change
- 待处理的一键换肤记录无效
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/ab072f3ded0b3f5e.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/native_browser.rs:512
for entry in fs::read_dir(&paths.state_root)? {
let entry = entry?;
let key = entry.file_name().to_string_lossy().to_string();
if !key_valid(&key) || keep == Some(key.as_str()) {
continue;
}
let dir = entry.path();
plain_path(&dir)?;
if !dir.join("journal.json").exists() {
continue;
}
let target = paths.runtime_root.join(&key).join(SERVICE);
if !target.exists() {
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!(View on GitHub (pinned to b1ed92e5e4)