fix(gateway): let /btw dispatch mid-turn instead of being rejected

/btw spawns a parallel ephemeral side-question task (self-guarded against
concurrent /btw on the same chat) — exactly like /background. But it was
missing from the running-agent bypass list in _handle_message(), so it
fell through to the catch-all and returned:

   Agent is running — /btw can't run mid-turn. Wait for the current
  response or /stop first.

That's the opposite of what /btw is for — asking a side question while
the main turn is still working. Add the bypass next to /background and a
regression test covering the mid-turn dispatch path.

Reported by @IuriiTiunov on Telegram.
This commit is contained in:
Teknium
2026-04-26 07:10:52 -07:00
committed by Teknium
parent 7fa70b6c87
commit 70f56e7605
2 changed files with 32 additions and 0 deletions

View File

@@ -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.

View File

@@ -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