Background review fork tools cache fragmentation fix
Cluster 29567. Data as of
Nothing open on GitHub. Fix merged in #29704 on 21 May 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #29704. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #29704. That is separate from the fix event. - Still open
- 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.
Open issues0
Open PRs0
Fixes on record
-
#29704 fix(background_review): inherit parent toolset config for tools[] cache parity (salvage #29568)
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.pyShow the diff
-
agent/background_review.py@@ -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, @@ -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@@ -718,6 +718,7 @@ "[email protected]": "cresslank", "[redacted:email]": "trevthefoolish", "[email protected]": "ziliangpeng", + "[redacted:email]": "ziliangpeng", "[email protected]": "centripetal-star", "[email protected]": "LeonSGP43", "[email protected]": "LeonSGP43", -
tests/run_agent/test_background_review_cache_parity.py@@ -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 @@ -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@@ -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 @@ -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) @@ -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), \ @@ -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}" )
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#29568 fix(background_review): inherit parent's toolset config to keep tools[] cache-stable (~50% fewer cache-write tokens on long sessions) · merged via another change →
#29704
Closed issues · 1
- Closed#29567background_review fork sends wider tools[] than parent, fragments Anthropic prefix cache (~50% wasted cache-write on long sessions)
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: 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
- Merged PRs
- #29704
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 21 May 2026