Memory nudge counters, caching, and session boundary bugs
NousResearch/hermes-agent · cluster 8506 · agent
Data as of
12 open items. Fix merged in #22774. Check it covers your case.
The short answer
- Known?
- Yes. 26 related items grouped here, 3 issues already closed.
- Fix event
- A fix was merged to main in PR #22774. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #22774. That is separate from the fix event. - Still open
- 8 issues and 4 PRs.
Next step: read #22774, then check whether your report is one of the 8 still open.
Read #22774Pick up a Lab taskThese 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.
Open issues8
- Issue#9973fix(memory): prefetch cache lost on every gateway turn due to AIAgent recreation
- Issue#13075[Bug]: Memory/Skill Review Issues in run_agent
- Issue#18369[Feature]: Nudge counters reset on /new — self-improvement never triggers for short-session users
- Issue#30812[Bug]: background memory review may be missed.
- Issue#30820[Improvement]: add hydration for background skill review.
- Issue#31597[Feature]: Trigger background memory review at session boundaries (reset, close, compression)
- Issue#38863[Bug]: The memory and skill nudge counters are at risk of unnecessary consumption in tool execution.
- Issue#58669Memory extraction gap: short sessions lose durable facts at session reset
Open PRs4
- PR#13092fix(run_agent): prevent duplicate memory reviews,implement conditional nudge resets: (#13092)
- PR#31609feat(memory): trigger background review at session boundaries (#31597)
- PR#38890fix(agent): reset nudge counters only after successful tool execution in concurrent path
- PR#62046fix(memory): review unprocessed turns at session end
Fixes on record
-
#22774 fix(agent): hydrate memory-nudge counters from conversation_history
files:
run_agent.py,tests/run_agent/test_memory_nudge_counter_hydration.pyShow the diff
-
run_agent.py@@ -11127,7 +11127,29 @@ def run_conversation( # recover the todo state from the most recent todo tool response in history) if conversation_history and not self._todo_store.has_items(): self._hydrate_todo_store(conversation_history) - + + # Hydrate per-session nudge counters from persisted history. + # Gateway creates a fresh AIAgent per inbound message (cache miss / + # 1h idle eviction / config-signature mismatch / process restart), so + # _turns_since_memory and _user_turn_count start at 0 every turn and + # the memory.nudge_interval trigger may never be reached. Reconstruct + # an effective count from prior user turns in conversation_history. + # Idempotent: a cached agent that already accumulated counters keeps + # them; only a freshly-built agent with empty in-memory state hydrates. + # See issue #22357. + if conversation_history and self._user_turn_count == 0: + prior_user_turns = sum( + 1 for m in conversation_history if m.get("role") == "user" + ) + if prior_user_turns > 0: + self._user_turn_count = prior_user_turns + if self._memory_nudge_interval > 0 and self._turns_since_memory == 0: + # % preserves original 1-in-N cadence rather than firing a + # review immediately on resume (which would surprise users + # whose session happened to land just past a multiple of N). + self._turns_since_memory = prior_user_turns % self._memory_nudge_interval + + # Prefill messages (few-shot priming) are injected at API-call time only, # never stored in the messages list. This keeps them ephemeral: they won't # be saved to session DB, session logs, or batch trajectories, but they're -
tests/run_agent/test_memory_nudge_counter_hydration.py@@ -0,0 +1,129 @@ +"""Regression test for issue #22357 — gateway memory-nudge counter hydration. + +The gateway creates a fresh AIAgent for each inbound message in several +common scenarios (cache miss, 1h idle eviction at gateway/run.py +_AGENT_CACHE_IDLE_TTL_SECS, config-signature mismatch, process restart). +A freshly built AIAgent has _turns_since_memory=0 and _user_turn_count=0. + +Without hydration from conversation_history, the memory.nudge_interval +trigger (`_turns_since_memory >= _memory_nudge_interval`) can never be +reached: every turn looks like turn 1 to the counter, so a user can chat +for hours without ever seeing a "💾 Self-improvement review:" message. + +This test pins the hydration behavior added at the top of run_conversation(). +""" + +from __future__ import annotations + + +def _make_minimal_agent(): + """Build the smallest object that can run the hydration block. + + The hydration code only touches attributes — no I/O, no API calls. + We can just set up a SimpleNamespace-like object with the right fields + and call run_conversation's prelude logic via a thin wrapper. + + The hydration block itself is straightforward enough that we test it + by replicating it inline against the same inputs — that's the only + way to test ~10 lines deep inside a 500+ line method without rewriting + the whole agent loop. + """ + + +def _run_hydration(conversation_history, memory_nudge_interval=10, + prior_turn_count=0, prior_turns_since_memory=0): + """Replicate the hydration block from run_agent.py:11128-11150. + Keeping this in sync with the production code is a one-line job; the + block has no dependencies on anything except primitives + history. + """ + user_turn_count = prior_turn_count + turns_since_memory = prior_turns_since_memory + + if conversation_history and user_turn_count == 0: + prior_user_turns = sum( + 1 for m in conversation_history if m.get("role") == "user" + ) + if prior_user_turns > 0: + user_turn_count = prior_user_turns + if memory_nudge_interval > 0 and turns_since_memory == 0: + turns_since_memory = prior_user_turns % memory_nudge_interval + + return user_turn_count, turns_since_memory + + +def test_no_history_leaves_counters_at_zero(): + user_turn, since_mem = _run_hydration([], memory_nudge_interval=10) + assert user_turn == 0 + assert since_mem == 0 + + +def test_seven_user_turns_history_hydrates_to_seven(): + """Mid-cycle history: 7 prior user turns, interval 10 → counter at 7.""" + history = [] + for i in range(7): + history.append({"role": "user", "content": f"q{i}"}) + history.append({"role": "assistant", "content": f"a{i}"}) + + user_turn, since_mem = _run_hydration(history, memory_nudge_interval=10) + + assert user_turn == 7 + assert since_mem == 7 # 7 % 10 = 7, next 3 turns will trigger review + + +def test_thirteen_turns_history_wraps_via_modulo(): + """13 prior user turns, interval 10 → counter at 3 (post-wrap), preserving cadence.""" + history = [{"role": "user", "content": f"q{i}"} for i in range(13)] + + user_turn, since_mem = _run_hydration(history, memory_nudge_interval=10) + + assert user_turn == 13 + assert since_mem == 3 # 13 % 10 = 3, next 7 turns to trigger + + +def test_idempotent_when_counters_already_set(): + """A cached agent with existing counters must NOT have them clobbered. + + Without the `_user_turn_count == 0` guard, cached agents would lose + their accumulated state every time they re-entered the function. + """ + history = [{"role": "user", "content": "q1"}, {"role": "assistant", "content": "a1"}] + user_turn, since_mem = _run_hydration( + history, memory_nudge_interval=10, + prior_turn_count=15, prior_turns_since_memory=5, + ) + # Existing counters preserved (cache hit case) + assert user_turn == 15 + assert since_mem == 5 + + +def test_zero_nudge_interval_disables_hydration_of_review_counter(): + """When memory.nudge_interval=0 (review disabled), don't touch the counter.""" + history = [{"role": "user", "content": "q1"}] + user_turn, since_mem = _run_hydration(history, memory_nudge_interval=0) + assert user_turn == 1 + assert since_mem == 0 # untouched when interval is 0 + + +def test_assistant_only_history_does_not_advance_user_turn_count(): + """Defensive: only role==user messages contribute. Other roles are noise.""" + history = [ + {"role": "system", "content": "sys"}, + {"role": "assistant", "content": "a"}, + {"role": "tool", "content": "t"}, + ] + user_turn, since_mem = _run_hydration(history, memory_nudge_interval=10) + assert user_turn == 0 + assert since_mem == 0 + + +def test_production_code_contains_hydration_block(): + """Smoke test: confirm the hydration code is actually wired into + run_conversation(). If someone deletes it, tests above still pass + against the inline replica — this fails them awake. + """ + from pathlib import Path + src = Path(__file__).resolve().parents[2] / "run_agent.py" + content = src.read_text(encoding="utf-8") + # Anchor on the unique comment + the modulo line. + assert "Hydrate per-session nudge counters from persisted history" in content + assert "self._turns_since_memory = prior_user_turns % self._memory_nudge_interval" in content
-
10 PRs closed without merging
From the close comments: done elsewhere 4 · duplicate 3 · superseded by another PR 1 · other 2
-
#8507 fix(memory): persist nudge counter across agent cache misses · done elsewhere →
86f69e8,#22774,#12138 -
#9976 fix(memory): preserve prefetch cache across AIAgent recreation in gateway · superseded by another PR →
#12126 -
#16415 fix(memory): cache provider instance in load_memory_provider() · done elsewhere →
342096b,#16155 - #22376 fix(agent): hydrate _turns_since_memory from history in gateway mode · done elsewhere
-
#22377 fix(agent): seed memory/skill nudge counters from history in gateway mode (#22357) · duplicate →
#22376
Show 5 more
-
#22559 fix(run_agent): hydrate memory nudge counter from history in gateway sessions · duplicate →
#22376,#22357 - #30817 fix(agent): bind memory/skill review counters to actual spawn (#30812) · other
-
#31622 feat(memory): trigger background memory review at session boundaries · duplicate →
#31609,#31597 - #38875 fix(agent): reset nudge counters only after successful tool execution · other
- #39681 fix(agent): reset nudge counters only after successful tool execution · done elsewhere
Closed issues · 3
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.06: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- agent
- Merged PRs
- #22774
- Closed issues
- 3
- Attempts
- 10 PRs closed without merging
- Fix link checked
- 9 May 2026