# NousResearch/hermes-agent · Background review fork tools cache fragmentation fix

## Open now

- Nothing open on GitHub.

These issues and PRs address a problem where background review forks send a wider tools\[\] array than the parent, breaking Anthropic prefix cache hits and wasting roughly 50% of cache-write tokens on long sessions. The proposed fix is to inherit the parent's toolset configuration in the background review fork to maintain cache parity.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #29704. Verify it addresses your case.
- **Recommended:** read PR #29704 (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: `resolved` · deciding PR(s): `pr-29704`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: 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.

## Closed / duplicate issues

- #29567: background_review fork sends wider tools\[\] than parent, fragments Anthropic prefix cache (~50% wasted cache-write on long sessions)

## Prior & competing PRs

- **#29704: fix(background_review): inherit parent toolset config for tools\[\] cache parity (salvage #29568)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/background_review.py`, `scripts/release.py`, `tests/run_agent/test_background_review_cache_parity.py`, `tests/run_agent/test_background_review_toolset_restriction.py`
  - fix hunks:
  - `agent/background_review.py`

    ```diff
    @@ -390,6 +390,9 @@ def _bg_review_auto_deny(command, description, **kwargs):
                 # parent below so memory(action="add") writes from
                 # the review still land on disk; the review just
                 # has zero side effects on external providers.
    +            # Match parent's toolset config so ``tools[]`` is byte-identical
    +            # in the request body — Anthropic's cache key includes it.
    +            # (The runtime whitelist below still restricts dispatch.)
                 review_agent = AIAgent(
                     model=agent.model,
                     max_iterations=16,
    ```

    ```diff
    @@ -401,6 +404,8 @@ def _bg_review_auto_deny(command, description, **kwargs):
                     api_key=_parent_runtime.get("api_key") or None,
                     credential_pool=[redacted:secret-assignment] "_credential_pool", None),
                     parent_session_id=agent.session_id,
    +                enabled_toolsets=getattr(agent, "enabled_toolsets", None),
    +                disabled_toolsets=getattr(agent, "disabled_toolsets", None),
                     skip_memory=True,
                 )
                 review_agent._memory_write_origin = "background_review"
    ```
  - `scripts/release.py`

    ```diff
    @@ -718,6 +718,7 @@
         "9219265+cresslank@users.noreply.github.com": "cresslank",
         "[redacted:email]": "trevthefoolish",
         "ziliangpeng@users.noreply.github.com": "ziliangpeng",
    +    "[redacted:email]": "ziliangpeng",
         "centripetal-star@users.noreply.github.com": "centripetal-star",
         "LeonSGP43@users.noreply.github.com": "LeonSGP43",
         "154585401+LeonSGP43@users.noreply.github.com": "LeonSGP43",
    ```
  - `tests/run_agent/test_background_review_cache_parity.py`

    ```diff
    @@ -38,6 +38,9 @@ def _make_agent_stub(agent_cls):
         agent._MEMORY_REVIEW_PROMPT = "review memory"
         agent._SKILL_REVIEW_PROMPT = "review skills"
         agent._COMBINED_REVIEW_PROMPT = "review both"
    +    # Non-None so the test catches a missing-kwarg regression.
    +    agent.enabled_toolsets = ["memory", "skills", "terminal"]
    +    agent.disabled_toolsets = ["spotify", "feishu_doc"]
         return agent
     
     
    ```

    ```diff
    @@ -183,3 +186,54 @@ def close(self):
             "Review fork did not inherit parent's session_id — "
             "system-prompt rebuild paths would diverge."
         )
    +
    +
    +def test_review_fork_inherits_parent_toolset_config():
    +    """``tools[]`` byte-stability: fork must inherit parent's toolset config."""
    +    import run_agent
    +
    +    agent = _make_agent_stub(run_agent.AIAgent)
    +
    +    captured = {}
    +
    +    class _Recorder:
    +        def __init__(self, *args, **kwargs):
    +            captured["enabled_toolsets"] = kwargs.get("enabled_toolsets")
    +            captured["disabled_toolsets"] = kwargs.get("disabled_toolsets")
    +            self._cached_system_prompt = None
    +            self._memory_write_origin = None
    +            self._memory_write_context = None
    +            self._memory_store = None
    +            self._memory_enabled = None
    +            self._user_profile_enabled = None
    +            self._memory_nudge_interval = None
    +            self._skill_nudge_interval = None
    +            self.suppress_status_output = None
    +            self.session_start = None
    +            self.session_id = None
    +
    +        def run_conversation(self, *args, **kwargs):
    +            raise RuntimeError("stop after recording — don't actually call the API")
    +
    +        def shutdown_memory_provider(self):
    +            pass
    +
    +        def close(self):
    +            pass
    +
    +    with patch.object(run_agent, "AIAgent", _Recorder), \
    +         patch("threading.Thread", _SyncThread):
    +        agent._spawn_background_review(
    +            messages_snapshot=[],
    +            review_memory=True,
    +            review_skills=False,
    +        )
    +
    +    assert captured.get("enabled_toolsets") == agent.enabled_toolsets, (
    +        f"enabled_toolsets mismatch: {captured.get('enabled_toolsets')!r} "
    +        f"vs expected {agent.enabled_toolsets!r}"
    +    )
    +    assert captured.get("disabled_toolsets") == agent.disabled_toolsets, (
    +        f"disabled_toolsets mismatch: {captured.get('disabled_toolsets')!r} "
    +        f"vs expected {agent.disabled_toolsets!r}"
    +    )
    ```
  - `tests/run_agent/test_background_review_toolset_restriction.py`

    ```diff
    @@ -38,6 +38,9 @@ def _make_agent_stub(agent_cls):
         agent._MEMORY_REVIEW_PROMPT = "review memory"
         agent._SKILL_REVIEW_PROMPT = "review skills"
         agent._COMBINED_REVIEW_PROMPT = "review both"
    +    # Non-None so the test catches a missing-kwarg regression.
    +    agent.enabled_toolsets = ["memory", "skills", "terminal"]
    +    agent.disabled_toolsets = ["spotify", "feishu_doc"]
         return agent
     
     
    ```

    ```diff
    @@ -52,13 +55,8 @@ def start(self):
                 self._target()
     
     
    -def test_background_review_does_not_narrow_toolset_schema():
    -    """The review fork must NOT pass enabled_toolsets to AIAgent.
    -
    -    Narrowing the schema diverges the ``tools`` cache key from the parent's,
    -    which sits above ``system`` in Anthropic's cache hierarchy and forces a
    -    full prefix-cache miss on every review (see #25322, PR #17276).
    -    """
    +def test_background_review_matches_parent_toolset_config():
    +    """Fork must receive parent's toolset config so ``tools[]`` cache key matches."""
         import run_agent
     
         agent = _make_agent_stub(run_agent.AIAgent)
    ```

    ```diff
    @@ -66,6 +64,7 @@ def test_background_review_does_not_narrow_toolset_schema():
     
         def _capture_init(self, *args, **kwargs):
             captured["enabled_toolsets"] = kwargs.get("enabled_toolsets", "UNSET")
    +        captured["disabled_toolsets"] = kwargs.get("disabled_toolsets", "UNSET")
             raise RuntimeError("stop after capturing init args")
     
         with patch.object(run_agent.AIAgent, "__init__", _capture_init), \
    ```

    ```diff
    @@ -77,11 +76,13 @@ def _capture_init(self, *args, **kwargs):
             )
     
         assert "enabled_toolsets" in captured, "AIAgent.__init__ was not called"
    -    # The kwarg must be absent — letting AIAgent inherit the default full
    -    # toolset so the schema bytes match the parent's.
    -    assert captured["enabled_toolsets"] == "UNSET", (
    -        f"Review fork narrowed the toolset schema (got {captured['enabled_toolsets']!r}), "
    -        "which breaks prefix-cache parity with the parent."
    +    assert captured["enabled_toolsets"] == agent.enabled_toolsets, (
    +        f"enabled_toolsets mismatch: {captured['enabled_toolsets']!r} "
    +        f"vs expected {agent.enabled_toolsets!r}"
    +    )
    +    assert captured["disabled_toolsets"] == agent.disabled_toolsets, (
    +        f"disabled_toolsets mismatch: {captured['disabled_toolsets']!r} "
    +        f"vs expected {agent.disabled_toolsets!r}"
         )
     
     
    ```
- **#29568: fix(background_review): inherit parent's toolset config to keep tools\[\] cache-stable (~50% fewer cache-write tokens on long sessions)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#29704`

## Fix status

- Merged to main in PR #29704 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-21`.
