fix: catch asyncio.CancelledError in close_mcp shutdown

When an MCP server (e.g. stdio browser-agent subprocess) does not
terminate within the AsyncExitStack.aclose() timeout, asyncio raises
CancelledError. The existing exception handler only caught RuntimeError
and BaseExceptionGroup, so CancelledError escaped the except block and
crashed nanobot with exit code 1 on every shutdown.

Add asyncio.CancelledError to the caught exception tuple so the error
is logged at debug level and shutdown completes cleanly.

Stack trace from the crash:

Traceback (most recent call last):
  File ".../nanobot/agent/loop.py", line 1194, in close_mcp
    await stack.aclose()
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../asyncio/__main__.py", line ?, in <module>
  File ".../nanobot/agent/loop.py", line ?, in close_mcp
    ...
RuntimeError: ... (or BaseExceptionGroup) not caught
SystemExit: 1
This commit is contained in:
Brian Noah
2026-07-14 16:17:35 +08:00
committed by chengyongru
parent 4916fc07ab
commit 86f6558707
+8
View File
@@ -1410,6 +1410,10 @@ async def _close_server(state: Any, server_name: str) -> None:
return
try:
await stack.aclose()
except asyncio.CancelledError:
if asyncio.current_task().cancelling() > 0:
raise
logger.debug("MCP server '{}' cleanup error (can be ignored)", server_name)
except (RuntimeError, BaseExceptionGroup):
logger.debug("MCP server '{}' cleanup error (can be ignored)", server_name)
@@ -1423,5 +1427,9 @@ async def close_mcp_servers(state: Any) -> None:
for name, connection in connections:
try:
await connection.aclose()
except asyncio.CancelledError:
if asyncio.current_task().cancelling() > 0:
raise
logger.debug("MCP server '{}' cleanup error (can be ignored)", name)
except (RuntimeError, BaseExceptionGroup):
logger.debug("MCP server '{}' cleanup error (can be ignored)", name)