The api_server platform builds a fresh AIAgent per request (per-request
callbacks, model route, ephemeral prompt), so the memory provider was
re-initialised on every request. External providers deliver recall as the
PREVIOUS turn's background prefetch held on the provider instance, so a
continued session (X-Hermes-Session-Id, previous_response_id, declared
session key) never received automatic recall, and for hindsight
local_embedded each init also restarted the embedded daemon, killing the
retain still in flight. Pre-existing: the same probe fails on main before
the hindsight catalog migration (526d135a96, bundled provider).
ApiServerMemorySessions parks the session's initialised MemoryManager
between requests (exclusive check-out/check-in, keyed by profile home +
session id, LRU/idle eviction under the owning profile's scope) and
AIAgent(memory_manager=...) adopts it instead of loading and initialising
the provider again. /v1/chat/completions, /v1/responses, session chat and
/v1/runs all go through the same two seams (_create_agent, turn finally).
44 lines
1.7 KiB
Python
44 lines
1.7 KiB
Python
"""api_server keeps one memory provider per session across requests (#120116)."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from gateway.platforms.api_server_memory_sessions import ApiServerMemorySessions
|
|
|
|
|
|
class _Manager:
|
|
def __init__(self):
|
|
self.shut_down = False
|
|
|
|
def flush_pending(self, timeout=None):
|
|
return True
|
|
|
|
def shutdown_all(self):
|
|
self.shut_down = True
|
|
|
|
|
|
def _agent(manager, session_id="sess-1"):
|
|
return SimpleNamespace(_memory_manager=manager, session_id=session_id)
|
|
|
|
|
|
def test_manager_survives_across_requests_and_is_exclusive_per_home(monkeypatch):
|
|
registry = ApiServerMemorySessions(max_size=8, idle_ttl_secs=3600.0)
|
|
home = ["home-a"]
|
|
monkeypatch.setattr(ApiServerMemorySessions, "_owner_home", staticmethod(lambda: (home[0], None)))
|
|
# Shutdowns run inline so the assertion below does not race a daemon thread.
|
|
monkeypatch.setattr(registry, "_shutdown_async", registry._shutdown)
|
|
|
|
assert registry.checkout("sess-1") is None # first request builds its own
|
|
manager = _Manager()
|
|
registry.checkin(_agent(manager))
|
|
assert registry.checkout("sess-1") is manager # next request on the session gets it back
|
|
assert registry.checkout("sess-1") is None # ...exclusively: a concurrent request builds a new one
|
|
loser = _Manager()
|
|
registry.checkin(_agent(manager))
|
|
registry.checkin(_agent(loser)) # the concurrent request checks in behind the winner
|
|
assert manager.shut_down and not loser.shut_down
|
|
# Same session id under another profile home is a different key (#120116 x multiplex).
|
|
home[0] = "home-b"
|
|
assert registry.checkout("sess-1") is None
|
|
home[0] = "home-a"
|
|
assert registry.checkout("sess-1") is loser
|