diff --git a/plugins/memory/hindsight/__init__.py b/plugins/memory/hindsight/__init__.py index bc82bc40fb..39dfe94f6c 100644 --- a/plugins/memory/hindsight/__init__.py +++ b/plugins/memory/hindsight/__init__.py @@ -1231,9 +1231,19 @@ class HindsightMemoryProvider(MemoryProvider): if self._client is not None: try: if self._mode == "local_embedded": - # Use the public close() API. The RuntimeError from - # aiohttp's "attached to a different loop" is expected - # and harmless — the daemon keeps running independently. + # HindsightEmbedded.close() delegates to its sync client.close(). + # When Hermes created/used that client on the shared async loop, + # closing it from this thread can raise "attached to a different + # loop" before aiohttp releases the session. Close the embedded + # inner async client on the shared loop first, then let the + # wrapper clean up daemon/UI bookkeeping. + inner_client = getattr(self._client, "_client", None) + if inner_client is not None and hasattr(inner_client, "aclose"): + _run_sync(inner_client.aclose()) + try: + self._client._client = None + except Exception: + pass try: self._client.close() except RuntimeError: diff --git a/tests/plugins/memory/test_hindsight_provider.py b/tests/plugins/memory/test_hindsight_provider.py index 5f1290b2f1..2f123b6f05 100644 --- a/tests/plugins/memory/test_hindsight_provider.py +++ b/tests/plugins/memory/test_hindsight_provider.py @@ -1102,3 +1102,22 @@ class TestSharedEventLoopLifecycle: mock_client.aclose.assert_called_once() assert provider._client is None + + +class TestShutdown: + def test_local_embedded_shutdown_closes_inner_async_client_on_shared_loop(self, provider): + inner_client = _make_mock_client() + embedded = MagicMock() + embedded._client = inner_client + embedded.close = MagicMock() + + provider._mode = "local_embedded" + provider._client = embedded + + provider.shutdown() + + inner_client.aclose.assert_awaited_once() + embedded.close.assert_called_once() + assert embedded._client is None + assert provider._client is None +