diff --git a/agent/skill_commands.py b/agent/skill_commands.py index ad1f03824d3f..ffcacafbc96e 100644 --- a/agent/skill_commands.py +++ b/agent/skill_commands.py @@ -219,7 +219,7 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]: Dict mapping "/skill-name" to {name, description, skill_md_path, skill_dir}. """ global _skill_commands - _skill_commands = {} + scanned_commands: Dict[str, Dict[str, Any]] = {} try: from tools.skills_tool import SKILLS_DIR, _parse_frontmatter, skill_matches_platform, _get_disabled_skill_names from agent.skill_utils import get_external_skills_dirs, iter_skill_index_files @@ -264,7 +264,7 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]: cmd_name = _SKILL_MULTI_HYPHEN.sub('-', cmd_name).strip('-') if not cmd_name: continue - _skill_commands[f"/{cmd_name}"] = { + scanned_commands[f"/{cmd_name}"] = { "name": name, "description": description or f"Invoke the {name} skill", "skill_md_path": str(skill_md), @@ -273,7 +273,8 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]: except Exception: continue except Exception: - pass + return _skill_commands + _skill_commands = scanned_commands return _skill_commands @@ -325,8 +326,9 @@ def _snapshot(cmds: Dict[str, Dict[str, Any]]) -> Dict[str, str]: before = _snapshot(_skill_commands) - # Rescan the skills dir. ``scan_skill_commands`` resets - # ``_skill_commands = {}`` internally and repopulates it. + # Rescan the skills dir. ``scan_skill_commands`` swaps the in-process cache + # only after a successful scan so a scan failure leaves existing commands + # available. new_commands = scan_skill_commands() after = _snapshot(new_commands) diff --git a/tests/agent/test_skill_commands.py b/tests/agent/test_skill_commands.py index 6879baed82f7..8e4fa802298e 100644 --- a/tests/agent/test_skill_commands.py +++ b/tests/agent/test_skill_commands.py @@ -5,6 +5,7 @@ from unittest.mock import patch import tools.skills_tool as skills_tool_module +import agent.skill_commands as skill_commands_module from agent.skill_commands import ( build_preloaded_skills_prompt, build_skill_invocation_message, @@ -125,6 +126,28 @@ def test_finds_skills_in_symlinked_category_dir(self, tmp_path): assert "/knowledge-brain" in result assert result["/knowledge-brain"]["name"] == "knowledge-brain" + def test_preserves_cached_commands_when_scan_setup_fails(self, monkeypatch): + cached = { + "/cached-skill": { + "name": "cached-skill", + "description": "existing skill command", + "skill_md_path": "/tmp/cached/SKILL.md", + "skill_dir": "/tmp/cached", + } + } + monkeypatch.setattr( + skill_commands_module, "_skill_commands", cached.copy(), raising=False + ) + + with patch( + "tools.skills_tool._get_disabled_skill_names", + side_effect=RuntimeError("scan setup failed"), + ): + result = scan_skill_commands() + + assert result == cached + assert skill_commands_module._skill_commands == cached + def test_special_chars_stripped_from_cmd_key(self, tmp_path): """Skill names with +, /, or other special chars produce clean cmd keys."""