Skip to content

Commit 044e59a

Browse files
whitehatjr1001teknium1
authored andcommitted
fix(gateway): enhance message handling during agent tasks with queue mode support
1 parent ee33e4c commit 044e59a

2 files changed

Lines changed: 54 additions & 21 deletions

File tree

gateway/run.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,27 +1551,23 @@ async def _handle_active_session_busy_message(self, event: MessageEvent, session
15511551
)
15521552
return True
15531553

1554-
# --- Normal busy case (agent actively running a task) ---
1555-
# The user sent a message while the agent is working. Interrupt the
1556-
# agent immediately so it stops the current tool-calling loop and
1557-
# processes the new message. The pending message is stored in the
1558-
# adapter so the base adapter picks it up once the interrupted run
1559-
# returns. A brief ack tells the user what's happening (debounced
1560-
# to avoid spam when they fire multiple messages quickly).
1561-
1554+
# Normal busy case (agent actively running a task)
15621555
adapter = self.adapters.get(event.source.platform)
15631556
if not adapter:
15641557
return False # let default path handle it
15651558

15661559
# Store the message so it's processed as the next turn after the
1567-
# interrupt causes the current run to exit.
1560+
# current run finishes (or is interrupted).
15681561
from gateway.platforms.base import merge_pending_message_event
15691562
merge_pending_message_event(adapter._pending_messages, session_key, event)
15701563

1571-
# Interrupt the running agent — this aborts in-flight tool calls and
1572-
# causes the agent loop to exit at the next check point.
1564+
is_queue_mode = self._busy_input_mode == "queue"
1565+
1566+
# If not in queue mode, interrupt the running agent immediately.
1567+
# This aborts in-flight tool calls and causes the agent loop to exit
1568+
# at the next check point.
15731569
running_agent = self._running_agents.get(session_key)
1574-
if running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
1570+
if not is_queue_mode and running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
15751571
try:
15761572
running_agent.interrupt(event.text)
15771573
except Exception:
@@ -1583,7 +1579,7 @@ async def _handle_active_session_busy_message(self, event: MessageEvent, session
15831579
now = time.time()
15841580
last_ack = self._busy_ack_ts.get(session_key, 0)
15851581
if now - last_ack < _BUSY_ACK_COOLDOWN:
1586-
return True # interrupt sent, ack already delivered recently
1582+
return True # interrupt sent (if not queue), ack already delivered recently
15871583

15881584
self._busy_ack_ts[session_key] = now
15891585

@@ -1608,10 +1604,16 @@ async def _handle_active_session_busy_message(self, event: MessageEvent, session
16081604
pass
16091605

16101606
status_detail = f" ({', '.join(status_parts)})" if status_parts else ""
1611-
message = (
1612-
f"⚡ Interrupting current task{status_detail}. "
1613-
f"I'll respond to your message shortly."
1614-
)
1607+
if is_queue_mode:
1608+
message = (
1609+
f"⏳ Queued for the next turn{status_detail}. "
1610+
f"I'll respond once the current task finishes."
1611+
)
1612+
else:
1613+
message = (
1614+
f"⚡ Interrupting current task{status_detail}. "
1615+
f"I'll respond to your message shortly."
1616+
)
16151617

16161618
thread_meta = {"thread_id": event.source.thread_id} if event.source.thread_id else None
16171619
try:

tests/gateway/test_busy_session_ack.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class TestBusySessionAck:
9595
async def test_sends_ack_when_agent_running(self):
9696
"""First message during busy session should get a status ack."""
9797
runner, sentinel = _make_runner()
98+
runner._busy_input_mode = "interrupt"
9899
adapter = _make_adapter()
99100

100101
event = _make_event(text="Are you working?")
@@ -127,16 +128,42 @@ async def test_sends_ack_when_agent_running(self):
127128
assert "Interrupting" in content or "respond" in content
128129
assert "/stop" not in content # no need — we ARE interrupting
129130

130-
# Verify message was queued in adapter pending
131-
assert sk in adapter._pending_messages
132-
133131
# Verify agent interrupt was called
134132
agent.interrupt.assert_called_once_with("Are you working?")
135133

134+
@pytest.mark.asyncio
135+
async def test_queue_mode_suppresses_interrupt_and_updates_ack(self):
136+
"""When busy_input_mode is 'queue', message is queued WITHOUT interrupt."""
137+
runner, sentinel = _make_runner()
138+
runner._busy_input_mode = "queue"
139+
adapter = _make_adapter()
140+
141+
event = _make_event(text="Add this to queue")
142+
sk = build_session_key(event.source)
143+
runner.adapters[event.source.platform] = adapter
144+
145+
agent = MagicMock()
146+
runner._running_agents[sk] = agent
147+
148+
with patch("gateway.run.merge_pending_message_event"):
149+
await runner._handle_active_session_busy_message(event, sk)
150+
151+
# VERIFY: Agent was NOT interrupted
152+
agent.interrupt.assert_not_called()
153+
154+
# VERIFY: Ack sent with queue-specific wording
155+
adapter._send_with_retry.assert_called_once()
156+
call_kwargs = adapter._send_with_retry.call_args
157+
content = call_kwargs.kwargs.get("content") or call_kwargs[1].get("content", "")
158+
assert "Queued for the next turn" in content
159+
assert "respond once the current task finishes" in content
160+
assert "Interrupting" not in content
161+
136162
@pytest.mark.asyncio
137163
async def test_debounce_suppresses_rapid_acks(self):
138164
"""Second message within 30s should NOT send another ack."""
139165
runner, sentinel = _make_runner()
166+
runner._busy_input_mode = "interrupt"
140167
adapter = _make_adapter()
141168

142169
event1 = _make_event(text="hello?")
@@ -172,13 +199,14 @@ async def test_debounce_suppresses_rapid_acks(self):
172199
assert result2 is True
173200
assert adapter._send_with_retry.call_count == 1 # still 1, no new ack
174201

175-
# But interrupt should still be called for both
202+
# But interrupt should still be called for both (since we are in interrupt mode)
176203
assert agent.interrupt.call_count == 2
177204

178205
@pytest.mark.asyncio
179206
async def test_ack_after_cooldown_expires(self):
180207
"""After 30s cooldown, a new message should send a fresh ack."""
181208
runner, sentinel = _make_runner()
209+
runner._busy_input_mode = "interrupt"
182210
adapter = _make_adapter()
183211

184212
event = _make_event(text="hello?")
@@ -212,6 +240,7 @@ async def test_ack_after_cooldown_expires(self):
212240
async def test_includes_status_detail(self):
213241
"""Ack message should include iteration and tool info when available."""
214242
runner, sentinel = _make_runner()
243+
runner._busy_input_mode = "interrupt"
215244
adapter = _make_adapter()
216245

217246
event = _make_event(text="yo")
@@ -243,6 +272,7 @@ async def test_draining_still_works(self):
243272
"""Draining case should still produce the drain-specific message."""
244273
runner, sentinel = _make_runner()
245274
runner._draining = True
275+
runner._busy_input_mode = "interrupt"
246276
adapter = _make_adapter()
247277

248278
event = _make_event(text="hello")
@@ -264,6 +294,7 @@ async def test_draining_still_works(self):
264294
async def test_pending_sentinel_no_interrupt(self):
265295
"""When agent is PENDING_SENTINEL, don't call interrupt (it has no method)."""
266296
runner, sentinel = _make_runner()
297+
runner._busy_input_mode = "interrupt"
267298
adapter = _make_adapter()
268299

269300
event = _make_event(text="hey")

0 commit comments

Comments
 (0)