Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions agent/skill_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,9 @@ def build_skill_invocation_message(
user_instruction: Optional text the user typed after the command.

Returns:
The formatted message string, or None if the skill wasn't found.
The formatted message string, or ``None`` if the skill was not found
or the skill payload could not be loaded (callers must not queue
``None`` as user input; see #14713).
"""
commands = get_skill_commands()
skill_info = commands.get(cmd_key)
Expand All @@ -448,7 +450,13 @@ def build_skill_invocation_message(

loaded = _load_skill_payload(skill_info["skill_dir"], task_id=task_id)
if not loaded:
return f"[Failed to load skill: {skill_info['name']}]"
logger.warning(
"Skill registered at %s but payload failed to load (name=%r); "
"not sending a synthetic user message (#14713)",
cmd_key,
skill_info.get("name"),
)
return None

loaded_skill, skill_dir, skill_name = loaded
activation_note = (
Expand Down
6 changes: 6 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -3820,6 +3820,12 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
if msg:
event.text = msg
# Fall through to normal message processing with skill content
else:
_disp = _skill_name or command
return (
f"Failed to load skill **{_disp}**. "
"Check SKILL.md and any required setup, then try again."
)
else:
# Not an active skill — check if it's a known-but-disabled or
# uninstalled skill and give actionable guidance.
Expand Down
9 changes: 9 additions & 0 deletions tests/agent/test_skill_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ def test_returns_none_for_unknown(self, tmp_path):
msg = build_skill_invocation_message("/nonexistent")
assert msg is None

def test_returns_none_when_payload_load_fails(self, tmp_path):
"""#14713: failed loads must not return a truthy sentinel queued as input."""
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "fragile-skill")
scan_skill_commands()
with patch("agent.skill_commands._load_skill_payload", return_value=None):
msg = build_skill_invocation_message("/fragile-skill", "task")
assert msg is None

def test_uses_shared_skill_loader_for_secure_setup(self, tmp_path, monkeypatch):
monkeypatch.delenv("TENOR_API_KEY", raising=False)
calls = []
Expand Down
6 changes: 6 additions & 0 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3025,6 +3025,11 @@ def _(rid, params: dict) -> dict:
"name": cmds[key].get("name", name),
},
)
return _err(
rid,
5030,
f"Failed to load skill: {cmds[key].get('name', name)}",
)
except Exception:
pass

Expand Down Expand Up @@ -3111,6 +3116,7 @@ def _(rid, params: dict) -> dict:
)
if msg:
return _ok(rid, {"type": "send", "message": msg})
return _err(rid, 5030, "Failed to load the bundled /plan skill")
except Exception as e:
return _err(rid, 5030, f"plan skill failed: {e}")

Expand Down
Loading