# NousResearch/hermes-agent · Memory nudge counters, caching, and session boundary bugs

## Open now

- 8 issues, 4 PRs open on GitHub.
- #9973: fix(memory): prefetch cache lost on every gateway turn due to AIAgent recreation
- #13075: \[Bug\]: Memory/Skill Review Issues in run_agent
- #18369: \[Feature\]: Nudge counters reset on /new — self-improvement never triggers for short-session users
- #30812: \[Bug\]: background memory review may be missed.
- #30820: \[Improvement\]: add hydration for background skill review.
- #31597: \[Feature\]: Trigger background memory review at session boundaries (reset, close, compression)
- #38863: \[Bug\]: The memory and skill nudge counters are at risk of unnecessary consumption in tool execution.
- #58669: Memory extraction gap: short sessions lose durable facts at session reset
- PR #13092: fix(run_agent): prevent duplicate memory reviews,implement conditional nudge resets: (#13092)
- PR #31609: feat(memory): trigger background review at session boundaries (#31597)
- PR #38890: fix(agent): reset nudge counters only after successful tool execution in concurrent path
- PR #62046: fix(memory): review unprocessed turns at session end

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.

## TL;DR

- 26 member(s): 8 open issue(s), 3 closed issue(s), 4 open PR(s), 1 merged PR(s), 10 PR(s) closed without merging.
- A fix was merged to main in PR #22774. Verify it addresses your case.
- **Recommended:** read PR #22774 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-22774`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Open related issues

- #9973: fix(memory): prefetch cache lost on every gateway turn due to AIAgent recreation
- #13075: \[Bug\]: Memory/Skill Review Issues in run_agent
- #18369: \[Feature\]: Nudge counters reset on /new — self-improvement never triggers for short-session users
- #30812: \[Bug\]: background memory review may be missed.
- #30820: \[Improvement\]: add hydration for background skill review.
- #31597: \[Feature\]: Trigger background memory review at session boundaries (reset, close, compression)
- #38863: \[Bug\]: The memory and skill nudge counters are at risk of unnecessary consumption in tool execution.
- #58669: Memory extraction gap: short sessions lose durable facts at session reset

## Closed / duplicate issues

- #8506: \[Bug\]: Memory nudge never triggers when smart model routing is enabled
- #16155: bug: memory provider re-initializes on every turn (per-message AIAgent creation in run_agent.py:1085)
- #22357: Gateway sessions reset memory nudge counter, so self-improvement review may never trigger

## Prior & competing PRs

- **#9976: fix(memory): preserve prefetch cache across AIAgent recreation in gateway**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#12126`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#22376: fix(agent): hydrate _turns_since_memory from history in gateway mode**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#22774: fix(agent): hydrate memory-nudge counters from conversation_history**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `run_agent.py`, `tests/run_agent/test_memory_nudge_counter_hydration.py`
  - fix hunks:
  - `run_agent.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```
- **#38875: fix(agent): reset nudge counters only after successful tool execution**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#39681: fix(agent): reset nudge counters only after successful tool execution**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#8507: fix(memory): persist nudge counter across agent cache misses**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `86f69e8c2`, `#22774`, `#12138`
- **#13092: fix(run_agent): prevent duplicate memory reviews,implement conditional nudge resets: (#13092)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `run_agent.py`, `tests/run_agent/test_memory_skill_nudge_trigger.py`
- **#16415: fix(memory): cache provider instance in load_memory_provider()**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `342096b4bdb7976db0353cbf93e0396845df2f8c`, `#16155`
- **#22377: fix(agent): seed memory/skill nudge counters from history in gateway mode (#22357)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#22376`
- **#22559: fix(run_agent): hydrate memory nudge counter from history in gateway sessions**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#22376`, `#22357`, `#22376`, `#22376`
- **#30817: fix(agent): bind memory/skill review counters to actual spawn (#30812)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#31609: feat(memory): trigger background review at session boundaries (#31597)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#16454`, `#14981`
  - files: `agent/agent_init.py`, `agent/background_review.py`, `agent/conversation_compression.py`, `cli-config.yaml.example`, `cli.py`, `gateway/run.py`, `gateway/slash_commands.py`, `tests/run_agent/test_memory_review_session_boundaries.py`, `tui_gateway/server.py`
- **#31622: feat(memory): trigger background memory review at session boundaries**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#31609`, `#31597`, `#31609`
- **#38890: fix(agent): reset nudge counters only after successful tool execution in concurrent path**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/tool_executor.py`, `tests/run_agent/test_run_agent.py`
- **#62046: fix(memory): review unprocessed turns at session end**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `d8bc4f242f6e127a330afedb2ecdf210f3946316`
  - files: `agent/agent_init.py`, `agent/background_review.py`, `cli.py`, `run_agent.py`, `tests/run_agent/test_session_end_memory_review.py`, `tests/test_tui_gateway_server.py`, `tui_gateway/server.py`

## Fix status

- Merged to main in PR #22774 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-09`.
- Subsystem: `agent`
