mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
2630 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f64aac9733
|
fix(proxy/gemini): None-guard token counts from usageMetadata (#2347)
## Description
The non-streaming Gemini/Vertex handler reads token counts straight from
the response's `usageMetadata`:
```python
try:
usage = resp_json.get("usageMetadata", {})
total_input_tokens = usage.get("promptTokenCount", optimized_tokens)
output_tokens = usage.get("candidatesTokenCount", 0)
cache_read_tokens = usage.get("cachedContentTokenCount", 0)
except (...):
...
uncached_input_tokens = max(0, total_input_tokens - cache_read_tokens) # OUTSIDE the try
```
`.get(key, default)` only falls back when the key is **absent**. When
`usageMetadata` carries a key with a **null** value — which Gemini can
do on a safety-blocked turn that produced no candidates — `.get` returns
`None`. That `None` then reaches:
- `max(0, total_input_tokens - cache_read_tokens)` (a `None - int` →
`TypeError`), and
- `RequestOutcome(output_tokens=...)`, whose field is `int` and which
the metrics recorder increments (`tokens_output_total += output_tokens`
→ `TypeError`).
Both run on the success (non-`except`) path, so a single such response
crashes the request and its outcome recording. The Gemini streaming path
already guards these with a `_usage_int` helper; the non-streaming path
(two sites) did not.
## Fix
Coerce the three counts with `int(... or fallback)`, matching the
streaming `_usage_int` guard and the LiteLLM usage mappings:
```python
total_input_tokens = int(usage.get("promptTokenCount", optimized_tokens) or optimized_tokens)
output_tokens = int(usage.get("candidatesTokenCount", 0) or 0)
cache_read_tokens = int(usage.get("cachedContentTokenCount", 0) or 0)
```
No change for a normal integer usage; only a `None` (or absent) value
now becomes the fallback/0. Applied to both non-streaming
usage-extraction sites in `handlers/gemini.py`.
## 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/proxy/handlers/gemini.py`: `int(... or fallback)`-guard
`promptTokenCount` / `candidatesTokenCount` / `cachedContentTokenCount`
at both non-streaming usage sites.
- `tests/test_proxy/test_gemini_savings_profile.py`: add a regression
driving a `generateContent` request whose
`usageMetadata.candidatesTokenCount` is `null`, asserting a 200, an
`int` `output_tokens == 0`, and `uncached_input_tokens == 20` (the
`max(0, …)` no longer raises).
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] 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
$ uvx ruff@0.15.17 check headroom/proxy/handlers/gemini.py tests/test_proxy/test_gemini_savings_profile.py
All checks passed!
$ uvx ruff@0.15.17 format --check headroom/proxy/handlers/gemini.py tests/test_proxy/test_gemini_savings_profile.py
2 files already formatted
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/handlers/gemini.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOMs this box (ML-stack import), so I
reproduced the extraction with a dependency-free script and left the
full pytest to CI.
- Exact command / steps: modeled the OLD (bare `.get`) and NEW (`int(...
or fallback)`) derivations for a blocked response
(`candidatesTokenCount: null`, valid prompt count), a null
`promptTokenCount`, a normal response, and an absent-usage response.
- Observed result: OLD raised `TypeError` at `max(0, None - …)` for a
null prompt count and left `output_tokens = None` (which crashes the
int-typed outcome/metrics recorder) for a null candidate count; NEW
produced `(20, 0)` for the blocked case, `(15, 0)` for the null-prompt
case (the `optimized_tokens` fallback), `(60, 30)` for a normal
response, and the fallbacks for absent usage. The added
`create_app`/`TestClient` test drives the handler end to end and asserts
a 200 with `int` outcome counts.
- Not tested: a live Gemini safety-blocked response; the added test uses
a mocked `_retry_request` returning a `usageMetadata` with a null count,
matching the existing Gemini test harness in this file.
## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because a local pytest
run imports the ML stack and OOMs this box; the added test reuses the
existing `create_app`/`TestClient` + mocked-`_retry_request` harness in
`test_gemini_savings_profile.py` and runs under the normal CI pytest
job, and the behavior is corroborated by the standalone proof above.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
494fb5a60e
|
fix(security): exclude compromised ast-grep-cli 0.44.1 (supply-chain trojan) (#2342)
## Description Fixes #2332. The `ast_grep_cli` **0.44.1** PyPI release was a compromised supply-chain build: it shipped an info-stealer `sg.exe` (212 KB, detected as `Trojan:Win64/Lazy!MTB`) alongside the legitimate `ast-grep` binary as camouflage. `headroom-ai` declares `ast-grep-cli>=0.30.0`, so a fresh PyPI install — `pip install "headroom-ai[all]"` or `uv tool install "headroom-ai[all]"` — can resolve the malicious 0.44.1 (the repo `uv.lock` protects only `uv sync`-from-source, not end users installing the published package). ## Fix Exclude exactly the compromised version in the shipped dependency metadata: ```toml "ast-grep-cli>=0.30.0,!=0.44.1", ``` `!=0.44.1` removes only the known-bad build, so every other release stays installable — older safe versions and any future patched release alike. The committed `uv.lock` already resolves to the safe **0.42.1**, which still satisfies the new constraint, so no re-resolution is needed; I updated the lock's `requires-dist` entry to match the new specifier to keep `uv lock --locked` consistent. ## 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 - `pyproject.toml`: `ast-grep-cli` constraint is now `>=0.30.0,!=0.44.1`, with a comment recording why. - `uv.lock`: update the `ast-grep-cli` `requires-dist` specifier to match (resolved version unchanged at 0.42.1). ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text # Verified the specifier semantics with packaging: $ python -c "from packaging.specifiers import SpecifierSet; from packaging.version import Version; s=SpecifierSet('>=0.30.0,!=0.44.1'); print(Version('0.44.1') in s, [str(v) for v in ['0.42.1','0.44.0','0.44.2','0.45.0'] if Version(v) in s])" False ['0.42.1', '0.44.0', '0.44.2', '0.45.0'] # pyproject still parses and carries the new constraint: $ python -c "import tomllib; print([d for d in tomllib.load(open('pyproject.toml','rb'))['project']['dependencies'] if 'ast-grep' in d])" ['ast-grep-cli>=0.30.0,!=0.44.1'] ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12. - Exact command / steps: evaluated the new `SpecifierSet('>=0.30.0,!=0.44.1')` against the compromised version and a range of safe versions, and re-parsed `pyproject.toml`. - Observed result: `0.44.1` is excluded (`in` -> False); `0.42.1` (the current lock pin), `0.44.0`, `0.44.2`, `0.45.0`, and `1.0.0` all remain allowed; the pre-0.30 floor is still enforced. So a resolver can no longer select the trojaned build, and no legitimate release is blocked. - Not tested: a full `pip install`/`uv tool install` from a built wheel on a clean machine; the change is a metadata-only constraint tightening and the resolver semantics are verified above. ## 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 - [ ] 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 ## Additional Notes This is the minimal, high-priority piece of the issue's recommended actions (pin away from the compromised version). The issue also suggests an install-docs warning and a `pip-audit` / `uv audit` CI step; those are worth doing but are separate follow-ups (a CI workflow change I can't meaningfully validate here), so I left them out to keep this fix small and obviously correct. No CHANGELOG entry is added since this is a dependency-metadata security pin, but I'm happy to add one if the project prefers it here. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
0fa337f64f
|
build(deps): refresh stale uv.lock (reconcile ~30 missing deps) (#2349)
## Description Refreshes the **stale `uv.lock`** and clears the mcp CVEs in one pass. main's lock had drifted far from `pyproject.toml` — a full `uv lock` (with the **CI-matching uv 0.11.29**, what `astral-sh/setup-uv@v5` installs) reconciles ~30 declared-but-unlocked dependencies and their transitives. This is the comprehensive counterpart to the minimal #2348. Closes # ## Type of Change - [x] Bug fix (security + dependency hygiene) ## Why it's this big `uv lock --check` **fails on `main`** (the committed lock predates several pyproject deps). CI hasn't caught it because the pipeline only ever runs `uv export --frozen` (consume-as-is), never a re-lock — so the drift accumulated silently. A correct lock is ~1100 lines of reconciliation. ## Changes Made - `pyproject.toml`: `mcp>=1.0.0` → `>=1.28.1` (core + `[mcp]` extra) — carried from the security fix. - `uv.lock`: full refresh via `uv lock` (uv 0.11.29). `mcp` → **1.28.1** (clears CVE-2026-52869/52870/59950); ~30 previously-missing deps added; transitives reconciled. ## Testing ```text uv 0.11.29 lock --check -> Resolved 269 packages (up to date, no error) uv export --frozen ... -> succeeds (CI pip-audit path) mcp in refreshed lock -> 1.28.1 ``` The transitive version changes are what `uv lock` produces for the current `pyproject.toml`; **CI's full test matrix is the validation gate** for behavior (that's the point of the shards). ## Relationship to #2348 **Superset.** #2348 is the minimal, surgical mcp bump (5-line diff) for immediate CVE closure with near-zero blast radius. This PR does the same mcp fix **plus** the full stale-lock reconciliation. Merge **one**: - Prefer low risk / fast → merge **#2348**, land this refresh separately afterwards. - Prefer fixing the lock drift now → merge **this**, close #2348. No `CHANGELOG.md` edit (release-please owns it). |
||
|
|
a90be94e32
|
fix(deps): bump mcp to 1.28.1 to clear 3 high-severity CVEs (#2348)
## Description Clears **all 3 open Dependabot alerts** (and the `pip-audit` CI failure) — every one is `mcp 1.26.0` in `uv.lock`: | Alert | CVE | Issue | Fix | |-------|-----|-------|-----| | #155 | CVE-2026-52870 | experimental task handlers leak cross-session tasks | 1.27.2 | | #156 | CVE-2026-52869 | HTTP transports serve session requests without auth check | 1.27.2 | | #157 | CVE-2026-59950 | deprecated WebSocket transport lacks Host/Origin validation | 1.28.1 | `mcp 1.28.1` satisfies all three. Closes # ## Type of Change - [x] Bug fix (security / dependency) ## Changes Made - `pyproject.toml`: raise the floor `mcp>=1.0.0` → `mcp>=1.28.1` (core dep **and** the `[mcp]` extra). - `uv.lock`: bump the `mcp` entry `1.26.0` → `1.28.1` (version + sdist/wheel URL, sha256, size from PyPI). **Surgical on purpose.** mcp 1.28.1's resolved dependency set is unchanged for this project's Python range (1.26 vs 1.28.1 differ only in `python_version>=3.14` conditionals and an httpx upper bound already satisfied), so no other locked package changes. Verified: `uv.lock` parses, `mcp = 1.28.1`, no `mcp-1.26.0` refs remain. ## Testing ```text python -c "import tomllib; ...; print(pkgs['mcp'])" -> 1.28.1 (uv.lock valid TOML) git diff --stat -> pyproject.toml | 4 ; uv.lock | 6 grep -c mcp-1.26.0 uv.lock -> 0 ``` mcp 1.28.1 ≥ every advisory's fixed-version, so all 3 alerts + pip-audit clear. ## Real Behavior Proof - Env: local; hashes fetched from `https://pypi.org/pypi/mcp/1.28.1/json`. - Steps: bumped the pyproject floor + the single mcp lock entry; validated TOML + version + absence of old refs. - Not tested: full `uv sync` (the lock is separately stale — see note). ## Note (deliberate scoping) A full `uv lock` refresh churns ~900 lines: the lock is **separately stale** (missing some declared deps) and local `uv` resolution diverges (major downgrades of protobuf/posthog/portalocker — likely an env artifact). That's a pre-existing lock-hygiene problem for its own PR — **not** bundled into this security fix. No `CHANGELOG.md` edit (release-please owns it). |
||
|
|
4381388d56
|
chore: release main (#1923)
## Description Release Please generated the 0.33.0 release PR for main. This updates release metadata, package versions, and the generated changelog for the 0.33.0 release. I also aligned the agent-hook plugin manifests, marketplace metadata, editable lockfile package version, and canonical MCP `server.json` descriptor to 0.33.0 so all package/plugin/registry version declarations match the Release Please version bump. ## Type of Change - [x] Documentation update - [x] Release / packaging metadata ## Changes Made - Updated `.release-please-manifest.json`, `pyproject.toml`, `plugins/openclaw/package.json`, and `sdk/typescript/package.json` to 0.33.0. - Updated the generated `CHANGELOG.md` release notes for 0.33.0. - Synced `plugins/headroom-agent-hooks` plugin manifests and marketplace metadata to 0.33.0. - Synced `uv.lock` editable `headroom-ai` package version to 0.33.0. - Regenerated the canonical MCP `server.json` descriptor to 0.33.0. ## Testing - [x] Version verification passes - [x] Version-sync tests pass - [x] MCP server descriptor test passes - [x] Whitespace check passes ### Test Output ```text uv run python scripts/verify-versions.py All versions aligned at 0.33.0 uv run pytest scripts/tests/test_version_sync.py scripts/tests/test_sync_plugin_versions.py -q 14 passed in 0.80s uv run pytest tests/test_mcp_registry/test_server_json.py -q 4 passed in 0.42s git diff --check # no output ``` ## Real Behavior Proof - Environment: Windows 11, local checkout of the Release Please branch. - Exact command / steps: Ran version verification and MCP descriptor tests after syncing release metadata, plugin marketplace versions, lockfile version, and `server.json`. - Observed result: All package, plugin manifest, marketplace, lockfile, and MCP descriptor release versions are aligned at 0.33.0. |
||
|
|
63945abe3a
|
chore: sync version state to released 0.31.0 to unblock release-please (v0.32.0) (#2338)
## Description **Fixes the Release Please pipeline so it emits `v0.32.0`.** pip / Docker / npm are out of sync because 0.32.0 was never actually released. ### Root cause Same failure mode as #1916. #2175 (a `fix(deps)` PR) bumped `pyproject.toml` + `.release-please-manifest.json` to **0.32.0 out-of-band**, so release-please reads 0.32.0 as the *current* version and computes the next release as **0.33.0** (#1923) — **skipping 0.32.0, which was never tagged, GitHub-released, or published to PyPI/Docker.** The last real release is `v0.31.0` (2026-07-09). The plugin/marketplace manifests were also left at 0.31.0, so version state was split-brained: ``` pyproject.toml / openclaw / sdk-typescript : 0.32.0 <- #2175 plugin.json (x2) / marketplace.json (x2) : 0.31.0 manifest : 0.32.0 ``` ### Fix Realign every version-tracked file **and** the RP manifest to the last real release, **0.31.0**, via the repo's own `scripts/version-sync.py --version 0.31.0`. Versions only — no code change. ## What happens after merge 1. Release Please runs on `main`, sees `manifest = 0.31.0` + releasable commits since `v0.31.0`, and **rewrites its release PR (#1923) to `chore: release 0.32.0`** (bumping every version file). 2. Merging that PR tags `v0.32.0` and fires `release: published`, which publishes **PyPI + npm (SDK + openclaw) + Docker** at 0.32.0 in one shot — bringing all registries back in sync. ## Changes Made - `.release-please-manifest.json` → `0.31.0` - `pyproject.toml`, `sdk/typescript/package.json`, `plugins/openclaw/package.json`, `.releasemetadata` → `0.31.0` (via `version-sync.py`) - Plugin/marketplace manifests were already `0.31.0` (unchanged). ## Testing ```text $ python scripts/verify-versions.py All versions aligned at 0.31.0 ``` ## Real Behavior Proof - Environment: local `.venv`. - Steps: `version-sync.py --version 0.31.0`, reset manifest, `verify-versions.py`. - Observed: all 9 version entries aligned at 0.31.0; no CHANGELOG touched (Changelog Guard passes). - Not tested: the live release-please recompute (will run on merge — expected to rewrite #1923 to `chore: release 0.32.0`). ## Note on downstream publish The release-please workflow only triggers `release.yml`/`docker.yml` if `RELEASE_PLEASE_TOKEN` (a PAT) is set — with the `GITHUB_TOKEN` fallback the release is created but downstream publishes don't fire. #1916 shipped 0.31.0 fully via this path, so the PAT was set then; if the 0.32.0 publish doesn't fire on merge, verify that secret still exists. |
||
|
|
4726c7343f
|
ci: repair mypy no-any-return in _win32_pid_alive (#1556 follow-up) (#2336)
## Description Main lint went red after #1556: `headroom/_subprocess.py:18` returns `Any` (ctypes `GetLastError()`) from a `-> bool` function → mypy `no-any-return`. Tests were unaffected. ## Changes Made - Wrap the comparison in `bool(...)`, matching `pid_alive()`'s existing style. ## Testing ```text mypy headroom --ignore-missing-imports -> Success: no issues found in 504 source files ruff check headroom/_subprocess.py -> All checks passed! ruff format --check -> 1 file already formatted ``` ## Real Behavior Proof - Environment: local .venv (ruff 0.15.17 / mypy 1.20.2, CI-pinned). - Result: `mypy headroom` clean; behavior unchanged (pure typing fix). |
||
|
|
793d20fb2a
|
fix(subscription): read newest transcript tail (#2310)
## Description
Large Claude Code transcript files were capped by reading the first 10
MB of each append-only JSONL file. Because recent entries are appended
at the end, current-window and weighted token usage could silently omit
the newest activity.
Oversized transcripts are now read from EOF. If the capped tail begins
within a JSONL record, only that partial record is discarded. A
complete record beginning exactly at the boundary remains included.
## Type of Change
- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation-only change
- [ ] Refactoring
## Changes Made
- Read the newest capped transcript bytes instead of the oldest prefix.
- Determine the tail offset using the opened file handle.
- Inspect the preceding byte to distinguish a partial record from an
exact line boundary.
- Remove partial bytes before UTF-8 decoding.
- Preserve existing behavior for transcripts below the 10 MB cap.
- Add direct session-tracking tests for aggregation and boundary
behavior.
- Add an Unreleased changelog entry.
## Testing
- [x] Added regression tests
- [x] Focused tests pass
- [x] Subscription test suite passes
- [x] Ruff checks pass
- [x] Mypy passes
- [x] Changed files pass formatting checks
- [ ] Entire repository test suite passes without baseline failures
Commands and results:
- `uv run --extra dev --frozen pytest
tests/test_subscription_session_tracking.py -q`
- `4 passed`
- Subscription-focused suite
- `53 passed`
- `uv run --extra dev --frozen ruff check .`
- Passed
- `uv run --extra dev --frozen mypy headroom --ignore-missing-imports`
- Success across 504 source files
- Changed-file Ruff formatting
- Passed
- `uv run --extra dev --frozen pytest -q`
- `9364 passed, 565 skipped, 4 failed`
- The four existing, unrelated failures are:
- `test_l2_appends_transform_label`
- `test_recovery_records_sockets_and_secures_both_backups`
- `test_dashboard_uses_cached_stats_and_lazy_history_feed_polling`
- `test_smart_crusher_log_fallback_runs_for_valid_json`
Repository-wide `ruff format --check .` identifies pre-existing
formatting drift only in the untouched
`headroom/proxy/handlers/anthropic.py`.
## Real Behavior Proof
A focused reproduction created a 10,485,787-byte transcript with a
marker entry appended after the 10 MB boundary.
Before the fix:
```text
{'file_bytes': 10485787, 'line_count': 1, 'newest_entry_present': False}
After the fix:
{'file_bytes': 10485787, 'line_count': 1, 'newest_entry_present': True}
The regression tests additionally verify that:
1. Recent token usage beyond the cap contributes to raw and weighted
totals.
2. A partial initial JSONL record is discarded.
3. A complete record exactly at the tail boundary is preserved.
4. Small transcripts retain their existing behavior.
Environment: macOS arm64, CPython 3.12.13.
Not tested: mutation of the transcript during the individual file read
by a live Claude Code process. Reads remain bounded to a single recent
snapshot.
## Review Readiness
- [x] I have performed a self-review before requesting human review.
- [x] This PR is ready for human review.
## Checklist
- [x] The change follows existing project style and error-handling
conventions.
- [x] Tests cover the reported failure and relevant boundary cases.
- [x] The 10 MB memory/read cap remains enforced.
- [x] No unrelated files or formatting changes are included.
- [x] No temporary logging or debug code remains.
- [x] The changelog has been updated.
## Additional Notes
The four full-suite failures listed above occur outside the modified
subscription code and are unrelated to this PR. All tests covering
transcript reading and subscription tracking pass.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
0924755591
|
fix(memory): serialize MCP backend initialization (#2309)
## Description
The Memory MCP server previously assigned its backend before
asynchronous embedder and vector-index warm-up completed. A tool call
arriving
during the handshake could therefore receive a partially initialized
backend.
Backend initialization is now atomic and shared between concurrent
callers. The backend is published only after warm-up succeeds. Failed
candidates are closed and discarded so later calls can retry with a
fresh backend.
## Type of Change
- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation-only change
- [ ] Refactoring
## Changes Made
- Keep the initializing backend local until warm-up completes
successfully.
- Share one initialization task between handshake and concurrent tool
calls.
- Await the shared task before exposing the backend to tool handlers.
- Shield shared initialization from cancellation by an individual tool
caller.
- Close failed or cancelled backend candidates.
- Clear failed initialization state so subsequent calls can retry.
- Retrieve and log background initialization failures.
- Add regression tests for handshake races, failure recovery, and
concurrent initialization.
- Add an Unreleased changelog entry.
## Testing
- [x] Added regression tests
- [x] Focused test suite passes
- [x] Ruff checks pass
- [x] Mypy passes
- [x] Changed files pass formatting checks
- [ ] Entire repository test suite passes without baseline failures
Commands and results:
- `uv run --extra dev --frozen pytest
tests/test_memory/test_mcp_server.py -q`
- `12 passed`
- `uv run --extra dev --frozen ruff check .`
- Passed
- `uv run --extra dev --frozen ruff format --check
headroom/memory/mcp_server.py tests/test_memory/test_mcp_server.py`
- Passed
- `uv run --extra dev --frozen mypy headroom --ignore-missing-imports`
- Success across 504 source files
- `uv run --extra dev --frozen pytest -q`
- `9363 passed, 565 skipped, 4 failed`
- The four failures are existing, unrelated failures outside the changed
code:
- `test_l2_appends_transform_label`
- `test_recovery_records_sockets_and_secures_both_backups`
- `test_dashboard_uses_cached_stats_and_lazy_history_feed_polling`
- `test_smart_crusher_log_fallback_runs_for_valid_json`
Repository-wide `ruff format --check .` also identifies pre-existing
formatting drift in the untouched
`headroom/proxy/handlers/anthropic.py`.
## Real Behavior Proof
The regression tests exercise the affected lifecycle directly:
1. Start backend initialization through the MCP handshake.
2. Suspend warm-up before it completes.
3. Issue a memory tool call and verify its handler is not invoked.
4. Release warm-up and verify the tool receives the initialized backend.
5. Force background initialization to fail and verify the candidate is
closed.
6. Issue another tool call and verify initialization retries with a
fresh backend.
7. Start two tool calls concurrently and verify only one backend is
constructed.
Observed behavior:
- Tool calls remain pending while handshake warm-up is incomplete.
- A partially initialized backend never reaches a tool handler.
- Failed candidates are closed and discarded.
- A later tool call successfully retries initialization.
- Concurrent calls share one initialization task and backend.
Environment: macOS arm64, CPython 3.12.13.
Not tested: a live stdio MCP client using the real ONNX model and
database. The affected initialization lifecycle is covered with
deterministic asynchronous regression tests.
## Review Readiness
- [x] I have performed a self-review before requesting human review.
- [x] This PR is ready for human review.
## Checklist
- [x] The implementation follows the repository’s existing style and
error-handling conventions.
- [x] Tests cover the reported race, concurrent initialization, and
failure recovery.
- [x] Failed initialization does not leave a partially published
backend.
- [x] Failed backend candidates are closed before retry.
- [x] No unrelated files or formatting changes are included.
- [x] No temporary logging, debug code, or commented-out code remains.
- [x] Public behavior changes are documented in the changelog.
- [x] The branch has been rebased from the intended base and is ready
for review.
## Additional Notes
The four full-suite failures listed above occur outside the changed
Memory MCP code and are unrelated to this PR. All tests covering the
modified initialization lifecycle pass.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
6decbd1e6e
|
fix(proxy/streaming): preserve non-standard content-block fields on SSE reconstruction (#2271)
## Description
When the proxy reconstructs a full response from an Anthropic SSE
stream, it silently drops the payload of any content block that isn't
`text` / `tool_use` / `thinking` / `redacted_thinking`.
`_parse_sse_to_response` builds each block on `content_block_start`:
```python
current_block = {"type": btype, "index": block_index}
if btype == "text":
current_block["text"] = block.get("text", "")
elif btype == "tool_use":
current_block["id"] = block.get("id")
current_block["name"] = block.get("name")
current_block["input"] = {}
elif btype == "thinking":
...
elif btype == "redacted_thinking":
...
blocks_by_index[block_index] = current_block
```
There's no branch for other block types. A `server_tool_use` or
`web_search_tool_result` block (Anthropic server-side tools) therefore
reconstructs as a bare `{"type": ..., "index": ...}`, losing its `id`,
`name`, `input`, and content.
This reconstructed response is what `has_memory_tool_calls` and the CCR
feedback recorder inspect, so a stream that used a server-side tool
feeds detection a gutted block. The sibling reconstructor
`_reconstruct_anthropic_response` (in
`headroom/ccr/response_handler.py`) already handles this correctly with
`elif btype: current_block = dict(block)` — this path just wasn't
updated.
## Fix
Add an `elif btype:` branch that copies through all of the block's
fields (except `type`, already set), mirroring the sibling:
```python
elif btype:
for _k, _v in block.items():
if _k != "type":
current_block[_k] = _v
```
Standard blocks are untouched; non-standard blocks keep their fields.
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
- `headroom/proxy/handlers/streaming.py`: add the non-standard-block
field copy in `_parse_sse_to_response`'s `content_block_start` handler.
- `tests/test_sse_thinking_blocks.py`: new test asserting a
`server_tool_use` block keeps `id` / `name` / `input`.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] 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
$ uvx ruff@0.15.17 check headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/handlers/streaming.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the block-construction logic with a dependency-free script
and left the full pytest to CI.
- Exact command / steps: ran a `server_tool_use` content_block_start
through the OLD (special-cases only) and NEW (`elif btype:` copy) logic,
plus a `text` block as a control.
- Observed result: OLD produces `{"type": "server_tool_use", "index":
0}` (id/name/input gone); NEW keeps `id`/`name`/`input`; the `text`
block is identical under both.
- Not tested: a live server-tool stream end-to-end; full local `pytest`
deferred to CI (OOM).
## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. The new test reuses the
existing `_Parser(StreamingMixin)` harness in
`tests/test_sse_thinking_blocks.py`, so it runs under the normal CI
pytest job; behaviour is additionally verified by the standalone proof
above.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
1612f06a4c
|
fix(ccr): don't crash tool-call detection on a null function/functionCall (#2269)
## Description
CCR tool-call detection crashes when an upstream response carries a tool
call whose `function` (or `functionCall`) field is explicitly `null`.
`is_ccr_tool_call` and `parse_tool_call` both read the nested name like
this:
```python
tool_call.get("function", {}).get("name")
tool_call.get("functionCall", {}).get("name")
```
`dict.get("function", {})` only substitutes `{}` when the key is
**missing**. When the key is present but `null` — `{"id": "call_1",
"type": "function", "function": null}`, which upstreams (and gateways
like LiteLLM/OpenRouter) emit for a partial or streamed tool call — the
result is `None`, and `None.get("name")` raises `AttributeError`.
These functions run over the untrusted upstream response
(`has_ccr_tool_calls` → `is_ccr_tool_call` for every tool call, and
`parse_tool_call` on the retrieve path), so a single malformed tool call
takes down CCR detection for the whole response. The sibling
`tool_call_id_for_provider` in the same module already guards this shape
(`if isinstance(function_call, dict)`); these two paths just weren't
updated to match.
## Fix
Coalesce with `or {}` so a `null` (or any falsy) value collapses to
`{}`:
```python
(tool_call.get("function") or {}).get("name")
(tool_call.get("functionCall") or {}).get("name")
```
and in `parse_tool_call`:
```python
function = tool_call.get("function") or {}
function_call = tool_call.get("functionCall") or {}
```
A null tool call now reports "not a CCR call" and is passed through as a
normal tool, and real CCR calls are still detected.
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
- `headroom/ccr/tool_calls.py`: `is_ccr_tool_call` coalesces `function`
/ `functionCall` with `or {}`.
- `headroom/ccr/tool_injection.py`: `parse_tool_call` coalesces
`function` (openai) and `functionCall` (google) with `or {}`.
- `tests/test_ccr_tool_calls.py`, `tests/test_ccr_tool_injection.py`:
new tests covering a null-function tool call in detection and parsing.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] 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
$ uvx ruff@0.15.17 check headroom/ccr/tool_calls.py headroom/ccr/tool_injection.py tests/test_ccr_tool_calls.py tests/test_ccr_tool_injection.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/ccr/tool_calls.py headroom/ccr/tool_injection.py
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the detection logic with a dependency-free script and left
the full pytest to CI.
- Exact command / steps: ran an OpenAI tool call `{"function": null}`
(plus a real CCR call) through the OLD `get("function", {})` form and
the NEW `get("function") or {}` form.
- Observed result: OLD raises `AttributeError` on the null function; NEW
returns `False`/`None` for it and still detects the real CCR call and
both `functionCall`/`name` shapes.
- Not tested: a live upstream emitting a null-function tool call; full
local `pytest` deferred to CI (OOM).
## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. The new tests live
alongside the existing CCR tool-call tests so they run under the normal
CI pytest job; behaviour is additionally verified by the standalone
proof above.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
8b7e797ed4
|
fix(proxy/memory): don't crash memory tool-call detection on a null function (#2272)
## Description
`MemoryHandler` crashes when an upstream response carries a tool call
whose `function` field is explicitly `null`.
Three sites read the nested name like `tool_call.get("function",
{}).get("name")`:
- `has_memory_tool_calls` (line ~1043) — over the response's tool calls.
- `handle_tool_calls` (line ~1110/1119) — resolving the tool name and
arguments.
- the memory tool-injection dedup (line ~562) — over the request's
tools.
`dict.get("function", {})` only substitutes `{}` for a *missing* key. A
present-but-null `{"id": "c1", "type": "function", "function": null}` —
a shape upstreams and gateways emit for a partial or streamed tool call
— makes the result `None`, and `None.get("name")` raises
`AttributeError`.
`has_memory_tool_calls` and `handle_tool_calls` both iterate the
untrusted upstream response, so a single malformed tool call takes down
memory tool-call detection and handling for the whole response.
## Fix
Coalesce `function` with `or {}` at all three sites, so a null (or any
falsy) value collapses to `{}`:
```python
name = tc.get("name") or (tc.get("function") or {}).get("name")
args_str = tc.get("arguments") or (tc.get("function") or {}).get("arguments") or "{}"
```
Real tool calls resolve exactly as before.
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
- `headroom/proxy/memory_handler.py`: coalesce `function` with `or {}`
in `has_memory_tool_calls`, `handle_tool_calls`, and the tool-injection
dedup.
- `tests/test_memory_handler_null_function.py`: new tests that a
null-function tool call doesn't crash detection and the real memory call
is still seen.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] 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
$ uvx ruff@0.15.17 check headroom/proxy/memory_handler.py tests/test_memory_handler_null_function.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/memory_handler.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the name-resolution logic with a dependency-free script and
left the full pytest to CI.
- Exact command / steps: ran a `{"function": null}` tool call (plus a
real `memory_save` call) through the OLD `get("function", {})` and NEW
`get("function") or {}` name resolution.
- Observed result: OLD raises `AttributeError` on the null function; NEW
returns `None` for it and still resolves the real `memory_save` name and
a plain `{"name": "memory"}`.
- Not tested: a live upstream emitting a null-function tool call; full
local `pytest` deferred to CI (OOM).
## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. `has_memory_tool_calls`
and `_extract_tool_calls` use no instance state, so the new test
exercises them on a bare instance via `object.__new__` — it runs under
the normal CI pytest job, and the standalone proof above corroborates
it. This is the memory-handler sibling of the same null-`function`
hazard I'm fixing in the CCR tool-call detection and the memory tool
adapter.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
ef1e7e403b
|
fix(proxy/memory): don't crash the memory tool adapter on a null function/arguments (#2270)
## Description
The memory tool adapter crashes when an upstream response carries a tool
call whose `function` or `arguments` field is explicitly `null`.
`_get_tool_name`, `_get_tool_id`, and `_get_tool_input` all read the
nested function like this:
```python
str(tool_call.get("function", {}).get("name", ""))
tool_call.get("function", {}).get("arguments", "{}")
```
Two distinct crashes:
1. **Null `function`** → `AttributeError`. `dict.get("function", {})`
only substitutes `{}` for a *missing* key. A present-but-null `{"id":
"c1", "type": "function", "function": null}` (which upstreams and
gateways emit for partial/streamed tool calls) makes the result `None`,
and `None.get("name")` raises.
2. **Null `arguments`** → `TypeError`. `tool_call.get("function",
{}).get("arguments", "{}")` returns `None` when `arguments` is null, and
`json.loads(None)` raises `TypeError` — which the surrounding `except
json.JSONDecodeError` does **not** catch.
Both parse the untrusted upstream response inside `handle_tool_calls`,
so a single malformed tool call takes down memory tool handling. Notably
`parse_tool_call` in `headroom/ccr/tool_injection.py` already catches
the `json.loads(None)` `TypeError` with an explicit comment, so the
null-arguments hazard is known in the codebase; this path just wasn't
hardened.
## Fix
- Coalesce `function` / `functionCall` with `or {}` so a null value
collapses to `{}`.
- Coalesce the arguments string with `or "{}"` and add `TypeError` to
the `except`, so a null `arguments` yields `{}` instead of crashing.
Real tool calls parse exactly as before.
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
- `headroom/proxy/memory_tool_adapter.py`: coalesce
`function`/`functionCall` (`or {}`) in
`_get_tool_name`/`_get_tool_id`/`_get_tool_input`; coalesce the
arguments string (`or "{}"`) and catch `TypeError`.
- `tests/test_memory_tool_adapter_null_fields.py`: new tests for null
function, null arguments, and that real calls still parse.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] 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
$ uvx ruff@0.15.17 check headroom/proxy/memory_tool_adapter.py tests/test_memory_tool_adapter_null_fields.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/memory_tool_adapter.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the parse helpers with a dependency-free script and left the
full pytest to CI.
- Exact command / steps: ran `{"function": null}` and `{"function":
{"arguments": null}}` (plus a real `memory_save` call) through the OLD
and NEW `_get_tool_name`/`_get_tool_input` logic.
- Observed result: OLD raises `AttributeError` on the null function and
`TypeError` on the null arguments; NEW returns `""`/`{}` for both and
still parses the real call to `{"content": "hi"}`.
- Not tested: a live upstream emitting a null field; full local `pytest`
deferred to CI (OOM).
## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. The parse helpers read
only their `tool_call` argument (no instance state), so the new test
exercises them on a bare instance via `object.__new__` — it runs under
the normal CI pytest job, and the standalone proof above corroborates
it.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
eed80dd4ba
|
fix(learn/claude): don't abort the whole scan on a null message line (#2299)
## Description
A single Claude session-log line with an explicit `{"message": null}`
crashes the entire `headroom learn` run.
`ClaudeCodePlugin._scan_session` reads the message object in four
places:
```python
usage = d.get("message", {}).get("usage", {}) # assistant line
...
msg = d.get("message", {}) # _extract_tool_uses
msg = d.get("message", {}) # _extract_tool_results
msg = d.get("message", {}) # _extract_user_events
```
`dict.get("message", {})` only substitutes `{}` for a **missing** key. A
present-but-null `{"type": "assistant", "message": null}` yields `None`,
and `None.get(...)` raises `AttributeError`.
The per-file guard only catches I/O errors:
```python
try:
with open(jsonl_path, ...) as f:
for line in f:
...
except (OSError, UnicodeDecodeError) as e:
...
return None
```
so the `AttributeError` propagates out of `_scan_session`, past
`scan_project` (which has no try/except around the scan), and aborts the
whole `learn` invocation — every project, not just the one bad line. One
malformed line takes down the entire run.
## Fix
Coalesce the message with `or {}` at all four sites, so a null (or any
falsy) value collapses to `{}`:
```python
usage = (d.get("message") or {}).get("usage", {})
msg = d.get("message") or {}
```
The malformed line is now skipped and scanning continues; valid lines
are parsed exactly as before.
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
- `headroom/learn/plugins/claude.py`: coalesce `d.get("message")` with
`or {}` in `_scan_session` and the three `_extract_*` helpers.
- `tests/test_learn/test_subagent_scanning.py`: new test that a session
containing `{"message": null}` lines scans without crashing and still
parses the valid tool call.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] 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
$ uvx ruff@0.15.17 check headroom/learn/plugins/claude.py tests/test_learn/test_subagent_scanning.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/learn/plugins/claude.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the per-line handling with a dependency-free script and left
the full pytest to CI.
- Exact command / steps: ran an assistant line `{"message": null}` (plus
a real assistant line and a missing-message line) through the OLD
`get("message", {})` and NEW `get("message") or {}` logic.
- Observed result: OLD raises `AttributeError` on the null message; NEW
returns `0` for it and still counts `42` input tokens for the real line
and `0` for a missing-message line.
- Not tested: a full `learn` run over a real history containing such a
line; full local `pytest` deferred to CI (OOM).
## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. The new test reuses the
existing `ClaudeCodePlugin` scanner harness in
`tests/test_learn/test_subagent_scanning.py`, so it runs under the
normal CI pytest job; behaviour is additionally verified by the
standalone proof above.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
ec12e18186
|
fix(savings): don't fabricate output savings for a free (zero-priced) model (#2298)
## Description
`_estimate_output_savings_usd` reports phantom output-shaping savings
for a model whose output price is legitimately `0.0`.
It reads the per-token output price from litellm and treats a falsy
value as "unavailable":
```python
output_cost_per_token = info.get("output_cost_per_token")
if not output_cost_per_token:
raise RuntimeError("output cost unavailable")
return float(tokens_saved) * float(output_cost_per_token)
```
`if not output_cost_per_token` is `True` for both a **missing** price
(`None`) *and* a real **`0.0`** (a free / local / vendored-at-zero
model). So for a free model it raises, hits the `except`, and bills the
saved output tokens at `DEFAULT_FALLBACK_OUTPUT_COST_PER_TOKEN` ($15/M)
— fabricating output savings for a model that costs nothing.
This is the exact bug that `_estimate_compression_savings_usd` was
already fixed for (it now uses `if input_cost_per_token is None:`, with
a comment explaining that `if not ...` "treated a real 0.0 as
unavailable and billed the $3/M fallback — phantom savings").
`_estimate_input_cost_usd` carries the same fix.
`_estimate_output_savings_usd` is the one that was missed.
## Fix
Fall back only when the price is truly missing:
```python
if output_cost_per_token is None:
raise RuntimeError("output cost unavailable")
```
A `0.0` price now correctly yields `$0` output savings; a missing price
still falls back to the estimate; a real price is unchanged.
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
- `headroom/proxy/savings_tracker.py`: `_estimate_output_savings_usd`
falls back on `output_cost_per_token is None` instead of `not
output_cost_per_token`.
- `tests/test_savings_tracker_zero_price.py`: new tests (free → $0,
unknown → fallback, paid → real price), alongside the existing
compression/input-cost zero-price tests.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] 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
$ uvx ruff@0.15.17 check headroom/proxy/savings_tracker.py tests/test_savings_tracker_zero_price.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/savings_tracker.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the estimator with a dependency-free script and left the full
pytest to CI.
- Exact command / steps: ran 1,000,000 saved output tokens through the
OLD `if not ...` and NEW `is None` logic for a free model
(`output_cost_per_token = 0.0`), a paid model, and an unknown model
(`None`).
- Observed result: OLD bills the free model at the $15/M fallback
(`$0.015` phantom savings); NEW returns `$0.00`. The paid model is
unchanged; the unknown model still falls back under both.
- Not tested: a live proxy run pricing a free model end-to-end; full
local `pytest` deferred to CI (OOM).
## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. The new tests reuse the
`_fake_litellm` harness in `tests/test_savings_tracker_zero_price.py`
(the same file that pins the compression/input-cost zero-price
behavior), so they run under the normal CI pytest job; behaviour is
additionally verified by the standalone proof above.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
6744833afe
|
fix(proxy): key drift detector on conversations, not credentials; canonicalize drift hashes (#2301)
## Description
The Rust proxy's cache-bust drift detector
(`crates/headroom-proxy/src/cache_stabilization/drift_detector.rs`,
PR-E6) cannot currently tell drift from normal operation on interactive
agentic traffic, so it warns on nearly every turn and a real bust drowns
in the noise. Three compounding defects, all verified against live
Claude Code traffic:
1. `derive_session_key` stops at the credential hash — Claude Code sends
one OAuth bearer for every conversation, so all concurrent conversations
share one LRU slot and every conversation switch logs a false
`cache_drift_observed` (with `drift_dims` computed against the wrong
conversation's baseline).
2. The `early_messages` axis hashes the raw first-3-messages window, so
a lone conversation's normal growth (1 → 3 messages) and the client
relocating its `cache_control` breakpoint to the newest block both fire
a false `early_messages` drift at turn 2–3 of essentially every session.
3. `x-headroom-session-id` — the explicit session identity the Python
proxy honors everywhere session-sticky state exists — is ignored on the
Rust path.
This PR makes the detector's session identity conversation-scoped and
its comparison canonical, the same shape as the merged Python-side fix
for #2085 (`SessionTrackerStore.resolve_tracker` lineage resolution +
`_canonicalize_for_prefix_compare`):
- **`derive_session_key`**: honors `x-headroom-session-id` first
(hashed, like every other key input), then folds a conversation
discriminator into the credential/network arms: a 16-hex-char SHA-256
fingerprint of `(model, canonicalized first message)`. Provider prompt
caches are per-model, so a small-model sidecar call (title generation)
that reuses a conversation's opener stays a separate session instead of
false-drifting on `system`.
- **`canonicalize_for_hash`** on all axes and the discriminator: objects
rebuilt with sorted keys (this workspace enables serde_json
`preserve_order`, so a plain re-serialize would keep client wire order
and leave the hashes key-order sensitive) and `cache_control` stripped
outside opaque tool payloads (`input`/`arguments`/`json`/`input_schema`
— mirroring the Python canonicalizer's `_OPAQUE_PAYLOAD_KEYS`, so a user
field that happens to be *named* `cache_control` still counts as drift).
- **`early_messages`** becomes per-message hashes (`[Option<[u8; 32]>;
3]`) with a prefix-aware comparison: growing into the window is benign;
a settled message changing or disappearing under a stable session key is
still drift. `observe_drift` now gates the warning on drifted dimensions
rather than raw hash inequality.
True positives are preserved (`system`/`tools` changes, in-place history
rewrites under a pinned identity), and the detector remains a pure
observer — no forwarded byte changes, `does_not_mutate_input` still pins
that.
**Documented trade-off** (module doc + `conversation_discriminator`
doc): without the explicit header, a client that rewrites its first
message (history compaction, rolling-window truncation, Responses
chained mode) re-keys to a fresh session — the rewrite surfaces as
`cache_drift_first_request` rather than `cache_drift_observed` against
the old baseline. That is deliberate: the credential-keyed alternative
false-warned on every conversation switch, which buried those same
events anyway. `x-headroom-session-id` pins the identity and reports
rewrites as drift. Byte-identical openers on the same model under one
credential still conflate (rare; documented).
Closes #2300
## 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
- `derive_session_key`: `x-headroom-session-id` (hashed) wins;
credential/IP arms fold in `conversation_discriminator` — `(model,
canonicalized first message)`, 16 hex chars
- New `canonicalize_for_hash`: sorted-key object rebuild +
`cache_control` stripped outside `OPAQUE_PAYLOAD_KEYS`; applied to the
`system`/`tools`/`early_messages` axes and the discriminator
- `StructuralHash.early_messages`: `[u8; 32]` → `[Option<[u8; 32]>;
EARLY_MESSAGES_WINDOW]` per-message hashes;
`drift_dims`/`early_window_drifted` implement the prefix-aware rule;
`observe_drift` warns on non-empty dims instead of `!=`
- `conversation_messages` shape guard: bare-string message containers
only count for the Responses `input` sugar
- Docs: module header (canonicalization, trade-off, honest cost),
`conversation_discriminator` rationale + blind spots,
`DRIFT_DETECTOR_CAPACITY` cardinality note (per-conversation keys,
163-byte entry), `structural_hash_log_prefix` hex-length fix
- Tests: 13 new unit tests (conversation separation, turn-growth key
stability, explicit header priority, marker relocation + growth not
drift, rewrite/shrink still drift, per-model separation, key-order
neutrality, opaque-payload fields still count, Responses/Chat
discriminator shapes, string-container gating)
- `CHANGELOG.md`: Unreleased → Fixed entry
## Testing
- [x] Unit tests pass (`cargo test -p headroom-proxy` — full crate: lib
+ integration suites)
- [x] Linting passes (`cargo clippy -p headroom-proxy --all-targets` —
zero warnings; `cargo fmt --check` clean)
- [ ] Type checking passes (`mypy headroom`) — n/a, no Python files
touched
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ cargo test -p headroom-proxy --lib drift_detector
test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 221 filtered out
$ cargo test -p headroom-proxy
(all suites) test result: ok. 248 passed (lib) + integration suites, 0 failed
$ cargo clippy -p headroom-proxy --all-targets
(no warnings)
```
## Real Behavior Proof
- Environment: macOS 15 (arm64), rustc 1.95.0, repo @
|
||
|
|
517bf992cf
|
fix(proxy): quarantine compression while timed-out workers run (#2292)
## Description
A request-side `asyncio.wait_for()` timeout stops waiting, but it cannot
preempt an executor thread that already started. The proxy counted those
late workers and still admitted more compression, so repeated slow calls
could consume the whole compression pool and charge every request
another full timeout.
This change tracks running post-timeout workers as timeout debt and
quarantines request-path compression while that debt is non-zero. New
attempts raise `CompressionQuarantinedError` before executor admission,
using an `asyncio.TimeoutError` subclass so Python 3.10 handlers apply
the existing compression-failure policy. Quarantine clears automatically
after all known timed-out workers genuinely exit.
Mitigates #946 and #810. It does not attempt to kill the first running
thread; Python cannot safely preempt it.
## 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)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Track started, finished, timed-out, and debt-recorded state under the
existing compression metrics lock.
- Reject new compression before enqueue while timed-out workers remain;
clear quarantine on the final worker exit.
- Preserve queued-timeout behavior: work cancelled before worker start
does not activate quarantine; a cancellation/start race is
conservatively tracked as running debt.
- Add `/health` and `/stats` runtime fields for quarantine state, worker
debt, activations, and skips.
- Add `headroom_compression_quarantine_total{event="activated|skipped"}`
Prometheus counters.
- Add regression, recovery, queue-race, runtime-payload, export, reset,
and Python 3.10 exception-class coverage.
- Update `CHANGELOG.md`; no dependency or lockfile changes.
## Reproduction
On base commit `
|
||
|
|
e7340aee65
|
ci(changelog): stop the CHANGELOG cascade — release-please owns it (#2329)
## Description Stops the recurring **CHANGELOG cascade** where several concurrent PRs all edit `CHANGELOG.md`'s `## Unreleased` section, so the first to merge turns the rest `DIRTY` — forcing a serial rebase-per-merge grind. Root cause: **release-please already generates `CHANGELOG.md`** from Conventional Commit titles (see `.release-please-config.json` `changelog-sections`), so the hand-written `## Unreleased` entries are *both redundant with release-please and the sole source of the conflicts*. The PR template and CONTRIBUTING were actively telling contributors to keep hand-editing it. `.gitattributes merge=union` (#2138) does not help — GitHub squash-merge ignores merge drivers. Fix: make release-please the only author of the changelog and stop hand-edits at the source. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) — CI/process ## Changes Made - **`.github/workflows/changelog-guard.yml`** (new): fails any PR that modifies `CHANGELOG.md`, except release-please's own release PR (head branch `release-please--*`). Uses the preinstalled `gh` CLI — no third-party action to pin. - **`.github/PULL_REQUEST_TEMPLATE.md`**: flips the "I have updated the CHANGELOG.md" checkbox to "I did **not** edit CHANGELOG.md — release-please generates it from my Conventional Commit PR title." - **`CONTRIBUTING.md`**: replaces step 6 ("Update `CHANGELOG.md`") with the release-please policy. ## Testing - [x] `actionlint .github/workflows/changelog-guard.yml` — clean - [x] YAML parses (`yaml.safe_load`) - [x] Guard match logic unit-checked locally: fires on root `CHANGELOG.md`, ignores nested paths (`docs/CHANGELOG.md`) and PRs that don't touch it. ### Test Output ```text YAML OK actionlint OK root CHANGELOG.md -> FIRES nested docs/CHANGELOG.md -> pass no changelog -> pass ``` ## Real Behavior Proof - Environment: local clone + `actionlint`. - Exact steps: created the workflow, validated with actionlint + a YAML parse, and exercised the `grep -qx 'CHANGELOG.md'` decision against representative changed-file lists. - Observed result: guard fires only on a root-level `CHANGELOG.md` edit; the release-please branch is exempted via the job-level `if`. - Not tested: a live PR run in Actions (will exercise on this PR itself — note this PR does **not** touch `CHANGELOG.md`, so the guard should pass green here). ## Additional Notes - To *enforce* (block merge, not just show red), add **Changelog Guard / no-manual-changelog** to the branch's required status checks. - Follow-up: the ~10 currently-open PRs that hand-edit `CHANGELOG.md` will need that edit removed (a clean deletion now, not a conflict resolution) before they merge — after which the cascade is gone for good. |
||
|
|
844d9caaa1
|
feat(text-crusher): fold full-width ASCII to half-width in CJK token keys (#2259)
## Description Real CJK content routinely mixes full-width and half-width forms (`API` vs `API`, `0` vs `0`, the ideographic space ` ` vs a normal space). After #1504's ICU tokenization, these width variants produced *different* token keys, so `API` and `API` didn't dedup or match as the same term on the CJK relevance and near-duplicate paths. This folds full-width ASCII (U+FF01–U+FF5E, via the U+FEE0 offset) and the ideographic space (U+3000 → space) to their half-width forms **when building the internal token key** inside `tokens_icu`. Only the token key is normalized — the kept output stays byte-verbatim, so TextCrusher's extractive / byte-faithful contract is preserved. CJK-gated (`tokens_icu` is the CJK path); the ASCII path is untouched. ## Type of Change - [x] Bug fix / enhancement (non-breaking) ## Changes Made - `crates/headroom-core/src/transforms/text_crusher/crusher.rs`: a `width_fold(c)` helper applied when building token keys in `tokens_icu`. - Rust unit tests: full-width ASCII folds to half-width in token keys; CJK segments split on full-width terminators. ## Testing - [x] Unit tests pass (`cargo test`) - [x] Linting passes (`cargo clippy` / `cargo fmt`) - [x] New tests added ### Test Output ```text $ cargo test -p headroom-core --lib text_crusher test result: ok. 13 passed; 0 failed $ cargo clippy / fmt # clean ``` ## Real Behavior Proof - Environment: macOS (Darwin), Rust via cargo, branch `feat/text-crusher-fullwidth-fold` off `main` (rebased after #1504 merged). - Exact command / steps: `cargo test -p headroom-core --lib text_crusher`. - Observed result: `fullwidth_ascii_folds_to_halfwidth` confirms `API` and `API` now produce the same token key (so they dedup/relevance-match as one term); `cjk_splits_on_full_width_terminators` confirms full-width `!`/`?` terminate segments. All 13 text_crusher tests pass; the kept output is byte-verbatim (only the internal key is folded). - Not tested: no Python side — TextCrusher is Rust-only, and output stays byte-verbatim, so no parity fixtures change. ## 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my change is effective - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — happy to add an entry if preferred. ## Additional Notes - Follow-up to #1504 (CJK-aware TextCrusher); it only touches the CJK-gated `tokens_icu` path, so English tokenization is unchanged. |
||
|
|
8951a264a2
|
fix(proxy): preserve content-part array structure in excluded-tool lossless fold write-back (#2261)
## Description
When a tool result is excluded from lossy compression (e.g. grep via
`HEADROOM_EXCLUDE_TOOLS`), the OpenAI Responses adapter performs a
byte-lossless fold on the output text. However, the excluded-tool fold
path joined all content-part text with `_responses_part_text()` and
recorded a `("output", None)` slot, which caused `_set_slot_text` to
replace the entire `output` with a plain string.
For content-part arrays (valid per OpenAI spec: `[{type: output_text,
text: "..."}, {type: input_image, ...}]`), this destroyed the array
structure — non-text parts like images and refusals were silently
dropped.
Closes #2235
## 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/proxy/handlers/openai.py` — In the excluded-tool lossless
fold path, detect list (content-part) outputs and fold each
`input_text`/`output_text` part individually using `("output_part",
index)` slots, matching the eligibility rule already used by
`_slot_texts()` in the normal compression path
- `tests/test_openai_responses_compression_units.py` — Strengthen
existing content-part test to assert output remains a list; add new test
with mixed parts (output_text + input_image + refusal) to verify
structure preservation and byte-identical non-text parts
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Formatting passes (`ruff format --check .`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ uv run pytest tests/test_openai_responses_compression_units.py -q --no-header
26 passed in 1.04s
$ uv run pytest tests/test_openai_responses_compression_units.py tests/test_openai_responses_context_compaction.py tests/test_openai_responses_traffic_learner.py -q --no-header
39 passed in 6.43s
```
## Real Behavior Proof
- Environment: Linux 6.8.0-124-generic, Python 3.12.3, headroom main @
|
||
|
|
4e2bbfee3f
|
fix(opencode): Use opencode.jsonc when present (#1590)
## Description Fix OpenCode proxy injection so it respects user configurations that use the `.jsonc` extension, preventing Headroom from creating a duplicate `.json` file that overrides it. Closes #1588 ## 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 - Updated `opencode_config_path` in `paths.py` to check for `.jsonc` - Updated backup creation in `config.py` to preserve the original extension ## 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 - [x] Manual testing performed ### Test Output ```text N/A ``` ## Real Behavior Proof - Environment: local headroom dev - Exact command / steps: creating a dummy `.config/opencode/opencode.jsonc` and running `headroom wrap opencode`. - Observed result: Headroom successfully injects into `.jsonc` and creates a backup named `opencode.jsonc.headroom-backup`. - Not tested: N/A ## 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 - [ ] 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 - [ ] 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) ## Additional Notes |
||
|
|
f42ce4a239
|
fix: harden fd lifecycle and SystemError handling in runtime and proxy kill (#1556)
## Description Make `pid_alive()` safe on Windows even when `psutil` is not installed, and harden `_kill_proxy_by_pid` exception handling for stale PIDs. ### Problem `headroom._subprocess.pid_alive()` falls back to `os.kill(pid, 0)` when `psutil` cannot be imported. On Windows, CPython routes `os.kill(pid, 0)` through `TerminateProcess` — a destructive call that **kills the target process**. Since `psutil` is not a declared runtime dependency in `pyproject.toml`, a normal lightweight install can hit that fallback, meaning `runtime_status()` can silently terminate a live proxy. ### Fix - **`headroom/_subprocess.py`**: On `win32`, bypass `os.kill` entirely and probe via `kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)`. If `ctypes` also fails, return `True` conservatively (assume alive) to prevent false-negative liveness from causing callers to kill a running process. - **`headroom/cli/wrap.py`**: Widen `_kill_proxy_by_pid` exception handlers on both SIGTERM and SIGKILL paths to catch `OSError` and `SystemError` (Windows `WinError 87`), preventing crashes from stale/invalid PIDs. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom/_subprocess.py`) ### New Tests - `test_pid_alive_win32_no_psutil_never_calls_os_kill` — simulates `win32` + broken `psutil`, asserts `os.kill` is never called and the `kernel32.OpenProcess` path is used instead - `test_pid_alive_win32_no_psutil_no_ctypes_returns_conservative` — simulates `win32` + broken `psutil` + broken `ctypes`, asserts `os.kill` is never called and `True` is returned conservatively |
||
|
|
5279c33b19
|
fix(memory): preserve semantically similar memories (#2303)
## Description
Prevent memory_save from automatically deleting semantically similar but
distinct memories. The previous fire-and-forget deduplication path
deleted existing memories at cosine similarity scores of 0.92 or higher
after the save had already returned success. Similarity remains
available as a consolidation hint, while supersession now requires an
explicit memory_update or memory_delete operation.
## 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
- Removed the automatic background deletion scheduled by memory_save.
- Removed the automatic-dedup threshold and background coroutine that
were no longer needed.
- Preserved the existing similarity search and consolidation hint.
- Kept explicit memory_update and memory_delete behavior unchanged.
- Added a regression test proving that distinct memories survive even at
0.99 simulated similarity.
- Added an Unreleased changelog entry.
## 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
$ uv run --extra dev --frozen pytest
tests/test_memory_handler_native_ops.py
33 passed
$ uv run --extra dev --frozen ruff check .
All checks passed!
$ uv run --extra dev --frozen ruff format --check
headroom/proxy/memory_handler.py tests/test_memory_handler_native_ops.py
2 files already formatted
$ uv run --extra dev --frozen mypy headroom --ignore-missing-imports
Success: no issues found in 504 source files
$ uv run --extra dev --frozen pytest
9361 passed, 565 skipped, 4 failed
The four full-suite failures are unrelated to this diff: the Anthropic
compaction test passed in isolation; the Codex recovery test exceeded
the macOS AF_UNIX path limit; the dashboard test expects text absent
from the existing implementation; and the content-router test expects a
fallback absent from the existing strategy chain.
The repository-wide format check also flags pre-existing formatting in
the untouched headroom/proxy/handlers/anthropic.py.
## Real Behavior Proof
- Environment: macOS on Apple Silicon, CPython 3.12.13, real
LocalBackend, temporary SQLite database, and the local
sentence-transformers
embedding backend; no external provider or model API.
- Exact command / steps: Ran uv run --extra dev --frozen python with a
temporary database, saved User's primary backend framework at work is
FastAPI., queried its similarity to User's primary backend framework at
home is FastAPI., saved the second fact through
MemoryHandler._execute_save, and listed the user's memories.
- Observed result: The real embedding similarity was 0.9387, above the
former 0.92 deletion threshold. The second save returned saved,
included the consolidation hint, retained the original memory, and left
both distinct facts in the database (memory_count: 2).
- Not tested: Live OpenAI or Anthropic provider calls, a deployed proxy
or MCP client session, and Qdrant or Neo4j memory backends. These
paths share the handler policy changed here; backend-specific explicit
update and delete behavior is unchanged.
## 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
- [ ] 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The documentation and code-comment checklist items are not applicable
because this change removes unsafe behavior without introducing a new
public interface or complex implementation. The full-suite checkbox
remains unchecked because four unrelated tests failed locally, as
documented above.
|
||
|
|
26b43f64d6
|
fix(proxy): keep anthropic ccr compression active across deferred injection (#2291) (#2297)
## Description Large native Claude Code requests on the Anthropic path can still forward with zero request compression after CCR tool injection is deferred on a frozen prefix. The stale skip branch treats deferred injection as a reason to bypass request compression entirely, even though the later sticky CCR path already knows when new markers actually require the tool. This removes that stale bypass so compression still runs while the reversible CCR path stays intact. Closes #2291. ## 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 - Remove the stale `should_skip_ccr_request_compression` branch from `headroom/proxy/handlers/anthropic.py`, so deferred CCR tool injection no longer bypasses request compression in token, non-cache, or cache mode. - Keep the existing sticky CCR injection path as the only place that decides whether historical markers need the retrieval tool reintroduced. - Update `tests/test_proxy/test_anthropic_ccr_deferred_injection.py` to cover the two broken zero-compression cases and preserve the already-reversible frozen-prefix path. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py::test_cache_mode_compresses_delta_but_replays_cached_prefix_when_markers_are_historical tests/test_proxy/test_anthropic_ccr_deferred_injection.py::test_non_token_non_cache_mode_keeps_compression_and_injects_tool_for_new_markers tests/test_proxy/test_anthropic_ccr_deferred_injection.py::test_existing_retrieve_tool_keeps_reversible_ccr_path_when_prefix_is_frozen tests/test_openai_tool_search_deferral.py tests/test_openai_responses_compression_units.py -q`) - [x] Linting passes (`uv run ruff check .`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text ======================= 53 passed, 1 warning in 12.71s ======================== All checks passed! 1310 files already formatted ``` ## Real Behavior Proof - Environment: synced branch and base worktrees on a local Windows proxy test host - Exact command / steps: run the updated Anthropic deferred-injection regressions directly against the base package tree and the branch package tree, then run the focused branch pytest suite above - Observed result: the base package tree fails the two updated zero-compression regressions (`FAIL test_cache_mode_compresses_delta_but_replays_cached_prefix_when_markers_are_historical`, `FAIL test_non_token_non_cache_mode_keeps_compression_and_injects_tool_for_new_markers`) while preserving the already-reversible path; the branch package tree prints three `PASS` lines for the same trio and keeps the neighboring OpenAI suites green in the 53-test focused run - Not tested: a live upstream Claude Code request with the reporter's exact provider/model credentials ## 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 - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes `CHANGELOG.md` is not applicable here because Headroom's release pipeline derives it from conventional commits. Scope is limited to the Anthropic CCR request-compression seam that current #2291 evidence exercises. OpenAI tool-search deferral is untouched because the current live issue is a native Claude Code path and the concrete stale skip branch on `origin/main` is in `anthropic.py`. Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
1d79e70f95
|
fix(tests): repair three main-branch test failures (#2306)
## Description `main` CI is red on three independent test failures. All three are **test-side** bugs (stale cache, semantic merge conflict, stale mock) — no product code regressed. Each test passed in isolation but failed on `main`, and each also blocks the `chore: release main` PR (#1923). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - **`test_l2_appends_transform_label`** — `tool_desc_max_chars()` memoises into a module global. An earlier test in shard 1 reads it with the env unset, pinning the cache to `0`, so this test's `setenv("HEADROOM_TOOL_DESC_MAX_CHARS=20")` was swallowed (`assert 0 == 20`). Reset the cache before reading and after, mirroring the sibling `test_l2_skips_label_when_disabled`. - **`test_dashboard_uses_cached_stats_and_lazy_history_feed_polling`** — semantic merge conflict: #2198 (persist lifetime metrics) intentionally retired the session-card `Filtered (lifetime)` row and moved CLI-filtering lifetime into the history tab as `Lifetime Saved`, while the assertion from #1433 still checked the old string. Assert the current `Lifetime Saved` label. - **`test_smart_crusher_log_fallback_runs_for_valid_json`** — stale mock: #1857 made token counting whitespace-aware, so the router now rates the JSON above the naive `len(content.split())==8` the no-op kompress mock reported, making it look like a saving and short-circuiting before the Log fallback. Mock now reports `_estimate_tokens(content)` to match the router. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) ### Test Output ```text $ pytest tests/test_anthropic_compaction_transforms.py \ tests/test_proxy_dashboard_stats_cache.py \ tests/test_transforms_content_router.py -q 78 passed, 1 skipped in 12.14s $ ruff check <the three files> All checks passed! $ ruff format --check <the three files> 3 files already formatted ``` ## Real Behavior Proof - Environment: local `.venv`, Python 3.12.6, pytest 9.0.2 (same three tests that fail on the `main` CI shards 1/3/4). - Exact command / steps: ran the three previously-failing tests by node id — all pass. Reproduced the shard-isolation failure for #1 by calling `tool_desc_max_chars()` with the env unset (cache → 0) before the test, confirmed the reset makes it pass. - Observed result: 3/3 target tests pass; 78 passed / 1 skipped across the three full files. - Not tested: full suite (unchanged product code); CI shards will re-run on this PR. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes `mypy headroom` (the CI-enforced scope) is unaffected — these edits touch only `tests/`, which CI does not type-check. Once this lands on `main`, the `chore: release main` PR (#1923) drops to just the `test_root_server_json_matches_builder` failure, which is the release version-bump `server.json` regen (not a code bug). |
||
|
|
718c8dc559
|
fix(proxy): repair main lint (ruff-format drift + mypy host_header) (#2268)
## Description
`main`'s `lint` CI job is currently **red** (latest main `
|
||
|
|
eac49656a1
|
feat(wrap): add headroom wrap kimi for Kimi CLI (#1426)
## Description Adds `headroom wrap kimi`, routing Kimi CLI through the Headroom proxy. Kimi CLI speaks an OpenAI-compatible `/chat/completions` API (its `kosong` backend wraps `AsyncOpenAI`) and lets the base URL be overridden via `KIMI_BASE_URL`. This wrapper points it at the local proxy. Kimi's own OAuth bearer is forwarded upstream unchanged, so — unlike the Copilot subscription path — no extra login or token exchange is needed. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `headroom/providers/kimi/`: new slice; `build_launch_env` sets `KIMI_BASE_URL` with the per-project base-URL prefix, mirroring the aider/vibe slices. - `headroom/cli/wrap.py`: `kimi` subcommand; falls back to the `kimi-cli` binary when `kimi` is not on `PATH`; `--kimi-api-url` overrides the upstream coding endpoint (default `https://api.kimi.com/coding/v1`). - `tests/test_cli/test_wrap_kimi.py`: 8 tests for the wrap command. - `README.md`: Kimi CLI row in the agent-compatibility matrix. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_cli/test_wrap_kimi.py -q ........ [100%] 8 passed in 0.36s $ ruff check headroom/providers/kimi headroom/cli/wrap.py tests/test_cli/test_wrap_kimi.py All checks passed! $ ruff format --check headroom/providers/kimi headroom/cli/wrap.py tests/test_cli/test_wrap_kimi.py 4 files already formatted ``` ## Real Behavior Proof - Environment: macOS; Kimi CLI (`kimi` / `kimi-cli`); `headroom proxy` started with `--openai-api-url https://api.kimi.com/coding/v1`. - Exact command / steps: start `headroom proxy --port 8787 --openai-api-url https://api.kimi.com/coding/v1`, then `curl -s http://localhost:8787/v1/chat/completions` with the Kimi OAuth bearer and a one-line `kimi-for-coding` chat request (`"Reply with exactly: PONG"`). - Observed result: `HTTP 200`; `choices[0].message.content == "PONG"` from `kimi-for-coding`; the OAuth bearer was forwarded and accepted upstream; the per-project path `/p/<name>/v1/chat/completions` also returned `HTTP 200`. - Not tested: Windows/Linux PATH discovery; the `--learn` / `--memory` live paths beyond flag wiring. ## 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 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 ## Additional Notes - `ruff check` and `ruff format --check` pass locally; `mypy` was run on the new `headroom/providers/kimi` slice only (clean), so the full-tree `mypy headroom` box is left unchecked and is left to CI. - The slice deliberately reuses `codex.proxy_base_url` and `with_project_prefix`, identical to the aider/vibe wrappers, so per-project savings attribution works without Kimi sending custom headers. - Kimi's separate search/fetch services are out of scope for `KIMI_BASE_URL` and continue to hit Kimi directly; only the LLM `/chat/completions` traffic is compressed. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
46d4378cf7
|
feat(evals): weekly HotpotQA answer-recall report on the prose path (#1188)
## Description
Follow-up to **#1187** (the offline fidelity gate). That gate is
hermetic and **structured-only** (JSON tool outputs via Rust
compressors) so it can block every PR with zero setup. This PR adds the
genuinely-uncovered piece: **prose answer-recall on a real dataset
(HotpotQA)** in the **model-allowed weekly job**, where compression
routes through Kompress (ModernBERT).
> **Stacked on #1187.** Until that merges, this PR's diff shows its
commit too; it reduces to just `
|
||
|
|
63f74aa3e6
|
fix: replace computer_call_output with apply_patch_call_output in output_shaper (#2250)
The _RESPONSES_TOOL_OUTPUT_TYPES frozenset in output_shaper.py had computer_call_output instead of apply_patch_call_output, making it inconsistent with the canonical definitions in handlers/openai.py and output_turn_policy.py. This caused apply_patch_call_output items to be misclassified, preventing effort routing optimization for those turns. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
81d40a6437
|
fix(proxy): record Prometheus metrics for POST /v1/compress (#2247)
## Description `POST /v1/compress` compressed messages correctly but never recorded Prometheus business metrics. Standalone compress microservice deployments (including LiteLLM `guardrail: headroom`) left `headroom_requests_total`, token counters, latency, and by_model/by_provider families at zero. This wires the existing request-outcome funnel into `handle_compress` so success and timeout paths update the same counters as reverse-proxy handlers, and hard failures call `record_failed`. Closes #2244 ## 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 - On successful compression, record a `RequestOutcome` with `provider="compress"`, model, token before/after/saved, latency, transforms, tags, and client. - On compression timeout (fail-open), record zero-savings outcome plus `record_compression_failed("timeout")`. - On hard compression errors (503), call `metrics.record_failed(provider="compress")`. - Leave bypass header and empty-message early returns unrecorded (no real compression work). - Response schemas and status codes unchanged. - Add regression tests for success, timeout, and hard-failure metric recording. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy_compress_endpoint.py -q # 16 passed uv run ruff check headroom/proxy/handlers/openai.py tests/test_proxy_compress_endpoint.py # passed ``` ## Real Behavior Proof - Environment: local checkout of this branch; FastAPI TestClient fixtures for `/v1/compress` (loopback client) - Exact command / steps: - `uv run pytest tests/test_proxy_compress_endpoint.py -q` - `uv run ruff check headroom/proxy/handlers/openai.py tests/test_proxy_compress_endpoint.py` - Observed result: - Success path awaits `_record_request_outcome` with `tokens_saved = max(0, before-after)` and `provider="compress"` - Timeout path records zero-savings outcome and `record_compression_failed("timeout")` - Hard failure path awaits `record_failed(provider="compress")` and still returns 503 - Not tested: live multi-process scrape of `GET /metrics` while a real headroom process handles LiteLLM guardrail POSTs ## 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 - [ ] 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 - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes - Focused compress endpoint suite only; full-repo mypy was not run. - No public API or response-schema changes. |
||
|
|
5424e99a65
|
Clarify uv tool install path on macOS (#1196)
## Description Clarifies the recommended install path for the Headroom CLI on macOS Apple Silicon and Linux. The docs now prefer `uv tool install --python 3.13 "headroom-ai[all]"` for host-level CLI use, keep `pip install` scoped to Python project environments, and call out absolute executable paths for MCP clients that do not inherit interactive shell `PATH`. ## 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added `uv tool install --python 3.13` guidance to the README, docs install page, quickstarts, and wiki install pages. - Documented `uv tool update-shell` for shells that cannot find the installed `headroom` command. - Clarified absolute MCP server command paths for clients that do not inherit the interactive shell `PATH`. - Pointed Intel macOS users at the Docker-native install path until native wheel support lands. ## Testing Describe the tests you ran to verify your changes: - [ ] Unit tests pass (`pytest`) - not run; docs-only change. - [ ] Linting passes (`ruff check .`) - not run; docs-only change. - [ ] Type checking passes (`mypy headroom`) - not run; docs-only change. - [ ] New tests added for new functionality - not applicable. - [x] Manual testing performed - [x] `git diff --check upstream/main...HEAD` ## Real Behavior Proof ```bash $ git diff --check upstream/main...HEAD # exits 0; no whitespace errors ``` `npm --prefix docs run types:check` was also attempted. It regenerated MDX and route types successfully, then failed in existing docs app code because `@/lib/...` imports cannot resolve from files such as `app/(home)/layout.tsx`, `app/api/search/route.ts`, and `components/button.tsx`. This PR only changes `README.md`, `docs/content/docs/installation.mdx`, `docs/content/docs/quickstart.mdx`, and `wiki/*.md` files. ## Review Readiness - [x] Draft PR; docs wording and install-path accuracy are ready for review. - [x] No code or runtime files changed. - [x] Known docs type-check blocker is documented above. ## Test Output ```bash $ git diff --check upstream/main...HEAD # no output ``` ```text $ npm --prefix docs run types:check [MDX] generated files ✓ Types generated successfully app/(home)/layout.tsx(2,29): error TS2307: Cannot find module @/lib/layout.shared or its corresponding type declarations. ... components/button.tsx(4,20): error TS2307: Cannot find module @/lib/cn or its corresponding type declarations. ``` ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - not applicable; docs-only change. - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - not applicable; docs-only change. - [ ] New and existing unit tests pass locally with my changes - not run; docs-only change. - [ ] I have updated the CHANGELOG.md if applicable - not applicable. ## Screenshots (if applicable) Not applicable. ## Additional Notes The PR remains a draft while docs verification is limited by the existing docs app `@/lib/*` resolution issue. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
7ddcbcb616
|
perf: surface optimization overhead diagnostics (#1212)
## Summary - add overhead diagnostics to perf JSON output - report optimization p50/p95/p99, slow request percentage, per-stage totals/percentiles, and top slow requests - update text report and recommendations to point at the slowest stage and HEADROOM_COMPRESSION_TIMEOUT_SECONDS when optimization is consistently slow ## Verification - python -m py_compile headroom/perf/analyzer.py tests/test_cli_perf_format.py - pytest tests/test_cli_perf_format.py could not run locally because pytest is not installed in this Python environment |
||
|
|
412db40a0b
|
fix(code): pin tree-sitter-language-pack <1.0.0 in [code] extra (#1219)
## 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. --> --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
1c50eca8b3
|
fix(proxy): skip Responses memory tools for ChatGPT auth (#1579)
## Description Fix ChatGPT/Codex session-auth Responses proxy handling so the ChatGPT backend always receives an explicit `store=false`, while keeping Responses memory tools limited to the regular API-key path where stored responses are supported. ## 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 - Detect ChatGPT auth before Responses memory-tool injection and force `store=false` for ChatGPT-auth Responses payloads. - Skip Responses memory tools and transparent memory-tool continuation handling for ChatGPT auth across HTTP, WebSocket first frames, WebSocket follow-up `response.create` frames, and WS-to-HTTP fallback. - Preserve API-key behavior after the current main merge: API-key requests that explicitly set `store=false` skip Responses memory tools, while API-key requests that receive injected memory tools are forced to `store=true` for continuation support. - Address Copilot formatter comments by making `_allow_responses_memory_tools` call sites formatter-stable. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv run --extra dev ruff format --check headroom/proxy/handlers/openai.py 1 file already formatted $ uv run --extra dev ruff check headroom/proxy/handlers/openai.py tests/test_openai_codex_routing.py tests/test_openai_codex_ws_timings.py tests/test_ws_http_fallback.py All checks passed! $ uv run --extra dev python -m pytest -q tests/test_openai_codex_routing.py tests/test_openai_codex_ws_timings.py tests/test_ws_http_fallback.py 37 passed in 0.34s ``` ## Real Behavior Proof - Environment: Local checkout of `fix/codex-store-false-memory-tools` using `uv run --extra dev`. - Exact command / steps: Ran the focused formatter, lint, and pytest commands listed in `Testing`. - Observed result: Formatting is stable, lint passes, and the focused OpenAI/Codex routing and fallback tests pass. - Not tested: Full test suite, `mypy headroom`, and a fresh live ChatGPT backend probe after the formatter-only follow-up. The original PR validation recorded that valid ChatGPT subscription backend requests return `200` with `store=false`, while identical `store=true` or omitted `store` requests return `400 Store must be set to false`. ## 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 - [ ] 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 - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes - Post-deploy monitoring terms: `Responses: forced store=false for ChatGPT auth`, `WS Responses: forced store=false for ChatGPT auth`, `chatgpt_store_false`, `Memory: forced store=true for Responses memory tool continuation`, and upstream 400s containing `Store must be set to false`. - Expected healthy signals: ChatGPT-auth Responses requests keep `store=false` and no longer fail with `Store must be set to false`; API-key memory-tool flows still inject memory tools and can continue via `previous_response_id`. - Rollback trigger: any increase in ChatGPT-auth 400s, API-key memory-tool continuation failures, or missing memory tool injection on API-key Responses requests. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
420dc9077b
|
feat(grok-build): add Grok Build wrap command and MCP integration (#1629)
## Description
Adds first-class Grok Build support to Headroom so Grok CLI sessions can
route through the local proxy for context compression and savings
tracking.
This PR introduces `headroom wrap grok-build` / `headroom unwrap
grok-build`, a `grok_build` provider slice, Grok MCP registrar support,
and install/telemetry wiring so Grok traffic is attributed correctly in
the proxy and dashboard.
Review follow-up (`9368c413`): when users already own
`[model.grok-build]` in `~/.grok/config.toml`, wrap rewrites `base_url`
in that table in place instead of appending a duplicate header (invalid
TOML).
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
## Changes Made
- Added `headroom/providers/grok_build/` with runtime helpers,
reversible `~/.grok/config.toml` injection, and install env builders.
- Added `headroom wrap grok-build` and `headroom unwrap grok-build` CLI
commands.
- Added `GrokRegistrar` for Headroom MCP registration in Grok config.
- Wired `grok_build` into install planner/registry, agent savings,
telemetry, and proxy client detection (`grok/` user agent).
- **Review fix:** rewrite `base_url` inside an existing user-owned
`[model.grok-build]` table in place (`# was: …` metadata).
- Added regression tests + docs (`grok-build.mdx`, `proxy.mdx`) and
CHANGELOG entry.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest -q tests/test_provider_grok_build.py tests/test_mcp_registry/test_grok_registrar.py
============================== 12 passed in 1.13s ==============================
```
See **Screenshots** below for terminal captures (pytest, review-fix
in-place rewrite, proxy `/readyz`, unwrap).
## Real Behavior Proof
- Environment: macOS, Python 3.11.12 venv, feat/grok-build @ `
|
||
|
|
ecf086f1f8
|
docs(compose): document the memory-stack docker-compose services (#1242)
## What Adds explanatory comments throughout `docker-compose.yml` so the "memory stack" is self-documenting for new users. Covers: - The **headroom-proxy** service — OpenAI-compatible endpoint, why it binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on` start-order caveat. - **Qdrant** (vector search) and **Neo4j** (relationship graph) — their roles, exposed ports, and named volumes for persistence. - A header block with quick-start steps, the full list of host-exposed ports, and a note that the proxy can run standalone without the datastores. - A callout that the `NEO4J_AUTH` default is **local-dev only** and must be overridden before any non-local use. ## Why The compose file previously had only minimal inline comments, making it unclear which services are optional and which port maps to what. These are documentation-only changes — no behavior, image, or configuration values changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
3f5067022f
|
[codex] Simulate Codex read maturation risk (#1395)
## Summary - add Codex support to `audit-reads --codex --simulate-maturation` - classify Codex edit targets from `apply_patch`, `sed -i`, `tee`, and shell redirects so maturation risk can count edits - include focused tests for Codex maturation metrics, CLI JSON/text output, and edit-risk buckets - refresh `uv.lock` to match the current `pyproject.toml` version/extras ## Validation - `uv run ruff check headroom/audit/codex.py headroom/audit/maturation.py headroom/audit/__init__.py headroom/cli/audit.py tests/test_audit_codex.py` - `uv run pytest tests/test_audit_codex.py tests/test_audit_reads.py tests/test_read_maturation.py tests/test_read_maturation_handler_nobust.py -q` - live local run: `uv run headroom audit-reads --codex --path /home/robert-briscoe/.codex/sessions --simulate-maturation` Co-authored-by: Robert Briscoe <robert@briscoe.dev> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
ac7ee4e0bf
|
fix(proxy): support Codex WS compatible gateways (#1281)
Adds opt-in compatibility for OpenAI-compatible WebSocket gateways used behind Codex /v1/responses. - HEADROOM_OPENAI_WS_FLATTEN_RESPONSE_CREATE=1 flattens Codex response.create frames before upstream send. - HEADROOM_OPENAI_WS_PROPAGATE_UPSTREAM_CLOSE=1 propagates upstream close code/reason back to the client. - Default behavior is unchanged. Tested: python -m pytest tests/test_openai_codex_ws_lifecycle.py -q 18 passed Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
02c77640a9
|
fix(transforms): guard Log fallback against invalid JSON + fix MIXED false-positive on source code (#1347)
## Summary Three related fixes in the content router/detector, addressing data-loss and misrouting bugs found via chaotic audit: - **SMART_CRUSHER → Log fallback guard (#1306):** Truncated/invalid JSON tool outputs were tagged `json_array` by the native magika detector (classifies by shape, not parseability), routed to SmartCrusher (no-op), Kompress (no-op), then collapsed by LogCompressor to a single CCR-retrieval marker — **99.9% data loss** when CCR retrieval isn't configured. A JSON-validity guard (`_content_is_valid_json`) now skips the Log fallback for content that fails `json.loads`; valid JSON arrays still reach it (LogCompressor is a no-op on them). - **MIXED false-positive on source code:** `is_mixed_content` regex heuristics misclassify Python with dict/list literals (`{`, `[` at line start → `has_json_blocks`) + docstrings (`has_prose`) as MIXED, wasting 1–1.4s latency with 0% compression. When the native detector confidently says `SOURCE_CODE` (confidence ≥ 0.8), `_determine_strategy` now trusts it over the regex heuristics. - **PASSTHROUGH for code when CodeAware disabled:** When `prefer_code_aware_for_code=False` (default), source code now uses `PASSTHROUGH` instead of `KOMPRESS`, honouring the config's "let code pass through unmangled" intent. KOMPRESS can destroy code semantics (98% compression, 11% fact recall on large blobs). - **RecursionError hardening:** Caught in both `_try_detect_json` and `_content_is_valid_json` so deeply nested JSON (`[[[[...]]]]` with 10k+ levels) no longer crashes the detector/router — also serves as a DoS mitigation. #### Test plan - [x] `tests/test_transforms_content_router.py` — 36 passed (8 new tests) - [x] `tests/test_transforms_content_detection.py` — 9 passed - [x] `tests/test_cache_aligner_detector_only.py` — 22 passed - [x] `tests/test_compression_decision.py`, `test_compression_policy.py`, `test_compress_api.py`, `test_compression_safety_rails.py` — 137 passed, 5 skipped - [x] `ruff check` on changed files — all checks passed - [x] `mypy` on changed files — no issues found New tests cover: - Invalid JSON skips Log fallback (content preserved verbatim) - Valid JSON arrays still reach Log fallback - MIXED false-positive overridden by high-confidence SOURCE_CODE detection - Low-confidence SOURCE_CODE does NOT override MIXED (safety) - Genuine mixed content (PLAIN_TEXT detection) still uses MIXED - PASSTHROUGH preserves code verbatim, never invokes Kompress - CodeAware explicitly enabled still uses CODE_AWARE #### Risks / rollback - Behaviour change: code blobs previously routed through MIXED→KOMPRESS now use PASSTHROUGH. This is the documented intent of `prefer_code_aware_for_code=False`; if a deployment relied on the accidental KOMPRESS compression of code, set `prefer_code_aware_for_code=True` to restore CODE_AWARE. - The JSON-validity guard adds one `json.loads` call in the narrow "no savings" fallback path only — negligible overhead. - Revert is a single-commit revert; no schema/migration changes. Generated with [Devin](https://devin.ai) Co-authored-by: monkeygold <monkeygold@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
a51bbfb6a5
|
fix(opencode): use type=local + environment field for MCP config (#1380) (#1388)
## Summary Fixes #1380 — OpenCode MCP config was written with the wrong schema in both `mcp install` and `wrap opencode`. ### Root Cause `_spec_to_entry` and `build_opencode_config_content` both generated: ```json { "type": "remote", "url": "http://127.0.0.1:<port>/mcp", "env": {...} } ``` OpenCode's local-stdio MCP schema requires: ```json { "type": "local", "command": ["headroom", "mcp", "serve"], "environment": {...} } ``` The proxy does not expose `/mcp`; it returns 404. The `env` field is also the wrong key — OpenCode expects `environment`. ### Changes - **`headroom/mcp_registry/opencode.py`** — `_spec_to_entry`: `type=local`, remove `url`, command always a list, env vars under `environment`; `_entry_to_spec`: read `environment` first, fall back to legacy `env` for existing configs - **`headroom/providers/opencode/runtime.py`** — `build_opencode_config_content`: local stdio entry with `HEADROOM_PROXY_URL` env var pointing to the proxy port (headroom mcp serve picks it up at startup) - **Tests** — updated two assertions to match corrected schema; added 3 regression tests for `type=local`, `environment` field, and legacy `env` fallback ## Test Plan - [x] `pytest tests/test_mcp_registry_opencode.py` — 64 passed - [x] `pytest tests/test_cli/test_wrap_opencode.py` — 64 passed - [x] All previously-passing tests remain green ## Remaining items from #1380 - `--no-mcp` still writes persistent MCP via `inject_opencode_provider_config()` — tracked in the issue, separate PR - `mcp uninstall/status` symmetry — tracked in the issue, larger scope - `--target opencode` CLI addition — tracked in the issue 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Real behavior proof **Setup:** macOS 14, Python 3.12, headroom-ai 0.27.0-dev, OpenCode 0.1.x **Steps after patch:** ```bash headroom mcp install --agent opencode cat ~/.config/opencode/opencode.json | python3 -m json.tool ``` **After-fix evidence — written config:** ```json { "mcp": { "headroom": { "type": "local", "command": ["headroom", "mcp", "serve"], "enabled": true } } } ``` Before fix: `type: "remote"`, `url: "http://127.0.0.1:8787/mcp"` (404 on proxy), `env` key (wrong field name). OpenCode failed to start headroom MCP server. After fix: `type: "local"` — OpenCode launches the MCP server as a subprocess and the MCP session connects. **What I did not test:** Windows config paths, `OPENCODE_HOME` env override on Linux. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
d50c73f2a1 | test: align savings schema assertions | ||
|
|
e8bff1cfe3
|
feat: add CrewAI and AutoGen tool compression integrations (#1384)
## Description Add CrewAI and AutoGen tool compression integrations, following the same patterns as the existing LangChain agent integration (`HeadroomToolWrapper` / `wrap_tools_with_headroom`). Both delegate compression to `compress_tool_result()` from the MCP integration, with per-tool metrics tracking via `ToolCompressionMetrics` / `ToolMetricsCollector`. Closes #1379 ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - Add `headroom/integrations/crewai/` — `HeadroomToolWrapper` subclasses CrewAI `BaseTool`, wraps `_run()` with compression - Add `headroom/integrations/autogen/` — `HeadroomToolWrapper` wraps AutoGen `FunctionTool` (sync and async) with compression - Wire both into `headroom/integrations/__init__.py` with aliased re-exports (avoids name collision with LangChain's `HeadroomToolWrapper`) - Add `[crewai]` and `[autogen]` optional dependency extras to `pyproject.toml` - Add 24 unit tests (12 per framework) under `tests/test_integrations/` - Add `.mdx` doc pages for both frameworks under `docs/content/docs/` - Update `CHANGELOG.md` with entries under `### Added` ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ ruff check headroom/integrations/crewai headroom/integrations/autogen tests/test_integrations/crewai tests/test_integrations/autogen All checks passed! $ pytest tests/test_integrations/autogen -v 12 passed $ pytest tests/test_integrations/crewai -v 12 passed ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.11, crewai 1.14.7, autogen-agentchat 0.7.5 - Exact command / steps: Ran standalone adapter demos and benchmark runner across 4 task types - Observed result: | Task | Tokens (raw) | Tokens (compressed) | Savings | |------|-------------|-------------------|---------| | Inventory JSON (80 items) | 5,044 | 1,532 | 69.6% | | Server logs (150 lines) | 8,712 | 314 | 96.4% | | Analytics query (100 rows) | 10,762 | 10,762 | 0% | | API docs (20 endpoints) | 8,043 | 8,043 | 0% | Compression results are identical across CrewAI and AutoGen — expected since both route through the same `compress_tool_result()` pipeline. - Not tested: Full end-to-end with a live LLM agent loop (demos test the compression pipeline standalone). LangGraph not included — headroom already has `headroom/integrations/langchain/langgraph.py`. ## 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 - [x] 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 ## Additional Notes - LangGraph integration is intentionally excluded — headroom already has one at `headroom/integrations/langchain/langgraph.py` - Re-exports in `__init__.py` are aliased (`CrewAIToolWrapper`, `AutoGenToolWrapper`) to avoid collision with the existing LangChain `HeadroomToolWrapper` - Both integrations follow the exact same conventions as the existing LangChain agents module: optional dep guard, `compress_tool_result()` delegation, metrics with 1000-entry cap, Google-style docstrings - `mypy` not checked due to Rust build dependency (`maturin`) that requires Application Control policy changes on this machine --------- Co-authored-by: Sneha27feb <sroy27.ai@gmail.com> |
||
|
|
3dd9660d91
|
feat: 3-layer context compression pipeline (L1+L2+L3) (#1405)
## Description > **Default behavior is unchanged:** only L1 (annotation-key stripping) is on by default. L2 (description truncation) and L3 (system-prompt compression) are **opt-in** via `HEADROOM_TOOL_DESC_MAX_CHARS` and `HEADROOM_SYSTEM_COMPACT=1` respectively — instruction-level compression never runs unless an operator explicitly enables it. Verified in `system_compact.py`: `system_compact_enabled()` returns `False` when the env var is unset. Reduces MCP-injected context overhead (~40K tokens / 20% of a 200K window) through a progressive 3-layer compression pipeline. Each layer is independently controlled, fail-safe, and additive — operators can enable L1 only (default) or opt into L2/L3 for deeper savings. ### Layer 1: Tool Schema Annotation Key Stripping (default on) - Strip JSON Schema annotation keys (`$schema`, `title`, `examples`, `deprecated`, `default`, `readOnly`, `writeOnly`) from tool definitions - Normalise whitespace in `description` fields - Zero risk — removes only non-constraint metadata that models ignore - ~8% savings on tool schema size ### Layer 2: Tool Description Truncation (opt-in: `HEADROOM_TOOL_DESC_MAX_CHARS`) - Truncate verbose tool/parameter descriptions to configurable length - Preserves first complete sentence (critical for model tool selection) - Optionally appends second sentence within 1.5× budget - Hard-truncates with `...` if a single sentence exceeds limit - Recursively processes nested `description` fields in `input_schema`/`parameters` - ~43% savings on description text (estimated ~17K tokens) ### Layer 3: System Prompt CCR Compression (opt-in: `HEADROOM_SYSTEM_COMPACT`) - Compress `system[]` content blocks using existing `ContentRouter.compress()` - Only compresses blocks exceeding `HEADROOM_SYSTEM_COMPACT_MIN_CHARS` (default 500) - Preserves `cache_control` markers and non-text blocks - Fail-safe: leaves block unchanged if compression fails or doesn't save size - ~14.5% savings on system prompt (estimated ~3.5K tokens) **Combined savings (all 3 layers enabled): ~40K → ~17K tokens (~58% reduction)** ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/proxy/tool_schema_compaction.py` — New shared module: L1 annotation stripping + L2 description truncation with `strip_annotation_keys()` and `truncate_descriptions()` - `headroom/proxy/system_compaction.py` — New module: L3 system prompt CCR compression with `compact_system_blocks()` - `headroom/proxy/handlers/anthropic.py` — Add L1+L2+L3 call sites (after tool assembly, before PRE_SEND) - `headroom/proxy/handlers/openai.py` — Add L1+L2+L3 call sites (parallel to Anthropic handler) - `tests/test_tool_schema_compaction.py` — 42 unit tests covering edge cases, nested schemas, fail-safe behavior - `tests/test_system_compaction.py` — Tests for L3 compression, cache_control preservation, min-chars gating ## 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_tool_schema_compaction.py tests/test_system_compaction.py tests/test_anthropic_compaction_transforms.py -v ===== 49 passed in 4.96s ===== $ uv run ruff check <changed files> All checks passed! $ uv run mypy headroom/proxy/tool_schema_compaction.py headroom/proxy/system_compaction.py headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/openai.py Success: no issues found in 4 source files # Manual verification with HEADROOM_TOOL_DESC_MAX_CHARS=120 # Single tool schema: 548→434 bytes (L1, 20.8% saved) → 315 bytes (L2, 27.4% saved) # Combined: 548→315, 42.5% saved # Full request with proxy: orig=39179 opt=31988 saved=7191 (18.4% compression) ``` ## Real Behavior Proof - Environment: macOS, Python 3.13, headroom proxy v0.28.0, Claude Code CLI - Exact command / steps: 1. Start proxy with `HEADROOM_TOOL_DESC_MAX_CHARS=120 HEADROOM_SYSTEM_COMPACT=1 headroom proxy` 2. Route Claude Code traffic through proxy 3. Check `/stats` endpoint for `transforms_applied` and byte savings - Observed result: L1/L2/L3 transforms applied correctly, ~58% token reduction on MCP-heavy context - Not tested: Windows, production deployment ## 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 - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes - L2 and L3 are **opt-in** via env vars. Default behavior is unchanged (only L1 active). - All layers have fail-safe fallbacks — if compaction fails or doesn't reduce size, the original payload passes through unchanged. - The Anthropic handler now appends `anthropic:tool_schema_compaction` (L1), `anthropic:tool_desc_compaction` (L2), and `anthropic:system_compact` (L3) to `transforms_applied`, so `/stats` and transformation accounting are no longer blind to compression that changed the request. Covered by handler-level e2e regression in `tests/test_anthropic_compaction_transforms.py` (positive + negative cases). The earlier follow-up #1423 is superseded — no longer needed. --------- Signed-off-by: lg320531124 <lg320531124@users.noreply.github.com> Co-authored-by: lg320531124 <lg320531124@users.noreply.github.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
4035c04187
|
feat(text-crusher): CJK-aware segmentation + relevance via ICU (#1504)
## Description `TextCrusher` (the native extractive prose compressor added in #1171) only handled ASCII: `split_segments` split on `.!?`+whitespace and `tokens` split on whitespace/alphanumeric runs. CJK (Chinese/Japanese/Korean) has neither spaces nor ASCII terminators, so a whole CJK paragraph collapsed into **one segment / one token** — it passed through at ~0% compression, and BM25 relevance + salience scored zero terms. This makes `TextCrusher` CJK-aware. CJK-bearing content takes an ICU (`icu_segmenter`, UAX#29 sentence + dictionary word) segmentation path, with a length fallback for terminator-sparse runs, a local BM25 over the ICU word tokens, and ICU-token salience. Dispatch is on **content only**, so pure-ASCII text is byte-identical to before — the shared `BM25Scorer` and the ASCII path are untouched. It also adds a committed, reproducible answer-retention eval (`benchmarks/i18n_compression_eval.py`) with a deterministic zh/ja/ko CI regression gate, so the improvement below is permanently verifiable rather than a one-off measurement. Extends #1171. ## Type of Change - [x] Bug fix (CJK passed through near-uncompressed) - [x] New feature (CJK segmentation / relevance support) - [x] Performance improvement (CJK now compresses; ICU segmenters cached, not rebuilt per call) ## Changes Made - `is_cjk` predicate gates a CJK path (ideographs, kana, Hangul, CJK punctuation, full/half-width forms). - `split_segments` → ICU `SentenceSegmenter` for CJK + a mandatory length fallback (whitespace / CJK punctuation / hard cap) for terminator-sparse runs; ASCII path unchanged. - `tokens` → ICU `WordSegmenter` (dictionary) for CJK; ASCII path unchanged. - `relevance_cjk`: a local BM25 over ICU word tokens — the shared ASCII `BM25Scorer` scores zero terms for CJK and is parity-locked, so this is an intentional separate scorer (documented in code). - CJK salience uses ICU tokens (whitespace-split gave one giant "word" → zero salience). - `count_tokens`: CJK-aware so `compression_ratio` isn't nonsense for space-free text. - ICU segmenters resolved once in `static LazyLock` (compiled_data is static) instead of rebuilt per call. - New dep `icu_segmenter` 2.2, `compiled_data` only (see Dependency below). - `benchmarks/i18n_compression_eval.py` + `tests/test_transforms/test_text_crusher_cjk_eval.py`: a zh/ja/ko answer-retention eval — a deterministic needle CI gate (always-runs, no external data), real-transcript fidelity with CJK-aware salient, and optional `multi-wiki-qa` natural-data retention (loaded via the `[evals]` `datasets` extra, skipped if absent; data never vendored — CC-BY-NC-SA). ## Testing - [x] Unit tests pass (`pytest` + `cargo test`) - [x] Linting passes (`ruff check`/`format` on the new eval + test — clean) - [ ] Type checking passes (`mypy headroom`) — N/A, the only Python added is a benchmark + test, not `headroom/` source - [x] New tests added for new functionality - [x] Manual testing performed (see Real Behavior Proof) ### Test Output ```text $ cargo test -p headroom-core --lib text_crusher running 12 tests test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 841 filtered out $ .venv/bin/python -m pytest tests/test_transforms/test_text_crusher*.py 15 passed $ .venv/bin/python -m pytest tests/test_transforms/test_text_crusher_cjk_eval.py 6 passed # deterministic zh/ja/ko needle CI gate $ cargo clippy -p headroom-core && ruff check benchmarks/i18n_compression_eval.py # both clean ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.3.0), Python in a uv venv, `headroom-core` built via `uv pip install -e .` (maturin), branch `feat/cjk-text-compression`. - Exact command / steps: built `_core`, then ran a mixed Chinese+Japanese doc (no spaces, `。` terminators) through `TextCrusher().compress(doc, "认证令牌缓存策略", 0.3)`; separately evaluated answer-retention on the public CMRC2018 Chinese QA dev set (bury the gold-answer paragraph among 25 distractors, query = the question, compress to 30%, check the gold answer survives), and end-to-end through `ContentRouter`. - Observed result: a mixed Chinese+Japanese doc compressed 189 → 78 tokens (ratio 0.41, kept 3/8 segments) with the query-relevant sentence surviving — before this change the same doc was a single segment → 100% passthrough. On the public CMRC2018 Chinese QA dev set, answer-retention under 30% compression rose 34% → 93% (multiple seeds). End-to-end through `ContentRouter` on real CJK content, aggregate savings rose 16% → 40%. Pure-ASCII (English) output stayed byte-identical (the English parity fixtures did not move). Demo terminal output: ```text ORIGINAL tokens= 189 chars=189 COMPRESS tokens= 78 ratio=0.41 segments kept 3/8 QUERY-RELEVANT sentence survived: True --- compressed output (verbatim kept CJK sentences) --- 认证令牌的缓存策略采用最近最少使用淘汰算法来管理过期条目。 请求重试使用指数退避并设置最大次数上限。 数据备份每天凌晨执行并保留最近三十天的快照。 ``` The committed eval now demonstrates this across all three CJK languages. The deterministic needle gate (in CI via `tests/test_transforms/test_text_crusher_cjk_eval.py`, 6 passed) has TextCrusher keep the query-relevant needle while truncate/random drop it in zh, ja, and ko. On real `multi-wiki-qa` natural data (n=80/lang), query-aware answer-retention is **zh 74% / ja 70% / ko 50%** vs **25–41%** for the truncate/random baselines: ```text === Part A: multi-wiki-qa answer-retention (n=80/lang, target_ratio=0.3) === lang text_crusher truncate random zh-cn 74% 25% 38% ja 70% 31% 39% ko 50% 26% 41% ``` Korean is measurably weaker (ICU has no Korean dictionary and falls back to UAX#29 word-breaking) — still well above baselines, and scoped as a follow-up. - Not tested: the live proxy HTTP path (validated at the `ContentRouter` / `TextCrusher` layer, not via a running proxy); no-space Korean (standard Korean is space-delimited and is covered); non-CJK SE-Asian scripts (out of scope). ## Dependency (per CONTRIBUTING supply-chain policy) `icu_segmenter` 2.2 (ICU4X), `features = ["compiled_data"]`: - **Why this package (vs. ourselves / existing deps):** CJK needs dictionary/UAX#29 segmentation. A hand-rolled char-bigram scored slightly worse on real data (CMRC2018 answer-retention: 92.5% ICU vs 91% bigram, 4 seeds); jieba/lindera are ZH-only or 13–207 MB dicts. ICU4X covers zh/ja/ko in one crate. The existing `unicode-segmentation` does UAX#29 only (no CJK dictionary), so it can't word-segment space-free CJK. - **Who maintains it:** the official `unicode-org` ICU4X project; active release cadence (2.2 in 2025); no known CVEs. - **Install surface:** ~13 new pure-Rust crates, no build scripts, no native code, no build/runtime network. `compiled_data` bundles locale data at compile time (hermetic). `auto`/`lstm` deliberately NOT enabled — LSTM covers SE-Asian scripts (Thai/Lao), not CJK, and would pull in `libm` for nothing. - **Why this version:** 2.x is the stabilized ICU4X API (1.x used a different data-provider model); floored at 2.2 (Cargo.lock pins the patch) since segmenter boundaries are observable in output and bumps should be deliberate. ## 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 - [x] I have made corresponding changes to the documentation (CHANGELOG) - [x] My changes generate no new warnings (clippy + fmt clean) - [x] I have added tests that prove my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md ## Additional Notes - **Parity:** the shared `BM25Scorer` (byte-exact parity-locked with `headroom/relevance/bm25.py`) is untouched. `relevance_cjk` is a separate local scorer because the shared one's tokenizer is ASCII-only. The whole CJK path lives in Rust (`text_crusher.py` is a thin wrapper over `_core`), so there is no Python mirror to keep in sync; the parity fixtures stay green (only the CJK `unicode` fixture was re-recorded, intentionally; English fixtures unchanged). - **Known by-design gap (not a bug):** CJK content + a pure-ASCII query yields no token overlap, so relevance falls back to recency + salience (cross-script query matching is unsupported). - The Python added is a benchmark (`benchmarks/i18n_compression_eval.py`) plus its test, not `headroom/` runtime source — both are `ruff`-clean; `mypy headroom` is unaffected. - **License:** the optional Part A pulls `alexandrainst/multi-wiki-qa` (CC-BY-NC-SA-4.0) at run time via the `[evals]` extra and is skipped if absent — the dataset is never vendored into the repo, and the always-run CI gate (Part C) uses only our own deterministic data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3757a7cef3
|
fix(core): avoid unidiff panic on bash xtrace (#1506)
## Summary
- preflight unified diff inputs before calling the Rust unidiff parser
- catch parser panics so malformed-but-diff-looking input falls back to
non-diff
- add regressions for Bash xtrace lines like `+++ test.sh` and `+++
dirname test.sh`
## Repro
`detect_content_type("+++ test.sh")` could panic through the Rust
detector because unidiff treated the lone `+++` line as a target header
without a preceding source header.
## Tests
- `cargo fmt --all --check`
- `cargo test -p headroom-core --lib
transforms::unidiff_detector::tests`
- `cargo test -p headroom-core --lib`
Co-authored-by: Michael Stattmann <mstattma@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
05932d7165
|
fix(proxy): compress OpenCode tool schemas and embedded JSON (#1535)
## Description
Fixes two remaining OpenCode/OpenAI Chat compression gaps after `main`
incorporated the original savings-profile threading and user
content-block work from this PR.
OpenCode requests can still report very low savings when most input
tokens live in verbose `tools` schemas rather than messages. They can
also route poorly when a short instruction wraps a valid JSON block but
does not satisfy the existing long-prose heuristic.
Closes #1534
## 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 causes existing functionality
to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Compact OpenAI Chat Completions `tools` schemas whenever request
compression is active, reusing the existing OpenAI Responses schema
compactor. The outbound tool invocation shape is preserved while
non-semantic annotations such as `$schema`, `title`, and `examples` are
removed.
- Include the tool-schema token delta in Headroom's savings accounting
and expose `openai:chat:tool_schema_compaction` in the applied
transforms.
- Detect valid JSON blocks surrounded by prose or log text as mixed
content, so short OpenCode instructions route through mixed/SmartCrusher
handling instead of falling through or producing a no-op.
- Adapt the mixed-content change to the new
`headroom.transforms.mixed_content` module introduced on `main` by
#1939.
## Why the Focus Changed
The original headline fix—threading savings-profile kwargs into
`/v1/chat/completions`—is now already present on `main`, as is the user
content-block opt-in behavior. Those duplicate changes were removed
during the merge.
The branch also no longer changes developer/system role protection or
forced-Kompress semantics. It follows `main` for both, so the earlier
instruction-role safety concern is outside the current diff.
The resulting PR is limited to two OpenCode-specific compression gaps
that remain reproducible on current `main`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check` on changed files)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for the fixed behavior
- [ ] Manual live-upstream testing performed after the latest rebase
### Test Output
```text
59 passed, 1 warning in 83.53s
All checks passed! # ruff check
4 files already formatted # ruff format --check
python -m py_compile: passed
git diff --check: passed
```
Focused test coverage includes:
- OpenAI Chat tool-schema compaction, transform reporting, outbound
schema shape, and positive token savings.
- Embedded JSON mixed-content detection, SmartCrusher routing, positive
savings, and preservation of a critical sentinel value.
- Current `main` regressions for savings-profile threading, user content
blocks, turn hooks, and forced-Kompress behavior.
## Real Behavior Proof
- Environment: Linux ARM64, Python 3.13.12, current `main` at `
|
||
|
|
942e916368
|
feat(cli): add headroom inspect to view original vs compressed content (#1595)
## Description
Headroom exposes plenty of *quantitative* compression telemetry (token
counts, ratios, `headroom perf`, `/metrics`) but no way to actually
**see what the compressor changed** in the content. That makes it hard
to trust compression or debug a quality regression ("did it drop
something I cared about?").
This adds a `headroom inspect` command (the issue's Option 1). It reads
the proxy's existing loopback `/transformations/feed` endpoint — which
already carries the pre/post-compression message snapshots when the
proxy runs with `--log-messages` — and renders, per request, the
original vs compressed content for each message with the changed
segments highlighted. No new dependencies (stdlib `difflib`).
```
headroom inspect # inspect the most recent request
headroom inspect --last 5 # the 5 most recent
headroom inspect --full # include unchanged messages
headroom inspect --format json # raw feed for offline tooling
```
Per request it shows the model, per-request token counts + savings, the
transforms applied, and a colorized unified diff of each changed message
(red = removed, green = added). Clear errors when no proxy is reachable
or when the proxy wasn't started with `--log-messages`.
Side-by-side / interactive rendering (the fuller form of Option 1) can
follow as a polish pass; this lands the core "see what changed"
capability on data Headroom already captures.
Closes #1267
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
## Changes Made
- `headroom/cli/inspect.py`: new `inspect` command +
content-flattening/diff-render helpers.
- `headroom/cli/__init__.py`, `headroom/cli/main.py`: register the
command.
- `tests/test_cli_inspect.py`: unit tests (content extraction, the
no-proxy / no-`--log-messages` / empty-feed paths, text render, json
output).
- `wiki/cli.md`: document the command + options.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] New tests added
### Test Output
```text
$ pytest tests/test_cli_inspect.py -q
7 passed
$ ruff check headroom/cli/ tests/test_cli_inspect.py
All checks passed!
```
## Real Behavior Proof
- Environment: repo main @ HEAD, local venv
- Exact command / steps: invoked the `inspect` command against a mocked
`/transformations/feed` payload (one request, a user message with a line
removed by SmartCrusher).
- Observed result: header shows `req-1 gpt-4o`, `tokens 100 → 40 (saved
60, 60.0%)`, `transforms: SmartCrusher`, and a unified diff with the
removed line on the original side; no-proxy and missing-`--log-messages`
cases raise actionable errors; `--format json` emits the raw feed.
- Not tested: live end-to-end against a real proxy with `--log-messages`
(the data source — the feed endpoint — is exercised via the mocked
payload that mirrors its shape).
## 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 made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove the feature works
|
||
|
|
4cbd5da673
|
feat(proxy): opt-in compression for catch-all passthrough routes (#1699)
## Description Requests whose path doesn't match a built-in API route fall through to `handle_passthrough`, which forwarded the body verbatim — bypassing ContentRouter/Kompress/TOIN entirely. Wrapper-proxy setups that front Headroom on custom paths (e.g. `/api/codex-proxy/<key>/v1/responses`) got zero compression on coding-agent traffic and hit context-limit 400s in long sessions. This adds an opt-in flag that routes OpenAI Responses-shaped passthrough bodies through the same compression path the native `/v1/responses` handler uses. Closes #1546 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] 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 - Added `ProxyConfig.compress_passthrough` (default `False`) + `--compress-passthrough` CLI flag + `HEADROOM_COMPRESS_PASSTHROUGH=1` env. - `handle_passthrough`: when enabled, POST requests whose path ends in `/responses` with an OpenAI Responses-shaped body are compressed via the existing `_compress_openai_responses_payload_in_executor` before forwarding; stale `Content-Length` is dropped so httpx recomputes it. - New `_maybe_compress_passthrough_responses` helper — fail-open: non-JSON, non-Responses payloads, unmodified results, and any compressor error forward the original body unchanged. - Documented the flag in `docs/content/docs/proxy.mdx`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/python -m pytest tests/test_compress_passthrough.py -q collected 6 items tests/test_compress_passthrough.py ...... [100%] ============================== 6 passed in 0.35s =============================== $ .venv/bin/ruff check headroom/proxy/handlers/openai.py headroom/proxy/models.py headroom/proxy/server.py tests/test_compress_passthrough.py All checks passed! ``` ## Real Behavior Proof - Environment: macOS arm64, Python 3.14, repo `.venv`. - Exact command / steps: `.venv/bin/python -m pytest tests/test_compress_passthrough.py -q` — covers a Responses-shaped body being compressed, non-JSON passthrough, non-Responses (`messages`) payload untouched, unmodified-result short-circuit, compressor-error fail-open, and `ProxyConfig().compress_passthrough is False` default. Plus import smoke: `ProxyConfig(compress_passthrough=True)`, server/handler modules import, helper present. - Observed result: 6 passed; flag defaults off; enabled path reuses the native Responses compressor and never raises out to the request. - Not tested: live end-to-end through a real second proxy to a real upstream (no external wrapper proxy / upstream credentials in sandbox); the compression call is the same one `/v1/responses` already exercises in CI. ## 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 - [x] 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 - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes Scoped to OpenAI Responses-shaped bodies (the reporter's exact case). Anthropic `/messages` and OpenAI `/chat/completions` passthrough compression are natural follow-ups — deliberately left out to keep this change focused and fail-safe. CHANGELOG is release-managed, left unchecked. |
||
|
|
6469fcd018
|
feat(transforms): language-aware stack-trace collapse for Go/Rust/.NET/Java/Node (#1791)
## Description
Stack-trace handling covered only Python tracebacks and a generic ` at
symbol(` pattern. Go panics and Rust panics flowed to prose compression;
.NET traces were unrecognized; Java chained exceptions split into
separate traces at every `Caused by:` (so later chain heads fell off the
`max_stack_traces` cliff); and oversized traces were blindly
head-truncated — keeping runtime scheduler noise while dropping the app
frames and chain heads an agent actually needs.
This PR adds language-aware trace flavors (Go, Rust, .NET, Java chains,
Node async) to the Rust core and both Python mirrors, and replaces blind
truncation with a runtime-frame collapse: message lines, chain heads,
the trace head, and app-code frames survive; contiguous runtime/stdlib
frames fold into `[... N frames collapsed]` markers. A 147-line Go panic
dump compresses to 19 lines with the panic message, signal line, and app
frame intact.
Note one intentional behavior change: now that Go/Rust panics are
*detected*, panics ≤8KB in tool outputs gain the existing error-output
protection (`protect_error_outputs`) they previously missed — small
panics stay verbatim, exactly like small Python tracebacks already do.
## Type of Change
- [x] New feature (non-breaking change which adds functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring
## Changes Made
- `crates/headroom-core/src/transforms/log_compressor.rs`:
- New `TraceFlavor::GoPanic` (`panic:` / `fatal error:` / `goroutine N
[state]:` openers, tab-indented `.go:` frame lines, `created by` /
call-line continuation, blank-separated goroutine blocks) and
`TraceFlavor::DotNet` (`Unhandled exception.`, `at … ) in file:line N`
frames — checked before Java since those frames also satisfy the Java
shape — plus `--->` inner-exception heads and `--- End of` separators).
- Renamed the misnamed `Go` flavor to `RustBacktrace` (its `is_go_frame`
matched `N: 0x<hex>` — the Rust backtrace shape) and gave it real
openers (`thread '…' panicked at`, `stack backtrace:`); the free-text
panic-message line after the opener stays in the trace (`terminates` now
receives `lines_so_far`).
- Java: continues across `Caused by:` / `Suppressed:` / `... N more`,
and `is_java_at_frame` admits `/` so JPMS module frames (`at
java.base/…`) pass the opener re-check — without this, modern JDK traces
fragmented at the parse cap into ≤20-line groups.
- Frame collapse (`collapse_trace_frames`): for traces over
`stack_trace_max_lines`, keeps message/chain-head lines, first
`trace_head_frames` frames, up to `trace_app_frames` app frames; runtime
frames (prefix + path marker tables per language) fold into `[... N
frames collapsed]` markers that occupy the run's first line slot and
carry score 0.8 so the global cap doesn't drop them first. Collapsed
frame indices are excluded from the context-line pass (otherwise ±3
context re-added them), and the parse cap re-opens on continuation lines
so selection sees one contiguous trace. New config:
`collapse_runtime_frames=true`, `trace_head_frames=3`,
`trace_app_frames=5`; new sidecar stat `runtime_frames_collapsed`.
- `crates/headroom-py/src/lib.rs`: the three new knobs on the
`LogCompressorConfig` PyO3 signature.
- `headroom/transforms/log_compressor.py`: dataclass fields +
constructor pass-through; `_parse_lines` opener patterns mirrored per
the documented contract.
- `headroom/transforms/content_detector.py`: `_LOG_PATTERNS` additions
(Go panic/goroutine/frame lines, Rust panic/backtrace/numbered frames,
.NET, Java chain heads, Node `at async`); JS/Java `at` pattern admits
JPMS module paths.
- `tests/test_transforms_stack_traces.py` (new, 10 tests) + 7 new Rust
unit tests (flavor open/continue/terminate, chain grouping, collapse
keeps chain heads/app frames, collapse-off comparison, small traces
untouched).
## Testing
- [x] Added new tests for the changes
- [x] All existing tests pass
### Test Output
```
$ cargo test -p headroom-core
928 passed; 3 ignored
$ python -m pytest tests/test_transforms_stack_traces.py tests/test_log_compressor.py \
tests/test_transforms_log_compressor.py tests/test_transforms_content_detection.py \
tests/test_transforms_content_router.py tests/test_transforms/test_content_router.py \
tests/test_lossless_mode.py tests/test_compression_fidelity_regression.py -q
191 passed
```
## Real Behavior Proof
- Environment: macOS arm64, Python 3.13, repo main @
|
||
|
|
737b332129
|
feat(config): fold TOML array-of-tables to csv-schema via SmartCrusher (#1799)
## Description Adds a schema-fold tier to the structured-config compressor (introduced in #1784). TOML files containing an `[[array-of-tables]]` are parsed with the stdlib `tomllib` reference parser and bridged to SmartCrusher's lossless `csv-schema` renderer, which folds the repeated per-record keys into a single schema over the rows. On lockfiles and override-lists — where the repeated keys dominate the byte count — this is a large win. **Stacked on #1784 — review only the top commit** (`feat(config): fold TOML array-of-tables to csv-schema via SmartCrusher`). The base commit is #1784's config-compressor PR; this PR will collapse to the single new commit once #1784 merges. Faithfulness is guaranteed by construction, not by a heuristic: - `tomllib` is the reference TOML parser, so the extracted records are ground-truth. - `csv-schema` is a lossless JSON renderer (`smart_crusher.py` documents it as such), so the model reads a faithful, reformatted view of the exact parsed data. - Byte-exact recovery rides the existing CCR path — the original is persisted to the `CompressionStore` and a `Retrieve original: hash=…` marker is emitted. The fold is only emitted when that store write succeeds, so nothing is ever unrecoverable. Scope is deliberately **TOML-only**: `tomllib` is stdlib, whereas PyYAML is only a *transitive* dependency (not declared in `pyproject.toml`), and INI record-sections would need a bespoke dict-of-dicts→records transform. Those flavors can follow in a separate PR with an explicit dependency decision. ## Type of Change - [x] New feature (non-breaking change which adds functionality) ## Changes Made - `headroom/transforms/config_compressor.py`: added Tier 3 (`_schema_fold`) — TOML `[[array-of-tables]]` → `tomllib` → JSON → `SmartCrusher(csv-schema)`. New `enable_schema_fold` config flag (default on; auto-off in lossless mode since it rides `enable_ccr`). The fold competes with the reversible text tiers and is adopted only when strictly smaller. Added `_load_toml` (stdlib parser with tomli backport) and `_json_default` (TOML date/time → ISO; bail on any other non-serializable value). - Recovery reuses the existing `CompressionStore` + `Retrieve original: hash=` marker; no new CCR plumbing. - `tests/test_transforms_config_compressor.py`: 14 new tests covering the fold, big-win assertion, byte-exact CCR round-trip, lossless-mode disable, flag-off, non-TOML skip, no-array skip, small-array `passthrough` decline, store-failure fallback, savings-floor rejection, unparseable/non-serializable bails, `_load_toml`/`_json_default` units, and a datetime-valued fold. ## Testing - [x] New and existing unit tests pass locally - [x] New tests added for the new behavior ### Test Output ``` tests/test_transforms_config_compressor.py ............................. [ 59%] headroom/transforms/config_compressor.py 127 0 36 0 100% ============================== 49 passed in 0.61s ============================== ``` Must-stay-green suites (`test_lossless_mode`, `test_lossless_excluded_compaction`, `test_transforms_content_detection`, `test_compression_fidelity_regression`) — 48 passed. Router/tabular/smart_crusher regression — 73 + 82 passed. `mypy --strict` clean on the changed module. ## Real Behavior Proof - Environment: local, Python 3.11.0, macOS (darwin), `HF_HUB_OFFLINE=1` - Exact command / steps: parsed a 25-record `[[tool.mypy.overrides]]` TOML through `ConfigCompressor(ConfigCompressorConfig(enable_ccr=True)).compress()`, then retrieved the CCR hash from the `CompressionStore`. - Observed result: `strategy=config_schema_fold`, 2765 → 840 chars (30% of original); the marker hash resolved to the byte-exact original (`recovered == original` True); with `enable_ccr=False` (lossless mode) the fold did not run and no marker was emitted; a 3-record long-valued `[[package]]` array correctly declined (SmartCrusher `passthrough`). - Not tested: the live proxy end-to-end path and non-TOML flavors (YAML/INI schema folding is intentionally out of scope for this PR). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |