Skip to content

Commit 5dcecdc

Browse files
authored
Merge pull request #306 from chopratejas/rust-stage-3d-audit-finalize
fix(smart_crusher): re-land orphaned audit close-out — CCR knob + scorer fail-loud
2 parents 1ee1bb2 + ca2cb45 commit 5dcecdc

11 files changed

Lines changed: 275 additions & 76 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
},
66
"metadata": {
77
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
8-
"version": "0.13.4"
8+
"version": "0.14.2"
99
},
1010
"plugins": [
1111
{
1212
"name": "headroom",
1313
"source": "./plugins/headroom-agent-hooks",
1414
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
15-
"version": "0.13.4",
15+
"version": "0.14.2",
1616
"author": {
1717
"name": "Headroom Contributors",
1818
"url": "https://github.com/chopratejas/headroom"

.github/plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
},
66
"metadata": {
77
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
8-
"version": "0.13.4"
8+
"version": "0.14.2"
99
},
1010
"plugins": [
1111
{
1212
"name": "headroom",
1313
"source": "./plugins/headroom-agent-hooks",
1414
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
15-
"version": "0.13.4",
15+
"version": "0.14.2",
1616
"author": {
1717
"name": "Headroom Contributors",
1818
"url": "https://github.com/chopratejas/headroom"

RUST_DEV.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ so they don't regress further or get forgotten.
172172
| Subsystem | State | Tracked by |
173173
|---|---|---|
174174
| TOIN learning loop | **Re-attached 2026-04-28.** Shim's `crush()` and `_smart_crush_content()` now call `toin.record_compression()` after a real compression. Filtered on `strategy != "passthrough"` to ignore JSON re-canonicalization. Best-effort: TOIN failures are logged at debug level and don't break compression. | `tests/test_smart_crusher_toin_attachment.py` |
175-
| CCR marker emission knob | **Open gap.** `ccr_config.inject_retrieval_marker=False` is not honored — the Rust port emits `<<ccr:HASH N_rows_offloaded>>` markers as part of `dropped_summary` unconditionally. Shim now logs a WARNING when callers pass `False`. **Fix needed:** add a `enable_ccr_marker: bool` gate in `crates/headroom-core/src/transforms/smart_crusher/crusher.rs::crush_array`, plumb through `SmartCrusherConfig` and the PyO3 bridge. | This file + warning at `headroom/transforms/smart_crusher.py` |
176-
| Custom relevance scorer | **Open gap.** `relevance_config` and `scorer` constructor args are accepted for source compatibility but the Rust default `HybridScorer` always runs. Shim now logs WARNING (previously debug). **Fix needed:** expose a Python-bridged scorer constructor surface from `crates/headroom-core/src/relevance/`. | Warning at `headroom/transforms/smart_crusher.py` |
175+
| CCR marker emission knob | **Honored end-to-end 2026-04-29.** New `enable_ccr_marker: bool` field on Rust `SmartCrusherConfig`; `crush_array` checks it before emitting the `<<ccr:HASH>>` marker text and the CCR store write. Python shim flips it from `ccr_config.enabled and ccr_config.inject_retrieval_marker` — both flags collapse to the same Rust gate, since storing payloads under either off-switch makes no sense. Scope: gates only the row-drop sentinel path; Stage-3c.2 opaque-string CCR substitutions still emit always (no Python equivalent, no production caller asks for suppression). | `tests/test_smart_crusher_toin_attachment.py` + `crates/headroom-core/.../crusher.rs::tests::enable_ccr_marker_*` |
176+
| Custom relevance scorer | **Closed (fail-loud) 2026-04-29.** `relevance_config` and `scorer` constructor args remain in the signature for source compat, but the shim raises `NotImplementedError` when either is non-None — silently dropping a user-supplied scorer is a textbook silent-fallback bug. Full plumbing waits on Stage-3c.2's relevance-crate Python bridge. | `tests/test_smart_crusher_toin_attachment.py::test_custom_*_arg_raises_not_implemented` |
177177
| Per-tool TOIN learning hook | **Re-attached partially.** `_smart_crush_content` accepts `tool_name` and now threads it into the TOIN record. The hook is best-effort — it improves `query_context` aggregation but doesn't drive per-tool overrides yet. | `tests/test_smart_crusher_toin_attachment.py::test_smart_crush_content_records_to_toin` |
178178

179179
### DiffCompressor
@@ -185,7 +185,7 @@ so they don't regress further or get forgotten.
185185

186186
### Watch list (potential regressions, not yet audited)
187187

188-
- `CCRConfig.enabled=False` end-to-end behavior. Currently the Rust port has a CCR store that's controlled by builder selection, not by a config flag. Sometimes-disabled paths haven't been audited.
188+
- `CCRConfig.enabled=False` end-to-end **closed 2026-04-29**. Both `enabled=False` and `inject_retrieval_marker=False` collapse to the same Rust `enable_ccr_marker=False` gate (no marker, no store write). See the SmartCrusher table above.
189189
- `SmartCrusherConfig.use_feedback_hints=False` — config field is forwarded to Rust but its honoring inside the Rust crusher hasn't been verified against a parity fixture for the disabled path.
190190

191191
When any item above changes, update both this section and the test file. The shim's docstring also references this section — keep them aligned.

crates/headroom-core/src/transforms/smart_crusher/config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ pub struct SmartCrusherConfig {
6262
/// lossless when available; set to `1.0` to effectively disable
6363
/// the lossless path (lossy + CCR always).
6464
pub lossless_min_savings_ratio: f64,
65+
/// Master gate for CCR-Dropped row-drop sentinels. When `false`,
66+
/// the lossy `crush_array` path skips both the `<<ccr:HASH>>`
67+
/// marker text AND the CCR-store write (no point storing a
68+
/// payload nothing in the prompt can reference).
69+
///
70+
/// The Python shim flips this from
71+
/// `ccr_config.enabled and ccr_config.inject_retrieval_marker`,
72+
/// so either off-switch on the Python side disables the gate.
73+
/// Default `true` — preserves prior behavior.
74+
///
75+
/// Scope: gates only the `crush_array` row-drop path. Stage-3c.2
76+
/// opaque-string CCR substitutions (in `walker::process_value`)
77+
/// still emit always; they have no Python equivalent and no
78+
/// production caller has asked for them to be suppressed.
79+
pub enable_ccr_marker: bool,
6580
}
6681

6782
impl Default for SmartCrusherConfig {
@@ -87,6 +102,7 @@ impl Default for SmartCrusherConfig {
87102
last_fraction: 0.15,
88103
relevance_threshold: 0.3,
89104
lossless_min_savings_ratio: 0.30,
105+
enable_ccr_marker: true,
90106
}
91107
}
92108
}
@@ -118,5 +134,6 @@ mod tests {
118134
assert_eq!(c.last_fraction, 0.15);
119135
assert_eq!(c.relevance_threshold, 0.3);
120136
assert_eq!(c.lossless_min_savings_ratio, 0.30);
137+
assert!(c.enable_ccr_marker);
121138
}
122139
}

crates/headroom-core/src/transforms/smart_crusher/crusher.rs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -660,13 +660,20 @@ impl SmartCrusher {
660660
);
661661
let result = self.execute_plan(&plan, items);
662662

663-
// Emit CCR-Dropped marker iff rows were actually dropped.
664-
// **This is the cornerstone of CCR's no-data-loss guarantee:**
665-
// we hash the full original, stash it in the configured store,
666-
// and emit a marker pointing at that hash. The runtime later
667-
// serves the original back via retrieval tool calls.
663+
// Emit CCR-Dropped marker iff rows were actually dropped AND
664+
// the marker gate is on. **The marker is the cornerstone of
665+
// CCR's no-data-loss guarantee:** we hash the full original,
666+
// stash it in the configured store, and emit a marker pointing
667+
// at that hash. The runtime later serves the original back via
668+
// retrieval tool calls.
669+
//
670+
// When `enable_ccr_marker` is false (Python shim's path for
671+
// `ccr_config.enabled=False` or `inject_retrieval_marker=False`)
672+
// we keep the row drops (compression is still requested) but
673+
// skip the marker text and the store write — there's no point
674+
// storing a payload that nothing in the prompt can reference.
668675
let dropped_count = items.len().saturating_sub(result.len());
669-
let (ccr_hash, dropped_summary) = if dropped_count > 0 {
676+
let (ccr_hash, dropped_summary) = if dropped_count > 0 && self.config.enable_ccr_marker {
670677
// Serialize the original array exactly ONCE. The hash is
671678
// taken over those bytes, and (if a store is configured) the
672679
// same bytes get stored — eliminating a redundant tree clone
@@ -1439,4 +1446,84 @@ mod tests {
14391446
assert!(try_parse_json_container("not json").is_none());
14401447
assert!(try_parse_json_container("{malformed").is_none());
14411448
}
1449+
1450+
// ---------- enable_ccr_marker gate (PR #301 re-land) ----------
1451+
1452+
#[test]
1453+
fn enable_ccr_marker_false_suppresses_marker_and_store() {
1454+
// The Rust-side gate. Compression still runs (rows drop) but
1455+
// the result carries no marker text, no hash, and the CCR
1456+
// store does NOT grow — there's no point storing what nothing
1457+
// in the prompt can reference.
1458+
use crate::ccr::InMemoryCcrStore;
1459+
use crate::transforms::smart_crusher::SmartCrusherBuilder;
1460+
use std::sync::Arc;
1461+
1462+
let store: Arc<dyn CcrStore> = Arc::new(InMemoryCcrStore::new());
1463+
let cfg = SmartCrusherConfig {
1464+
lossless_min_savings_ratio: 0.99, // force lossy path
1465+
enable_ccr_marker: false,
1466+
..SmartCrusherConfig::default()
1467+
};
1468+
let c = SmartCrusherBuilder::new(cfg)
1469+
.with_ccr_store(Arc::clone(&store))
1470+
.build();
1471+
let items: Vec<Value> = (0..50).map(|_| json!({"status": "ok"})).collect();
1472+
1473+
let store_len_before = store.len();
1474+
let result = c.crush_array(&items, "", 1.0);
1475+
let store_len_after = store.len();
1476+
1477+
// Rows were dropped (we built 50, kept fewer).
1478+
assert!(result.items.len() < items.len(), "lossy path didn't fire");
1479+
// Gate held: no marker, no hash.
1480+
assert!(result.ccr_hash.is_none(), "ccr_hash should be None");
1481+
assert!(
1482+
result.dropped_summary.is_empty(),
1483+
"dropped_summary should be empty, got: {:?}",
1484+
result.dropped_summary
1485+
);
1486+
// Store did NOT grow.
1487+
assert_eq!(
1488+
store_len_after, store_len_before,
1489+
"ccr_store grew despite enable_ccr_marker=false"
1490+
);
1491+
}
1492+
1493+
#[test]
1494+
fn enable_ccr_marker_true_is_default_behavior() {
1495+
// Default config still emits markers + stores when rows drop.
1496+
// Sanity: the gate is opt-out, not opt-in.
1497+
use crate::ccr::InMemoryCcrStore;
1498+
use crate::transforms::smart_crusher::SmartCrusherBuilder;
1499+
use std::sync::Arc;
1500+
1501+
let store: Arc<dyn CcrStore> = Arc::new(InMemoryCcrStore::new());
1502+
let cfg = SmartCrusherConfig {
1503+
lossless_min_savings_ratio: 0.99, // force lossy path
1504+
..SmartCrusherConfig::default()
1505+
};
1506+
// Default: enable_ccr_marker = true.
1507+
assert!(cfg.enable_ccr_marker);
1508+
let c = SmartCrusherBuilder::new(cfg)
1509+
.with_ccr_store(Arc::clone(&store))
1510+
.build();
1511+
let items: Vec<Value> = (0..50).map(|_| json!({"status": "ok"})).collect();
1512+
1513+
let store_len_before = store.len();
1514+
let result = c.crush_array(&items, "", 1.0);
1515+
let store_len_after = store.len();
1516+
1517+
assert!(result.items.len() < items.len(), "lossy path didn't fire");
1518+
assert!(result.ccr_hash.is_some(), "default should produce a hash");
1519+
assert!(
1520+
result.dropped_summary.contains("<<ccr:"),
1521+
"default should produce a marker: {:?}",
1522+
result.dropped_summary
1523+
);
1524+
assert!(
1525+
store_len_after > store_len_before,
1526+
"default should write to ccr_store"
1527+
);
1528+
}
14421529
}

crates/headroom-parity/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,13 @@ impl TransformComparator for SmartCrusherComparator {
397397
.get("lossless_min_savings_ratio")
398398
.and_then(|v| v.as_f64())
399399
.unwrap_or(defaults.lossless_min_savings_ratio),
400+
// Rust-only audit-fix knob — fixtures don't carry this; use
401+
// default (true). Recorded fixtures predate the gate and
402+
// their expected outputs assume markers fire as before.
403+
enable_ccr_marker: config
404+
.get("enable_ccr_marker")
405+
.and_then(|v| v.as_bool())
406+
.unwrap_or(defaults.enable_ccr_marker),
400407
};
401408

402409
// Use without_compaction so the legacy fixtures (recorded

crates/headroom-py/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ impl PySmartCrusherConfig {
456456
last_fraction = 0.15,
457457
relevance_threshold = 0.3,
458458
lossless_min_savings_ratio = 0.30,
459+
enable_ccr_marker = true,
459460
))]
460461
#[allow(clippy::too_many_arguments)]
461462
fn new(
@@ -476,6 +477,7 @@ impl PySmartCrusherConfig {
476477
last_fraction: f64,
477478
relevance_threshold: f64,
478479
lossless_min_savings_ratio: f64,
480+
enable_ccr_marker: bool,
479481
) -> Self {
480482
Self {
481483
inner: RustSmartCrusherConfig {
@@ -496,6 +498,7 @@ impl PySmartCrusherConfig {
496498
last_fraction,
497499
relevance_threshold,
498500
lossless_min_savings_ratio,
501+
enable_ccr_marker,
499502
},
500503
}
501504
}
@@ -564,6 +567,10 @@ impl PySmartCrusherConfig {
564567
fn relevance_threshold(&self) -> f64 {
565568
self.inner.relevance_threshold
566569
}
570+
#[getter]
571+
fn enable_ccr_marker(&self) -> bool {
572+
self.inner.enable_ccr_marker
573+
}
567574

568575
fn __repr__(&self) -> String {
569576
format!(

headroom/transforms/smart_crusher.py

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,27 @@
1818
fallback. Build it locally with `scripts/build_rust_extension.sh`
1919
(wraps `maturin develop`) or install a prebuilt wheel.
2020
21-
# Functionality state (post-audit, 2026-04-28)
21+
# Functionality state (post-audit, 2026-04-29)
2222
2323
- **TOIN learning** — re-attached. `crush()` and `_smart_crush_content`
2424
call `toin.record_compression()` after a real compression (filtered on
2525
`strategy != "passthrough"` to ignore JSON re-canonicalization).
2626
The retired Python class did this inline; the bridge keeps the
2727
highest-traffic strategy fueling the learning loop.
28-
- **CCR marker emission** — partial gap. The Rust port emits
29-
`<<ccr:HASH N_rows_offloaded>>` markers unconditionally as part of
30-
`dropped_summary`; the `ccr_config.inject_retrieval_marker=False`
31-
knob is therefore not honored end-to-end. The shim now logs a
32-
WARNING when callers pass `False`. Suppression needs a Rust-side
33-
gate; see RUST_DEV.md.
34-
- **Custom relevance scorer / scorer override** — still not wired.
35-
`relevance_config` and `scorer` constructor args are accepted for
36-
source compat but the Rust default `HybridScorer` is always used.
37-
The shim now logs a WARNING (was: debug) when these are passed.
28+
- **CCR marker emission** — honored end-to-end. Both
29+
`ccr_config.enabled=False` and
30+
`ccr_config.inject_retrieval_marker=False` flip the Rust crusher's
31+
`enable_ccr_marker` field; the lossy row-drop path then skips both
32+
the marker text and the CCR store write. Scope: gates only the
33+
row-drop sentinel path. Stage-3c.2 opaque-string CCR substitutions
34+
still emit always — they have no Python equivalent.
35+
- **Custom relevance scorer / scorer override** — fails loud.
36+
`relevance_config` and `scorer` constructor args remain in the
37+
signature for source compat, but the shim raises
38+
`NotImplementedError` when either is non-None. Silently dropping a
39+
user-supplied scorer is a silent-fallback bug we explicitly refuse
40+
to ship; full plumbing lands with Stage-3c.2's relevance-crate
41+
Python bridge.
3842
"""
3943

4044
from __future__ import annotations
@@ -176,37 +180,48 @@ def __init__(
176180
self._observer = observer
177181

178182
# CCR config is preserved on `self` for callers that read it
179-
# back (`headroom.proxy.server` does). Storage-side semantics
180-
# (CCR cache lookups) are honored via the Rust crusher's own
181-
# CCR store. *Marker emission* is the gap: the Rust port emits
182-
# `<<ccr:HASH N_rows_offloaded>>` markers unconditionally as
183-
# part of `dropped_summary`, so `inject_retrieval_marker=False`
184-
# has no effect on the prompt today. We log a WARNING (not
185-
# debug) so it's visible. Suppression of marker emission needs
186-
# a Rust-side gate; tracked in RUST_DEV.md.
183+
# back (`headroom.proxy.server` does). Both `enabled=False` and
184+
# `inject_retrieval_marker=False` collapse to the Rust crusher's
185+
# `enable_ccr_marker=False` gate — when either is off, the
186+
# lossy row-drop path skips marker emission AND the CCR store
187+
# write (no point storing a payload nothing in the prompt can
188+
# reference; storing it under `enabled=False` would also be a
189+
# surprise side effect the user explicitly disabled).
190+
#
191+
# Default falls through to `CCRConfig()` so direct callers
192+
# (the proxy and tests that don't pass an explicit config) get
193+
# the documented dataclass defaults (`enabled=True,
194+
# inject_retrieval_marker=True`). The previous override here
195+
# set `inject_retrieval_marker=False` as a no-op-intent hack
196+
# back when the Rust port silently ignored the flag; now that
197+
# the flag is honored, that override would actively suppress
198+
# markers + store writes for every caller.
199+
#
200+
# Scope: gates ONLY the row-drop sentinel path. Stage-3c.2
201+
# opaque-string CCR substitutions still emit always — they have
202+
# no Python equivalent and no production caller has asked for
203+
# them to be suppressed.
187204
if ccr_config is None:
188-
self._ccr_config = CCRConfig(enabled=True, inject_retrieval_marker=False)
205+
self._ccr_config = CCRConfig()
189206
else:
190207
self._ccr_config = ccr_config
191-
if not self._ccr_config.inject_retrieval_marker:
192-
logger.warning(
193-
"SmartCrusher: ccr_config.inject_retrieval_marker=False "
194-
"is currently NOT honored by the Rust port — CCR row-drop "
195-
"markers (`<<ccr:HASH N_rows_offloaded>>`) will still appear "
196-
"in compressed output. Tracked as a known regression in "
197-
"RUST_DEV.md until a Rust-side gate lands."
198-
)
199208

200-
# `relevance_config` and `scorer` are accepted for source
201-
# compatibility but currently dropped — Stage 3c.1 ships with
202-
# the Rust default `HybridScorer`. Custom scorers re-attach in
203-
# Stage 3c.2 when the relevance crate gains a Python-bridged
204-
# constructor surface.
209+
# `relevance_config` and `scorer` remain in the signature for
210+
# source compatibility, but the Rust port doesn't support
211+
# overrides yet (it always uses `HybridScorer` from the
212+
# relevance crate; the Python-bridged constructor surface
213+
# arrives in Stage 3c.2). Silently dropping a user-supplied
214+
# scorer would be a textbook silent fallback — if a caller
215+
# depends on a custom scoring function and we ignore it, the
216+
# compression they get back is wrong in a way they cannot see.
217+
# Fail loud instead. See `feedback_no_silent_fallbacks.md`.
205218
if relevance_config is not None or scorer is not None:
206-
logger.warning(
207-
"SmartCrusher: custom relevance_config/scorer args are "
208-
"currently ignored (Rust port uses default HybridScorer). "
209-
"Tracked as a known regression in RUST_DEV.md."
219+
raise NotImplementedError(
220+
"SmartCrusher: custom `relevance_config` / `scorer` "
221+
"overrides are not yet supported by the Rust-backed "
222+
"implementation. Pass `None` to use the default "
223+
"HybridScorer. Tracked in RUST_DEV.md; full support "
224+
"lands with Stage 3c.2's relevance-crate Python bridge."
210225
)
211226

212227
# Lazy TOIN handle. Loaded on first compression that has items
@@ -236,6 +251,9 @@ def __init__(
236251
first_fraction=cfg.first_fraction,
237252
last_fraction=cfg.last_fraction,
238253
relevance_threshold=0.3,
254+
enable_ccr_marker=(
255+
self._ccr_config.enabled and self._ccr_config.inject_retrieval_marker
256+
),
239257
)
240258
# Default: lossless-first compaction (PR4). Lossless wins for
241259
# cleanly tabular input where it saves ≥ 30% bytes; otherwise

0 commit comments

Comments
 (0)