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
- 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.
- Close applications that may lock the skill directories (editors, file watchers, sync clients) and retry the update.
- Reinstall or re-sync the affected skill from source to overwrite the inconsistent state.
- Check filesystem permissions and free disk space on the destination volume.
- 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
- Close editors, watchers and cloud-sync tools that touch skill directories before updating
- Run updates with sufficient permissions on the target volume
- Keep enough free disk space for staged temp copies
- Retry the failed update promptly — kept backup dirs are the recovery source
- Prefer per-skill (single-destination) updates to limit rollback blast radius
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
- Imported Skills; skipped MiniMax Code entries
- MCode Skill 与托管副本内容不同,拒绝替换链接
- Restore target already exists
- Skill 存储目录不能与 的 Skills 目录相同
- Skill backup is not a directory
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)