mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
2 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c5493ea93b
|
fix(content-router): token-measure lossless folds at the acceptance gate (#1772)
## Description Unit-mismatch bug in the compression acceptance gate. `router.apply()` computes `compression_ratio` from `len(text.split())` (word count), but a **lossless** search/log fold (`compact_lossless`) saves **bytes** by collapsing a repeated path prefix into a single heading — word count stays flat or even *rises* (the heading adds a word). So the gate saw `ratio ≥ 1.0` and discarded every free, byte-recoverable win as `ratio_too_high`. (Raising the floor to 1.0 in #1771 did **not** fix this — the word-ratio was already ≥ 1.0.) Measure lossless results (those whose `strategy_chain` carries a `lossless_*` entry) by **byte ratio** at the gate and in the result cache — the real saving. Lossy strategies are unchanged (word count tracks their token savings), and the reversibility gate is untouched (`LOG`/`SEARCH`/`DIFF` aren't in `LOSSY_UNMARKED_STRATEGIES`). The excluded-tool and bash-search paths already bypass this gate via `continue`; this fixes the **main strategy dispatch** (the lossless-mode `LOG`/`SEARCH`/`DIFF` path). Follow-up to #1771. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - At the `apply()` acceptance gate: compute `accept_ratio` = byte ratio for lossless results (`strategy_chain` has `lossless_*`), else the existing word ratio. Gate + result-cache entry now use `accept_ratio`. - Added an end-to-end regression test that drives the full `router.apply()` path. ## Testing - [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 tests/test_lossless_mode.py::test_router_apply_accepts_lossless_search_byte_measured PASSED tests/test_content_router_tool_role_reversibility.py .......... (10 passed) # broader (pre-move) sweep on the same change: tests/test_lossless_mode.py / test_transforms/test_content_router.py / test_lossless_excluded_compaction.py / test_bash_search_lossless_fold.py — 121 passed ruff check headroom/transforms/content_router.py -> All checks passed! mypy headroom/transforms/content_router.py -> Success: no issues found ``` ## Real Behavior Proof - Environment: local worktree, Python 3.12, `PYTHONPATH` pinned to the branch. - Exact command / steps: new regression test constructs a single-file grep result, runs it through `ContentRouter(lossless=True).apply(...)`, and asserts the tool output is byte-smaller and recovers exactly (`search_unheading(out) == original`). - Observed result: before this fix the fold was rejected (`out == original`, counted `ratio_too_high`); after, it's applied (`len(out) < len(original)`, marker-free, byte-exact recovery). The test also asserts the fold's word count is ≥ the original's, so the test is meaningless if "fixed" by word count. - Not tested: no live end-to-end proxy run; validated via the full `apply()` path in unit tests. ## 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 - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable (handled at release time) ## Additional Notes Why prior tests missed it: `compress()` and `_apply_strategy_to_content` return the folded result directly and never touch the `apply()` acceptance gate, so the existing lossless-mode unit tests (which call those) passed while the real proxy path silently discarded the fold. The new test exercises `apply()` end-to-end. |
||
|
|
c75ebdee6d
|
feat(proxy): add --lossless no-CCR mode with format-native compaction (#1721)
## Description A new `--lossless` / `HEADROOM_LOSSLESS` proxy mode for deployments **without an MCP retrieve tool** (e.g. bash-only coding agents), where a `<<ccr:…>>` retrieval marker is a dangling, unrecoverable reference. In this mode the ContentRouter compresses tool outputs but **never emits a retrieval marker**, so no MCP round-trip is needed. Routing and prefix caching are unchanged. The guarantee is **no-CCR**, not "everything lossless": the structural compressors get format-native *lossless* compaction, while the ML/prose paths keep their existing (lossy) compression — just made marker-free. Closes # ## Type of Change - [x] New feature (non-breaking; opt-in flag, default off) ## Changes Made - **Flag plumbing** (both proxy entry paths), mirroring `--force-kompress-all`: `ProxyConfig.lossless` (models.py), `--lossless` Click option + argparse arg + `HEADROOM_LOSSLESS` env (cli/proxy.py, server.py), `ContentRouterConfig.lossless`. When on: `smart_crusher_lossless_only=True`, `ccr_inject_marker=False`, and retrieve-tool injection off. - **`headroom/transforms/lossless_compaction.py`** (new, pure stdlib): format-native reversible transforms, each with an exact inverse + runtime round-trip self-check (returns original if it can't safely shrink; never raises): - LOG → `strip_ansi` + `collapse_runs`/`expand_runs` (syslog `repeated ×N`) - SEARCH → `search_heading`/`search_unheading` (ripgrep `--heading` fold) - DIFF → `diff_strip_index` (drop `index <sha>..<sha>`; diff still applies) - **Router disposition**: in lossless mode LOG/SEARCH/DIFF route through `compact_lossless` instead of the lossy Rust drop path; SmartCrusher is marker-free via `smart_crusher_lossless_only`. - **Kompress made marker-free**: `_get_kompress` now builds Kompress with `enable_ccr` tied to `ccr_inject_marker` (previously always `True`). In lossless mode Kompress still drops tokens (lossy, as intended) but no longer appends a `Retrieve more: hash=` marker or writes the CCR store — closing the one path that would otherwise leak an unredeemable marker in production. ## Testing - [x] Unit tests pass - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added ### Test Output ```text $ pytest tests/test_lossless_mode.py -q 25 passed in 4.88s $ pytest tests/test_transforms_content_router.py tests/test_transforms_content_detection.py -q 46 passed in 0.56s $ ruff check <changed files> -> All checks passed! $ ruff format --check <changed files> -> already formatted $ mypy headroom/transforms/content_router.py -> Success: no issues found ``` ## Real Behavior Proof - Environment: local, Python 3.12.6. - No-CCR invariant: `ContentRouter(ContentRouterConfig(lossless=True))` on repetitive log + grep + diff payloads produces output with **no `<<ccr:` and no `Retrieve ` substring** (chains `lossless_log` / `lossless_search` / `lossless_diff`). - Marker-free Kompress proven **without the model loaded** (the case tests previously couldn't cover): `test_lossless_mode_builds_kompress_marker_free` asserts the router builds Kompress with `enable_ccr=False` in lossless mode and `True` in normal mode. - Reversibility: `collapse_runs`/`expand_runs`, `search_heading`/`search_unheading` round-trip byte-exactly; `compact_lossless` reverts to the original on any round-trip mismatch or non-shrink. - Not tested: end-to-end proxy request replay; live Kompress model output. ## Review Readiness - [x] Self-reviewed - [x] Ready for human review ## Additional Notes - **Stage B (follow-up):** split the low-value KEEP/DROP tail and run Kompress on the *tail* inline (with identifiers registered as Kompress protected tokens), rather than only whole-block ML paths. Not in this PR. - Savings are content-dependent: high on repetitive logs and path-heavy grep, low on diffs and source reads. |