From 5af22042ec65b169e83fd05b30dc4b684c883b1f Mon Sep 17 00:00:00 2001 From: chengyongru Date: Thu, 2 Jul 2026 14:29:54 +0800 Subject: [PATCH] fix(trigger): keep local worker alive on turn cancel --- nanobot/agent/automation_turns.py | 4 +++ tests/agent/test_runner_injections.py | 37 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/nanobot/agent/automation_turns.py b/nanobot/agent/automation_turns.py index b00ead24..c16168c4 100644 --- a/nanobot/agent/automation_turns.py +++ b/nanobot/agent/automation_turns.py @@ -79,6 +79,8 @@ class AutomationTurnCoordinator: return await future except asyncio.CancelledError: raise + except AutomationTurnError: + raise except Exception as exc: raise AutomationTurnError(str(exc) or exc.__class__.__name__) from exc finally: @@ -118,6 +120,8 @@ class AutomationTurnCoordinator: if future is None or future.done(): return if error is not None: + if isinstance(error, asyncio.CancelledError): + error = AutomationTurnError(str(error) or error.__class__.__name__) future.set_exception(error) else: future.set_result(response) diff --git a/tests/agent/test_runner_injections.py b/tests/agent/test_runner_injections.py index 507b0d6d..9311f5eb 100644 --- a/tests/agent/test_runner_injections.py +++ b/tests/agent/test_runner_injections.py @@ -920,6 +920,43 @@ async def test_submitted_local_trigger_turn_reports_pending_until_completed(tmp_ assert loop.pending_local_trigger_ids_for_session(session_key) == set() +@pytest.mark.asyncio +async def test_local_trigger_turn_cancellation_reports_agent_failure(tmp_path): + """A cancelled agent turn should not cancel the local-trigger worker.""" + from nanobot.agent.automation_turns import AutomationTurnError + from nanobot.bus.events import InboundMessage + from nanobot.triggers.local_session_turns import LOCAL_TRIGGER_META + + loop = _make_loop(tmp_path) + loop._running = True + + session_key = "websocket:chat-1" + msg = InboundMessage( + channel="websocket", + sender_id="trigger", + chat_id="chat-1", + content="review failed CI", + metadata={ + LOCAL_TRIGGER_META: { + "trigger_id": "trg_123", + "trigger_name": "CI review", + "delivery_id": "tdl_123", + }, + }, + session_key_override=session_key, + ) + + submit_task = asyncio.create_task(loop.submit_local_trigger_turn(msg)) + assert await asyncio.wait_for(loop.bus.consume_inbound(), timeout=0.5) is msg + + loop._local_trigger_turns.complete(msg, error=asyncio.CancelledError()) + + with pytest.raises(AutomationTurnError, match="CancelledError"): + await asyncio.wait_for(submit_task, timeout=0.5) + assert not submit_task.cancelled() + assert loop.pending_local_trigger_ids_for_session(session_key) == set() + + @pytest.mark.asyncio async def test_pending_queue_preserves_overflow_for_next_injection_cycle(tmp_path): """Pending queue should leave overflow messages queued for later drains."""