headroom/tests/test_proxy/test_ccr_frozen_prefix_coupling.py
Lucas Santos 43494ff526
fix(proxy): stop re-compressing headroom_retrieve output and emitting unredeemable markers (#1323)
## 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>
2026-06-25 10:11:42 -05:00

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"
)