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

  1. Delete the stale state directory `~/.codex/plugins/state/<key>/original.mjs` (the unjournaled backup) so prepare can re-snapshot the current runtime fresh.
  2. If the current runtime file is the correct pristine version, remove the whole `<key>` state dir and rerun reconcile.
  3. If the runtime file was modified by hand, restore it from the plugin cache (reinstall/update codex) so `current` matches the backup.
  4. 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

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


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(&current) == contract.service_sha,
            "Runtime changed outside Codex++"
        );
    }
    {
        let candidate = transform(&current, &control, contract)?;
        if backup.exists() {
            ensure!(
                read_regular(&backup, MAX_SERVICE)? == current,
                "Unjournaled backup conflict"
            );
        } else {
            write_new(&backup, &current)?;
        }
        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)