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(&current_skill.directory)?;
        current_skill.apps.pi = Self::skill_exists_in_app(&current_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

  1. Retry the update once from a freshly loaded skill record — the new snapshot will match the new generation
  2. Serialize update/uninstall/reinstall per skill id in the UI
  3. 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

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


AI-assisted analysis of farion1231/cc-switch@0b5da51016 (2026-08-20). Data as JSON: /api/errors/1ee5f27e540dc596. Report an issue: GitHub.