diff --git a/gateway/run.py b/gateway/run.py index 6cd1083b..1ab57984 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -3501,6 +3501,14 @@ class GatewayRunner: if _cmd_def_inner and _cmd_def_inner.name == "background": return await self._handle_background_command(event) + # /btw must bypass the running-agent guard for the same reason + # as /background: it spawns a parallel ephemeral side-question + # task (see _handle_btw_command) that doesn't interrupt the + # active conversation and self-guards against concurrent /btw + # on the same chat. + if _cmd_def_inner and _cmd_def_inner.name == "btw": + return await self._handle_btw_command(event) + # Session-level toggles that are safe to run mid-agent — # /yolo can unblock a pending approval prompt, /verbose cycles # the tool-progress display mode for the ongoing stream. diff --git a/tests/gateway/test_running_agent_session_toggles.py b/tests/gateway/test_running_agent_session_toggles.py index fbe0d516..d60e5b15 100644 --- a/tests/gateway/test_running_agent_session_toggles.py +++ b/tests/gateway/test_running_agent_session_toggles.py @@ -165,3 +165,27 @@ async def test_reasoning_rejected_mid_run(): assert result is not None assert "can't run mid-turn" in result assert "/reasoning" in result + + +@pytest.mark.asyncio +async def test_btw_dispatches_mid_run(): + """/btw mid-run must dispatch to its handler, not hit the catch-all. + + /btw spawns a parallel ephemeral side-question task that does NOT + interrupt the active conversation (see _handle_btw_command). It's the + whole point of the command — asking a side question while the main + turn is still working. Before the mid-turn bypass was added, /btw + fell through to the "Agent is running — wait or /stop first" catch-all, + making it useless in exactly the scenario it was designed for. + """ + runner = _make_runner() + runner._handle_btw_command = AsyncMock( + return_value='💬 /btw: "what module owns titles?"\nReply will appear here shortly.' + ) + + result = await runner._handle_message(_make_event("/btw what module owns titles?")) + + runner._handle_btw_command.assert_awaited_once() + assert result is not None + assert "💬 /btw" in result + assert "can't run mid-turn" not in result