farion1231/cc-switch · critical · anyhow::Error

; MiniMax Code Skill rollback failed: ; backups

Error message

{error}; MiniMax Code Skill rollback failed: {}; backups: {backups:?}

What it means

This error is thrown when a multi-destination skill update partially applied and then both the original operation AND the rollback of already-swapped destinations failed. The rollback loop tries to remove each new destination and rename the staged 'old' copy back; if any restore fails, the error is wrapped with the per-destination rollback failures and the paths of preserved temp backup directories (via TempDir::keep, since kept dirs are not auto-deleted). This signals a corrupted/half-updated skill installation that needs manual repair.

Solutions

  1. Inspect the 'backups: [...]' paths in the message — kept temp dirs (prefixed .mcode-skill-update-) contain the 'old' original copies; restore them manually to the destinations listed in the rollback failure.
  2. Close applications that may lock the skill directories (editors, file watchers, sync clients) and retry the update.
  3. Reinstall or re-sync the affected skill from source to overwrite the inconsistent state.
  4. Check filesystem permissions and free disk space on the destination volume.
  5. If backups are intact and irrelevant, delete the leftover .mcode-skill-update-* staging directories.

Example fix

// before: blindly retrying update after failure leaves mixed state
update_skills(targets)?;
// after: recover from kept backup dirs, then retry
for backup in parse_backups(err_message) {
    fs::rename(backup.join("old"), target_for(backup))?;
}
fs::remove_dir_all(backup)?;
update_skills(targets)?;
Defensive patterns

Strategy: try-catch

Validate before calling

fn can_update(dest: &Path) -> Result<(), String> {
    if !dest.parent().map(|p| p.exists()).unwrap_or(false) { return Err("parent missing".into()); }
    let probe = dest.parent().unwrap().join(".write-probe");
    fs::write(&probe, b"x").map_err(|e| e.to_string())?;
    let _ = fs::remove_file(&probe);
    Ok(())
}

Try / catch

match update_skills(targets) {
    Err(e) if e.to_string().contains("rollback failed") => {
        // parse "backups: [...]" from message; restore each <backup>/old to its destination
        recover_from_backups(&e.to_string())?;
    }
    Err(e) => return Err(e),
    Ok(v) => Ok(v),
}

Prevention

When it happens

Trigger: Calling the skill update/deploy routine that stages new copies and swaps them via fs::rename when: (1) an fs::rename or commit() fails mid-way (e.g. destination locked by a running editor/process, cross-device rename, permission issue), and (2) the restore() of an already-changed destination then also fails (e.g. remove_path fails on a locked dir, or the 'old' backup rename fails).

Common situations: The skill directory is open/locked by an IDE or file watcher during update; antivirus or sync tools (OneDrive/Dropbox) hold locks on the destination; partial permissions on the app skills directory; disk full preventing rename; the update succeeded for some destinations but the final commit step failed leaving multiple targets inconsistent.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


AI-assisted analysis of farion1231/cc-switch@06082e189d (2026-09-19). Data as JSON: /api/errors/aa1c82f055179ea3. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/src/services/skill.rs:1643

        })();
        if let Err(error) = result {
            let mut rollback_errors = Vec::new();
            for (destination, staging) in staged[..changed].iter().rev() {
                let restore = || -> Result<()> {
                    Self::remove_path(destination)?;
                    let old = staging.path().join("old");
                    if old.exists() || Self::is_symlink(&old) {
                        fs::rename(old, destination)?;
                    }
                    Ok(())
                };
                if let Err(restore_error) = restore() {
                    rollback_errors.push(format!("{}: {restore_error}", destination.display()));
                }
            }
            if !rollback_errors.is_empty() {
                let backups: Vec<_> = staged.into_iter().map(|(_, stage)| stage.keep()).collect();
                return Err(anyhow!(
                    "{error}; MiniMax Code Skill rollback failed: {}; backups: {backups:?}",
                    rollback_errors.join("; ")
                ));
            }
            return Err(error);
        }
        result
    }

    /// 为缺少 content_hash 的已安装 Skill 补算哈希
    pub fn backfill_content_hashes(db: &Arc<Database>) -> Result<usize> {
        let _state_guard = skill_state_write_guard();
        let skills = db.get_all_installed_skills()?;
        let ssot_dir = Self::get_ssot_dir()?;
        let mut count = 0;

        for skill in skills.values() {
            if skill.content_hash.is_some() {

View on GitHub (pinned to 06082e189d)