Summary
When a skill is triggered via a slash command (e.g. /lodestone) in the OpenCode protocol server, the execution path diverges completely from the normal prompt path. This causes three user-visible problems.
Symptoms
-
TUI shows Loading skill: lodestone (skill://lodestone) as the AI's reply — the skill_bridge's ctx.print(...) output is captured into the assistant message's TextPart, so the frontend renders it as if the AI said it.
-
The user's first message is swallowed — _execute_slashed_command (in src/agentpool_server/opencode_server/routes/session_routes.py) only creates an assistant_message; it never creates a role="user" message. The user's /lodestone <args> input never becomes a user message in session history, so the TUI never displays it.
-
Multiple mechanisms break because the path is different — skill commands route through session_pool.run_stream(...) instead of the normal _route_message → send_message → _consume_run path:
- Double prompt injection:
skill_bridge.execute_skill injects the full <skill-instruction> prompt into staged_content, AND _execute_slashed_command assembles a second agent_prompt ("用户执行了命令 '...' 并说: ...") passed to run_stream. The model receives both, with the second being hardcoded Chinese template text.
- Broken parent_id chain: assistant message has
parent_id="" instead of linking to a user message.
- Message ID mismatch:
run_stream(..., message_id=assistant_msg_id) passes the assistant ID where the normal path expects a user message ID.
Root Cause
Skill commands are registered as generic slashed commands (OpenCodeSkillBridge → CommandStore) and dispatched through _execute_slashed_command, which was designed for simple commands (like /help) that print text. A skill command needs the full prompt lifecycle: user message creation → instruction injection → agent processing → assistant reply. The generic command path lacks user-message creation entirely.
_execute_skill_command (line ~343) — which would have the correct behavior (creates user message, links parent_id) — is dead code: skill_cmd = None is never assigned, so it always raises 404.
Related Context
Expected Behavior
Skill commands should route through the same EventBus-only path as normal prompts:
- Create and broadcast a
role="user" message (user's input visible in TUI).
- Inject skill instructions via
staged_content (already done by skill_bridge).
- Route via
route_message → send_message → _consume_run (EventBus-only, exactly-once events).
- Do NOT use
session_pool.run_stream() for skill commands.
- Fix or remove the dead
_execute_skill_command path.
Acceptance Criteria
Summary
When a skill is triggered via a slash command (e.g.
/lodestone) in the OpenCode protocol server, the execution path diverges completely from the normal prompt path. This causes three user-visible problems.Symptoms
TUI shows
Loading skill: lodestone (skill://lodestone)as the AI's reply — the skill_bridge'sctx.print(...)output is captured into the assistant message'sTextPart, so the frontend renders it as if the AI said it.The user's first message is swallowed —
_execute_slashed_command(insrc/agentpool_server/opencode_server/routes/session_routes.py) only creates anassistant_message; it never creates arole="user"message. The user's/lodestone <args>input never becomes a user message in session history, so the TUI never displays it.Multiple mechanisms break because the path is different — skill commands route through
session_pool.run_stream(...)instead of the normal_route_message→send_message→_consume_runpath:skill_bridge.execute_skillinjects the full<skill-instruction>prompt intostaged_content, AND_execute_slashed_commandassembles a secondagent_prompt("用户执行了命令 '...' 并说: ...") passed torun_stream. The model receives both, with the second being hardcoded Chinese template text.parent_id=""instead of linking to a user message.run_stream(..., message_id=assistant_msg_id)passes the assistant ID where the normal path expects a user message ID.Root Cause
Skill commands are registered as generic slashed commands (
OpenCodeSkillBridge→CommandStore) and dispatched through_execute_slashed_command, which was designed for simple commands (like/help) that print text. A skill command needs the full prompt lifecycle: user message creation → instruction injection → agent processing → assistant reply. The generic command path lacks user-message creation entirely._execute_skill_command(line ~343) — which would have the correct behavior (creates user message, links parent_id) — is dead code:skill_cmd = Noneis never assigned, so it always raises 404.Related Context
session_pool.run_streamdual-drain), fixed in PR fix(orchestrator): eliminate duplicate events in session_pool.run_stream #338.send_message→ EventBus-only).Expected Behavior
Skill commands should route through the same EventBus-only path as normal prompts:
role="user"message (user's input visible in TUI).staged_content(already done by skill_bridge).route_message→send_message→_consume_run(EventBus-only, exactly-once events).session_pool.run_stream()for skill commands._execute_skill_commandpath.Acceptance Criteria
/lodestone <args>shows the user's message in the TUILoading skill: ...text rendered as AI reply (or moved to a proper notification part)