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

8open issues
4open PRs
26items grouped
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.

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.

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

Merged to main. A stamp counts the issues a fix is linked to.

  • #22774 fix(agent): hydrate memory-nudge counters from conversation_history

    merged to main

    files: run_agent.py, tests/run_agent/test_memory_nudge_counter_hydration.py

    Show 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
  • Closed#8506[Bug]: Memory nudge never triggers when smart model routing is enabled
  • Closed#16155bug: memory provider re-initializes on every turn (per-message AIAgent creation in run_agent.py:1085)
  • Closed#22357Gateway sessions reset memory nudge counter, so self-improvement review may never trigger
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.00 means 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 Lab

At a glance

Subsystem
agent
Merged PRs
#22774
Closed issues
3
Attempts
10 PRs closed without merging
Fix link checked
9 May 2026

For your agent

The same facts, typed.