You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(learn): aggregate verbosity baselines across projects instead of overwriting (headroomlabs-ai#1288)
## Description
`headroom learn --verbosity --apply --all` was building the
output-shaper's savings baseline from only **one** project.
`_run_verbosity` wrote the savings ledger *inside* the per-project loop
(`ledger.baseline = baseline; ledger.save(...)`), so each project
replaced the previous baseline and only the last project processed
survived — frequently a near-empty one. The synthetic-control estimate
that `/stats` exposes (`savings.by_layer.output_shaping`) was then
computed against a tiny, unrepresentative sample.
This PR makes `--all` aggregate across every targeted project and write
the ledger **once**, after the loop.
Closes #
## 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
- `BaselineModel.merge()` / `_Accum.merge()`
(`headroom/proxy/output_savings.py`): fold one baseline into another.
The accumulators hold additive online stats (`n` / `sum` / `sumsq`), so
merging is element-wise and order-independent — identical to having
observed both corpora against a single model.
- `_run_verbosity` (`headroom/cli/learn.py`): accumulate a single
`BaselineModel` across all targeted projects and persist it once after
the loop, instead of overwriting per project. The applied verbosity
level now comes from the project with the most samples (strongest
signal) rather than whichever sorted last. Single-project runs are
unchanged (an aggregate of one). When no transcripts are found, it
prints a clear message and writes nothing.
- Tests: unit test for `BaselineModel.merge`; CLI test that `--all
--apply` across two projects aggregates both strata (totals summed, not
last-wins) and applies the busier project's level.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ uv run pytest tests/test_output_savings.py tests/test_cli_learn.py tests/test_verbosity_learn.py -q
tests/test_output_savings.py ............................... [ 54%]
tests/test_cli_learn.py ........... [ 73%]
tests/test_verbosity_learn.py ............... [100%]
============================== 57 passed in 0.51s ==============================
$ uv run ruff check headroom/cli/learn.py headroom/proxy/output_savings.py
All checks passed!
$ uv run mypy headroom/cli/learn.py headroom/proxy/output_savings.py
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: macOS, Python 3.12.13, this branch off `upstream/main`.
- Exact command / steps: `headroom learn --verbosity --apply --all` (run
across a multi-project transcript corpus), then inspect
`~/.headroom/output_savings.json` (`baseline.glob.n`); compared against
`headroom learn --verbosity --apply` for a single busy project.
- Symptom (pre-fix, installed build): `headroom learn --verbosity
--apply --all` across a multi-project transcript corpus wrote
`~/.headroom/output_savings.json` with `baseline.glob.n = 2` (the last
project processed was a near-empty `…/venv/bin` dir), while targeting a
single busy project gave `baseline.glob.n = 15658`.
- With this change: the new CLI test
(`test_verbosity_all_apply_aggregates_baselines_across_projects`) drives
`--all --apply` over two projects (3 samples + 1 sample) and asserts the
persisted ledger has `total_samples == 4` with both strata present, plus
the busier project's level applied.
- Observed result: aggregated baseline persisted once; both strata
retained; level taken from the higher-sample project.
- Not tested: re-running the patched `--all` end-to-end on a live
multi-project machine (covered instead by the unit merge-math test and
the faked-`analyze` CLI test).
## 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
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — CLI/behavioral change.
## Additional Notes
- No linked issue (`Closes #` left blank intentionally).
- Documentation checklist item is N/A — no user-facing docs describe the
per-project overwrite behavior.
- Level-selection note: for `--all`, the applied verbosity level is now
deterministic (most-samples project) instead of last-processed; this is
the intended improvement, not a behavior to preserve.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
34
35
35
### Bug Fixes
36
36
37
+
***learn (verbosity):**`--verbosity --apply --all` now aggregates the savings baseline across every project instead of overwriting it per project (last-project-wins), which previously left the output shaper with a tiny, unrepresentative baseline. The applied verbosity level comes from the project with the most samples ([#1288](https://github.com/headroomlabs-ai/headroom/pull/1288)).
37
38
***proxy/anthropic:** restore token-mode compression on continued Claude Code turns with a frozen prefix and deferred CCR tool injection. Token mode now runs request-side compression even when the client did not pre-register `headroom_retrieve`, relying on the existing marker-triggered injection override to keep emitted CCR markers redeemable ([#1487](https://github.com/headroomlabs-ai/headroom/issues/1487)).
38
39
***subscription:** stop zeroing the 5-hour headroom contribution counters on every poll. The rollover check compared `five_hour.resets_at` with a bare `!=`, but the usage API reports that timestamp with second-level jitter (observed flapping between `01:59:59Z` and `02:00:00Z` on consecutive polls within the same window), so a spurious "5h window rolled over" reset fired every poll interval (~5 min) and the dashboard's per-window savings stuck near 0%. Only a forward jump larger than `_ROLLOVER_MIN_ADVANCE` (1 minute) now counts as a real rollover.
39
40
***transforms/content_router:** stop replacing `role="tool"` output with a lossy-unrecoverable summary on the live compression path (refs [#1307](https://github.com/chopratejas/headroom/issues/1307)). `ContentRouter.apply()` routed OpenAI-style `role="tool"` string messages — `Bash`/`grep`/`ls`/`cat` output — through the ML/word-drop summarizers; when the result carried no CCR retrieve marker (CCR off, ratio >= 0.8, or the size-gate fallback) the original was unrecoverable and the agent acted on a fabricated summary. Tool-role string content is now kept verbatim unless the compressed form is CCR-recoverable. Assistant/user text is unaffected, and structurally-lossless passes (SmartCrusher/Log/Search) still apply. The Anthropic `tool_result` block path is tracked separately.
0 commit comments