{"record":{"id":"a7d6a81299005f93","repo":"farion1231/cc-switch","slug":"skill-directory-changed-during-install-please-ret","errorCode":null,"errorMessage":"Skill directory changed during install; please retry","messagePattern":"Skill directory changed during install; please retry","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":908,"sourceCode":"            &skill.directory,\n        );\n\n        let readme_url =\n            Self::build_skill_doc_url(&skill.repo_owner, &skill.repo_name, &repo_branch, &doc_path);\n\n        // Re-check after the network download: another install/uninstall may have\n        // completed while the lock was intentionally released around `.await`.\n        let _state_guard = skill_state_write_guard();\n        if let Some(existing) = Self::reuse_existing_install(db, skill, &install_name, current_app)?\n        {\n            return Ok(existing);\n        }\n\n        if !dest.exists() {\n            let source = downloaded_source\n                .as_ref()\n                .map(|(_, source)| source)\n                .ok_or_else(|| anyhow!(\"Skill directory changed during install; please retry\"))?;\n            Self::preflight_install_destination(source, &install_name, current_app)?;\n            Self::copy_dir_recursive(source, &dest)?;\n        }\n\n        // 创建 InstalledSkill 记录\n        // 计算内容哈希\n        let content_hash = Self::compute_dir_hash(&dest).map(Some).unwrap_or_else(|e| {\n            log::warn!(\"Failed to compute content hash for {}: {e}\", install_name);\n            None\n        });\n\n        let installed_skill = InstalledSkill {\n            id: skill.key.clone(),\n            name: skill.name.clone(),\n            description: if skill.description.is_empty() {\n                None\n            } else {\n                Some(skill.description.clone())","sourceCodeStart":890,"sourceCodeEnd":926,"githubUrl":"https://github.com/farion1231/cc-switch/blob/a2e22f330273a5b6ffa87cb8b82b624601bac562/src-tauri/src/services/skill.rs#L890-L926","documentation":"Thrown during skill installation when the SSOT destination directory did not exist at the final copy step but no downloaded source is available. The install flow skips the network download when dest already exists (src-tauri/src/services/skill.rs:812); downloaded_source is only populated in that download branch. A concurrent install/uninstall removed dest between the fast-path check and re-acquiring the write guard, so the code has neither an existing directory nor a downloaded copy to install from.","triggerScenarios":"Calling install_skill for a skill whose SSOT directory already exists (no download occurs), while a concurrent uninstall of the same skill (or a cloud-sync snapshot import that wipes SSOT) deletes that directory during the window where the state write guard is released around .await. On re-check reuse_existing_install returns None and dest.exists() is false with downloaded_source == None.","commonSituations":"Double-clicking install in the UI firing two commands, uninstall and install racing from different windows, or a background cloud-sync import mutating SSOT while the user reinstalls. Typically self-healing on the next attempt because the retry path sees dest missing and downloads.","solutions":["Retry the install command — the second attempt takes the download branch and succeeds","Avoid firing install and uninstall for the same skill concurrently from the frontend (disable the button while a command is in flight)","If it persists, inspect the SSOT dir for a half-deleted directory and remove it, then retry"],"exampleFix":"// before\nconst skill = await invoke('install_skill', { repo });\n\n// after\nasync function installWithRetry(repo: SkillRepo) {\n  try { return await invoke('install_skill', { repo }); }\n  catch (e) {\n    if (String(e).includes('changed during install'))\n      return invoke('install_skill', { repo }); // second pass downloads\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"const skills = await invoke('list_installed_skills');\nconst busy = skills.find(s => s.directory === skill.directory);\n// ensure no uninstall/install for this directory is currently in flight\nif (inFlight.has(skill.directory)) return; // serialize per-directory","typeGuard":null,"tryCatchPattern":"try { await invoke('install_skill', { repo }); }\ncatch (e) {\n  if (String(e).includes('changed during install'))\n    return invoke('install_skill', { repo }); // second pass downloads\n  throw e;\n}","preventionTips":["Serialize install/uninstall per skill directory in the frontend (disable buttons while a command is in flight)","Retry once on this specific message — the retry path always downloads"],"tags":["rust","tauri","race-condition","filesystem","install"],"backgroundTag":null,"analyzedSha":"a2e22f330273a5b6ffa87cb8b82b624601bac562","analyzedAt":"2026-08-16T03:46:07.889Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}