BigPizzaV3/CodexPlusPlus · error
Unjournaled backup conflict
Error message
Unjournaled backup conflict
What it means
During `prepare` (invoked from `reconcile_locked`), Codex++ journals a pristine copy of the native browser service file in `original.mjs` under the state root before patching the runtime. If a backup file already exists but has no journal entry ("unjournaled"), its content must match the current runtime file byte-for-byte; a mismatch means the stored backup disagrees with what is actually deployed, so Codex++ refuses to proceed rather than overwrite either. This protects against corrupt or tampered state that could make later recovery restore the wrong original.
Solutions
- Delete the stale state directory `~/.codex/plugins/state/<key>/original.mjs` (the unjournaled backup) so prepare can re-snapshot the current runtime fresh.
- If the current runtime file is the correct pristine version, remove the whole `<key>` state dir and rerun reconcile.
- If the runtime file was modified by hand, restore it from the plugin cache (reinstall/update codex) so `current` matches the backup.
- Never edit `original.mjs` manually; let Codex++ recreate it.
Example fix
// before (shell) rm -f ~/.codex/plugins/state/<key>/journal.json # breaks journaling invariant // after # keep journal and backup consistent; to reset state: rm -rf ~/.codex/plugins/state/<key> && codex reconcile --browser-enabled
Defensive patterns
Strategy: validation
Validate before calling
let backup = state_root.join(key).join("original.mjs");
let journal = state_root.join(key).join("journal.json");
if backup.exists() && !journal.exists() {
let saved = std::fs::read(&backup)?;
let current = std::fs::read(runtime_root.join(key).join("service.mjs"))?;
if saved != current { /* delete stale backup or reinstall runtime before reconcile */ }
} Try / catch
match reconcile(&paths, true) {
Err(e) if e.to_string().contains("Unjournaled backup conflict") => {
// reset the per-key state dir and retry
std::fs::remove_dir_all(state_root.join(&key))?;
reconcile(&paths, true)?;
}
other => other?,
} Prevention
- Never delete journal.json while leaving original.mjs in place
- Reset the whole state_root/<key> directory instead of individual files
- Avoid running codex plugin updates concurrently with reconcile
- Keep CODEX_HOME on a local, non-synced filesystem
When it happens
Trigger: `reconcile(paths, true)` runs when `state_root/<key>/original.mjs` exists but `journal.json` does not (or was deleted), and `read_regular(backup) != current` — i.e. the runtime file was rewritten (e.g. by a codex upgrade) while a stale unjournaled backup remained on disk.
Common situations: A previous prepare crashed between writing `original.mjs` and `journal.json`, and the plugin cache changed since; the user manually deleted `journal.json` while the runtime auto-updated; disk-sync or backup-restore tools restored an old `original.mjs`.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- Candidate backup conflict
- Runtime changed outside Codex++; refusing to overwrite
- Ambiguous runtime selection; no cache was modified
- .codex-global-state.json changed while deleting thread
- Concurrent adapter upgrade
AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19).
Data as JSON: /api/errors/1c55988f3a4f3a92.
Report an issue: GitHub.
Appendix: source
Thrown at crates/codex-plus-core/src/native_browser.rs:412
ensure!(
read_regular(&target, MAX_SERVICE)? == current,
"Concurrent adapter upgrade"
);
let modified = UNIX_EPOCH
.checked_add(Duration::new(journal.modified_secs, journal.modified_nanos))
.context("Invalid recovery timestamp")?;
atomic_write_with_modified(&target, &original, Some(modified))?;
current = original;
}
ensure!(
sha(¤t) == contract.service_sha,
"Runtime changed outside Codex++"
);
}
{
let candidate = transform(¤t, &control, contract)?;
if backup.exists() {
ensure!(
read_regular(&backup, MAX_SERVICE)? == current,
"Unjournaled backup conflict"
);
} else {
write_new(&backup, ¤t)?;
}
let candidate_path = backup_dir.join(format!("candidate-{}.mjs", sha(&candidate)));
if candidate_path.exists() {
ensure!(
read_regular(&candidate_path, MAX_SERVICE)? == candidate,
"Candidate backup conflict"
);
} else {
write_new(&candidate_path, &candidate)?;
}
let modified = fs::metadata(&target)?
.modified()?
.duration_since(UNIX_EPOCH)?;View on GitHub (pinned to b1ed92e5e4)