From aacbb95313727d7388e28d77410d46f68dbdea39 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Fri, 20 Mar 2026 11:24:05 +0000 Subject: [PATCH] fix(agent): preserve external cancellation in message loop Made-with: Cursor --- nanobot/agent/loop.py | 6 +++--- tests/test_restart_command.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index ea801b1..e8e2064 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -265,9 +265,9 @@ class AgentLoop: except asyncio.TimeoutError: continue except asyncio.CancelledError: - # anyio/MCP cancel scopes surface as CancelledError (a BaseException subclass). - # Re-raise only if the loop itself is being shut down; otherwise keep running. - if not self._running: + # Preserve real task cancellation so shutdown can complete cleanly. + # Only ignore non-task CancelledError signals that may leak from integrations. + if not self._running or asyncio.current_task().cancelling(): raise continue except Exception as e: diff --git a/tests/test_restart_command.py b/tests/test_restart_command.py index c495347..5cd8aa7 100644 --- a/tests/test_restart_command.py +++ b/tests/test_restart_command.py @@ -65,6 +65,18 @@ class TestRestartCommand: mock_handle.assert_called_once() + @pytest.mark.asyncio + async def test_run_propagates_external_cancellation(self): + """External task cancellation should not be swallowed by the inbound wait loop.""" + loop, _bus = _make_loop() + + run_task = asyncio.create_task(loop.run()) + await asyncio.sleep(0.1) + run_task.cancel() + + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(run_task, timeout=1.0) + @pytest.mark.asyncio async def test_help_includes_restart(self): loop, bus = _make_loop()