Commit c5493ea
authored
fix(content-router): token-measure lossless folds at the acceptance gate (headroomlabs-ai#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 headroomlabs-ai#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 headroomlabs-ai#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.1 parent 6c31db9 commit c5493ea
2 files changed
Lines changed: 59 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3410 | 3410 | | |
3411 | 3411 | | |
3412 | 3412 | | |
3413 | | - | |
| 3413 | + | |
| 3414 | + | |
| 3415 | + | |
| 3416 | + | |
| 3417 | + | |
| 3418 | + | |
| 3419 | + | |
| 3420 | + | |
| 3421 | + | |
| 3422 | + | |
| 3423 | + | |
| 3424 | + | |
| 3425 | + | |
| 3426 | + | |
| 3427 | + | |
| 3428 | + | |
| 3429 | + | |
| 3430 | + | |
| 3431 | + | |
| 3432 | + | |
3414 | 3433 | | |
3415 | 3434 | | |
3416 | 3435 | | |
| |||
3433 | 3452 | | |
3434 | 3453 | | |
3435 | 3454 | | |
3436 | | - | |
| 3455 | + | |
3437 | 3456 | | |
3438 | 3457 | | |
3439 | 3458 | | |
| |||
3450 | 3469 | | |
3451 | 3470 | | |
3452 | 3471 | | |
3453 | | - | |
3454 | | - | |
3455 | | - | |
3456 | | - | |
| 3472 | + | |
3457 | 3473 | | |
| 3474 | + | |
3458 | 3475 | | |
3459 | 3476 | | |
3460 | 3477 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
279 | 279 | | |
280 | 280 | | |
281 | 281 | | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
282 | 318 | | |
283 | 319 | | |
284 | 320 | | |
| |||
0 commit comments