headroom/.github/pr-images
Ashish Patel 7ab83c5107
fix(router): stop protecting passing build/test output as error traces (#1740)
## Description

![Error output protection false-positive
fix](https://raw.githubusercontent.com/ashishpatel26/headroom/fix/1696-error-protection-false-positive/.github/pr-images/issue-1696-error-protection-fix.svg)

`content_has_strong_error_indicators()`
(`headroom/transforms/error_detection.py`) protects any
message/content-block from compression when it contains 2+ distinct
indicator keywords (`error`, `fail`, `exception`, `traceback`, `fatal`,
`panic`, `crash`). That heuristic false-positives on **passing**
build/test tool output: `tsc`'s `"Found 0 errors"` plus a passing test
run's `"0 failures"` trips both `error` and `fail` — 2 distinct hits —
despite nothing failing. In a long JS/TS coding session this fired on
nearly every request (confirmed against the `stats.json` attached to
#1696), permanently protecting legitimate tool output from ever being
compressed and explaining the reported 0.3% savings vs. the advertised
60-95%.

Closes #1696

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `headroom/transforms/error_detection.py`: strip common zero-result
phrases before the 2-keyword scan in
`content_has_strong_error_indicators()` — both `"N word"`/`"word N"`
forms (`"0 errors"`, `"no failures"`) and
`"label:value"`/`"label=value"` forms (`"Failures: 0"`, `"errors=0"`),
covering `error(s)` and `fail`/`failed`/`failing`/`failure(s)` (the scan
matches `fail` by substring, so all inflections needed covering).
- `tests/test_error_detection.py` (new file — no prior coverage
existed): 7 tests covering real error/traceback detection,
single-keyword safety, `tsc`/`eslint` passing summaries, the `"0
failed"` regression a reviewer caught, label:value formats, and that a
genuine second indicator elsewhere in the same blob still triggers
protection.
- `.github/pr-images/issue-1696-error-protection-fix.svg`: diagram
explaining the mechanism (embedded above).

## Testing

- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`) — not run locally, CI `lint` check
is green
- [ ] Type checking passes (`mypy headroom`) — not run locally, CI is
green
- [x] New tests added for new functionality
- [x] Manual testing performed (see Real Behavior Proof)

### Test Output

```text
$ .venv/Scripts/python -m pytest tests/test_error_detection.py tests/test_transforms/test_content_router.py tests/test_transforms_content_router.py -q
tests\test_error_detection.py .......                                    [  7%]
tests\test_transforms\test_content_router.py ........................... [ 34%]
...........................                                              [ 62%]
tests\test_transforms_content_router.py ................................ [ 94%]
.....                                                                    [100%]
============================= 98 passed in 1.21s ==============================
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.14.5, local venv, `headroom._core`
built via `maturin develop --release` (not prebuilt in a fresh checkout)
- Exact command / steps: ran the reporter's exact scenario patterns
(`"Found 0 errors\nTests: 0 failures, 42 passed"`, eslint's `"0 problems
(0 errors, 0 warnings)"`) through
`content_has_strong_error_indicators()` directly, before and after the
fix
- Observed result: before → `True` (wrongly protected); after → `False`
(correctly compressible). Real failure text (`Traceback... fatal error`)
still returns `True` after the fix.
- Not tested: have not reproduced the full KiloCode/proxy session
end-to-end locally (no access to the reporter's actual traffic) — root
cause was confirmed via the `stats.json` they attached to the issue,
which shows `router:protected:error_output` firing on nearly every
request in their session.

## 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 (N/A —
internal heuristic, no user-facing docs reference it)
- [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 (release-please
generates this automatically from commit messages)

## Screenshots (if applicable)

See the diagram embedded in Description above.

## Additional Notes

**Investigation trail**: ruled out `protect_recent_reads_fraction`
(proxy already overrides its `0.0` dataclass default to `0.3` in token
mode, the default) before confirming the error-protection false-positive
via the reporter's `stats.json`.

**Response to review comments** (@AbelVM, @sparkbugz): prose mentions
like `"Fix the errors in the code."` or `"console.error(...)"` contain
only 1 distinct indicator keyword and were already safe under the
pre-existing 2-keyword threshold — not something this PR changes. The
broader concern about other CI summary formats is addressed above
(label:value forms). A case like genuine prose that happens to mention
*two* distinct keywords together (e.g. "there are errors and it failed")
is a known limitation of a keyword-substring heuristic in general,
predates this PR, and is out of scope here — downstream compressors
(LogCompressor) still preserve real error lines even when a block isn't
gate-protected, so the failure mode there is "slightly stricter than
ideal," not data loss.

**Response to @JerrettDavis's CHANGES_REQUESTED**: fixed in the
follow-up commit — `"0 failed"` is now stripped (previously only
`failing`/`failure(s)` were), with a regression test for the exact
reproduction given.
2026-07-14 13:25:49 -04:00
..
issue-1696-error-protection-fix.svg fix(router): stop protecting passing build/test output as error traces (#1740) 2026-07-14 13:25:49 -04:00