{"repo":"NousResearch/hermes-agent","public_id":"8506","display_label":"Memory nudge counters, caching, and session boundary bugs","summary":"These issues and PRs address a cluster of related problems: memory and skill nudge counters resetting or failing to fire correctly, the memory provider and AIAgent being unnecessarily re-initialized on every turn, and prefetch/session caches being lost across gateway turns or agent recreations. A secondary theme involves triggering background memory review at session boundaries to prevent fact loss in short sessions.","subsystem":"agent","counts":{"members":26,"open_issues":8,"open_issues_distinct":8,"open_prs":4,"closed_issues":3,"merged_prs":1,"attempt_prs":14},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":22774},"resolution":{"state":"partially_resolved","by":"pr-22774"},"freshness":{"state":"verified","last_verified_at":"2026-05-09T19:48:03+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.06473846153846151},"fix_edges":[{"issue_number":8506,"pr_number":22774,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":22357,"pr_number":22774,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":8506,"title":"[Bug]: Memory nudge never triggers when smart model routing is enabled","url":null,"state":"closed"},{"kind":"issue","number":9973,"title":"fix(memory): prefetch cache lost on every gateway turn due to AIAgent recreation","url":null,"state":"open"},{"kind":"issue","number":13075,"title":"[Bug]: Memory/Skill Review Issues in run_agent","url":null,"state":"open"},{"kind":"issue","number":16155,"title":"bug: memory provider re-initializes on every turn (per-message AIAgent creation in run_agent.py:1085)","url":null,"state":"closed"},{"kind":"issue","number":18369,"title":"[Feature]: Nudge counters reset on /new — self-improvement never triggers for short-session users","url":null,"state":"open"},{"kind":"issue","number":22357,"title":"Gateway sessions reset memory nudge counter, so self-improvement review may never trigger","url":null,"state":"closed"},{"kind":"issue","number":30812,"title":"[Bug]: background memory review may be missed.","url":null,"state":"open"},{"kind":"issue","number":30820,"title":"[Improvement]: add hydration for background skill review.","url":null,"state":"open"},{"kind":"issue","number":31597,"title":"[Feature]: Trigger background memory review at session boundaries (reset, close, compression)","url":null,"state":"open"},{"kind":"issue","number":38863,"title":"[Bug]: The memory and skill nudge counters are at risk of unnecessary consumption in tool execution.","url":null,"state":"open"},{"kind":"issue","number":58669,"title":"Memory extraction gap: short sessions lose durable facts at session reset","url":null,"state":"open"},{"kind":"pr","number":8507,"title":"fix(memory): persist nudge counter across agent cache misses","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"86f69e8c2","number":0},{"kind":"pr","value":"#22774","number":22774},{"kind":"pr","value":"#12138","number":12138}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":9976,"title":"fix(memory): preserve prefetch cache across AIAgent recreation in gateway","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12126","number":12126}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":13092,"title":"fix(run_agent): prevent duplicate memory reviews,implement conditional nudge resets: (#13092)","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":16415,"title":"fix(memory): cache provider instance in load_memory_provider()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"342096b4bdb7976db0353cbf93e0396845df2f8c","number":0},{"kind":"pr","value":"#16155","number":16155}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":22376,"title":"fix(agent): hydrate _turns_since_memory from history in gateway mode","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":22377,"title":"fix(agent): seed memory/skill nudge counters from history in gateway mode (#22357)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22376","number":22376}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":22559,"title":"fix(run_agent): hydrate memory nudge counter from history in gateway sessions","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22376","number":22376},{"kind":"pr","value":"#22357","number":22357},{"kind":"pr","value":"#22376","number":22376},{"kind":"pr","value":"#22376","number":22376}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":22774,"title":"fix(agent): hydrate memory-nudge counters from conversation_history","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["run_agent.py","tests/run_agent/test_memory_nudge_counter_hydration.py"],"fix_hunks":[{"file":"run_agent.py","hunks":["@@ -11127,7 +11127,29 @@ def run_conversation(\n         # recover the todo state from the most recent todo tool response in history)\n         if conversation_history and not self._todo_store.has_items():\n             self._hydrate_todo_store(conversation_history)\n-        \n+\n+        # Hydrate per-session nudge counters from persisted history.\n+        # Gateway creates a fresh AIAgent per inbound message (cache miss /\n+        # 1h idle eviction / config-signature mismatch / process restart), so\n+        # _turns_since_memory and _user_turn_count start at 0 every turn and\n+        # the memory.nudge_interval trigger may never be reached. Reconstruct\n+        # an effective count from prior user turns in conversation_history.\n+        # Idempotent: a cached agent that already accumulated counters keeps\n+        # them; only a freshly-built agent with empty in-memory state hydrates.\n+        # See issue #22357.\n+        if conversation_history and self._user_turn_count == 0:\n+            prior_user_turns = sum(\n+                1 for m in conversation_history if m.get(\"role\") == \"user\"\n+            )\n+            if prior_user_turns > 0:\n+                self._user_turn_count = prior_user_turns\n+                if self._memory_nudge_interval > 0 and self._turns_since_memory == 0:\n+                    # % preserves original 1-in-N cadence rather than firing a\n+                    # review immediately on resume (which would surprise users\n+                    # whose session happened to land just past a multiple of N).\n+                    self._turns_since_memory = prior_user_turns % self._memory_nudge_interval\n+\n+\n         # Prefill messages (few-shot priming) are injected at API-call time only,\n         # never stored in the messages list. This keeps them ephemeral: they won't\n         # be saved to session DB, session logs, or batch trajectories, but they're"]},{"file":"tests/run_agent/test_memory_nudge_counter_hydration.py","hunks":["@@ -0,0 +1,129 @@\n+\"\"\"Regression test for issue #22357 — gateway memory-nudge counter hydration.\n+\n+The gateway creates a fresh AIAgent for each inbound message in several\n+common scenarios (cache miss, 1h idle eviction at gateway/run.py\n+_AGENT_CACHE_IDLE_TTL_SECS, config-signature mismatch, process restart).\n+A freshly built AIAgent has _turns_since_memory=0 and _user_turn_count=0.\n+\n+Without hydration from conversation_history, the memory.nudge_interval\n+trigger (`_turns_since_memory >= _memory_nudge_interval`) can never be\n+reached: every turn looks like turn 1 to the counter, so a user can chat\n+for hours without ever seeing a \"💾 Self-improvement review:\" message.\n+\n+This test pins the hydration behavior added at the top of run_conversation().\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+\n+def _make_minimal_agent():\n+    \"\"\"Build the smallest object that can run the hydration block.\n+\n+    The hydration code only touches attributes — no I/O, no API calls.\n+    We can just set up a SimpleNamespace-like object with the right fields\n+    and call run_conversation's prelude logic via a thin wrapper.\n+\n+    The hydration block itself is straightforward enough that we test it\n+    by replicating it inline against the same inputs — that's the only\n+    way to test ~10 lines deep inside a 500+ line method without rewriting\n+    the whole agent loop.\n+    \"\"\"\n+\n+\n+def _run_hydration(conversation_history, memory_nudge_interval=10,\n+                   prior_turn_count=0, prior_turns_since_memory=0):\n+    \"\"\"Replicate the hydration block from run_agent.py:11128-11150.\n+    Keeping this in sync with the production code is a one-line job; the\n+    block has no dependencies on anything except primitives + history.\n+    \"\"\"\n+    user_turn_count = prior_turn_count\n+    turns_since_memory = prior_turns_since_memory\n+\n+    if conversation_history and user_turn_count == 0:\n+        prior_user_turns = sum(\n+            1 for m in conversation_history if m.get(\"role\") == \"user\"\n+        )\n+        if prior_user_turns > 0:\n+            user_turn_count = prior_user_turns\n+            if memory_nudge_interval > 0 and turns_since_memory == 0:\n+                turns_since_memory = prior_user_turns % memory_nudge_interval\n+\n+    return user_turn_count, turns_since_memory\n+\n+\n+def test_no_history_leaves_counters_at_zero():\n+    user_turn, since_mem = _run_hydration([], memory_nudge_interval=10)\n+    assert user_turn == 0\n+    assert since_mem == 0\n+\n+\n+def test_seven_user_turns_history_hydrates_to_seven():\n+    \"\"\"Mid-cycle history: 7 prior user turns, interval 10 → counter at 7.\"\"\"\n+    history = []\n+    for i in range(7):\n+        history.append({\"role\": \"user\", \"content\": f\"q{i}\"})\n+        history.append({\"role\": \"assistant\", \"content\": f\"a{i}\"})\n+\n+    user_turn, since_mem = _run_hydration(history, memory_nudge_interval=10)\n+\n+    assert user_turn == 7\n+    assert since_mem == 7  # 7 % 10 = 7, next 3 turns will trigger review\n+\n+\n+def test_thirteen_turns_history_wraps_via_modulo():\n+    \"\"\"13 prior user turns, interval 10 → counter at 3 (post-wrap), preserving cadence.\"\"\"\n+    history = [{\"role\": \"user\", \"content\": f\"q{i}\"} for i in range(13)]\n+\n+    user_turn, since_mem = _run_hydration(history, memory_nudge_interval=10)\n+\n+    assert user_turn == 13\n+    assert since_mem == 3  # 13 % 10 = 3, next 7 turns to trigger\n+\n+\n+def test_idempotent_when_counters_already_set():\n+    \"\"\"A cached agent with existing counters must NOT have them clobbered.\n+\n+    Without the `_user_turn_count == 0` guard, cached agents would lose\n+    their accumulated state every time they re-entered the function.\n+    \"\"\"\n+    history = [{\"role\": \"user\", \"content\": \"q1\"}, {\"role\": \"assistant\", \"content\": \"a1\"}]\n+    user_turn, since_mem = _run_hydration(\n+        history, memory_nudge_interval=10,\n+        prior_turn_count=15, prior_turns_since_memory=5,\n+    )\n+    # Existing counters preserved (cache hit case)\n+    assert user_turn == 15\n+    assert since_mem == 5\n+\n+\n+def test_zero_nudge_interval_disables_hydration_of_review_counter():\n+    \"\"\"When memory.nudge_interval=0 (review disabled), don't touch the counter.\"\"\"\n+    history = [{\"role\": \"user\", \"content\": \"q1\"}]\n+    user_turn, since_mem = _run_hydration(history, memory_nudge_interval=0)\n+    assert user_turn == 1\n+    assert since_mem == 0  # untouched when interval is 0\n+\n+\n+def test_assistant_only_history_does_not_advance_user_turn_count():\n+    \"\"\"Defensive: only role==user messages contribute. Other roles are noise.\"\"\"\n+    history = [\n+        {\"role\": \"system\", \"content\": \"sys\"},\n+        {\"role\": \"assistant\", \"content\": \"a\"},\n+        {\"role\": \"tool\", \"content\": \"t\"},\n+    ]\n+    user_turn, since_mem = _run_hydration(history, memory_nudge_interval=10)\n+    assert user_turn == 0\n+    assert since_mem == 0\n+\n+\n+def test_production_code_contains_hydration_block():\n+    \"\"\"Smoke test: confirm the hydration code is actually wired into\n+    run_conversation(). If someone deletes it, tests above still pass\n+    against the inline replica — this fails them awake.\n+    \"\"\"\n+    from pathlib import Path\n+    src = Path(__file__).resolve().parents[2] / \"run_agent.py\"\n+    content = src.read_text(encoding=\"utf-8\")\n+    # Anchor on the unique comment + the modulo line.\n+    assert \"Hydrate per-session nudge counters from persisted history\" in content\n+    assert \"self._turns_since_memory = prior_user_turns % self._memory_nudge_interval\" in content\n"]}]},{"kind":"pr","number":30817,"title":"fix(agent): bind memory/skill review counters to actual spawn (#30812)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":31609,"title":"feat(memory): trigger background review at session boundaries (#31597)","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#16454","number":16454},{"kind":"pr","value":"#14981","number":14981}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":31622,"title":"feat(memory): trigger background memory review at session boundaries","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#31609","number":31609},{"kind":"pr","value":"#31597","number":31597},{"kind":"pr","value":"#31609","number":31609}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":38875,"title":"fix(agent): reset nudge counters only after successful tool execution","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":38890,"title":"fix(agent): reset nudge counters only after successful tool execution in concurrent path","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":39681,"title":"fix(agent): reset nudge counters only after successful tool execution","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":62046,"title":"fix(memory): review unprocessed turns at session end","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"d8bc4f242f6e127a330afedb2ecdf210f3946316","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}