mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
The #54878 self-heal (SessionStore.get_or_create_session) drops a routing key pointing at a session already ended in state.db and recovers/recreates a fresh session_id under the same session_key. The #54947 fix (agent-cache cache-hit guard in gateway/run.py) treats a cached agent whose snapshot session_id differs from the current session_id, under the same session_key, as an intentional /resume-/branch-style switch between two live sibling conversations, and reuses it unchanged to protect the prompt cache. These two fixes compose incorrectly: when the #54878 self-heal just fired, the cached agent's session_id is not a live sibling — it's the dead session just routed away from. #54947's "different session_id -> reuse freely" rule reuses it anyway. The stale agent runs the turn, and the post-run "session split" sync (agent.session_id != session_id) then writes the routing key straight back onto the dead session_id, undoing the self-heal. This repeats on every subsequent message until an interrupt (e.g. /stop) happens to race in before that post-run sync, silently discarding conversation context. Reproduced live on the engineering gateway (2026-07-12, routing key agent:main:telegram:dm:170829464:544520): 5 consecutive self-heal log lines over ~40 minutes, each followed by the dead session_id being reused and re-synced back, until an interrupted /stop finally let a fresh session stick — at which point all prior context was gone. No open upstream issue tracks this specific interaction as of 2026-07-12 (checked #54878, #54947, #59580, #59597, #61220 — all cover adjacent but distinct edges of the self-heal / agent-cache system). Fix: before applying #54947's reuse-on-mismatch rule, check (outside the cache lock, via SessionStore._is_session_ended_in_db) whether the cached snapshot's session_id is itself ended in state.db. If so, treat it as a stale self-heal artifact and evict/rebuild fresh -- same as a genuine cross-process write -- instead of reusing it. Re-validates the peeked verdict against the tuple actually held under the lock so a race can't apply a stale verdict to a different (possibly live) cache entry. Tests: tests/gateway/test_stale_self_heal_agent_cache_eviction.py (5 new cases: dead-session eviction, live-sibling reuse preserved [#54947 intact], cross-process invalidation preserved [#45966 intact], same-session_id dead edge case, lock-race re-validation). Full tests/gateway/ suite: 14 failed, 9040 passed, 11 skipped -- all 14 failures verified pre-existing on unpatched main (confirmed via git stash + re-run), unrelated to this change. |
||
|---|---|---|
| .. | ||
| assets | ||
| builtin_hooks | ||
| platforms | ||
| relay | ||
| __init__.py | ||
| authz_mixin.py | ||
| cgroup_cleanup.py | ||
| channel_directory.py | ||
| code_skew.py | ||
| config.py | ||
| cwd_placeholder.py | ||
| dead_targets.py | ||
| delivery.py | ||
| display_config.py | ||
| drain_control.py | ||
| hooks.py | ||
| kanban_watchers.py | ||
| memory_monitor.py | ||
| message_timestamps.py | ||
| mirror.py | ||
| pairing.py | ||
| platform_registry.py | ||
| profile_routing.py | ||
| readiness.py | ||
| response_filters.py | ||
| restart.py | ||
| restart_loop_guard.py | ||
| rich_sent_store.py | ||
| run.py | ||
| runtime_footer.py | ||
| scale_to_zero.py | ||
| session.py | ||
| session_context.py | ||
| shutdown_forensics.py | ||
| slash_access.py | ||
| slash_commands.py | ||
| status.py | ||
| status_phrases.py | ||
| sticker_cache.py | ||
| stream_consumer.py | ||
| stream_dispatch.py | ||
| stream_events.py | ||
| whatsapp_identity.py | ||