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:
|
if self._client is not None:
|
||||||
try:
|
try:
|
||||||
if self._mode == "local_embedded":
|
if self._mode == "local_embedded":
|
||||||
# Use the public close() API. The RuntimeError from
|
# HindsightEmbedded.close() delegates to its sync client.close().
|
||||||
# aiohttp's "attached to a different loop" is expected
|
# When Hermes created/used that client on the shared async loop,
|
||||||
# and harmless — the daemon keeps running independently.
|
# 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:
|
try:
|
||||||
self._client.close()
|
self._client.close()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
|||||||
@@ -1102,3 +1102,22 @@ class TestSharedEventLoopLifecycle:
|
|||||||
|
|
||||||
mock_client.aclose.assert_called_once()
|
mock_client.aclose.assert_called_once()
|
||||||
assert provider._client is None
|
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