farion1231/cc-switch · error · anyhow::Error
Skill changed during update: {}
Error message
Skill changed during update: {} What it means
The row still exists after the download, but directory, repo_owner, repo_name, repo_branch, or installed_at differs from the snapshot taken before the download. Another update/reinstall replaced the record during the await window, and acting on the mixed identity (old snapshot paths + new row) would be unsafe, so the operation aborts.
Source
Thrown at src-tauri/src/services/skill.rs:1454
})?;
// Downloads do not mutate local state, so acquire only now and hold the
// guard through the SSOT replacement, DB metadata update, and app sync.
let _state_guard = skill_state_write_guard();
// 下载和扫描期间用户可能已经卸载了该 Skill。必须在任何备份、删除或
// 复制之前重新确认记录仍存在;否则即使最终的 metadata UPDATE 能发现
// 缺行,这里也会先把已卸载的 SSOT 目录重新创建出来。
let mut current_skill = db
.get_installed_skill(&skill.id)?
.ok_or_else(|| anyhow!("Skill no longer installed: {}", skill.id))?;
if current_skill.directory != skill.directory
|| current_skill.repo_owner != skill.repo_owner
|| current_skill.repo_name != skill.repo_name
|| current_skill.repo_branch != skill.repo_branch
|| current_skill.installed_at != skill.installed_at
{
return Err(anyhow!("Skill changed during update: {}", skill.id));
}
Self::require_valid_directory(¤t_skill.directory)?;
current_skill.apps.pi = Self::skill_exists_in_app(¤t_skill.directory, &AppType::Pi);
let skill = current_skill;
let dest = ssot_dir.join(&skill.directory);
let pi_deployment = if skill.apps.pi {
let pi_dir = Self::get_distinct_app_skills_dir(&ssot_dir, &AppType::Pi)?;
let pi_destination = pi_dir.join(&skill.directory);
Self::inspect_pi_skill_destination(&dest, &pi_destination, &skill.directory)?
} else {
None
};
// 备份旧文件
let _ = Self::create_uninstall_backup(&skill);
// 删除旧 SSOT 目录并复制新文件View on GitHub (pinned to 0b5da51016)
Solutions
- Retry the update once from a freshly loaded skill record — the new snapshot will match the new generation
- Serialize update/uninstall/reinstall per skill id in the UI
- Avoid switching profiles or triggering syncs while batch updates are running
Defensive patterns
Strategy: retry
Try / catch
try {
await invoke('update_skill', { id });
} catch (e) {
if (String(e).includes('Skill changed during update')) {
await refreshSkillList(); // load the new generation
return invoke('update_skill', { id }); // retry once from fresh snapshot
}
throw e;
} Prevention
- Serialize update/reinstall per skill id — one operation at a time
- Debounce double-clicked Update buttons
- Do not switch profiles or trigger syncs while updates are running
When it happens
Trigger: Two concurrent update_skill calls for the same id; uninstall followed by reinstall racing an in-flight update; profile sync overwriting the row with a different generation (installed_at is the generation marker).
Common situations: Double-clicking Update; 'update all' racing a manual update of the same skill; switching profiles while an update runs.
Related errors
- Skill no longer installed: {}
- Skill directory changed during install; please retry
- Skill not found: {skill_id}
- Cannot update local skill: {skill_id}
- Pi 中的 Skill 已在操作期间发生变化,拒绝覆盖: {directory}
AI-assisted analysis of farion1231/cc-switch@0b5da51016 (2026-08-20).
Data as JSON: /api/errors/1ee5f27e540dc596.
Report an issue: GitHub.