fix(opencode): route skill commands through EventBus-only path - #341
fix(opencode): route skill commands through EventBus-only path#341Leoyzen wants to merge 11 commits into
Conversation
Skill slash commands (e.g. /lodestone) were dispatched through _execute_slashed_command, which: 1. Never created a user message — the user's input was swallowed (TUI showed 'Loading skill: ...' as the AI reply, user message invisible) 2. Called run_stream(agent_prompt) which double-injected the prompt (skill_bridge injected staged_content + agent_prompt was passed separately, both reaching the model) 3. Used a different path from normal prompts (run_stream vs route_message → _consume_run) Fix: when command.category == 'skill', create a proper USER message (visible in TUI) and route via session_pool_integration.route_message() → send_message → _consume_run (EventBus-only, exactly-once events). Skill instructions remain in staged_content (injected by skill_bridge's command.execute), consumed automatically by turn.py. Non-skill commands keep the legacy run_stream() behavior. Regression tests verify: - skill command broadcasts a user message + calls route_message - skill command does NOT call run_stream - non-skill commands still use run_stream See issue #339 for full problem analysis.
|
All three specialist reviews are complete and my spot-checks confirm the load-bearing claims. Posting the synthesized review. Review:
|

Problem
Skill slash commands (e.g.
/lodestone) in the OpenCode protocol server were dispatched through_execute_slashed_command, a path designed for simple commands (like/help). This caused three user-visible problems — see issue #339 for full analysis:_execute_slashed_commandonly created anassistant_message— never arole="user"message. The user's/lodestone <args>input never appeared in session history or the TUI.skill_bridge'sctx.print("Loading skill: ...")was captured into the assistant message'sTextPart, so the TUI rendered it as if the AI said it.skill_bridgeinjected the full<skill-instruction>intostaged_content, AND_execute_slashed_commandassembled a secondagent_prompt("用户执行了命令 '...' 并说: ...") passed torun_stream. The model received both, with the second being hardcoded Chinese template text.Fix
When
command.category == "skill"(orstate.skill_bridge.get_command()matches),_execute_slashed_commandnow:role="user"+ TextPart containing the user's arguments) and broadcasts it — visible in the TUI, matching the normal prompt path.session_pool_integration.route_message()→send_message→_consume_run(EventBus-only, exactly-once events) — the same path as normal prompts.run_stream()— no double prompt injection. Skill instructions remain instaged_content(injected byskill_bridge'scommand.execute), consumed automatically byturn.pywhich prependsstaged_contentto the prompt.Non-skill commands (e.g.
/help) keep the legacyrun_stream()behavior unchanged.Architecture Alignment
This aligns the OpenCode skill command path with the ACP protocol's skill handling (
acp_server/handler.py: inject intostaged_content→send_message→ EventBus-only), and with the normal OpenCode prompt path (message_routes.py: create user message →route_message→_consume_run).Tests
test_skill_command_routes_via_route_message_not_run_stream: verifies skill command broadcasts a user message, callsroute_message, does NOT callrun_stream.test_plain_commands_keeps_run_stream_path: verifies non-skill commands still userun_stream.Related