Hindsight provider can submit retain work during interpreter shutdown
Bug description
The native Hermes Hindsight memory provider can still submit async retain work after provider shutdown has started. In the shutdown window this can call asyncio.run_coroutine_threadsafe(), which may fail during Python interpreter teardown with:
RuntimeError: cannot schedule new futures after interpreter shutdown
This also risks secondary teardown noise such as unclosed aiohttp client/session warnings if cleanup is interrupted.
Affected area
- Repository:
NousResearch/hermes-agent
- Provider:
plugins/memory/hindsight/__init__.py
- Tests:
tests/plugins/memory/test_hindsight_provider.py
Observed behavior
During process/session shutdown, Hindsight retain sync can run late enough that Python is already tearing down executor/future machinery. The provider currently waits for existing background threads in shutdown(), but normal sync_turn() can still accept new work during or after shutdown begins.
Expected behavior
Once provider shutdown begins:
- Normal
sync_turn() calls should not submit new async work.
- Existing in-flight background work should be joined/drained.
- Any pending partial auto-retain batch should be flushed before client close, so recent conversation turns are not lost.
- Client cleanup (
aclose() / close path) should still be allowed through a shutdown-owned path.
Root cause hypothesis
The provider shutdown path joins existing _prefetch_thread / _sync_thread and then closes the Hindsight client, but there is no lifecycle gate that freezes normal retain submission once shutdown starts.
A late sync_turn() can therefore still reach the async scheduling path:
asyncio.run_coroutine_threadsafe(coro, loop)
while the Python interpreter is shutting down.
Proposed fix
Add provider lifecycle state and a shutdown-safe final flush path:
- Add guarded provider state such as
_shutting_down, _closed, and _state_lock.
- Have
sync_turn() return early once shutdown starts.
- Guard normal
_run_sync() calls after shutdown begins.
- Allow only explicit shutdown-owned cleanup/final-flush paths via an
allow_shutdown=True style escape hatch.
- Extract retain submission into a helper that can be used both by normal sync and shutdown final flush.
- Track the last retained turn counter to avoid duplicating an already retained full-session document during shutdown.
- Add regression tests for:
sync_turn() after shutdown does not call asyncio.run_coroutine_threadsafe().
- shutdown flushes a pending partial batch before closing the client.
Local validation from a candidate fix
A local candidate fix was tested with:
python -m pytest tests/plugins/memory/test_hindsight_provider.py -q -o 'addopts='
python -m py_compile plugins/memory/hindsight/__init__.py tests/plugins/memory/test_hindsight_provider.py
git diff --check -- plugins/memory/hindsight/__init__.py tests/plugins/memory/test_hindsight_provider.py
Result:
The local candidate commit only touches:
plugins/memory/hindsight/__init__.py
tests/plugins/memory/test_hindsight_provider.py
Local commit reference, if useful for comparison:
cdd9953c fix(hindsight): prevent late retain sync during shutdown
Additional test isolation note
Some Hindsight provider tests can be polluted by a real host-level ~/.hindsight/config.json. Tests that rely on env fallback or no legacy config should isolate HOME / Path.home() to a temporary directory.
Suggested labels
Hindsight provider can submit retain work during interpreter shutdown
Bug description
The native Hermes Hindsight memory provider can still submit async retain work after provider shutdown has started. In the shutdown window this can call
asyncio.run_coroutine_threadsafe(), which may fail during Python interpreter teardown with:This also risks secondary teardown noise such as unclosed aiohttp client/session warnings if cleanup is interrupted.
Affected area
NousResearch/hermes-agentplugins/memory/hindsight/__init__.pytests/plugins/memory/test_hindsight_provider.pyObserved behavior
During process/session shutdown, Hindsight retain sync can run late enough that Python is already tearing down executor/future machinery. The provider currently waits for existing background threads in
shutdown(), but normalsync_turn()can still accept new work during or after shutdown begins.Expected behavior
Once provider shutdown begins:
sync_turn()calls should not submit new async work.aclose()/ close path) should still be allowed through a shutdown-owned path.Root cause hypothesis
The provider shutdown path joins existing
_prefetch_thread/_sync_threadand then closes the Hindsight client, but there is no lifecycle gate that freezes normal retain submission once shutdown starts.A late
sync_turn()can therefore still reach the async scheduling path:while the Python interpreter is shutting down.
Proposed fix
Add provider lifecycle state and a shutdown-safe final flush path:
_shutting_down,_closed, and_state_lock.sync_turn()return early once shutdown starts._run_sync()calls after shutdown begins.allow_shutdown=Truestyle escape hatch.sync_turn()after shutdown does not callasyncio.run_coroutine_threadsafe().Local validation from a candidate fix
A local candidate fix was tested with:
python -m pytest tests/plugins/memory/test_hindsight_provider.py -q -o 'addopts=' python -m py_compile plugins/memory/hindsight/__init__.py tests/plugins/memory/test_hindsight_provider.py git diff --check -- plugins/memory/hindsight/__init__.py tests/plugins/memory/test_hindsight_provider.pyResult:
The local candidate commit only touches:
Local commit reference, if useful for comparison:
Additional test isolation note
Some Hindsight provider tests can be polluted by a real host-level
~/.hindsight/config.json. Tests that rely on env fallback or no legacy config should isolateHOME/Path.home()to a temporary directory.Suggested labels
bugmemoryhindsight