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

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