mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
fix(memory): close embedded Hindsight async client cleanly
HindsightEmbedded.close() delegates to its sync client.close(). When Hermes created/used that client on the shared async loop, closing it from the main thread raises 'attached to a different loop' before aiohttp releases the session — so the ClientSession / TCPConnector leak past provider teardown. Close the embedded inner async client on the shared loop first via _run_sync(inner_client.aclose()), then let the wrapper's sync close() do its daemon/UI bookkeeping. Salvage of #14605: test placement rebased — appended TestShutdown class after TestSharedEventLoopLifecycle (which landed on main after the PR was written). Original author attribution preserved.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user