You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In fast agentic sessions (automated tool calls, minimal human wait between turns), the proactive context-expansion path re-injects compressed content from tasks that are long finished. The staleness gate in ContextTracker.analyze_query() is wall-clock only (max_context_age_seconds = 300). When many compressing turns happen within a few minutes, a context that was compressed dozens of turns ago still passes the 5-minute freshness check and gets surfaced as [Proactive Context Expansion - relevant to your query], even though the conversation has fully moved on.
The tracker already stores a turn_number on every CompressedContext and already receives current_turn in analyze_query(), but neither is used to gate or decay relevance — only wall-clock age is.
Expected Behavior
Proactive expansion should only inject content that is recent relative to the current conversational position, not just recent in wall-clock time. A context from many compressing-turns ago should not be considered fresh regardless of its timestamp.
Actual Behavior
[Proactive Context Expansion] blocks appear in the active context containing content from much earlier, unrelated tasks.
First-hand reproduction (this is not second-hand): during a single investigation session on this very file, headroom/ccr/context_tracker.py was re-injected 5+ times as a "relevant" proactive expansion across turns whose actual subject had nothing to do with it. The keyword overlap (context_tracker, proactive, expansion) kept matching, the wall-clock age stayed under 300s, and nothing else stopped it. The injected block carried the exact provenance string produced at headroom/ccr/context_tracker.py:485:
{original_item_count} items compressed in turn {turn_number}
i.e. content from a turn far in the conversational past, re-surfaced verbatim.
Root Cause
headroom/ccr/context_tracker.py, analyze_query() — wall-clock age is the only freshness signal:
# context_tracker.py:247age=now-context.timestampifage>self.config.max_context_age_seconds: # :87 → 300.0 (5 minutes), wall-clock onlycontinue
...
# context_tracker.py:255-256age_factor=1.0- (age/self.config.max_context_age_seconds) *0.5# max 50% penalty even at 4m59srelevance*=age_factor
current_turn is received and stored (context_tracker.py:234, self._current_turn = current_turn) but never used to gate or decay.
context.turn_number is stored on every compressed context but never read in analyze_query().
The call site passes the turn correctly: analyze_query(user_query, self._turn_counter, workspace_key=...) at headroom/proxy/handlers/anthropic.py:1354-1357.
Note on the turn counter: self._turn_counter is incremented only on turns that actually compress content (anthropic.py:1309, gated on injector.has_compressed_content), not on every API turn. So "turn distance" here measures elapsed compressing turns — a strong staleness signal, since a context 10+ compressions deep is almost always from a different task.
Environment
headroom run as a local HTTP proxy (ANTHROPIC_BASE_URL=http://localhost:8787)
Claude Code CLI as the upstream client
Fast agentic sessions: many automated tool calls per minute, minimal human turn cadence
Problem
In fast agentic sessions (automated tool calls, minimal human wait between turns), the proactive context-expansion path re-injects compressed content from tasks that are long finished. The staleness gate in
ContextTracker.analyze_query()is wall-clock only (max_context_age_seconds = 300). When many compressing turns happen within a few minutes, a context that was compressed dozens of turns ago still passes the 5-minute freshness check and gets surfaced as[Proactive Context Expansion - relevant to your query], even though the conversation has fully moved on.The tracker already stores a
turn_numberon everyCompressedContextand already receivescurrent_turninanalyze_query(), but neither is used to gate or decay relevance — only wall-clock age is.Expected Behavior
Proactive expansion should only inject content that is recent relative to the current conversational position, not just recent in wall-clock time. A context from many compressing-turns ago should not be considered fresh regardless of its timestamp.
Actual Behavior
[Proactive Context Expansion]blocks appear in the active context containing content from much earlier, unrelated tasks.First-hand reproduction (this is not second-hand): during a single investigation session on this very file,
headroom/ccr/context_tracker.pywas re-injected 5+ times as a "relevant" proactive expansion across turns whose actual subject had nothing to do with it. The keyword overlap (context_tracker,proactive,expansion) kept matching, the wall-clock age stayed under 300s, and nothing else stopped it. The injected block carried the exact provenance string produced atheadroom/ccr/context_tracker.py:485:i.e. content from a turn far in the conversational past, re-surfaced verbatim.
Root Cause
headroom/ccr/context_tracker.py,analyze_query()— wall-clock age is the only freshness signal:current_turnis received and stored (context_tracker.py:234,self._current_turn = current_turn) but never used to gate or decay.context.turn_numberis stored on every compressed context but never read inanalyze_query().analyze_query(user_query, self._turn_counter, workspace_key=...)atheadroom/proxy/handlers/anthropic.py:1354-1357.Note on the turn counter:
self._turn_counteris incremented only on turns that actually compress content (anthropic.py:1309, gated oninjector.has_compressed_content), not on every API turn. So "turn distance" here measures elapsed compressing turns — a strong staleness signal, since a context 10+ compressions deep is almost always from a different task.Environment
ANTHROPIC_BASE_URL=http://localhost:8787)e81c96ae(Merge PR docs: explain Claude MCP usage attribution #699)A proposed fix (turn-distance gate + combined decay) follows as a comment below.