## Description
The lossless search fold (`search_heading`) factors a repeated **file**
(many matches in one file → path once + `line:content` rows), but `grep
-rn` across many **distinct** files has one match each, so it saved ~0%
— the shared directory repeated on every row. This adds
`search_dir_heading`/`search_dir_unheading`, which factor the shared
**directory** across distinct files (dir once as a header,
`base:line:content` beneath). `compact_lossless('search')` now tries
both folds and keeps the smallest that round-trips exactly.
Matters because grep is ~23.5% of observed agent output tokens.
Closes #
## Type of Change
- [x] Performance improvement (lossless)
## Changes / Behavior
- File fold wins many-matches-one-file; dir fold wins the `grep -rn`
case (0% → ~16-40% depending on path depth / match length).
**Byte-lossless** — round-trip verified, fold discarded on any mismatch.
- Never touches source reads / diffs (unchanged class gating).
## Testing
```text
pytest tests/test_bash_search_lossless_fold.py -q → 30 passed
pytest test_lossless_excluded_compaction / _then_lossy / _mode → 72 passed
ruff + mypy → clean
```
Round-trip verified on: distinct-files (sorted), many-matches-one-file,
mixed+passthrough, colon-in-content.
## Note for reviewers
The dir-grouped output is byte-lossless but a slightly **non-standard**
format the model reads directly (`dir/` header + `base:line:content`) —
like the existing `rg --heading` fold but less standard. Low
comprehension risk; flagging it explicitly. If preferred, we can gate it
to only fire above a larger savings threshold.
## Checklist
- [x] Self-reviewed; tests pass; no CHANGELOG edit
## Description
<!-- Briefly explain the change and why it is needed. -->
Closes #
## Type of Change
- [ ] 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
-
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
# Paste relevant command output or artifact links here
```
## Real Behavior Proof
- Environment:
- Exact command / steps:
- Observed result:
- Not tested:
## Review Readiness
- [ ] I have performed a self-review
- [ ] This PR is ready for human review
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
Add screenshots to help explain your changes.
## Additional Notes
<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->
Builds on the now-merged personas (#1732). Two pieces:
### 1. Lossless compaction for EXCLUDED tool output
Excluded tools (Read/Grep/Glob/Write/Edit) stay out of *lossy*
compression, but their output is compacted by detected shape:
| shape | transform | guarantee |
|---|---|---|
| grep (SEARCH) | ripgrep --heading fold | **byte-lossless**
(`search_unheading` recovers) |
| log (BUILD_OUTPUT) | ANSI strip + run-collapse | **byte-lossless**
modulo non-semantic ANSI |
| json | whitespace-minify | **data-lossless** (`json.loads` equal), NOT
byte-exact |
Source code + glob path-lists → verbatim. grep gated on
`_try_detect_search` (the general/Magika classifier calls grep-over-code
SOURCE_CODE and would miss it). Off by default
(`compact_excluded_lossless`).
### 2. Enable it in the coding/general personas
`compact_excluded_lossless=True` on the coding + general profiles,
threaded via `proxy_env` + `proxy_pipeline_kwargs` + a per-request
`ContentRouter.apply` override. So `HEADROOM_SAVINGS_PROFILE=coding`
auto-folds excluded grep/log/json.
## Why
The coding persona was getting ~2.5% on OpenCode because its dominant
traffic (Grep/Read) is excluded, and RTK (shell-only, lossy) never sees
OpenCode's *native* tools. This recovers those savings losslessly.
## Measured (end-to-end via coding-persona kwargs, real `rg` output)
41,589 → 26,562 chars (**−36%**), `router:excluded:lossless_search`,
byte-recoverable.
## Accuracy
grep/log = byte-lossless → edit-safe. json = data-lossless (edit-caveat
for read-then-edit-JSON, documented). Read of source code → untouched
(tested).
47 tests (personas + all three tiers + persona-enablement + end-to-end).
ruff + mypy clean. **No personas duplication** — rebased onto main after
#1732 landed. Supersedes #1755.
🤖 Generated with [Claude Code](https://claude.com/claude-code)