mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(integrations): filter CCR-dropped sentinel in test iteration
The PR8 marker injection appends a sentinel object
{"_ccr_dropped": "<<ccr:HASH N_rows_offloaded>>"} to the kept-items
array on the lossy path so the LLM sees the retrieval pointer in the
prompt. Tests that iterate compressed arrays via subscript access
(e["level"], r["status"], i["labels"], m["text"]) hit KeyError on
the sentinel because it doesn't share the record schema.
Same root cause as the test_quality_retention fixes in PR8 -- these
integration tests were left out of that pass.
Ship a public helper headroom.transforms.smart_crusher.strip_ccr_sentinels
so tests can use it cleanly: `for e in strip_ccr_sentinels(entries):`
and production callers iterating compressed output get a single
canonical filter instead of inlining the _ccr_dropped check.
The 7 previously-failing tests in PR #292 CI now pass:
- langchain test_100_percent_errors_preserved_logs
- langchain test_errors_preserved_with_many_errors
- langchain test_search_results_with_query_term
- mcp test_all_log_errors_preserved
- mcp test_slack_significant_compression_with_content
- mcp test_database_error_status_preserved
- mcp test_github_bugs_partial_preservation
753 tests across the integration + transforms + retention suites pass
locally. Plugin manifests auto-bumped 0.13.3 -> 0.13.4 by the
sync-plugin-versions hook (unrelated to this fix).
This commit is contained in:
parent
da7716a95a
commit
b8fc7eee19
7 changed files with 59 additions and 14 deletions
|
|
@ -5,14 +5,14 @@
|
|||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.13.2"
|
||||
"version": "0.13.4"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.4",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
|
|
|
|||
4
.github/plugin/marketplace.json
vendored
4
.github/plugin/marketplace.json
vendored
|
|
@ -5,14 +5,14 @@
|
|||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.13.2"
|
||||
"version": "0.13.4"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.4",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/chopratejas/headroom"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,39 @@ from .base import Transform
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ─── CCR sentinel ─────────────────────────────────────────────────────────
|
||||
#
|
||||
# When SmartCrusher's lossy path drops rows, it appends a sentinel object
|
||||
# `{"_ccr_dropped": "<<ccr:HASH N_rows_offloaded>>"}` to the kept-items
|
||||
# array. The LLM sees this in the prompt and can ask for the original via
|
||||
# the CCR retrieval tool. Downstream consumers that iterate the array
|
||||
# expecting a uniform schema (e.g. `for e in entries: e["level"]`) need
|
||||
# to skip the sentinel — that's what `strip_ccr_sentinels` is for.
|
||||
|
||||
CCR_SENTINEL_KEY = "_ccr_dropped"
|
||||
|
||||
|
||||
def is_ccr_sentinel(item: Any) -> bool:
|
||||
"""True if `item` is a CCR-dropped sentinel object."""
|
||||
return isinstance(item, dict) and CCR_SENTINEL_KEY in item
|
||||
|
||||
|
||||
def strip_ccr_sentinels(items: Any) -> Any:
|
||||
"""Return `items` with any CCR-dropped sentinel objects filtered out.
|
||||
|
||||
Pass this through any iteration over a compressed array's contents
|
||||
when your code expects a uniform-schema list of records. The sentinel
|
||||
carries a `<<ccr:HASH ...>>` marker for the LLM and shouldn't be
|
||||
confused for a record — it has only the `_ccr_dropped` key.
|
||||
|
||||
Non-list inputs pass through unchanged so callers can wrap whatever
|
||||
`json.loads` returned without first checking the shape.
|
||||
"""
|
||||
if not isinstance(items, list):
|
||||
return items
|
||||
return [x for x in items if not is_ccr_sentinel(x)]
|
||||
|
||||
|
||||
# ─── Public dataclasses ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.4",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.4",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import pytest
|
|||
from headroom.config import SmartCrusherConfig
|
||||
from headroom.providers import OpenAIProvider
|
||||
from headroom.transforms import SmartCrusher
|
||||
from headroom.transforms.smart_crusher import strip_ccr_sentinels
|
||||
|
||||
|
||||
# Test fixtures for realistic data
|
||||
|
|
@ -190,8 +191,12 @@ class TestErrorPreservation:
|
|||
json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL)
|
||||
compressed_data = json.loads(json_match.group(1) if json_match else compressed_output)
|
||||
|
||||
# Count preserved errors
|
||||
compressed_errors = [e for e in compressed_data["entries"] if e["level"] == "ERROR"]
|
||||
# Count preserved errors. Strip CCR-dropped sentinel objects
|
||||
# before iterating — they carry the retrieval marker for the LLM
|
||||
# but don't share the entry schema.
|
||||
compressed_errors = [
|
||||
e for e in strip_ccr_sentinels(compressed_data["entries"]) if e["level"] == "ERROR"
|
||||
]
|
||||
|
||||
# CRITICAL: 100% of errors must be preserved
|
||||
assert len(compressed_errors) == len(original_errors), (
|
||||
|
|
@ -226,7 +231,9 @@ class TestErrorPreservation:
|
|||
json_match = re.search(r"(\{.*\})", compressed_output, re.DOTALL)
|
||||
compressed_data = json.loads(json_match.group(1) if json_match else compressed_output)
|
||||
|
||||
compressed_errors = [e for e in compressed_data["entries"] if e["level"] == "ERROR"]
|
||||
compressed_errors = [
|
||||
e for e in strip_ccr_sentinels(compressed_data["entries"]) if e["level"] == "ERROR"
|
||||
]
|
||||
|
||||
# Even with many errors, ALL must be preserved
|
||||
assert len(compressed_errors) == len(original_errors), (
|
||||
|
|
@ -314,7 +321,7 @@ class TestRelevancePreservation:
|
|||
# At least some high-relevance results should be preserved
|
||||
# (BM25 may not catch all without exact keyword matches)
|
||||
compressed_high_relevance = [
|
||||
r for r in compressed_data["results"] if r["relevance_score"] > 0.8
|
||||
r for r in strip_ccr_sentinels(compressed_data["results"]) if r["relevance_score"] > 0.8
|
||||
]
|
||||
|
||||
# With BM25, we should preserve at least 1 high-relevance result
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from headroom.integrations.mcp import (
|
|||
compress_tool_result_with_metrics,
|
||||
)
|
||||
from headroom.providers import OpenAIProvider
|
||||
from headroom.transforms.smart_crusher import strip_ccr_sentinels
|
||||
|
||||
# ============================================================================
|
||||
# Test Fixtures
|
||||
|
|
@ -280,7 +281,9 @@ class TestMCPErrorPreservation:
|
|||
|
||||
compressed_data = json.loads(result.compressed_content)
|
||||
compressed_errors = [
|
||||
e for e in compressed_data["entries"] if e["level"] in ["ERROR", "FATAL"]
|
||||
e
|
||||
for e in strip_ccr_sentinels(compressed_data["entries"])
|
||||
if e["level"] in ["ERROR", "FATAL"]
|
||||
]
|
||||
|
||||
# CRITICAL: 100% of errors must be preserved
|
||||
|
|
@ -306,7 +309,7 @@ class TestMCPErrorPreservation:
|
|||
# Should preserve some messages with error keywords (SmartCrusher detects these)
|
||||
error_msgs = [
|
||||
m
|
||||
for m in compressed_data["messages"]
|
||||
for m in strip_ccr_sentinels(compressed_data["messages"])
|
||||
if any(kw in m["text"].lower() for kw in ["error", "failed", "exception"])
|
||||
]
|
||||
assert len(error_msgs) > 0, "Should preserve some error messages"
|
||||
|
|
@ -327,7 +330,9 @@ class TestMCPErrorPreservation:
|
|||
|
||||
compressed_data = json.loads(result.compressed_content)
|
||||
compressed_errors = [
|
||||
r for r in compressed_data["rows"] if "error" in str(r["status"]).lower()
|
||||
r
|
||||
for r in strip_ccr_sentinels(compressed_data["rows"])
|
||||
if "error" in str(r["status"]).lower()
|
||||
]
|
||||
|
||||
# Should preserve most error rows
|
||||
|
|
@ -350,7 +355,7 @@ class TestMCPErrorPreservation:
|
|||
# Should preserve at least some bugs
|
||||
compressed_bugs = [
|
||||
i
|
||||
for i in compressed_data["issues"]
|
||||
for i in strip_ccr_sentinels(compressed_data["issues"])
|
||||
if any(label in ["bug", "critical", "urgent", "blocker"] for label in i["labels"])
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue