From 737fab3b77bf0d26025f4976a239a0716f59928a Mon Sep 17 00:00:00 2001 From: Sam Schillace Date: Tue, 10 Feb 2026 07:02:50 -0800 Subject: [PATCH] fix: catch BaseException in cleanup loops to survive asyncio.CancelledError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit asyncio.CancelledError inherits from BaseException (not Exception) since Python 3.9. The coordinator cleanup loop and session cleanup used `except Exception`, which misses CancelledError. When cancellation arrives during shutdown, it escapes the try/except, aborts the for-loop, and all remaining module cleanup functions are skipped — causing "Unclosed client session" warnings from aiohttp in modules like tool-web and hooks-notify. Changes: - coordinator.cleanup(): except Exception → except BaseException - coordinator.collect_contributions(): same fix for consistency - session.cleanup(): wrap coordinator.cleanup() so loader.cleanup() always runs - session.execute(): except Exception → except BaseException so CancelledError reaches the cancellation status/event tracking code - session._safe_exception_str(): widen type to BaseException to match callers 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --- amplifier_core/coordinator.py | 4 ++-- amplifier_core/session.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/amplifier_core/coordinator.py b/amplifier_core/coordinator.py index cf0f62a..ff8dc35 100644 --- a/amplifier_core/coordinator.py +++ b/amplifier_core/coordinator.py @@ -335,7 +335,7 @@ async def collect_contributions(self, channel: str) -> list[Any]: if result is not None: contributions.append(result) - except Exception as e: + except BaseException as e: logger.warning( f"Contributor '{contributor['name']}' on channel '{channel}' failed: {e}" ) @@ -353,7 +353,7 @@ async def cleanup(self): result = cleanup_fn() if inspect.iscoroutine(result): await result - except Exception as e: + except BaseException as e: logger.error(f"Error during cleanup: {e}") def reset_turn(self): diff --git a/amplifier_core/session.py b/amplifier_core/session.py index 44f7a32..a58e2de 100644 --- a/amplifier_core/session.py +++ b/amplifier_core/session.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -def _safe_exception_str(e: Exception) -> str: +def _safe_exception_str(e: BaseException) -> str: """ CRITICAL: Explicitly handle exception string conversion for Windows cp1252 compatibility. Default encoding can fail on non-cp1252 characters, causing a crash during error handling. @@ -432,7 +432,7 @@ async def execute(self, prompt: str) -> str: self.status.status = "completed" return result - except Exception as e: + except BaseException as e: # Check if this was a cancellation-related exception if self.coordinator.cancellation.is_cancelled: self.status.status = "cancelled" @@ -455,8 +455,11 @@ async def execute(self, prompt: str) -> str: async def cleanup(self: "AmplifierSession") -> None: """Clean up session resources.""" - await self.coordinator.cleanup() - # Clean up sys.path modifications + try: + await self.coordinator.cleanup() + except BaseException as e: + logger.error(f"Error during coordinator cleanup: {e}") + # Clean up sys.path modifications - must always run if self.loader: self.loader.cleanup()