2026-07-08 16:29:35 -04:00
|
|
|
# ruff: noqa: E402 — test sections import after helper/setup code by design.
|
fix(proxy): freeze must forward cached (compressed) prefix byte-identical — stop token-mode cache busting (#1850)
The freeze path (both providers) emits the agent's ORIGINAL bytes for a
frozen message, but the provider cached whatever we FORWARDED last turn
(the compressed form). Forwarding original then mismatches the cached
prefix and busts it from that point — re-creating the whole suffix.
Measured on a real SWE-bench run: 100% of attributed misses were
prefix_change, ~56% of ALL cache-writes were bust-induced (2.8M tokens),
driving cache_create +150% and cost +41% vs baseline.
Cache mode already avoided this via _extract_cache_stable_delta (replay
the previously-forwarded prefix, compress only the delta). Token mode
called apply(frozen_count) directly, which forwards original for the
frozen region.
Fix: add a shared, provider-agnostic overlay_cached_prefix() that
replays the previously-forwarded (cached, compressed) prefix
byte-identical, append-only guarded and idempotent, and apply it in BOTH
the Anthropic and OpenAI handlers right before forwarding. This makes
freezing byte-identical in every mode, so the only remaining difference
between "token" and "cache" mode is how large a mutable
(still-compressible) tail each leaves — not whether the frozen prefix
busts the cache.
Tests:
- test_cache_prefix_overlay.py: the helper (replay, append-only guard,
idempotence).
- test_cross_turn_cache_safety.py: the invariant that was missing —
drive the REAL tracker + freeze + overlay over multiple append-only
turns against a simulated provider prefix cache and assert the forwarded
prefix stays byte-identical turn-over-turn. Load-bearing: it fails
(detects the bust) without the overlay.
## Description
<!-- Briefly explain the change and why it is needed. -->
Closes #
## Type of Change
- [ ] 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
-
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
# Paste relevant command output or artifact links here
```
## Real Behavior Proof
- Environment:
- Exact command / steps:
- Observed result:
- Not tested:
## Review Readiness
- [ ] I have performed a self-review
- [ ] This PR is ready for human review
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
Add screenshots to help explain your changes.
## Additional Notes
<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->
2026-07-06 14:54:39 -07:00
|
|
|
"""overlay_cached_prefix: freeze must forward the CACHED (compressed) bytes.
|
|
|
|
|
|
|
|
|
|
The freeze path can emit the agent's ORIGINAL bytes for a frozen message, but
|
|
|
|
|
the provider cached whatever we FORWARDED last turn (the compressed form).
|
|
|
|
|
Forwarding original then mismatches the cached prefix and busts the prompt cache
|
|
|
|
|
(observed: 100% of misses were this ``prefix_change``, ~56% of all cache-writes).
|
|
|
|
|
``overlay_cached_prefix`` replays the previously-forwarded prefix byte-identical
|
|
|
|
|
so the cache still hits — in BOTH proxy modes.
|
|
|
|
|
"""
|
|
|
|
|
|
fix(proxy/anthropic): don't replay recorded prefix over live history (#3026) (#3052)
## Description
A Claude Code session that reads a large tool result through `headroom
proxy` can fail on turn 2 with Anthropic's `400 prompt_too_long`. The
reporter's controlled comparison completed eight turns through 0.33.0
with 187,986 input tokens, while 0.35.0 failed after five requests with
753,077 input tokens. The local regression uses an actual prior
optimized request to populate tracker state, then a decision-false
bypass turn with Claude-shaped tool-result content. The old
unconditional replay path substitutes the compressed prefix; the
eligibility gate preserves the client's outbound body without claiming a
live provider reproduction.
The Anthropic `/v1/messages` route computes whether a request should be
compressed, but cached-prefix replay currently runs outside that
decision. The replay helper also derives its prefix length from the
original message list and applies that index to the optimized list
without proving the two lists still align. A stale forwarded prefix can
therefore be grafted onto the wrong positions and enlarge later
requests.
This change limits replay to requests whose existing compression
decision permits it and whose pre-upstream backpressure path is
inactive. It also makes `overlay_cached_prefix()` decline misaligned or
inflating candidates while preserving normal append-only replay.
Reported by @itsumonotakumi, whose controlled comparison isolated the
failure from compression, headers, one-request serialization, memory,
code graph, and CCR.
Closes #3026
## 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
- Gate Anthropic cached-prefix replay on the existing
`CompressionDecision.should_compress` result and the existing
pre-upstream backpressure state.
- Require positional alignment between optimized and original message
arrays before replay.
- Reject replay candidates that would serialize larger than the current
optimized messages.
- Add focused handler coverage for the decision-false tool-result
regression, bypass and backpressure paths, and outbound optimize-on
preservation.
- Add direct unit coverage for positional mismatch, no-inflation, and
JSON sizing-failure bailouts.
- Update the moved-cache-control and pure-block-append regression
fixtures to keep the no-inflation contract explicit.
- Run the unchanged OpenAI cache-stability preservation proof; no OpenAI
production code was edited.
## Testing
- [x] Unit tests pass (153 focused proxy, helper, cache-control,
block-append, cross-turn, byte-faithful, Anthropic, OpenAI, and
backpressure tests)
- [x] Linting passes (Ruff check and format validation on the seven
changed repository files)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed with the in-process proxy and local stub
upstream
### Test Output
```text
python -m pytest tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cross_turn_cache_safety.py tests/test_cache_control_move_bust.py tests/test_proxy_byte_faithful_forwarding.py tests/test_proxy_anthropic_cache_stability.py tests/test_anthropic_pre_upstream_backpressure.py -q
python -m pytest tests/test_proxy_openai_cache_stability.py -q
python -m pytest tests/test_issue_2671_block_growth_cache.py::test_pure_append_replays_forwarded_blocks_and_advances_breakpoint -q
153 passed across focused invocations, exit code 0
optimize_off turn2_message_count=3 marker_count=1 outbound_compact_utf8_bytes=2293 client_compact_utf8_bytes=2293
optimize_on turn2_message_count=3 client_message_count=3 marker_count=0 outbound_compact_utf8_bytes=171 client_compact_utf8_bytes=182
python -m ruff check headroom/proxy/handlers/anthropic.py headroom/cache/prefix_tracker.py tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cache_control_move_bust.py tests/test_issue_2671_block_growth_cache.py tests/test_proxy_openai_cache_stability.py
All checks passed!, exit code 0
python -m ruff format headroom/proxy/handlers/anthropic.py headroom/cache/prefix_tracker.py tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cache_control_move_bust.py tests/test_issue_2671_block_growth_cache.py tests/test_proxy_openai_cache_stability.py --check
7 files already formatted, exit code 0
git diff --check
clean, exit code 0
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12 via `uv`, real Headroom proxy app
with a local stub Anthropic upstream
- Exact command / steps: send an actual optimize-on first request
through the in-process proxy with a deterministic production-pipeline
seam, then send a decision-false bypass turn containing a large
Claude-shaped `tool_result` with moved `cache_control`; separately send
an aligned optimize-on turn with a new suffix
- Observed result: the exact base checkout fails with `AssertionError:
assert 'compressed-tool-result' == 'large-tool-result-marker ...'`; the
guarded path passes with the client marker present once and outbound
compact JSON no larger than the client body. The optimize-on
preservation run records `optimize_on turn2_message_count=3
client_message_count=3 marker_count=0 outbound_compact_utf8_bytes=171
client_compact_utf8_bytes=182`, proving the actual compressed prefix is
outbound before the new suffix without turn-2 growth.
- Not tested: live Claude Code session against api.anthropic.com on this
host
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: cached-prefix replay now follows the
existing compression and backpressure decision and rejects misaligned or
inflating candidates.
- Kill switch / disable path: no new switch; the existing optimize and
bypass controls remain available.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert the implementation commit.
## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] 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
## Additional Notes
`CHANGELOG.md` is not modified because Headroom's release automation
generates it from conventional commits.
This change does not add a context-limit guard or alter compression,
streaming tracker provenance, outbound-body selection, OpenAI behavior,
or provider limits. Local tests prove request-body ownership and replay
bounds. The reporter's live Claude Code completion and Anthropic token
acceptance remain external to this local proof.
2026-08-17 18:02:18 -04:00
|
|
|
import copy
|
|
|
|
|
|
fix(proxy): freeze must forward cached (compressed) prefix byte-identical — stop token-mode cache busting (#1850)
The freeze path (both providers) emits the agent's ORIGINAL bytes for a
frozen message, but the provider cached whatever we FORWARDED last turn
(the compressed form). Forwarding original then mismatches the cached
prefix and busts it from that point — re-creating the whole suffix.
Measured on a real SWE-bench run: 100% of attributed misses were
prefix_change, ~56% of ALL cache-writes were bust-induced (2.8M tokens),
driving cache_create +150% and cost +41% vs baseline.
Cache mode already avoided this via _extract_cache_stable_delta (replay
the previously-forwarded prefix, compress only the delta). Token mode
called apply(frozen_count) directly, which forwards original for the
frozen region.
Fix: add a shared, provider-agnostic overlay_cached_prefix() that
replays the previously-forwarded (cached, compressed) prefix
byte-identical, append-only guarded and idempotent, and apply it in BOTH
the Anthropic and OpenAI handlers right before forwarding. This makes
freezing byte-identical in every mode, so the only remaining difference
between "token" and "cache" mode is how large a mutable
(still-compressible) tail each leaves — not whether the frozen prefix
busts the cache.
Tests:
- test_cache_prefix_overlay.py: the helper (replay, append-only guard,
idempotence).
- test_cross_turn_cache_safety.py: the invariant that was missing —
drive the REAL tracker + freeze + overlay over multiple append-only
turns against a simulated provider prefix cache and assert the forwarded
prefix stays byte-identical turn-over-turn. Load-bearing: it fails
(detects the bust) without the overlay.
## Description
<!-- Briefly explain the change and why it is needed. -->
Closes #
## Type of Change
- [ ] 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
-
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
# Paste relevant command output or artifact links here
```
## Real Behavior Proof
- Environment:
- Exact command / steps:
- Observed result:
- Not tested:
## Review Readiness
- [ ] I have performed a self-review
- [ ] This PR is ready for human review
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
Add screenshots to help explain your changes.
## Additional Notes
<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->
2026-07-06 14:54:39 -07:00
|
|
|
from headroom.cache.prefix_tracker import overlay_cached_prefix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def M(role, text):
|
|
|
|
|
return {"role": role, "content": text}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Previous turn: 2 messages. Original was big; we FORWARDED the compressed form,
|
|
|
|
|
# so that compressed form is what the provider cached.
|
|
|
|
|
PREV_ORIG = [M("user", "READ foo.py:\n<2000 original lines>"), M("assistant", "ok")]
|
|
|
|
|
PREV_FWD = [M("user", "READ foo.py:\n<compressed>"), M("assistant", "ok")]
|
|
|
|
|
# This turn: agent appended one new message (append-only growth).
|
|
|
|
|
CUR_ORIG = PREV_ORIG + [M("user", "grep result:\n<800 original lines>")]
|
|
|
|
|
# What apply() produced in the buggy freeze path: ORIGINAL bytes for the frozen
|
|
|
|
|
# prefix (== PREV_ORIG) + compressed new tail.
|
|
|
|
|
OPTIMIZED_BUGGY = [PREV_ORIG[0], PREV_ORIG[1], M("user", "grep result:\n<compressed>")]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_replays_cached_compressed_prefix_byte_identical():
|
|
|
|
|
out = overlay_cached_prefix(OPTIMIZED_BUGGY, CUR_ORIG, PREV_ORIG, PREV_FWD)
|
|
|
|
|
# The frozen prefix now equals what the provider cached (compressed), NOT the
|
|
|
|
|
# agent's original bytes → cache hits instead of busting.
|
|
|
|
|
assert out[:2] == PREV_FWD
|
|
|
|
|
assert out[:2] != PREV_ORIG
|
|
|
|
|
# This turn's compressed tail is preserved.
|
|
|
|
|
assert out[2] == OPTIMIZED_BUGGY[2]
|
|
|
|
|
assert len(out) == len(CUR_ORIG)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_a_noop_relative_to_cache_when_already_correct():
|
|
|
|
|
# If the freeze path already forwarded the compressed (cached) prefix, the
|
|
|
|
|
# overlay reproduces exactly that — idempotent.
|
|
|
|
|
already_correct = [PREV_FWD[0], PREV_FWD[1], M("user", "grep result:\n<compressed>")]
|
|
|
|
|
out = overlay_cached_prefix(already_correct, CUR_ORIG, PREV_ORIG, PREV_FWD)
|
|
|
|
|
assert out == already_correct
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_not_append_only_returns_unchanged():
|
|
|
|
|
# An early message changed → previous forwarded bytes may not correspond to
|
|
|
|
|
# the same positions; do NOT overlay (accept a possible bust over corruption).
|
|
|
|
|
changed = [M("user", "TOTALLY DIFFERENT"), PREV_ORIG[1], M("user", "x")]
|
|
|
|
|
out = overlay_cached_prefix(OPTIMIZED_BUGGY, changed, PREV_ORIG, PREV_FWD)
|
|
|
|
|
assert out == OPTIMIZED_BUGGY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_previous_state_returns_unchanged():
|
|
|
|
|
assert overlay_cached_prefix(OPTIMIZED_BUGGY, CUR_ORIG, None, None) == OPTIMIZED_BUGGY
|
|
|
|
|
assert overlay_cached_prefix(OPTIMIZED_BUGGY, CUR_ORIG, [], []) == OPTIMIZED_BUGGY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_forwarded_count_mismatch_returns_unchanged():
|
|
|
|
|
# Defensive: not exactly one forwarded message per original → bail.
|
|
|
|
|
assert (
|
|
|
|
|
overlay_cached_prefix(OPTIMIZED_BUGGY, CUR_ORIG, PREV_ORIG, PREV_FWD[:1]) == OPTIMIZED_BUGGY
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_shorter_current_or_optimized_returns_unchanged():
|
|
|
|
|
assert overlay_cached_prefix([M("user", "x")], [M("user", "x")], PREV_ORIG, PREV_FWD) == [
|
|
|
|
|
M("user", "x")
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
fix(proxy/anthropic): don't replay recorded prefix over live history (#3026) (#3052)
## Description
A Claude Code session that reads a large tool result through `headroom
proxy` can fail on turn 2 with Anthropic's `400 prompt_too_long`. The
reporter's controlled comparison completed eight turns through 0.33.0
with 187,986 input tokens, while 0.35.0 failed after five requests with
753,077 input tokens. The local regression uses an actual prior
optimized request to populate tracker state, then a decision-false
bypass turn with Claude-shaped tool-result content. The old
unconditional replay path substitutes the compressed prefix; the
eligibility gate preserves the client's outbound body without claiming a
live provider reproduction.
The Anthropic `/v1/messages` route computes whether a request should be
compressed, but cached-prefix replay currently runs outside that
decision. The replay helper also derives its prefix length from the
original message list and applies that index to the optimized list
without proving the two lists still align. A stale forwarded prefix can
therefore be grafted onto the wrong positions and enlarge later
requests.
This change limits replay to requests whose existing compression
decision permits it and whose pre-upstream backpressure path is
inactive. It also makes `overlay_cached_prefix()` decline misaligned or
inflating candidates while preserving normal append-only replay.
Reported by @itsumonotakumi, whose controlled comparison isolated the
failure from compression, headers, one-request serialization, memory,
code graph, and CCR.
Closes #3026
## 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
- Gate Anthropic cached-prefix replay on the existing
`CompressionDecision.should_compress` result and the existing
pre-upstream backpressure state.
- Require positional alignment between optimized and original message
arrays before replay.
- Reject replay candidates that would serialize larger than the current
optimized messages.
- Add focused handler coverage for the decision-false tool-result
regression, bypass and backpressure paths, and outbound optimize-on
preservation.
- Add direct unit coverage for positional mismatch, no-inflation, and
JSON sizing-failure bailouts.
- Update the moved-cache-control and pure-block-append regression
fixtures to keep the no-inflation contract explicit.
- Run the unchanged OpenAI cache-stability preservation proof; no OpenAI
production code was edited.
## Testing
- [x] Unit tests pass (153 focused proxy, helper, cache-control,
block-append, cross-turn, byte-faithful, Anthropic, OpenAI, and
backpressure tests)
- [x] Linting passes (Ruff check and format validation on the seven
changed repository files)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed with the in-process proxy and local stub
upstream
### Test Output
```text
python -m pytest tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cross_turn_cache_safety.py tests/test_cache_control_move_bust.py tests/test_proxy_byte_faithful_forwarding.py tests/test_proxy_anthropic_cache_stability.py tests/test_anthropic_pre_upstream_backpressure.py -q
python -m pytest tests/test_proxy_openai_cache_stability.py -q
python -m pytest tests/test_issue_2671_block_growth_cache.py::test_pure_append_replays_forwarded_blocks_and_advances_breakpoint -q
153 passed across focused invocations, exit code 0
optimize_off turn2_message_count=3 marker_count=1 outbound_compact_utf8_bytes=2293 client_compact_utf8_bytes=2293
optimize_on turn2_message_count=3 client_message_count=3 marker_count=0 outbound_compact_utf8_bytes=171 client_compact_utf8_bytes=182
python -m ruff check headroom/proxy/handlers/anthropic.py headroom/cache/prefix_tracker.py tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cache_control_move_bust.py tests/test_issue_2671_block_growth_cache.py tests/test_proxy_openai_cache_stability.py
All checks passed!, exit code 0
python -m ruff format headroom/proxy/handlers/anthropic.py headroom/cache/prefix_tracker.py tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cache_control_move_bust.py tests/test_issue_2671_block_growth_cache.py tests/test_proxy_openai_cache_stability.py --check
7 files already formatted, exit code 0
git diff --check
clean, exit code 0
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12 via `uv`, real Headroom proxy app
with a local stub Anthropic upstream
- Exact command / steps: send an actual optimize-on first request
through the in-process proxy with a deterministic production-pipeline
seam, then send a decision-false bypass turn containing a large
Claude-shaped `tool_result` with moved `cache_control`; separately send
an aligned optimize-on turn with a new suffix
- Observed result: the exact base checkout fails with `AssertionError:
assert 'compressed-tool-result' == 'large-tool-result-marker ...'`; the
guarded path passes with the client marker present once and outbound
compact JSON no larger than the client body. The optimize-on
preservation run records `optimize_on turn2_message_count=3
client_message_count=3 marker_count=0 outbound_compact_utf8_bytes=171
client_compact_utf8_bytes=182`, proving the actual compressed prefix is
outbound before the new suffix without turn-2 growth.
- Not tested: live Claude Code session against api.anthropic.com on this
host
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: cached-prefix replay now follows the
existing compression and backpressure decision and rejects misaligned or
inflating candidates.
- Kill switch / disable path: no new switch; the existing optimize and
bypass controls remain available.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert the implementation commit.
## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] 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
## Additional Notes
`CHANGELOG.md` is not modified because Headroom's release automation
generates it from conventional commits.
This change does not add a context-limit guard or alter compression,
streaming tracker provenance, outbound-body selection, OpenAI behavior,
or provider limits. Local tests prove request-body ownership and replay
bounds. The reporter's live Claude Code completion and Anthropic token
acceptance remain external to this local proof.
2026-08-17 18:02:18 -04:00
|
|
|
def test_overlay_requires_positional_alignment_with_originals():
|
|
|
|
|
optimized = [M("user", "x")]
|
|
|
|
|
current = [M("user", "x"), M("assistant", "ok")]
|
|
|
|
|
assert overlay_cached_prefix(optimized, current, PREV_ORIG, PREV_FWD) == optimized
|
|
|
|
|
|
|
|
|
|
optimized = [M("user", "x"), M("assistant", "ok"), M("user", "tail")]
|
|
|
|
|
current = [M("user", "x"), M("assistant", "ok")]
|
|
|
|
|
previous = [M("user", "x"), M("assistant", "ok")]
|
|
|
|
|
forwarded = [M("user", "compressed"), M("assistant", "ok")]
|
|
|
|
|
assert overlay_cached_prefix(optimized, current, previous, forwarded) == optimized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_overlay_never_inflates_forwarded_payload():
|
|
|
|
|
optimized = [M("user", "small"), M("assistant", "ok"), M("user", "tail")]
|
|
|
|
|
inflated_forwarded = [M("user", "x" * 1000), M("assistant", "ok")]
|
|
|
|
|
previous = [M("user", "small"), M("assistant", "ok")]
|
|
|
|
|
current = previous + [M("user", "tail")]
|
|
|
|
|
assert overlay_cached_prefix(optimized, current, previous, inflated_forwarded) == optimized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_overlay_returns_optimized_when_json_sizing_fails(monkeypatch):
|
|
|
|
|
optimized = [M("user", "stable"), M("user", "tail")]
|
|
|
|
|
current = [M("user", "stable"), M("user", "tail")]
|
|
|
|
|
previous = [M("user", "stable")]
|
|
|
|
|
forwarded = [M("user", "compressed")]
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"headroom.cache.prefix_tracker.json.dumps",
|
|
|
|
|
lambda *args, **kwargs: (_ for _ in ()).throw(TypeError("cannot size")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert overlay_cached_prefix(optimized, current, previous, forwarded) == optimized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_overlay_never_inflates_cache_control_only_replay():
|
|
|
|
|
previous = [M("user", "stable"), M("assistant", "ok")]
|
|
|
|
|
current = [
|
|
|
|
|
M("user", "stable"),
|
|
|
|
|
{**M("assistant", "ok"), "cache_control": {"type": "ephemeral"}},
|
|
|
|
|
]
|
|
|
|
|
optimized = copy.deepcopy(current)
|
|
|
|
|
inflated_forwarded = [M("user", "x" * 1000), M("assistant", "ok")]
|
|
|
|
|
assert overlay_cached_prefix(optimized, current, previous, inflated_forwarded) == optimized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_block_append_overlay_never_inflates_forwarded_payload():
|
|
|
|
|
previous = [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [{"type": "text", "text": "stable"}],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
current = [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "text", "text": "stable"},
|
|
|
|
|
{"type": "text", "text": "tail"},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
optimized = copy.deepcopy(current)
|
|
|
|
|
forwarded = [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [{"type": "text", "text": "x" * 1000}],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
assert overlay_cached_prefix(optimized, current, previous, forwarded) == optimized
|
|
|
|
|
|
|
|
|
|
|
fix(proxy): freeze must forward cached (compressed) prefix byte-identical — stop token-mode cache busting (#1850)
The freeze path (both providers) emits the agent's ORIGINAL bytes for a
frozen message, but the provider cached whatever we FORWARDED last turn
(the compressed form). Forwarding original then mismatches the cached
prefix and busts it from that point — re-creating the whole suffix.
Measured on a real SWE-bench run: 100% of attributed misses were
prefix_change, ~56% of ALL cache-writes were bust-induced (2.8M tokens),
driving cache_create +150% and cost +41% vs baseline.
Cache mode already avoided this via _extract_cache_stable_delta (replay
the previously-forwarded prefix, compress only the delta). Token mode
called apply(frozen_count) directly, which forwards original for the
frozen region.
Fix: add a shared, provider-agnostic overlay_cached_prefix() that
replays the previously-forwarded (cached, compressed) prefix
byte-identical, append-only guarded and idempotent, and apply it in BOTH
the Anthropic and OpenAI handlers right before forwarding. This makes
freezing byte-identical in every mode, so the only remaining difference
between "token" and "cache" mode is how large a mutable
(still-compressible) tail each leaves — not whether the frozen prefix
busts the cache.
Tests:
- test_cache_prefix_overlay.py: the helper (replay, append-only guard,
idempotence).
- test_cross_turn_cache_safety.py: the invariant that was missing —
drive the REAL tracker + freeze + overlay over multiple append-only
turns against a simulated provider prefix cache and assert the forwarded
prefix stays byte-identical turn-over-turn. Load-bearing: it fails
(detects the bust) without the overlay.
## Description
<!-- Briefly explain the change and why it is needed. -->
Closes #
## Type of Change
- [ ] 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
-
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
# Paste relevant command output or artifact links here
```
## Real Behavior Proof
- Environment:
- Exact command / steps:
- Observed result:
- Not tested:
## Review Readiness
- [ ] I have performed a self-review
- [ ] This PR is ready for human review
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
Add screenshots to help explain your changes.
## Additional Notes
<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->
2026-07-06 14:54:39 -07:00
|
|
|
def test_cache_hit_property_prefix_matches_last_forward():
|
|
|
|
|
# The invariant that guarantees a cache hit: forwarded[:n] this turn ==
|
|
|
|
|
# forwarded[:n] last turn (== what the provider cached).
|
|
|
|
|
out = overlay_cached_prefix(OPTIMIZED_BUGGY, CUR_ORIG, PREV_ORIG, PREV_FWD)
|
|
|
|
|
n = len(PREV_FWD)
|
|
|
|
|
assert out[:n] == PREV_FWD # exact byte-identical prefix → provider cache hit
|
2026-07-08 16:29:35 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# OpenAI function-calling frozen-count: tool_calls must be counted (Kimi bug)
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# _estimate_message_tokens only counted `content` + Anthropic content-blocks,
|
|
|
|
|
# never OpenAI top-level `tool_calls`. So a function-calling assistant turn
|
|
|
|
|
# (content None, command in tool_calls) estimated to ~0, the frozen-prefix
|
|
|
|
|
# estimate overshot the real cache boundary, and the NEWEST delta got frozen —
|
|
|
|
|
# giving OpenAI/Kimi tool harnesses ~zero compression. These lock in the fix.
|
|
|
|
|
import json as _json
|
|
|
|
|
|
|
|
|
|
from headroom.cache.prefix_tracker import PrefixCacheTracker, PrefixFreezeConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _openai_asst(cmd):
|
|
|
|
|
return {
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": None,
|
|
|
|
|
"tool_calls": [
|
|
|
|
|
{
|
|
|
|
|
"id": "c1",
|
|
|
|
|
"type": "function",
|
|
|
|
|
"function": {"name": "bash", "arguments": _json.dumps({"command": cmd})},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_estimate_counts_openai_tool_calls():
|
|
|
|
|
est = PrefixCacheTracker._estimate_message_tokens
|
|
|
|
|
cmd = "cd /tmp/core && cat suma/apps/underwriting/followup/service.py"
|
|
|
|
|
with_calls = est([_openai_asst(cmd)])[0]
|
|
|
|
|
# empty content + no tool_calls counted => only the +20 overhead (~5 tok)
|
|
|
|
|
bare = est([{"role": "assistant", "content": None}])[0]
|
|
|
|
|
assert with_calls > bare + 5, (with_calls, bare) # the command is now counted
|
|
|
|
|
# legacy function_call shape too
|
|
|
|
|
fc = est(
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": None,
|
|
|
|
|
"function_call": {"name": "bash", "arguments": _json.dumps({"command": cmd})},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
)[0]
|
|
|
|
|
assert fc > bare + 5, (fc, bare)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_frozen_count_leaves_openai_tool_delta_mutable():
|
|
|
|
|
# A tool-based turn: cached prefix (system+task+prior tool obs) then a NEW
|
|
|
|
|
# assistant tool_call + its observation. After update_from_response reports
|
|
|
|
|
# the prefix cached, the frozen count must NOT swallow the newest delta.
|
|
|
|
|
trk = PrefixCacheTracker("openai", PrefixFreezeConfig(min_cached_tokens=10))
|
|
|
|
|
msgs = [
|
|
|
|
|
{"role": "system", "content": "s" * 400},
|
|
|
|
|
{"role": "user", "content": "task " * 200},
|
|
|
|
|
_openai_asst("cd /tmp/core && rg -n foo ."),
|
|
|
|
|
{"role": "tool", "tool_call_id": "c1", "content": "hit\n" * 300}, # cached prefix ends here
|
|
|
|
|
_openai_asst("cd /tmp/core && cat foo.py"), # NEW delta (assistant)
|
|
|
|
|
{
|
|
|
|
|
"role": "tool",
|
|
|
|
|
"tool_call_id": "c1",
|
|
|
|
|
"content": "code\n" * 400,
|
|
|
|
|
}, # NEW delta (observation)
|
|
|
|
|
]
|
|
|
|
|
counts = PrefixCacheTracker._estimate_message_tokens(msgs)
|
|
|
|
|
# cache_read ~= the first 4 messages' real tokens (prefix cached)
|
|
|
|
|
cached_prefix_tokens = sum(counts[:4])
|
|
|
|
|
trk.update_from_response(
|
|
|
|
|
cache_read_tokens=cached_prefix_tokens,
|
|
|
|
|
cache_write_tokens=0,
|
|
|
|
|
messages=msgs,
|
|
|
|
|
message_token_counts=counts,
|
|
|
|
|
)
|
|
|
|
|
frozen = trk.get_frozen_message_count()
|
|
|
|
|
# must freeze ~the cached prefix (<=4), NOT the whole 6 (which would freeze
|
|
|
|
|
# the newest observation delta and block all compression).
|
|
|
|
|
assert frozen <= 4, f"frozen={frozen} swallowed the delta (len={len(msgs)})"
|