{"record":{"id":"aa1c82f055179ea3","repo":"farion1231/cc-switch","slug":"error-minimax-code-skill-rollback-failed-backups-backups","errorCode":null,"errorMessage":"{error}; MiniMax Code Skill rollback failed: {}; backups: {backups:?}","messagePattern":"(.+?); MiniMax Code Skill rollback failed: (.+?); backups: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"critical","filePath":"src-tauri/src/services/skill.rs","lineNumber":1643,"sourceCode":"        })();\n        if let Err(error) = result {\n            let mut rollback_errors = Vec::new();\n            for (destination, staging) in staged[..changed].iter().rev() {\n                let restore = || -> Result<()> {\n                    Self::remove_path(destination)?;\n                    let old = staging.path().join(\"old\");\n                    if old.exists() || Self::is_symlink(&old) {\n                        fs::rename(old, destination)?;\n                    }\n                    Ok(())\n                };\n                if let Err(restore_error) = restore() {\n                    rollback_errors.push(format!(\"{}: {restore_error}\", destination.display()));\n                }\n            }\n            if !rollback_errors.is_empty() {\n                let backups: Vec<_> = staged.into_iter().map(|(_, stage)| stage.keep()).collect();\n                return Err(anyhow!(\n                    \"{error}; MiniMax Code Skill rollback failed: {}; backups: {backups:?}\",\n                    rollback_errors.join(\"; \")\n                ));\n            }\n            return Err(error);\n        }\n        result\n    }\n\n    /// 为缺少 content_hash 的已安装 Skill 补算哈希\n    pub fn backfill_content_hashes(db: &Arc<Database>) -> Result<usize> {\n        let _state_guard = skill_state_write_guard();\n        let skills = db.get_all_installed_skills()?;\n        let ssot_dir = Self::get_ssot_dir()?;\n        let mut count = 0;\n\n        for skill in skills.values() {\n            if skill.content_hash.is_some() {","sourceCodeStart":1625,"sourceCodeEnd":1661,"githubUrl":"https://github.com/farion1231/cc-switch/blob/06082e189d65e6d6dbadc35dacdac1ce6c79d89a/src-tauri/src/services/skill.rs#L1625-L1661","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: blindly retrying update after failure leaves mixed state\nupdate_skills(targets)?;\n// after: recover from kept backup dirs, then retry\nfor backup in parse_backups(err_message) {\n    fs::rename(backup.join(\"old\"), target_for(backup))?;\n}\nfs::remove_dir_all(backup)?;\nupdate_skills(targets)?;","handlingStrategy":"try-catch","validationCode":"fn can_update(dest: &Path) -> Result<(), String> {\n    if !dest.parent().map(|p| p.exists()).unwrap_or(false) { return Err(\"parent missing\".into()); }\n    let probe = dest.parent().unwrap().join(\".write-probe\");\n    fs::write(&probe, b\"x\").map_err(|e| e.to_string())?;\n    let _ = fs::remove_file(&probe);\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"match update_skills(targets) {\n    Err(e) if e.to_string().contains(\"rollback failed\") => {\n        // parse \"backups: [...]\" from message; restore each <backup>/old to its destination\n        recover_from_backups(&e.to_string())?;\n    }\n    Err(e) => return Err(e),\n    Ok(v) => Ok(v),\n}","preventionTips":["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"],"tags":["filesystem","rollback","atomic-update","skill-sync","io"],"backgroundTag":"file-write-failed","analyzedSha":"06082e189d65e6d6dbadc35dacdac1ce6c79d89a","analyzedAt":"2026-09-19T08:37:02.504Z","contentChangedAt":"2026-09-19T08:37:02.504Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}