fix(ccr): skip compact summaries for proactive expansion (#2242)

## Description

Fixes #2186.

Claude Code `/compact` continuation summaries are already session
context. When Headroom tracks those summaries for CCR proactive
expansion, later fresh sessions can receive stale compacted history
again inside `<headroom_proactive_expansion>` blocks, increasing token
usage and busting cache stability.

This PR keeps CCR storage/retrieval intact but excludes probable Claude
Code compact-summary payloads from the proactive-expansion tracker.

## 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

- Added a narrow Claude Code compact-summary detector to the CCR context
tracker.
- Skipped tracking compact summaries when feeding Anthropic CCR metadata
into proactive expansion.
- Added an original-content preview to CCR metadata so the Anthropic
feed point can classify compact summaries even when compressed text
loses the distinctive header.
- Added regression coverage proving compact summaries are not tracked
and ordinary summaries are still eligible.

## Testing

- [x] Unit tests pass
- [x] Linting passes
- [x] Formatting check passes
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ pytest tests/test_ccr_context_tracker.py -q
37 passed

$ uvx ruff==0.15.17 check headroom/ccr/context_tracker.py headroom/cache/compression_store.py headroom/proxy/handlers/anthropic.py tests/test_ccr_context_tracker.py --output-format concise
All checks passed!

$ uvx ruff==0.15.17 format --check headroom/ccr/context_tracker.py headroom/cache/compression_store.py headroom/proxy/handlers/anthropic.py tests/test_ccr_context_tracker.py
4 files already formatted
```

## Real Behavior Proof

- Environment: local checkout on macOS, Python test environment used by
the repository.
- Exact command / steps: ran the focused CCR context tracker suite after
adding compact-summary detection and tracker-feed filtering.
- Observed result: compact-summary payloads are not tracked for
proactive expansion, ordinary summary-like tool output is still
eligible, and the existing tracker behavior remains covered by the full
focused suite.
- Not tested: live Claude Code `/compact` session through a running
proxy.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review
- [x] I have added tests that prove the fix is effective
- [ ] I have updated the CHANGELOG.md if applicable
This commit is contained in:
Vinay Gupta 2026-07-15 14:57:46 -05:00 committed by GitHub
parent fcf455a7eb
commit 3f241e472b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 0 deletions

View file

@ -480,6 +480,7 @@ class CompressionStore:
"compressed_item_count": entry.compressed_item_count,
"query_context": entry.query_context,
"compressed_content": entry.compressed_content,
"original_content_preview": entry.original_content[:2000],
"created_at": entry.created_at,
"ttl": entry.ttl,
}

View file

@ -31,6 +31,38 @@ from ..cache.compression_store import get_compression_store
logger = logging.getLogger(__name__)
def looks_like_claude_code_compact_summary(*texts: str | None) -> bool:
"""Return true for Claude Code `/compact` continuation summaries.
Claude Code can carry a previous session forward by injecting a compact
conversation summary into a fresh session. Those summaries are already
context; tracking them for CCR proactive expansion makes Headroom re-add
stale session state to later turns. Keep this detector deliberately narrow
so ordinary tool output that happens to mention "summary" remains eligible.
"""
combined = " ".join(text.strip() for text in texts if text and text.strip())
if not combined:
return False
normalized = " ".join(combined.lower().split())
has_summary = "summary" in normalized or "summarized" in normalized
if "this session is being continued from a previous conversation" in normalized and has_summary:
return True
if "conversation is summarized below" in normalized and (
"ran out of context" in normalized or "previous conversation" in normalized
):
return True
return (
"/compact" in normalized
and "claude" in normalized
and has_summary
and ("conversation" in normalized or "session" in normalized)
)
@dataclass
class CompressedContext:
"""Represents a piece of compressed context from the conversation.
@ -157,6 +189,13 @@ class ContextTracker:
if not self.config.enabled:
return
if looks_like_claude_code_compact_summary(query_context, sample_content):
logger.debug(
"CCR Tracker: skipped Claude Code compact summary %s for proactive expansion",
hash_key,
)
return
context = CompressedContext(
hash_key=hash_key,
turn_number=turn_number,

View file

@ -24,6 +24,7 @@ if TYPE_CHECKING:
import httpx
from headroom.agent_savings import proxy_pipeline_kwargs
from headroom.ccr.context_tracker import looks_like_claude_code_compact_summary
from headroom.copilot_auth import build_copilot_upstream_url
from headroom.pipeline import PipelineStage, summarize_routing_markers
from headroom.proxy.auth_mode import classify_auth_mode, classify_client
@ -1934,6 +1935,16 @@ class AnthropicHandlerMixin:
store = get_compression_store()
entry = store.get_metadata(hash_key)
if entry:
if looks_like_claude_code_compact_summary(
entry.get("query_context"),
entry.get("compressed_content"),
entry.get("original_content_preview"),
):
logger.info(
f"[{request_id}] CCR: skipping proactive "
f"tracking for Claude Code compact summary {hash_key}"
)
continue
self.ccr_context_tracker.track_compression(
hash_key=hash_key,
turn_number=self._turn_counter,

View file

@ -23,6 +23,7 @@ from headroom.ccr.context_tracker import (
ContextTrackerConfig,
ExpansionRecommendation,
get_context_tracker,
looks_like_claude_code_compact_summary,
reset_context_tracker,
)
@ -58,6 +59,37 @@ class TestContextTrackerBasics:
stats = tracker.get_stats()
assert stats["tracked_contexts"] == 1
def test_claude_code_compact_summary_not_tracked(self):
"""Claude Code `/compact` summaries are not proactively re-expanded."""
tracker = ContextTracker()
tracker.track_compression(
hash_key="compact_summary",
turn_number=1,
tool_name="Claude Code",
original_count=1,
compressed_count=1,
query_context="Claude Code /compact summary",
sample_content=(
"This session is being continued from a previous conversation "
"that ran out of context. The conversation is summarized below: "
"We changed the auth middleware and still need to run tests."
),
workspace_key="ws-test",
)
assert tracker.get_tracked_hashes() == []
def test_claude_code_compact_summary_detector_is_narrow(self):
"""The compact-summary detector does not reject ordinary summaries."""
assert looks_like_claude_code_compact_summary(
"This session is being continued from a previous conversation. "
"The conversation is summarized below."
)
assert not looks_like_claude_code_compact_summary(
"search results summary for auth middleware files"
)
def test_track_multiple_compressions(self):
"""Track multiple compression events."""
tracker = ContextTracker()