jdx/mise · error

brew-cask: unresolved recovery for

Error message

brew-cask: unresolved recovery for {} still preserves its original at {}

What it means

Before touching a target, ensure_no_unresolved_flight_recovery scans $STATE/brew-cask/flight-recovery/*.recovery records. Finding one whose target matches with a backup path set means a previous install/upgrade/uninstall was interrupted after moving the original file aside: that original is still preserved at the printed backup path and the transaction never committed. mise refuses the new operation rather than destroy the stranded original.

Solutions

  1. Re-run the same mise brew command — pending recovery records are processed first and the operation then proceeds
  2. Or restore manually: move the printed backup file back onto the target path, then delete the matching *.recovery record
  3. If the target is disposable, delete both the target and the recovery record, then retry
  4. Do not delete only the record while the backup file exists — that loses the original

Example fix

# before: stale recovery record
$ mise brew install --cask foo   # refuses, names backup path

# after: manual resolve then retry
$ mv "<backup-path-from-error>" "<target-path-from-error>"
$ rm "<state>/brew-cask/flight-recovery/<hash>.recovery"
$ mise brew install --cask foo
Defensive patterns

Strategy: validation

Validate before calling

// Before installing, check for pending recovery records naming your targets.
fn pending_recovery_for(target: &Path) -> Option<PathBuf> {
    let dir = state_dir().join("brew-cask/flight-recovery");
    std::fs::read_dir(dir).ok()?.flatten().map(|e| e.path())
        .filter(|p| p.extension().is_some_and(|e| e == "recovery"))
        .find(|p| std::fs::read_to_string(p).ok()
            .and_then(|b| serde_json::from_str::<FlightRecoveryRecord>(&b).ok())
            .is_some_and(|r| r.target == target && r.backup.is_some()))
}

Try / catch

if let Err(e) = install_cask(&cask) {
    if let Some(msg) = e.to_string().strip_prefix("brew-cask: unresolved recovery for") {
        // surface the preserved-backup path to the user; do NOT auto-delete
    }
}

Prevention

When it happens

Trigger: A prior cask operation crashed, was Ctrl-C'd, or lost power mid-flight after the backup rename but before commit; the leftover .recovery JSON still names this target with a non-null backup field.

Common situations: Interrupted `mise` brew-cask installs or upgrades; CI jobs killed mid-run; laptop shutdown during install; anything that re-triggers the same target (reinstall/upgrade) afterward.

Related errors


AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22). Data as JSON: /api/errors/1d0f7cf6cd7d9a05. Report an issue: GitHub.

Appendix: source

Thrown at src/system/packages/brew/cask.rs:3300

        return Ok(());
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path
            .extension()
            .is_none_or(|extension| extension != "recovery")
        {
            continue;
        }
        let Ok(body) = file::read_to_string(&path) else {
            continue;
        };
        let Ok(record) = serde_json::from_str::<FlightRecoveryRecord>(&body) else {
            continue;
        };
        if record.target == target {
            if let Some(backup) = record.backup {
                bail!(
                    "brew-cask: unresolved recovery for {} still preserves its original at {}",
                    target.display(),
                    backup.display()
                );
            }
            bail!(
                "brew-cask: unresolved recovery for newly created target {}",
                target.display()
            );
        }
    }
    Ok(())
}

fn recover_flight_backups() -> Result<()> {
    let root = flight_recovery_root();
    recover_flight_backups_in(&root)
}

View on GitHub (pinned to 6f52dcdf99)