fix(gateway): rebuild cached agent when a session grows in another process - #45949
Conversation
…ocess The gateway caches an AIAgent per session_key for prompt-prefix caching. The cached agent holds an in-memory replay of the transcript as it existed when the agent was built. When a SECOND process sharing the same HERMES_HOME (e.g. the desktop app's `hermes dashboard` backend) appends turns to the SAME session in the shared SessionDB, the gateway's cached agent never re-reads from disk — so the next platform turn replies with stale context and the on-disk transcript diverges from the context the live agent reasons over (split-brain). Existing eviction triggers (/reset, /model, compression, auto-reset) only cover SAME-process mutations; there was no signal for external growth. Fix: snapshot the session's on-disk message count (an indexed SessionDB.message_count COUNT) alongside the cached agent, and on a cache-hit compare it to the live count. If the transcript grew externally, evict and rebuild from the current transcript; otherwise reuse as before (prompt cache preserved — the unchanged case is untouched). The cache entry widens from (agent, sig) to (agent, sig, msg_count); all existing readers use index access and tolerate the legacy 2-tuple (snapshot=None → check skipped → reuse). The probe fails safe: any DB error or missing session_id returns None and degrades to current behavior, never crashing a turn or falsely evicting. Adds tests/gateway/test_agent_cache.py::TestAgentCacheCrossProcessCoherence covering: live-count probe, fail-safe paths, grew→rebuild vs unchanged→reuse invariant, and legacy-2-tuple reuse — all against a real temp SessionDB.
|
Verification: LGTM ✅ Reviewed the full diff and test suite. The cross-process cache coherence mechanism is well-designed:
|
|
Verification: Reviewed the full diff — clean concurrency fix for split-brain agent cache. When another process sharing |
|
Thanks for the sharp diagnosis here @aldoeliacim — the split-brain write-up in #45966 was spot-on and drove the fix. We've landed the resolution in #46237 (merged as
#46237 adds a post-turn re-baseline ( Really appreciate the clear repro and issue — it made this an easy fix to get right. 🙏 |
Fixes #45966
What & why
The gateway caches an
AIAgentper session for prompt-prefix caching. The cached agent carries an in-memory replay of the transcript as it existed when the agent was built. When a second process that shares the sameHERMES_HOMEappends turns to the same session in the sharedSessionDB, the gateway's cached agent never re-reads from disk, so the next platform turn replies with stale context and the on-disk transcript diverges from what the live agent reasons over (split-brain).Concrete trigger: the desktop app spawns a
hermes dashboardbackend (a separate process fromhermes gateway run). Both share the session DB, but each keeps its own_agent_cache. Reply to a platform-origin session from the desktop, then send another message from the platform — the gateway answers as if the desktop turn never happened, and the recorded transcript no longer matches the context the gateway used.The existing eviction triggers (
/reset,/model, context compression, auto-reset) all fire on same-process mutations via_evict_cached_agent. There was no signal for external (cross-process) transcript growth.The fix
Snapshot the session's on-disk message count next to the cached agent, and on a cache-hit compare it against the live count:
(agent, sig)→(agent, sig, msg_count). All existing readers use index access (cached[0]/cached[1],isinstance(x, tuple)) and tolerate a legacy 2-tuple, so in-flight caches survive the rollout (snapshot=None→ check skipped → reuse).live_count > snapshot, evict (releasing the stale agent's client pool) and fall through to the existing rebuild path, which reads the current transcript from disk. Otherwise reuse exactly as before — the unchanged case is untouched, so prompt caching is preserved.SessionDB.message_count(session_id)(an indexedCOUNT). A new_session_transcript_lenhelper wraps it and fails safe: missing DB / missingsession_id/ any probe error returnsNone, which degrades to the current reuse-the-cached-agent behavior — it never crashes a turn and never falsely evicts on a transient DB hiccup.How to test
Repro (no fix): run a gateway and a
hermes dashboardagainst the sameHERMES_HOME, open a platform session, reply to it through the dashboard's/api/ws, then send another platform message — the reply ignores the dashboard-added turn.Automated:
Adds
TestAgentCacheCrossProcessCoherence(4 tests, real tempSessionDB):test_transcript_len_reads_live_count— probe reflects external appends immediately.test_transcript_len_fails_safe— no DB / falsy session_id / raising probe →None, never raises.test_external_growth_invalidates_cache_reuse— the reuse decision flipsTrue→Falseafter an external append; asserts the unchanged case still reuses (prompt-cache preserved).test_legacy_two_tuple_entry_reuses— a pre-existing 2-tuple entry skips the check and is never falsely evicted.Verified the whole gateway cache/session/eviction surface stays green (
test_agent_cache.py69, plustest_fallback_eviction.py,test_session.py,test_session_boundary_hooks.py,test_session_reset_notify.py,test_session_model_reset.py,test_model_switch_persistence.py— 186 together), and the adjacenttest_run_progress_topics.py/test_telegram_*files (which exercise the no-cache dispatch path) pass — that path is what caught an early version that referenced an unboundcachedvar outside the cache block, now fixed by an explicit_stale_cache_entryflag.Platforms
Developed and tested on Linux (Python 3.11). No platform-specific code; the change is in
gateway/run.py's session-agnostic cache path.