farion1231/cc-switch · warning · anyhow::Error
Skill directory changed during install; please retry
Error message
Skill directory changed during install; please retry
What it means
Thrown by SkillService install after the skill-state write lock is deliberately released around the network download (.await) and then re-acquired. At re-check time the destination directory does not exist, but the in-memory downloaded_source slot is None, so there is no local copy to install from. It means another install/uninstall that ran during the download window consumed or invalidated the shared download state.
Solutions
- Retry the install once — state is normally consistent on the second attempt
- Disable the install button / debounce while an install request for that skill is in flight
- Serialize installs client-side (queue per skill name) so only one install flow runs at a time
- If it reproduces consistently, check whether a second app instance or another process is mutating the SSOT directory concurrently
Example fix
// before (frontend)
document.getElementById('install')!.onclick = () => invoke('install_skill_unified', { ... });
// after: single-flight per install name + one retry on the race signal
const inflight = new Set<string>();
async function install(payload: unknown, name: string) {
if (inflight.has(name)) return;
inflight.add(name);
try {
await invoke('install_skill_unified', payload);
} catch (e) {
if (String(e).includes('changed during install')) {
await invoke('install_skill_unified', payload); // one retry
} else throw e;
} finally {
inflight.delete(name);
}
} Defensive patterns
Strategy: retry
Validate before calling
const inflightInstalls = new Set<string>();
function canStartInstall(name: string): boolean {
return !inflightInstalls.has(name.toLowerCase());
} Try / catch
try {
await invoke('install_skill_unified', payload);
} catch (e) {
const msg = String(e);
if (msg.includes('changed during install')) {
await sleep(500);
return invoke('install_skill_unified', payload); // exactly one retry
}
throw e;
} Prevention
- Disable the install button while its request is in flight
- Single-flight per skill name: queue or ignore duplicate installs
- Run only one app instance against a given SSOT directory
When it happens
Trigger: Invoking install_skill_unified twice for the same skill in quick succession (double-click, duplicate window); an uninstall of the same skill completing while an install is still downloading; two install flows racing so one takes the reuse_existing_install path and leaves the other without a downloaded source.
Common situations: Install button not disabled while the request is in flight; frontend auto-retry firing while the first request is still running; slow network widening the await window; two instances of the app pointing at the same SSOT directory.
Related errors
- Pi 中的 Skill 已在操作期间发生变化,拒绝覆盖
- Skill changed during update
- Skill no longer installed
- Cannot update local skill
- INVALID_SKILL_DIRECTORY
AI-assisted analysis of farion1231/cc-switch@06082e189d (2026-08-20).
Data as JSON: /api/errors/a7d6a81299005f93.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/services/skill.rs:909
&skill.directory,
);
let readme_url =
Self::build_skill_doc_url(&skill.repo_owner, &skill.repo_name, &repo_branch, &doc_path);
// Re-check after the network download: another install/uninstall may have
// completed while the lock was intentionally released around `.await`.
let _state_guard = skill_state_write_guard();
if let Some(existing) = Self::reuse_existing_install(db, skill, &install_name, current_app)?
{
return Ok(existing);
}
if !dest.exists() {
let source = downloaded_source
.as_ref()
.map(|(_, source)| source)
.ok_or_else(|| anyhow!("Skill directory changed during install; please retry"))?;
Self::preflight_install_destination(source, &install_name, current_app)?;
Self::copy_dir_recursive(source, &dest)?;
}
// 创建 InstalledSkill 记录
// 计算内容哈希
let content_hash = Self::compute_dir_hash(&dest).map(Some).unwrap_or_else(|e| {
log::warn!("Failed to compute content hash for {}: {e}", install_name);
None
});
let installed_skill = InstalledSkill {
id: skill.key.clone(),
name: skill.name.clone(),
description: if skill.description.is_empty() {
None
} else {
Some(skill.description.clone())View on GitHub (pinned to 06082e189d)