mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(gateway): keep idle cached agents alive until session actually expires
The idle-TTL sweep (_sweep_idle_cached_agents) was evicting agents as soon as they passed _AGENT_CACHE_IDLE_TTL_SECS, even when the session hadn't expired yet. In daily-reset mode the reset can fire hours after the last user message — evicting the agent early means the session-expiry watcher has no agent in cache to call on_session_end() with, so memory providers miss the live transcript. Now the sweep checks the session store before evicting: if the session still exists and hasn't expired, the agent stays in cache so the expiry watcher can tear it down properly later. When the session store is unavailable or throws, falls back to the original eviction behavior (safe default). Fixes: #11205
This commit is contained in:
parent
46ada7ed42
commit
90b618f48a
2 changed files with 73 additions and 0 deletions
|
|
@ -15727,6 +15727,27 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if last_activity is None:
|
||||
continue
|
||||
if (now - last_activity) > _AGENT_CACHE_IDLE_TTL_SECS:
|
||||
# Check whether the session has actually expired in the
|
||||
# session store. If it hasn't (e.g. daily-reset mode
|
||||
# where the reset fires hours after the user's last
|
||||
# message), keep the agent in cache so the session-store
|
||||
# expiry watcher can still find it and call
|
||||
# on_session_end() with the live transcript. Skipping
|
||||
# eviction here means the agent stays alive until the
|
||||
# session genuinely expires, at which point the watcher
|
||||
# (gateway/run.py _session_expiry_watcher) tears it down
|
||||
# properly. (#11205 follow-up)
|
||||
session_entry = None
|
||||
try:
|
||||
_store = getattr(self, "session_store", None)
|
||||
if _store is not None:
|
||||
_store._ensure_loaded()
|
||||
session_entry = _store._entries.get(key)
|
||||
except Exception:
|
||||
pass
|
||||
if session_entry is not None:
|
||||
if not _store._is_session_expired(session_entry):
|
||||
continue # keep agent — session hasn't expired
|
||||
to_evict.append((key, agent))
|
||||
for key, _ in to_evict:
|
||||
_cache.pop(key, None)
|
||||
|
|
|
|||
|
|
@ -708,6 +708,58 @@ class TestAgentCacheBoundedGrowth:
|
|||
assert runner._sweep_idle_cached_agents() == 0
|
||||
assert "s" in runner._agent_cache
|
||||
|
||||
def test_idle_sweep_keeps_agent_when_session_not_expired(self, monkeypatch):
|
||||
"""Agents past idle TTL are kept if the session hasn't expired yet.
|
||||
|
||||
In daily-reset mode the reset can fire hours after the last
|
||||
user message — evicting the agent early means the
|
||||
session-expiry watcher has nothing to call on_session_end()
|
||||
with, and memory providers miss the live transcript.
|
||||
"""
|
||||
from gateway import run as gw_run
|
||||
|
||||
monkeypatch.setattr(gw_run, "_AGENT_CACHE_IDLE_TTL_SECS", 0.01)
|
||||
runner = self._bounded_runner()
|
||||
runner._cleanup_agent_resources = MagicMock()
|
||||
|
||||
import time as _t
|
||||
stale = self._fake_agent(last_activity=_t.time() - 10.0)
|
||||
|
||||
# Session store says the session is still alive.
|
||||
session_entry = MagicMock()
|
||||
runner.session_store = MagicMock()
|
||||
runner.session_store._entries = {"stale-session": session_entry}
|
||||
runner.session_store._is_session_expired.return_value = False
|
||||
|
||||
runner._agent_cache["stale-session"] = (stale, "sig")
|
||||
|
||||
evicted = runner._sweep_idle_cached_agents()
|
||||
assert evicted == 0
|
||||
assert "stale-session" in runner._agent_cache
|
||||
|
||||
def test_idle_sweep_evicts_when_session_is_expired(self, monkeypatch):
|
||||
"""Agent IS evicted when past idle TTL AND session store says expired."""
|
||||
from gateway import run as gw_run
|
||||
|
||||
monkeypatch.setattr(gw_run, "_AGENT_CACHE_IDLE_TTL_SECS", 0.01)
|
||||
runner = self._bounded_runner()
|
||||
runner._cleanup_agent_resources = MagicMock()
|
||||
|
||||
import time as _t
|
||||
stale = self._fake_agent(last_activity=_t.time() - 10.0)
|
||||
|
||||
# Session store says the session has expired.
|
||||
session_entry = MagicMock()
|
||||
runner.session_store = MagicMock()
|
||||
runner.session_store._entries = {"stale-session": session_entry}
|
||||
runner.session_store._is_session_expired.return_value = True
|
||||
|
||||
runner._agent_cache["stale-session"] = (stale, "sig")
|
||||
|
||||
evicted = runner._sweep_idle_cached_agents()
|
||||
assert evicted == 1
|
||||
assert "stale-session" not in runner._agent_cache
|
||||
|
||||
def test_plain_dict_cache_is_tolerated(self):
|
||||
"""Test fixtures using plain {} don't crash _enforce_agent_cache_cap."""
|
||||
from gateway.run import GatewayRunner
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue