Skip to content

Commit b7be381

Browse files
authored
feat: compression extraction — Rust knob exposure, CCR hardening, traffic audits (#818)
## Description A data-driven push for better compression savings without accuracy loss, in four parts: expose and tune the Rust compressor knobs, harden the CCR retrieval store, add traffic-audit tooling that sizes opportunities from real transcripts, and introduce **read maturation** — a new, live-validated mechanism that compresses Read outputs *before* they ever enter the provider prefix cache. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made ### 1. Rust compressor extraction - Expose `lossless_min_savings_ratio` end-to-end and lower the default 0.30 → 0.15 (lockstep across Rust, PyO3, and both Python config classes) so the lossless Table/CSV compaction path wins more often. - Expose the `CompactConfig` heuristics (core-field fraction, heterogeneity ratio, flatten cap, bucket bounds) through PyO3 + Python. - `SearchCompressor` grouped-by-file output (`rg --heading` style — path once per file instead of per match). Library default off; the proxy enables it in token mode. - Complete `factor_out_constants`: constant fields now emit once in a `_constant_fields` sentinel with slim rows (defensive per-item value match; default off). - `ContentRouter` accepts a SmartCrusher config override and the search-grouping knob. ### 2. CCR store hardening - Session-scale TTL: 300s → 1800s (CCRConfig, CompressionEntry, CompressionStore, Rust `DEFAULT_TTL` — lockstep). - **SQLite is the default CCR backend** (`~/.headroom/ccr_store.db`, WAL): survives proxy restarts and is shared across workers. `HEADROOM_CCR_BACKEND=memory` opts out. - Multi-worker safety: `busy_timeout`, and corruption detection narrowed so transient `SQLITE_BUSY` errors can never trigger database deletion. - Data-at-rest hygiene: `chmod 600` on db + sidecars, expired rows swept at open. - Retrieval-miss messages are actionable (re-read the file / re-run the command). ### 3. Traffic audit tooling (measure before tuning) - `headroom audit-reads`: sizes Read opportunities from local Claude Code transcripts (read share, stale %, line-number overhead, context residency, cache-death windows). - `--simulate-maturation`: Mechanism B risk sizing (re-read rates, never-touched-again share, quiesce coverage, at-risk edits). - `--codex`: shell-read classifier for Codex transcripts (rtk-wrapper aware, workdir resolution). - Findings that shaped this PR (81 sessions): Reads are 67% of tool bytes; median Read lingers 118 turns (~13x lifetime cost); a prototyped repeat-Read dedup measured 0.1% and was **removed** rather than shipped as dead code. ### 4. Read maturation (Mechanism B) — experimental, default OFF - Activity-based: a fresh large Read is held **out** of the provider cache (trailing breakpoint relocated before it), stays verbatim while its file is active, and matures into a CCR-backed marker once the file is quiet for `quiesce_turns` (default 5; `max_hold_turns` bounds busy files). - Only the final compressed form ever enters the cache — **no cached byte is ever mutated**; matured markers replay byte-identically. - Wired into the Anthropic handler behind `--read-maturation` / `HEADROOM_READ_MATURATION=1`; session state rides on the prefix tracker; advisory (can never fail a request). - Live-validated against the Anthropic API: held content excluded from cache_creation; after maturation the prior cached prefix still served — the no-bust invariant holds end-to-end. ### 5. Rebase / CI fixups (this update) - Rebased onto latest `main` (was 28 commits behind): picks up `ci: pass CODECOV_TOKEN to coverage uploads (#968)`, which is what was turning the 4 test shards red — the tests themselves passed (1528) but the post-test codecov upload exited non-zero on a protected branch. - Resolved the duplicate `lossless_min_savings_ratio` that two independent main/branch additions left in `SmartCrusherConfig` and the Rust-config kwarg (import-time `SyntaxError` + mypy `no-redef`). - Aligned CCR tests with the new defaults (SQLite backend, 1800s TTL) across `test_ccr`, `test_adapter_hooks`, `test_compression_store`, `test_proxy_ccr`, and the lossy row-drop bridge test. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_proxy_ccr.py tests/test_ccr.py tests/test_compression_store.py tests/test_adapter_hooks.py tests/test_ccr_row_drop_store_bridge.py -q 170 passed, 4 warnings in 42.49s $ python -m pytest tests/test_audit_reads.py tests/test_audit_codex.py tests/test_read_maturation.py tests/test_transforms_content_router.py tests/test_smart_crusher_toin_attachment.py -q 83 passed $ mypy headroom/ Success: no issues found in 365 source files $ python -m compileall headroom/ -q COMPILE-OK # CI (run 27488990477, pre-rebase head): all 4 shards ran to completion — # "1528 passed, 120 skipped, 4922 deselected" # The red shards were the codecov upload step, not test failures; fixed by # the #968 rebase above. ``` ## Real Behavior Proof - Environment: macOS (darwin), Python 3.12 venv; branch `feat/compression-extraction` rebased onto `origin/main` (head 7cb0f43); GitHub Actions CI run 27488990477 for the test shards - Exact command / steps: rebased onto latest main (clean, 13 commits replayed, 0 conflicts); ran the pytest suites and mypy above locally; inspected CI shard logs to confirm the failure was the codecov upload, not the test phase - Observed result: 253 targeted tests pass locally; mypy clean on 365 files; CI test phase reports `1528 passed, 120 skipped`; the only red step (codecov `upload-coverage` → "Token required because branch is protected") is resolved by the rebased-in #968 CODECOV_TOKEN fix - Not tested: the read-maturation live-API no-bust validation (`tests/test_live/`) was not re-run in this rebase pass (requires provider keys); it was validated when the feature first landed, and no maturation code changed in the rebase — only CCR-default test assertions and the duplicate-field resolution ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes CHANGELOG is generated by release-please from the conventional commits, so the CHANGELOG box is intentionally left unchecked. "Manual testing performed" is unchecked deliberately — see `Real Behavior Proof` → `Not tested` for the exact boundary (the live-API maturation validation was not re-run in this rebase pass). ### Follow-ups (tracked, not in this PR) - Mechanism B provider extensions: OpenAI-family wiring (no breakpoint hold — bounded near-tail bust) and the Codex runtime read-detector (the audit classifier is the prototype). - Pilot enablement playbook: run `audit-reads --simulate-maturation` on target traffic → pick `quiesce_turns` → enable via env → watch cache hit rate + `read_maturation:N` transform tags.
1 parent ff221e6 commit b7be381

38 files changed

Lines changed: 2607 additions & 77 deletions

crates/headroom-core/src/ccr/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ pub trait CcrStore: Send + Sync {
5959
/// Default capacity — matches Python's `CompressionStore` default.
6060
pub const DEFAULT_CAPACITY: usize = 1000;
6161

62-
/// Default TTL — 5 minutes, matching Python.
63-
pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
62+
/// Default TTL — 30 minutes, matching Python
63+
/// (`CCRConfig.store_ttl_seconds`). Session-scale: agentic sessions
64+
/// routinely outlive the old 5-minute default, and an expired entry
65+
/// silently converts "lossless with retrieval" into "lossy".
66+
pub const DEFAULT_TTL: Duration = Duration::from_secs(1800);
6467

6568
/// Compute the canonical CCR key for `payload`. BLAKE3 → first 24 hex
6669
/// chars (96 bits — collision-resistant for the bounded LRU population

crates/headroom-core/src/transforms/search_compressor.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ pub struct SearchCompressorConfig {
140140
/// to a config field here (Python had it inline) so a future
141141
/// pipeline can tune per-content-type.
142142
pub min_compression_ratio_for_ccr: f64,
143+
/// Group output by file (`rg --heading` style): emit each file path
144+
/// once as a header line, then `line:content` rows beneath it, with
145+
/// a blank line between file groups. Eliminates per-match path
146+
/// repetition — the dominant remaining token waste on large result
147+
/// sets (a 70-char path repeated 15× is ~250 wasted tokens).
148+
/// Default `false` (classic `file:line:content`) for parity; the
149+
/// proxy enables it in token mode.
150+
pub group_by_file: bool,
143151
}
144152

145153
impl Default for SearchCompressorConfig {
@@ -155,6 +163,7 @@ impl Default for SearchCompressorConfig {
155163
enable_ccr: true,
156164
min_matches_for_ccr: 10,
157165
min_compression_ratio_for_ccr: 0.8,
166+
group_by_file: false,
158167
}
159168
}
160169
}
@@ -527,15 +536,31 @@ impl SearchCompressor {
527536
) -> (String, BTreeMap<String, String>) {
528537
let mut lines: Vec<String> = Vec::new();
529538
let mut summaries: BTreeMap<String, String> = BTreeMap::new();
539+
let grouped = self.config.group_by_file;
530540

531541
for (file, fm) in selected {
532-
for m in &fm.matches {
533-
lines.push(format!("{}:{}:{}", m.file, m.line_number, m.content));
542+
if grouped {
543+
// `rg --heading` style: path once, then line:content rows.
544+
if !lines.is_empty() {
545+
lines.push(String::new());
546+
}
547+
lines.push(file.clone());
548+
for m in &fm.matches {
549+
lines.push(format!("{}:{}", m.line_number, m.content));
550+
}
551+
} else {
552+
for m in &fm.matches {
553+
lines.push(format!("{}:{}:{}", m.file, m.line_number, m.content));
554+
}
534555
}
535556
if let Some(orig_fm) = original.get(file) {
536557
if orig_fm.matches.len() > fm.matches.len() {
537558
let omitted = orig_fm.matches.len() - fm.matches.len();
538-
let summary = format!("[... and {} more matches in {}]", omitted, file);
559+
let summary = if grouped {
560+
format!("[... and {} more matches]", omitted)
561+
} else {
562+
format!("[... and {} more matches in {}]", omitted, file)
563+
};
539564
lines.push(summary.clone());
540565
summaries.insert(file.clone(), summary);
541566
}

crates/headroom-core/src/transforms/smart_crusher/compaction/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ impl CompactionStage {
5959
}
6060
}
6161

62+
/// CSV+schema formatter with an explicit config. Used by
63+
/// `SmartCrusher::new` to honor the compaction heuristics carried
64+
/// on `SmartCrusherConfig` instead of pinning `CompactConfig::default()`.
65+
pub fn csv_schema(config: CompactConfig) -> Self {
66+
Self {
67+
config,
68+
formatter: Box::new(CsvSchemaFormatter::new()),
69+
}
70+
}
71+
6272
/// JSON formatter, default config — useful for debugging or for
6373
/// downstream consumers that want structured rather than CSV-shaped
6474
/// output.

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ pub struct SmartCrusherConfig {
5353
/// path to be chosen over lossy. Computed as
5454
/// `1 - len(rendered) / len(input)`. If lossless saves less than
5555
/// this fraction, `crush_array` falls through to the lossy path
56-
/// (with CCR-Dropped retrieval markers). Default `0.30`.
56+
/// (with CCR-Dropped retrieval markers). Default `0.15` — kept in
57+
/// lockstep with the Python `SmartCrusherConfig` dataclass default
58+
/// (lowered from 0.30 so cleanly tabular input takes the lossless
59+
/// path more often; lossless needs no CCR retrieval round-trip).
5760
///
5861
/// **Override semantics.** OSS users can tune this via the config
5962
/// directly. Enterprise plug-ins replace the entire decision via
@@ -77,6 +80,27 @@ pub struct SmartCrusherConfig {
7780
/// still emit always; they have no Python equivalent and no
7881
/// production caller has asked for them to be suppressed.
7982
pub enable_ccr_marker: bool,
83+
/// Compaction heuristic: a field is "core" if it appears in at
84+
/// least this fraction of rows. Mirrors
85+
/// `CompactConfig::core_field_fraction`. Default 0.8.
86+
pub compaction_core_field_fraction: f64,
87+
/// Compaction heuristic: when fewer than this fraction of all
88+
/// observed keys are core, treat the array as heterogeneous and
89+
/// look for a discriminator. Mirrors
90+
/// `CompactConfig::heterogeneous_core_ratio`. Default 0.6.
91+
pub compaction_heterogeneous_core_ratio: f64,
92+
/// Compaction heuristic: cap on inner-key count for
93+
/// nested-uniform flattening. Mirrors
94+
/// `CompactConfig::max_flatten_inner_keys`. Default 6.
95+
pub compaction_max_flatten_inner_keys: usize,
96+
/// Compaction heuristic: minimum bucket count before a candidate
97+
/// discriminator is "useful". Mirrors `CompactConfig::min_buckets`.
98+
/// Default 2.
99+
pub compaction_min_buckets: usize,
100+
/// Compaction heuristic: maximum bucket count — too many buckets
101+
/// means the discriminator is too granular (e.g. an ID column).
102+
/// Mirrors `CompactConfig::max_buckets`. Default 8.
103+
pub compaction_max_buckets: usize,
80104
}
81105

82106
impl Default for SmartCrusherConfig {
@@ -101,8 +125,13 @@ impl Default for SmartCrusherConfig {
101125
first_fraction: 0.3,
102126
last_fraction: 0.15,
103127
relevance_threshold: 0.3,
104-
lossless_min_savings_ratio: 0.30,
128+
lossless_min_savings_ratio: 0.15,
105129
enable_ccr_marker: true,
130+
compaction_core_field_fraction: 0.8,
131+
compaction_heterogeneous_core_ratio: 0.6,
132+
compaction_max_flatten_inner_keys: 6,
133+
compaction_min_buckets: 2,
134+
compaction_max_buckets: 8,
106135
}
107136
}
108137
}
@@ -133,7 +162,12 @@ mod tests {
133162
assert_eq!(c.first_fraction, 0.3);
134163
assert_eq!(c.last_fraction, 0.15);
135164
assert_eq!(c.relevance_threshold, 0.3);
136-
assert_eq!(c.lossless_min_savings_ratio, 0.30);
165+
assert_eq!(c.lossless_min_savings_ratio, 0.15);
137166
assert!(c.enable_ccr_marker);
167+
assert_eq!(c.compaction_core_field_fraction, 0.8);
168+
assert_eq!(c.compaction_heterogeneous_core_ratio, 0.6);
169+
assert_eq!(c.compaction_max_flatten_inner_keys, 6);
170+
assert_eq!(c.compaction_min_buckets, 2);
171+
assert_eq!(c.compaction_max_buckets, 8);
138172
}
139173
}

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

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use super::builder::SmartCrusherBuilder;
4242
use super::classifier::{classify_array, ArrayType};
4343
use super::compaction::{
4444
classify_cell, emit_opaque_ccr_marker, try_parse_json_container, CellClass, ClassifyConfig,
45-
Compaction, CompactionStage,
45+
CompactConfig, Compaction, CompactionStage,
4646
};
4747
use super::config::SmartCrusherConfig;
4848
use super::crushers::{compute_k_split, crush_number_array, crush_object, crush_string_array};
@@ -154,9 +154,20 @@ impl SmartCrusher {
154154
/// CCR cache, not to nowhere — same semantics as Python's
155155
/// SmartCrusher with CCR enabled.
156156
pub fn new(config: SmartCrusherConfig) -> Self {
157+
// Carry the compaction heuristics from the crusher config into
158+
// the compaction stage; everything not exposed on
159+
// SmartCrusherConfig keeps its CompactConfig default.
160+
let compact_cfg = CompactConfig {
161+
core_field_fraction: config.compaction_core_field_fraction,
162+
heterogeneous_core_ratio: config.compaction_heterogeneous_core_ratio,
163+
max_flatten_inner_keys: config.compaction_max_flatten_inner_keys,
164+
min_buckets: config.compaction_min_buckets,
165+
max_buckets: config.compaction_max_buckets,
166+
..CompactConfig::default()
167+
};
157168
SmartCrusherBuilder::new(config)
158169
.with_default_oss_setup()
159-
.with_default_compaction()
170+
.with_compaction(CompactionStage::csv_schema(compact_cfg))
160171
.with_default_ccr_store()
161172
.build()
162173
}
@@ -263,16 +274,49 @@ impl SmartCrusher {
263274
/// kept-items list in original-array order. Mirrors Python's
264275
/// `_execute_plan` (line 3617-3633).
265276
///
266-
/// Schema-preserving: each kept item is cloned unchanged. No
267-
/// summary objects, generated fields, or wrapper metadata.
277+
/// Schema-preserving by default: each kept item is cloned unchanged.
278+
/// No summary objects, generated fields, or wrapper metadata.
279+
///
280+
/// When `factor_out_constants` is enabled (default off), fields the
281+
/// analyzer found constant across ALL items are stripped from each
282+
/// kept object and emitted once in a leading
283+
/// `{"_constant_fields": {...}}` sentinel — same output-shape
284+
/// convention as the `_ccr_dropped` sentinel. Stripping is
285+
/// defensive: a key is only removed from an item when its value
286+
/// equals the recorded constant, so a drifted item keeps its own
287+
/// value. The CCR store always holds the full unfactored original.
268288
pub fn execute_plan(&self, plan: &CompressionPlan, items: &[Value]) -> Vec<Value> {
269289
let mut indices = plan.keep_indices.clone();
270290
indices.sort_unstable();
271-
indices
291+
let mut kept: Vec<Value> = indices
272292
.into_iter()
273293
.filter(|&idx| idx < items.len())
274294
.map(|idx| items[idx].clone())
275-
.collect()
295+
.collect();
296+
297+
if self.config.factor_out_constants && !plan.constant_fields.is_empty() && kept.len() >= 2 {
298+
let mut any_stripped = false;
299+
for item in kept.iter_mut() {
300+
if let Value::Object(map) = item {
301+
for (key, constant) in &plan.constant_fields {
302+
if map.get(key) == Some(constant) {
303+
map.remove(key);
304+
any_stripped = true;
305+
}
306+
}
307+
}
308+
}
309+
if any_stripped {
310+
let mut sentinel = serde_json::Map::new();
311+
sentinel.insert(
312+
"_constant_fields".to_string(),
313+
Value::Object(plan.constant_fields.clone().into_iter().collect()),
314+
);
315+
kept.insert(0, Value::Object(sentinel));
316+
}
317+
}
318+
319+
kept
276320
}
277321

278322
/// Top-level entry point. Mirrors Python `SmartCrusher.crush`
@@ -1023,6 +1067,80 @@ mod tests {
10231067
assert_eq!(result.len(), 2);
10241068
}
10251069

1070+
#[test]
1071+
fn execute_plan_factors_constants_when_enabled() {
1072+
let cfg = SmartCrusherConfig {
1073+
factor_out_constants: true,
1074+
..Default::default()
1075+
};
1076+
let c = SmartCrusher::new(cfg);
1077+
let items: Vec<Value> = (0..4)
1078+
.map(|i| json!({"id": i, "region": "us-west-2", "status": "ok"}))
1079+
.collect();
1080+
let mut constant_fields = std::collections::BTreeMap::new();
1081+
constant_fields.insert("region".to_string(), json!("us-west-2"));
1082+
constant_fields.insert("status".to_string(), json!("ok"));
1083+
let plan = CompressionPlan {
1084+
keep_indices: vec![0, 1, 2],
1085+
constant_fields,
1086+
..CompressionPlan::default()
1087+
};
1088+
let result = c.execute_plan(&plan, &items);
1089+
// Sentinel first, then 3 slim items.
1090+
assert_eq!(result.len(), 4);
1091+
assert_eq!(result[0]["_constant_fields"]["region"], "us-west-2");
1092+
assert_eq!(result[0]["_constant_fields"]["status"], "ok");
1093+
for item in &result[1..] {
1094+
assert!(item.get("region").is_none());
1095+
assert!(item.get("status").is_none());
1096+
assert!(item.get("id").is_some());
1097+
}
1098+
}
1099+
1100+
#[test]
1101+
fn execute_plan_keeps_drifted_values_when_factoring() {
1102+
// Defensive strip: an item whose value differs from the recorded
1103+
// constant keeps its own value.
1104+
let cfg = SmartCrusherConfig {
1105+
factor_out_constants: true,
1106+
..Default::default()
1107+
};
1108+
let c = SmartCrusher::new(cfg);
1109+
let items = vec![
1110+
json!({"id": 0, "status": "ok"}),
1111+
json!({"id": 1, "status": "FAILED"}),
1112+
];
1113+
let mut constant_fields = std::collections::BTreeMap::new();
1114+
constant_fields.insert("status".to_string(), json!("ok"));
1115+
let plan = CompressionPlan {
1116+
keep_indices: vec![0, 1],
1117+
constant_fields,
1118+
..CompressionPlan::default()
1119+
};
1120+
let result = c.execute_plan(&plan, &items);
1121+
assert_eq!(result.len(), 3);
1122+
assert!(result[1].get("status").is_none()); // matched → stripped
1123+
assert_eq!(result[2]["status"], "FAILED"); // drifted → kept
1124+
}
1125+
1126+
#[test]
1127+
fn execute_plan_default_off_leaves_items_unchanged() {
1128+
// factor_out_constants defaults to false: schema preserved even
1129+
// when the plan carries constant_fields.
1130+
let c = crusher();
1131+
let items: Vec<Value> = (0..3).map(|i| json!({"id": i, "k": "v"})).collect();
1132+
let mut constant_fields = std::collections::BTreeMap::new();
1133+
constant_fields.insert("k".to_string(), json!("v"));
1134+
let plan = CompressionPlan {
1135+
keep_indices: vec![0, 1, 2],
1136+
constant_fields,
1137+
..CompressionPlan::default()
1138+
};
1139+
let result = c.execute_plan(&plan, &items);
1140+
assert_eq!(result.len(), 3);
1141+
assert_eq!(result[0]["k"], "v");
1142+
}
1143+
10261144
// ---------- crush_array ----------
10271145

10281146
#[test]

crates/headroom-parity/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ impl TransformComparator for SmartCrusherComparator {
404404
.get("enable_ccr_marker")
405405
.and_then(|v| v.as_bool())
406406
.unwrap_or(defaults.enable_ccr_marker),
407+
// Compaction heuristics are moot here: this comparator uses
408+
// `without_compaction` (fixtures were recorded against the
409+
// lossy-only path). Take the defaults wholesale.
410+
..defaults
407411
};
408412

409413
// Use without_compaction so the legacy fixtures (recorded

0 commit comments

Comments
 (0)