fix(gateway): agent-cache eviction commits memory under the OWNING profile, not the requesting one
#108319 made the release thread carry the caller's context and enter the owner's profile scope only when no scope was present. But the LRU-cap eviction runs inside the REQUESTING turn (run_turn_runner: _enforce_agent_cache_cap / _release_evicted_agent), and the LRU agent it evicts may belong to another profile — so with profile A's scope active, profile B's end-of-session memory commit ran under A's scope and A's home: B's transcript extracted into A's provider namespace with A's credentials. Before #108319 the same commit ran under the launch profile; the fix moved the leak, it did not close it. _run_release_in_profile_scope now resolves the owner from the session key first — a named profile's home, else the DEFAULT profile at the root Hermes dir (agent:main: keys), which is not the launch profile when the gateway runs under `hermes -p x` — and enters that profile's scope whenever the current one is not already the owner's (compared via hermes_home_key). Same-profile in-turn evictions and the unscoped housekeeping sweep behave as before. A failed owner lookup is logged at WARNING instead of silently swallowed, since it reassigns the commit to the default profile. Three invariant tests: A's turn evicting B's agent commits under B; a secondary's turn evicting a default-profile agent commits under the default home; the same with the gateway launched under a named profile still commits under the root. All red on main.
This commit is contained in:
+26
-17
@@ -9,6 +9,7 @@ import logging
|
||||
import threading
|
||||
import time
|
||||
from contextlib import nullcontext, suppress
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from agent.interrupt_compat import _accepts_keyword
|
||||
@@ -691,24 +692,32 @@ class GatewayAgentCacheMixin:
|
||||
ctx.run(self._run_release_in_profile_scope, target, args, session_key)
|
||||
|
||||
def _run_release_in_profile_scope(self, target, args: tuple, session_key: Optional[str]) -> None:
|
||||
"""Call ``target(*args)`` under the profile that owns ``session_key``. Threads start with an
|
||||
EMPTY context, so a bare thread would commit end-of-session memory (provider ``on_session_end``
|
||||
reads credentials/home at call time) under the LAUNCH profile — lost memories or a secondary's
|
||||
transcript extracted into the default profile's provider namespace. In-turn callers already
|
||||
carry the scope (``copy_context`` preserves it); the unscoped housekeeping sweep resolves the
|
||||
owner from the session key (``agent:<profile>:...``) and enters that profile's scope."""
|
||||
"""Call ``target(*args)`` under the profile that OWNS ``session_key``.
|
||||
|
||||
Threads start with an EMPTY context, so a bare thread would commit end-of-session memory
|
||||
(provider ``on_session_end`` reads credentials/home at call time) under the launch profile.
|
||||
And the LRU-cap eviction runs inside the REQUESTING turn, whose agent may belong to another
|
||||
profile — so "some scope is present" is not enough either. The owner comes from the session
|
||||
key: a named profile's home, else the DEFAULT profile (``agent:main:`` keys), which is the
|
||||
root Hermes dir even when the gateway was launched under a named profile. Its scope is
|
||||
entered unless the current one already is the owner's."""
|
||||
from agent.secret_scope import current_secret_scope, is_multiplex_active
|
||||
if current_secret_scope() is not None or not is_multiplex_active():
|
||||
target(*args)
|
||||
return
|
||||
from gateway.run import _profile_runtime_scope
|
||||
from hermes_constants import get_hermes_home
|
||||
home = None
|
||||
store = getattr(self, "session_store", None)
|
||||
if session_key and store is not None:
|
||||
with suppress(Exception):
|
||||
home = store._profile_home_for_key(session_key)
|
||||
with _profile_runtime_scope(home or get_hermes_home()):
|
||||
scope = nullcontext()
|
||||
if is_multiplex_active():
|
||||
from gateway.run import _profile_runtime_scope
|
||||
from hermes_constants import get_default_hermes_root, get_hermes_home, hermes_home_key
|
||||
owner = None
|
||||
store = getattr(self, "session_store", None)
|
||||
if session_key and store is not None:
|
||||
try:
|
||||
owner = store._profile_home_for_key(session_key)
|
||||
except Exception:
|
||||
logger.warning("Could not resolve the owning profile for %s; releasing under the default profile",
|
||||
session_key, exc_info=True)
|
||||
owner_home = Path(owner) if owner else get_default_hermes_root()
|
||||
if current_secret_scope() is None or hermes_home_key(get_hermes_home()) != hermes_home_key(owner_home):
|
||||
scope = _profile_runtime_scope(owner_home)
|
||||
with scope:
|
||||
target(*args)
|
||||
|
||||
def _commit_memory_before_soft_evict(self, agent: Any, key: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user