mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description Two related CCR problems that both end in unreadable content. The first one (#1077) is an infinite loop. Any tool output over ~500 bytes gets replaced with a `<<ccr:hash>>` marker, and you call `headroom_retrieve` to get the original back. But the proxy then compresses the *retrieve response too*, so what comes back is a brand new marker. Retrieve that one and you get another marker. The second one (#1006), the proxy makes two independent decisions per request: SmartCrusher compresses, and the `headroom_retrieve` tool gets injected. The injection is deferred when there's a frozen message prefix (`frozen_message_count > 0`), but compression keeps running anyway. So the agent receives `[... compressed to N. Retrieve more: hash=...]` markers with no `headroom_retrieve` tool to redeem them. For #1077, SmartCrusher now skips `headroom_retrieve` results. Before crushing a tool message (OpenAI `role=tool`) or tool-result block (Anthropic `type=tool_result`), it checks whether that tool id maps to the CCR tool, and if so leaves it alone. Retrieved content stays readable. For #1006, compression and injection are no longer decided in isolation. The injection decision is extracted into `should_inject_ccr_tool`, which the Anthropic handler calls: when injection was deferred because of a frozen prefix but compression just emitted new markers, it injects the tool anyway, so a marker is never handed to an agent that can't act on it. The existing session-sticky dedup means sessions that already have the tool don't get it re-injected and don't lose their cache. Closes #1077 Closes #1006 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/transforms/smart_crusher.py`: exempt `headroom_retrieve` results from compression on both the OpenAI `role=tool` and Anthropic `type=tool_result` paths. - `headroom/proxy/helpers.py`: add `should_inject_ccr_tool`, the deferral-plus-override decision the handler used to inline, so the #1006 behaviour is testable at the decision point. - `headroom/proxy/handlers/anthropic.py`: call `should_inject_ccr_tool` to couple injection with compression; rename the misleading `frozen_prefix=` log key to `frozen_message_count=`. - `tests/test_transforms/test_smart_crusher_ccr_retrieve_exemption.py` and `tests/test_proxy/test_ccr_frozen_prefix_coupling.py`: new tests; the frozen-prefix test now drives `should_inject_ccr_tool` so it would fail if the override were removed. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run --extra dev python -m pytest tests/test_proxy/test_ccr_frozen_prefix_coupling.py tests/test_transforms/test_smart_crusher_ccr_retrieve_exemption.py -q 5 passed, 1 skipped ruff: All checks passed! mypy: Success: no issues found ``` The SmartCrusher test skips locally because the Rust extension `.so` is built for a different OS, the same skip the existing SmartCrusher tests take locally. It runs in CI where the extension is built. ## Real Behavior Proof - Environment: macOS, Python 3.13, this branch. - Exact command / steps: `uv run --extra dev python -m pytest tests/test_proxy/test_ccr_frozen_prefix_coupling.py tests/test_transforms/test_smart_crusher_ccr_retrieve_exemption.py -q`. The frozen-prefix test calls `should_inject_ccr_tool` (the function the Anthropic handler now uses) with a frozen prefix and freshly emitted markers, then drives `apply_session_sticky_ccr_tool` end to end and asserts `headroom_retrieve` lands in the outbound tools. The exemption test runs a `headroom_retrieve` tool result through SmartCrusher on both the OpenAI and Anthropic shapes. - Observed result: 5 passed, 1 skipped. The retrieve tool is injected even under a frozen prefix once markers exist, and is not injected when no markers were emitted. Removing the handler override flips `should_inject_ccr_tool` and fails the test. - Not tested: a full live proxy session. The behaviours are covered at the decision, transform, and handler-call level by the new tests. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes This one touches compression gating, so it's worth a careful read on the injection coupling, that's the part where a wrong call would re-introduce data loss. 1. Tool results with no id mapping still compress, marked with `# ponytail:` comments. Only ids we can positively identify as the CCR tool are exempted. 2. The injection coupling keys off `injector.has_compressed_content`, so the tool only shows up when there's actually something to retrieve. --------- Co-authored-by: JD Davis <mxjerrett@gmail.com>
137 lines
5.6 KiB
Python
137 lines
5.6 KiB
Python
"""Regression test for #1006: the proxy must not emit unredeemable CCR markers.
|
|
|
|
When frozen_message_count > 0, the old code deferred headroom_retrieve tool
|
|
injection unconditionally — even if compression just emitted NEW <<ccr:hash>>
|
|
markers the agent has no tool to redeem.
|
|
|
|
The fix: if new markers were emitted this turn, override the deferral and inject
|
|
the tool (one cache miss is acceptable; silent data loss is not). That decision
|
|
lives in ``should_inject_ccr_tool``, which the Anthropic handler calls; this test
|
|
pins the decision at that function so removing the override would fail here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from headroom.ccr.tool_injection import CCR_TOOL_NAME, CCRToolInjector
|
|
from headroom.proxy.helpers import (
|
|
apply_session_sticky_ccr_tool,
|
|
should_inject_ccr_tool,
|
|
)
|
|
|
|
|
|
class TestShouldInjectCCRTool:
|
|
"""The decision the handler used to inline. This is where #1006 lived."""
|
|
|
|
def test_overrides_deferral_when_markers_emitted(self):
|
|
"""Frozen prefix would normally defer, but fresh markers force injection."""
|
|
should_inject, is_override = should_inject_ccr_tool(
|
|
configured_inject_tool=True,
|
|
frozen_message_count=3,
|
|
has_compressed_content=True,
|
|
)
|
|
assert should_inject, "must inject to keep markers redeemable (#1006)"
|
|
assert is_override, "this is the deferral override path"
|
|
|
|
def test_defers_when_no_markers(self):
|
|
"""Frozen prefix with no new markers stays deferred — no spurious tool."""
|
|
should_inject, is_override = should_inject_ccr_tool(
|
|
configured_inject_tool=True,
|
|
frozen_message_count=3,
|
|
has_compressed_content=False,
|
|
)
|
|
assert not should_inject
|
|
assert not is_override
|
|
|
|
def test_injects_normally_without_frozen_prefix(self):
|
|
"""No frozen prefix → inject as configured, not via the override path."""
|
|
should_inject, is_override = should_inject_ccr_tool(
|
|
configured_inject_tool=True,
|
|
frozen_message_count=0,
|
|
has_compressed_content=False,
|
|
)
|
|
assert should_inject
|
|
assert not is_override
|
|
|
|
|
|
class TestCCRInjectionEndToEnd:
|
|
"""The decision feeds apply_session_sticky_ccr_tool; assert the tool lands."""
|
|
|
|
def test_marker_in_frozen_prefix_yields_injected_tool(self):
|
|
# Injector detects a fresh marker, i.e. compression ran this turn.
|
|
injector = CCRToolInjector(provider="anthropic")
|
|
injector.scan_for_markers(
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "tool_result",
|
|
"tool_use_id": "toolu_bash_x",
|
|
"content": "[50 items compressed to 5. Retrieve more: hash=abc123def456abc123def456]",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
)
|
|
assert injector.has_compressed_content, "test setup: injector should detect marker"
|
|
|
|
# Drive the real decision the handler makes under a frozen prefix.
|
|
should_inject, _ = should_inject_ccr_tool(
|
|
configured_inject_tool=True,
|
|
frozen_message_count=3,
|
|
has_compressed_content=injector.has_compressed_content,
|
|
)
|
|
assert should_inject
|
|
|
|
with patch("headroom.proxy.helpers.get_session_ccr_tracker") as mock_tracker_fn:
|
|
mock_tracker = MagicMock()
|
|
mock_tracker.has_done_ccr.return_value = False # first CCR ever
|
|
mock_tracker.get_golden_tool_bytes.return_value = None
|
|
mock_tracker_fn.return_value = mock_tracker
|
|
|
|
tools_out, _was_injected = apply_session_sticky_ccr_tool(
|
|
provider="anthropic",
|
|
session_id="session-frozen-test",
|
|
request_id="req-test-1",
|
|
existing_tools=[],
|
|
has_compressed_content_this_turn=injector.has_compressed_content,
|
|
)
|
|
|
|
tool_names = [t.get("name") for t in tools_out]
|
|
assert CCR_TOOL_NAME in tool_names, (
|
|
f"headroom_retrieve not injected when markers emitted and prefix frozen (#1006). "
|
|
f"tools={tool_names}"
|
|
)
|
|
|
|
def test_no_marker_in_frozen_prefix_skips_tool(self):
|
|
injector = CCRToolInjector(provider="anthropic")
|
|
injector.scan_for_markers([{"role": "user", "content": "hello"}])
|
|
assert not injector.has_compressed_content, "test setup: no markers expected"
|
|
|
|
should_inject, _ = should_inject_ccr_tool(
|
|
configured_inject_tool=True,
|
|
frozen_message_count=3,
|
|
has_compressed_content=injector.has_compressed_content,
|
|
)
|
|
assert not should_inject, "no markers → no forced injection"
|
|
|
|
with patch("headroom.proxy.helpers.get_session_ccr_tracker") as mock_tracker_fn:
|
|
mock_tracker = MagicMock()
|
|
mock_tracker.has_done_ccr.return_value = False
|
|
mock_tracker.get_golden_tool_bytes.return_value = None
|
|
mock_tracker_fn.return_value = mock_tracker
|
|
|
|
tools_out, _was_injected = apply_session_sticky_ccr_tool(
|
|
provider="anthropic",
|
|
session_id="session-frozen-no-markers",
|
|
request_id="req-test-2",
|
|
existing_tools=[],
|
|
has_compressed_content_this_turn=False,
|
|
)
|
|
|
|
tool_names = [t.get("name") for t in tools_out]
|
|
assert CCR_TOOL_NAME not in tool_names, (
|
|
"headroom_retrieve should NOT be injected when no markers and frozen prefix"
|
|
)
|