Skip to content

fix(content-router): protect_tool_results must not be weakened by profile-derived read_protection_window#2105

Merged
chopratejas merged 2 commits into
headroomlabs-ai:mainfrom
ingmarkruschHF:fix/protect-tool-results-precedence
Jul 13, 2026
Merged

fix(content-router): protect_tool_results must not be weakened by profile-derived read_protection_window#2105
chopratejas merged 2 commits into
headroomlabs-ai:mainfrom
ingmarkruschHF:fix/protect-tool-results-precedence

Conversation

@ingmarkruschHF

@ingmarkruschHF ingmarkruschHF commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Description

ContentRouter.apply() computes read_protection_window from protect_recent_reads_fraction, where 0.0 (the sentinel --protect-tool-results sets, per #1374's documented contract) means "protect all excluded-tool output regardless of conversation depth." The method then unconditionally overwrote that window with a per-request read_protection_window kwarg whenever one was present. proxy_pipeline_kwargs() supplies that kwarg on every request from the active AgentSavingsProfile.protect_recent (the default coding profile sets protect_recent=2), so in practice only the last 2 messages ever kept read-protection regardless of --protect-tool-results — older excluded-tool output (Read, Glob, Grep, Write, Edit results) silently fell through to lossy Kompress compression.

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

  • headroom/transforms/content_router.py: the runtime read_protection_window kwarg may now only narrow the window when self.config.protect_recent_reads_fraction > 0. It can no longer override the 0.0 ("protect everything") sentinel that --protect-tool-results sets.
  • tests/test_content_router_exclude_tools.py: regression coverage that --protect-tool-results-equivalent config (protect_recent_reads_fraction=0.0) stays fully protected even when a savings-profile kwarg would otherwise shrink the window.
  • tests/test_transforms/test_content_router.py: unit coverage of the precedence logic itself (kwarg narrows when fraction > 0, kwarg is ignored when fraction == 0.0).
  • CHANGELOG.md: added an ### Bug Fixes entry under Unreleased.

Testing

  • Unit tests pass (pytest)
  • Linting passes (ruff check .)
  • Type checking passes (mypy headroom)
  • New tests added for new functionality
  • Manual testing performed

Test Output

$ uv run pytest tests/test_content_router_exclude_tools.py tests/test_transforms/test_content_router.py -q
============================= test session starts ==============================
platform darwin -- Python 3.13.13, pytest-9.0.3, pluggy-1.6.0
collected 64 items

tests/test_content_router_exclude_tools.py ......                        [  9%]
tests/test_transforms/test_content_router.py ........................... [ 51%]
...............................                                          [100%]

============================== 64 passed in 2.77s ==============================

$ uv run ruff check headroom/transforms/content_router.py tests/test_content_router_exclude_tools.py tests/test_transforms/test_content_router.py
All checks passed!

$ uv run mypy headroom/transforms/content_router.py
Success: no issues found in 1 source file

Real Behavior Proof

  • Environment: personal fork deployed as a real proxy (macOS launchd service, headroom install apply) with --backend bedrock --mode token --code-aware --protect-tool-results Bash, HEADROOM_SAVINGS_PROFILE=coding (library default protect_recent=2), fronting a live Claude Code session.
  • Exact command / steps: in a long-running Claude Code session against this deployment, Read a source file, continue the conversation past 2 more assistant turns (so the file's Read result ages past the profile's protect_recent=2 window), then have the agent re-read or reference the same file.
  • Observed result: before the fix, the aged Read output for a plain (non-code) file came back as [N items compressed to M. Retrieve more: hash=...] despite --protect-tool-results being set and Read sitting in DEFAULT_EXCLUDE_TOOLS — confirmed by direct proxy log inspection (content_router.py's override silently winning over the 0.0 sentinel) and by byte-diffing the installed pipx package against this same fork's git source to rule out a stale build. After applying the fix, the same sequence leaves the aged Read output intact (no compression marker) — verified via pytest regression tests plus a fresh live-session check post-deploy.
  • Not tested: this deployment has since switched to --mode cache (upstream's tested/benchmarked default for the coding profile as of 68676daa), where the whole read_protection_window mechanism this bug lives in is structurally unreachable for anything inside the frozen prefix — so the precedence fix in this PR is primarily relevant to token-mode deployments (or any deployment where cache mode's frozen-prefix boundary hasn't yet advanced past the affected message). It has not been independently re-verified live under --mode token after the most recent rebase onto main (only the automated test suite was rerun post-rebase).

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)

N/A — backend logic change, no UI surface.

Additional Notes

  • "I have made corresponding changes to the documentation" is unchecked: no file in docs/, README.md, or CONTRIBUTING.md documents read_protection_window, protect_recent_reads_fraction, or --protect-tool-results precedence at all, so there was no existing section to update, and no new section was added either. This is arguably a pre-existing documentation gap this PR doesn't close.
  • No linked issue number: this was found via independent investigation of a personal deployment, not filed as a headroomlabs-ai/headroom issue first.

…file-derived read_protection_window

ContentRouter.apply() computed read_protection_window from
protect_recent_reads_fraction (0.0 = "protect all excluded-tool output
regardless of conversation depth", the sentinel --protect-tool-results
sets per headroomlabs-ai#1374's documented contract), but then unconditionally
overwrote that window with a per-request read_protection_window kwarg
whenever one was present. proxy_pipeline_kwargs() supplies that kwarg
on every request from the active AgentSavingsProfile.protect_recent
(the default "coding" profile sets protect_recent=2), so in practice
only the last 2 messages ever kept read-protection regardless of
--protect-tool-results.

The runtime kwarg may now only narrow the window when
protect_recent_reads_fraction > 0; it can no longer shrink the
"protect everything" guarantee set by --protect-tool-results.

Adds a regression test to each of the two existing exclude-tools test
files, both verified to fail without the fix and pass with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR governance

This PR does not yet satisfy the required template fields:

  • Fill in Real Behavior ProofEnvironment.
  • Fill in Real Behavior ProofExact command / steps.
  • Fill in Real Behavior ProofObserved result.
  • Fill in Real Behavior ProofNot tested.

Please update the PR body, or move the PR back to draft while it is still in progress.

@github-actions github-actions Bot added the status: needs author action Pull request body or readiness checklist still needs author updates label Jul 13, 2026
@chopratejas chopratejas merged commit 3d0e59e into headroomlabs-ai:main Jul 13, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: needs author action Pull request body or readiness checklist still needs author updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants