Commit graph

2630 commits

Author SHA1 Message Date
Tejas Chopra
2990b7457d
chore: sync version state to released 0.30.0 to unblock release-please (#1916)
## Description

**Fixes the Release Please pipeline**, which stopped opening the release
PR, leaving pip / Docker / npm out of sync.

### Root cause
Recent releases (0.28 → 0.30) were cut **out-of-band** (manual tag +
release), so Release Please's state drifted from reality:
- `.release-please-manifest.json` was frozen at **0.29.0**, and the
in-repo version files at **0.29.0** — even the `v0.30.0` tag commit has
`pyproject.toml = 0.29.0`. The plugin/marketplace manifests were frozen
even further back, at **0.22.3**.
- `v0.30.0` was tagged and published to PyPI (CI stamps the version from
the tag at build time via `version-sync.py`, which is why PyPI got
0.30.0 despite the committed 0.29.0).
- With the manifest at 0.29.0, RP kept computing the next version as
**0.30.0**, saw that tag already exists, and produced **no PR** — so
nothing new could ship, and Docker/npm fell behind.

### Fix
Realign the repo with the last real release (0.30.0) so RP can drive the
next one:
- `.release-please-manifest.json` → `0.30.0`
- `pyproject.toml`, `sdk/typescript/package.json`,
`plugins/openclaw/package.json`, both `plugin.json`, both
`marketplace.json` → `0.30.0` (via `scripts/version-sync.py --version
0.30.0`, which also fixes the 0.22.3 drift).

### What happens after merge
1. Release Please runs on `main`, sees manifest = 0.30.0 + 69 releasable
commits since `v0.30.0`, and opens a clean **`chore: release 0.31.0`**
PR (bumping every version file).
2. Merging that PR tags `v0.31.0` and fires `release: published`, which
publishes **PyPI + npm (SDK + openclaw) + Docker** at 0.31.0 in one shot
— bringing all three registries back in sync.

No behavior/code change — versions only.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Advance the Release Please manifest to the last released version
(0.30.0).
- Sync all 7 version-tracked files to 0.30.0 with the repo's own
`version-sync.py`.

## Testing

- [x] Linting passes (`ruff check .`)
- [x] Manual testing performed (`scripts/verify-versions.py`)

### Test Output

```text
$ python scripts/version-sync.py --version 0.30.0
Version synchronized to 0.30.0

$ python scripts/verify-versions.py
All versions aligned at 0.30.0
Packages: pyproject.toml, plugins/openclaw/package.json, sdk/typescript/package.json,
  plugins/headroom-agent-hooks/.claude-plugin/plugin.json,
  plugins/headroom-agent-hooks/.github/plugin/plugin.json,
  .claude-plugin/marketplace.json, .github/plugin/marketplace.json
```

## Real Behavior Proof

- Environment: local macOS, project `.venv` (Python 3.12.6).
- Exact command / steps: ran `scripts/version-sync.py --version 0.30.0`,
set the RP manifest to 0.30.0, then `scripts/verify-versions.py`.
- Observed result: `verify-versions.py` reports all seven version
locations aligned at 0.30.0; `git diff` shows version-field changes only
(no code). Confirmed PyPI latest is 0.30.0 and a `v0.30.0` tag/release
exists, while the manifest was 0.29.0 — the drift this PR corrects.
- Not tested: the downstream release itself (that runs when the
follow-up `chore: release 0.31.0` PR is merged); npm registry state
could not be read from this environment (network), but the `publish-npm`
job in `release.yml` publishes both npm packages on release.

## 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] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
2026-07-09 07:08:06 -07:00
Rod Boev
62cd3072a2
feat(ccr): wire retrieve-tool interception into OpenAI Responses handler (#1898)
## Description

Refs #1877. `handle_openai_responses` (the `/v1/responses` HTTP handler)
had zero CCR / `headroom_retrieve` wiring, so a retrieve `function_call`
in a Responses API reply passed straight through to the client instead
of being resolved server-side, unlike the parallel chat-completions
backend path (`handle_openai_chat`, ~2775-2848), which already
intercepts `headroom_retrieve` tool calls via
`ccr_response_handler.has_ccr_tool_calls()` / `handle_response()`.

This PR is scoped to the core interception gap only. The issue's
proposals A (egress scrubber) and B/C (event-level SSE parsing/splicing
for true mid-stream interception) are out of scope here; the streaming
case is instead handled by forcing a buffered (non-streaming) upstream
call when `headroom_retrieve` is offered, matching the existing
buffered-CCR pattern in the Anthropic handler.

## 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

- `headroom/ccr/response_handler.py`: added an `"openai_responses"`
provider branch to `CCRResponseHandler`; `_extract_tool_calls` reads
flat `function_call` items from the top-level `output[]` array,
tool-call IDs key off `call_id`, and `_extract_assistant_message` /
`_create_tool_result_message` return sentinel-keyed item lists that
`handle_response()` extends into the running item history.
- `headroom/ccr/tool_injection.py`: added a `parse_tool_call` branch for
`"openai_responses"` where name and arguments are flat on the item.
- `headroom/proxy/handlers/openai.py`: detects non-streaming
`headroom_retrieve` function calls and runs
`ccr_response_handler.handle_response()` with a stateless continuation
that resends the full `input[]` item history.
- `headroom/proxy/handlers/openai.py`: forces `stream:true` requests
with `headroom_retrieve` available through a buffered `stream:false`
upstream call, resolves retrieval server-side, then reconstructs a
minimal Responses SSE stream for the client.
- `headroom/proxy/handlers/openai.py`: treats `ccr_response_handler` as
optional on `OpenAIHandlerMixin` consumers, so handlers without CCR
support keep the existing Codex routing, streaming, header stripping,
memory timeout, and compression fail-open behavior.
- Non-CCR streaming requests are unaffected; they still go through
`_stream_response()` as before.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_ccr_response_handler_openai_responses.py -q`)
- [x] Integration tests pass (`uv run pytest
tests/test_proxy/test_openai_responses_ccr.py -q`)
- [x] Regression tests pass (`uv run pytest
tests/test_openai_codex_routing.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/proxy/handlers/openai.py tests/test_openai_codex_routing.py
tests/test_proxy/test_openai_responses_ccr.py
tests/test_ccr_response_handler_openai_responses.py`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
$ uv run pytest tests/test_openai_codex_routing.py -q
tests\test_openai_codex_routing.py ....................                  [100%]
20 passed in 0.51s

$ uv run pytest tests/test_proxy/test_openai_responses_ccr.py tests/test_ccr_response_handler_openai_responses.py -q
tests\test_proxy\test_openai_responses_ccr.py ....                       [ 25%]
tests\test_ccr_response_handler_openai_responses.py ............         [100%]
16 passed, 1 warning in 27.51s

$ uv run ruff check headroom/proxy/handlers/openai.py tests/test_openai_codex_routing.py tests/test_proxy/test_openai_responses_ccr.py tests/test_ccr_response_handler_openai_responses.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows, Python 3.12 via uv-managed venv, local PR
worktree on branch `pr/1877-ccr-responses-interception`, no live LLM
provider needed because the tests stub upstream HTTP and CCR
continuation behavior.
- Exact command / steps: Ran `uv run pytest
tests/test_openai_codex_routing.py -q` to reproduce the CI-failing Codex
routing surface after the optional-handler fix; ran `uv run pytest
tests/test_proxy/test_openai_responses_ccr.py
tests/test_ccr_response_handler_openai_responses.py -q` to cover the
positive Responses CCR interception path; ran targeted Ruff on the
touched handler and related tests.
- Observed result: Codex routing tests that previously failed with
`AttributeError: '_DummyOpenAIHandler' object has no attribute
'ccr_response_handler'` now pass; Responses CCR still detects and
resolves `headroom_retrieve` when a real proxy installs
`ccr_response_handler`; non-CCR streaming requests still route through
`_stream_response()`.
- Not tested: true event-level mid-stream Responses SSE splicing and
client-bound egress marker scrubbing are out of scope for this PR and
remain future work from issue #1877's broader proposals.

## 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

No documentation or changelog update was made because this is internal
proxy CCR behavior, not a user-facing command or configuration change.
2026-07-09 09:41:06 -04:00
Abhay Singh
3a33af1af3
fix(cli/proxy): preserve explicit HEADROOM_MIN_TOKENS=0 / MAX_ITEMS=0 (#1886)
## Description

The Click `proxy` command builds two `ProxyConfig` fields like this:

```python
min_tokens_to_crush=_get_env_int_optional("HEADROOM_MIN_TOKENS") or 500,
max_items_after_crush=_get_env_int_optional("HEADROOM_MAX_ITEMS") or 50,
```

`_get_env_int_optional` correctly returns `0` for
`HEADROOM_MIN_TOKENS=0`, but
the trailing `or 500` treats that legitimate `0` as falsy and replaces
it with
the default. `0` is a meaningful setting — `smart_crusher` gates on
`if tokens > self.config.min_tokens_to_crush`, so
`min_tokens_to_crush=0` means
"crush every item with any tokens." The user asking for `0` silently
gets `500`
instead (and `HEADROOM_MAX_ITEMS=0` → `50`).

This is provably unintended: the argparse `headroom proxy` path sets the
**same**
fields via `_get_env_int("HEADROOM_MIN_TOKENS", args.min_tokens)`, a
helper that
preserves `0` — so the two entry points disagree on the identical env
var. And
the adjacent
`protect_recent=_get_env_int_optional("HEADROOM_PROTECT_RECENT")`
line deliberately avoids `or`, showing the distinction was understood.

Closes: no issue filed — found while auditing env-var → config parsing.

## Fix

Add a `_get_env_int(name, default)` helper (mirroring
`headroom.proxy.server._get_env_int`)
that substitutes the default only when the var is unset/empty, and use
it for
both fields:

```python
def _get_env_int(name: str, default: int) -> int:
    value = _get_env_int_optional(name)
    return default if value is None else value
...
min_tokens_to_crush=_get_env_int("HEADROOM_MIN_TOKENS", 500),
max_items_after_crush=_get_env_int("HEADROOM_MAX_ITEMS", 50),
```

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/cli/proxy.py`: add `_get_env_int(name, default)` and use it
for `min_tokens_to_crush` / `max_items_after_crush` instead of `... or
<default>`.
- `tests/test_cli_proxy_env.py`: regression test asserting
`HEADROOM_MIN_TOKENS=0` / `HEADROOM_MAX_ITEMS=0` reach `ProxyConfig` as
`0`.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.

## Testing

- [x] New regression test added (`tests/test_cli_proxy_env.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uv run ruff check headroom/cli/proxy.py tests/test_cli_proxy_env.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, headroom from this branch.
Importing `headroom` loads the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the helper logic
with a dependency-free script and left the full pytest to CI.
- Exact command / steps: replicated `_get_env_int_optional` + the new
`_get_env_int` in a standalone script (only stdlib) and ran the env
values `"0"`, `"120"`, unset, and empty through both the old `or 500`
expression and the new helper.
- Observed result: `"0"` now yields `0` (the old `or 500` gave `500`),
`"120"` → `120`, unset/empty → the default:

```text
OK: '0' -> 0 (old `or 500` gave 500)
OK: '120' -> 120
OK: unset -> 500 default
OK: empty -> 500 default
ENV-INT LOGIC VERIFIED
```

- Not tested: booting the full proxy with `HEADROOM_MIN_TOKENS=0`
end-to-end (needs the heavy stack); the value now flows through as `0`
and the regression test exercises the whole `proxy` command with
`run_server` mocked. Full local `pytest` deferred to CI (OOM, per
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
- [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 — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- No new dependencies; a small helper plus two call-site swaps and a
test.
- @JerrettDavis tagging you — tiny, contained parity fix with the
argparse path if you have a moment.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-09 09:40:39 -04:00
Rod Boev
bb112dd176
feat(compression): add audit-safe mode with protected pattern matching (#1899)
## Description

`SmartCrusher.crush_array_json` (`headroom/transforms/smart_crusher.py`)
selects rows to keep using statistical signals such as variance,
structural anomaly, and position. It has no concept of "this row is
audit/compliance-relevant and must stay visible in the prompt." A rare
row, such as a leakage flag, compliance marker, or non-standard failure
line, can be sampled out like any routine row, or moved behind an opaque
`<<ccr:HASH ...>>` retrieval marker the model has no reason to ask for.
In audit, SRE, and quant-falsification workloads, rare rows are
frequently the most important evidence, so silent disappearance is a
real safety issue rather than only a lossy-compression tradeoff.

This adds an opt-in `audit_safe` mode to `SmartCrusher`:

- `SmartCrusherConfig(audit_safe=True, protected_patterns=[...],
fail_closed_on_protected_loss=True)`
- Rows are scanned for pattern matches, string or regex, against each
row's canonical JSON text before compression runs.
- After compression, any protected row missing from the output is
spliced back in verbatim, whether it was dropped by the statistical
selector or left only behind a CCR marker.
- A verification pass re-counts protected-row survivors after splicing.
If the count is still short, the crusher fails closed and returns the
original, uncompressed content instead of shipping a result with fewer
protected matches than the input had. Setting
`fail_closed_on_protected_loss=False` ships the best-effort spliced
result with a logged warning instead.

Protection applies on both `crush_array_json`, the dict-shaped API used
by direct callers and the CCR retrieval flow, and
`_smart_crush_content`, the tuple-shaped API `apply()` actually calls
for every compressed tool/tool_result message. It is live on the real
tool-output compression path.

Scope: this covers JSON-array-shaped content routed through
`SmartCrusher`, the common case for tool outputs such as API results,
log lines, and DB rows returned as JSON. Raw CSV/plain-text content
compressed by other transforms, including Kompress and log/tabular
compressors, is out of scope for this PR; `protected_patterns` only has
row structure to match against when the content is or renders to a JSON
array.

Closes #1705

## 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

- `headroom/transforms/smart_crusher.py`: added `audit_safe`,
`protected_patterns`, and `fail_closed_on_protected_loss` fields to
`SmartCrusherConfig`; these stay Python-side and do not reach the Rust
config because this is post-processing around existing Rust-backed
compression.
- Added `_compile_protected_patterns`, `_canon`,
`_row_matches_protected`, `_scan_protected_rows`, and
`_splice_missing_protected` as the shared scan/match/splice primitives.
- Added `_apply_audit_safe_protection` for dict-shaped
`crush_array_json` results and `_apply_audit_safe_protection_to_content`
for tuple-shaped `_smart_crush_content` / `apply()` results. Both splice
missing protected rows back in, then verify and fail closed or warn on
residual loss.
- Wired both `crush_array_json` and `_smart_crush_content` to scan for
protected rows before compression and apply protection after.
- `CHANGELOG.md`: added an `Unreleased / Features` entry.
- Default `audit_safe=False`, so existing callers keep current behavior.
A regression test compares a configured-but-disabled crusher's output
byte-for-byte against an unconfigured one.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_transforms/test_smart_crusher_audit_safe.py`)
- [x] Linting passes (`uv run ruff check .`)
- [x] Type checking passes (`uv run mypy
headroom/transforms/smart_crusher.py`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
$ uv run pytest tests/ -k "(smart_crusher or crush or audit) and not test_optimizer_not_called_in_audit_mode" --no-header -q
...
tests\test_transforms\test_smart_crusher_audit_safe.py ...........                                               [ 65%]
...
169 passed, 10 skipped, 8176 deselected, 1 warning in 21.84s

$ uv run ruff check .
All checks passed!

$ uv run mypy headroom/transforms/smart_crusher.py
Success: no issues found in 1 source file
```

`-k` excludes `test_optimizer_not_called_in_audit_mode`
(`tests/test_cache/test_client_integration.py`), a pre-existing,
unrelated Windows temp-path failure in SQLite storage init that
reproduces identically on a clean `origin/main` checkout with none of
this PR's changes applied; it matched the `-k audit` filter by name
coincidence only.

## Real Behavior Proof

- Environment: Windows, Python 3.12 via uv-managed venv,
`headroom._core` built locally via `maturin` / cargo 1.95.0, no LLM
provider needed because this is pure transform-layer behavior.
- Exact command / steps: Ran `uv run pytest tests/ -k "(smart_crusher or
crush or audit) and not test_optimizer_not_called_in_audit_mode"
--no-header -q`, `uv run ruff check .`, and `uv run mypy
headroom/transforms/smart_crusher.py`; also exercised the audit-safe
tests that build a 62-row JSON array with two `AUDIT_FLAG` rows, run it
through `SmartCrusher(SmartCrusherConfig(audit_safe=True,
protected_patterns=["AUDIT_FLAG"]), with_compaction=False)` via both
`crush_array_json` and `Transform.apply()` over a synthetic tool
message, parse the compressed output back to JSON, and drive the
splice/verify/fail-closed helper paths with engineered row-drop and
forced-mismatch scenarios.
- Observed result: Protected rows are present in the compressed output
in every tested scenario; the fail-closed branch returns the original
content byte-for-byte with `strategy_info == "audit_safe:fail_closed"`
when verification detects residual loss; `audit_safe=False` produces
output byte-identical to a crusher with no audit-safe configuration.
- Not tested: Raw CSV/plain-text tool output compressed via
non-SmartCrusher transforms, including Kompress and log/tabular
compressors, is out of scope. Top-level `headroom.compress()` /
`CompressConfig` wiring for `audit_safe` and `protected_patterns` is a
natural follow-up and is not included here.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

No user-facing docs were updated because I did not find an existing
`SmartCrusherConfig` field reference doc to extend. The top-level
`compress()` / `CompressConfig` wiring mentioned in "Not tested" is a
reasonable immediate follow-up if this mechanism is the right shape.
2026-07-09 09:39:35 -04:00
Rod Boev
361adcd1a0
fix(dashboard): distinguish unavailable RTK from zero stats in Docker (#1901)
## Description

Dockerized Headroom shows `0` for RTK/context-tool dashboard figures
whenever the `rtk` binary isn't reachable inside the proxy's runtime —
indistinguishable from "genuinely nothing saved yet." The backend
already computes this distinction (an `installed`/`available` flag on
the context-tool stats payload) but it never reaches two of the JSON
surfaces the dashboard reads from, and the dashboard template never
checks the one surface that already has it. This PR threads that
existing availability flag through to both surfaces and updates the
dashboard to show a distinct "not installed" message instead of a bare
`0`, plus a short Docker note so operators know `rtk` needs to be
installed inside the container for those figures to populate at all.

Closes #1831

## 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/server.py`: reuse the existing context-tool
`installed` flag as one `available` boolean, add it to
`savings.by_layer.cli_filtering` in `/stats`, and add it to the curated
`cli_filtering` block in `/stats-history`; corrected that endpoint's
stale docstring claim that `cli_filtering` is `None` whenever RTK is
absent.
- `headroom/dashboard/templates/dashboard.html`: added
`cliFilteringAvailable`/`historyCliFilteringAvailable` getters and used
them to show a "not installed" message instead of `0` in the session
view's Token Usage panel and Token Savings breakdown, and to keep the
Historical tab's lifetime card hidden (its existing behavior) instead of
showing a stale zero.
- `docker-compose.yml` and `docker/docker-compose.native.yml`: added a
one-line comment noting that `rtk` needs to be installed inside the
container for CLI-filtering dashboard figures to populate.
- `docs/content/docs/docker-install.mdx`: added a note to the existing
Notes section about the same requirement.
- Added focused pytest coverage for the new JSON field on both endpoints
(installed, not-installed, and hard-failure cases) and a new Playwright
spec covering the rendered not-installed / genuine-zero / Historical-tab
states.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_proxy_dashboard_stats_cache.py
tests/test_proxy_savings_history.py -q`)
- [x] Linting passes (`uv run ruff check .`)
- [ ] Type checking passes (`uv run mypy headroom`) or explain N/A
truthfully
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_proxy_dashboard_stats_cache.py tests/test_proxy_savings_history.py -q
51 passed, 1 skipped, 1 failed

uv run ruff check headroom/proxy/server.py tests/test_proxy_dashboard_stats_cache.py tests/test_proxy_savings_history.py tests/test_dashboard_context_tool_availability_playwright.py
All checks passed!
```

The one failure (`test_savings_tracker_save_fsyncs_parent_directory`) is
pre-existing and unrelated to this change; it reproduces identically on
a clean `origin/main` checkout with this diff removed (Windows
filesystem fsync behavior).

## Real Behavior Proof

- Environment: Windows sandbox, Python (uv-managed), no live Docker
container
- Exact command / steps: `GET /stats` and `GET /stats-history` against a
`TestClient` app with the context-tool stats source monkeypatched to a
not-installed payload (mirrors the exact shape
`_context_tool_zero_payload` produces when `rtk` is absent), then the
same with an installed-but-zero payload
- Observed result: `savings.by_layer.cli_filtering.available` and
`/stats-history`'s `cli_filtering.available` are `False` for the
not-installed payload and `True` for the installed-but-zero payload,
matching the pre-existing `context_tool.available` field; the new
Playwright spec exercises the corresponding dashboard rendering states
and runs in CI's "Dashboard Playwright" check
- Not tested: real rendering in a live browser against a live Docker
container (this sandbox cannot run the CI-only Dashboard Playwright job
locally); the fix is proved locally at the JSON-contract level and the
rendering claim is proved by the contributed CI-executed Playwright spec

## 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
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

CHANGELOG.md was intentionally left unchanged — release automation
derives changelog entries from conventional commits per this repo's
convention, and this is a dashboard/docs clarity fix rather than a new
user-facing command or config option. Type checking was not re-run in
isolation for this change; it's covered by the repo's CI lint job.
2026-07-09 09:39:16 -04:00
dependabot[bot]
8872bbc6a2
deps: bump the npm-minor-patch group across 4 directories with 18 updates (#1907)
Bumps the npm-minor-patch group with 12 updates in the /docs directory:

| Package | From | To |
| --- | --- | --- |
| [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.10.3` |
`16.11.1` |
| [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.0.12` |
`15.1.0` |
| [fumadocs-twoslash](https://github.com/fuma-nama/fumadocs) | `3.1.15`
| `3.3.0` |
| [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.10.3` |
`16.11.1` |
| [next](https://github.com/vercel/next.js) | `16.2.6` | `16.2.10` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) |
`19.2.4` | `19.2.7` |
|
[@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react)
| `19.2.14` | `19.2.17` |
|
[react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom)
| `19.2.4` | `19.2.7` |
| [recharts](https://github.com/recharts/recharts) | `3.8.1` | `3.9.2` |
|
[@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss)
| `4.2.2` | `4.3.2` |
|
[@types/mdx](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mdx)
| `2.0.13` | `2.0.14` |
| [postcss](https://github.com/postcss/postcss) | `8.5.15` | `8.5.16` |

Bumps the npm-minor-patch group with 1 update in the /plugins/openclaw
directory:
[vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).
Bumps the npm-minor-patch group with 2 updates in the /plugins/opencode
directory:
[vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)
and @opencode-ai/plugin.
Bumps the npm-minor-patch group with 3 updates in the /sdk/typescript
directory:
[vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest),
[@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript)
and [dotenv](https://github.com/motdotla/dotenv).

Updates `fumadocs-core` from 16.10.3 to 16.11.1
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ab09f500cb"><code>ab09f50</code></a>
Version Packages (<a
href="https://redirect.github.com/fuma-nama/fumadocs/issues/3405">#3405</a>)</li>
<li><a
href="889a296d06"><code>889a296</code></a>
docs: update stale content</li>
<li><a
href="a5c081d0c6"><code>a5c081d</code></a>
fix: UI inconsistencies</li>
<li><a
href="9a269030df"><code>9a26903</code></a>
Version Packages</li>
<li><a
href="3e33f4362f"><code>3e33f43</code></a>
perf(satteri): reduce clones</li>
<li><a
href="3597c9d1e6"><code>3597c9d</code></a>
perf(satteri): persist results</li>
<li><a
href="0f389cf3de"><code>0f389cf</code></a>
feat(satteri): decouple imports/exports from <code>compile()</code></li>
<li><a
href="4611f97d49"><code>4611f97</code></a>
feat(satteri): full rehype-toc functionality</li>
<li><a
href="d095300760"><code>d095300</code></a>
fix(satteri): workaround common issues</li>
<li><a
href="0297e25477"><code>0297e25</code></a>
configure pretrust</li>
<li>Additional commits viewable in <a
href="https://github.com/fuma-nama/fumadocs/compare/fumadocs-core@16.10.3...fumadocs@16.11.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `fumadocs-mdx` from 15.0.12 to 15.1.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fuma-nama/fumadocs/releases">fumadocs-mdx's
releases</a>.</em></p>
<blockquote>
<h2>fumadocs-mdx@15.1.0</h2>
<h3>Default to Base UI</h3>
<p>Internal packages &amp; templates now use Base UI rather than Radix
UI.</p>
<h2>fumadocs-mdx@15.0.13</h2>
<h3>Require <code>collection</code> query param at regex matching</h3>
<p>Instead of passing through all JSON/YAML files, the meta loader now
requires <code>collection</code> query param to be triggered.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9a269030df"><code>9a26903</code></a>
Version Packages</li>
<li><a
href="3e33f4362f"><code>3e33f43</code></a>
perf(satteri): reduce clones</li>
<li><a
href="3597c9d1e6"><code>3597c9d</code></a>
perf(satteri): persist results</li>
<li><a
href="0f389cf3de"><code>0f389cf</code></a>
feat(satteri): decouple imports/exports from <code>compile()</code></li>
<li><a
href="4611f97d49"><code>4611f97</code></a>
feat(satteri): full rehype-toc functionality</li>
<li><a
href="d095300760"><code>d095300</code></a>
fix(satteri): workaround common issues</li>
<li><a
href="0297e25477"><code>0297e25</code></a>
configure pretrust</li>
<li><a
href="02c242b0da"><code>02c242b</code></a>
chore(satteri): clean code</li>
<li><a
href="3d80b8b242"><code>3d80b8b</code></a>
fix(mdx): ensure satteri integration is optional</li>
<li><a
href="0ec19af868"><code>0ec19af</code></a>
feat(satteri): more tests &amp; move remark-include</li>
<li>Additional commits viewable in <a
href="https://github.com/fuma-nama/fumadocs/compare/fumadocs-mdx@15.0.12...fumadocs-mdx@15.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `fumadocs-twoslash` from 3.1.15 to 3.3.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fuma-nama/fumadocs/releases">fumadocs-twoslash's
releases</a>.</em></p>
<blockquote>
<h2>fumadocs-twoslash@3.3.0</h2>
<h3>Default to Base UI</h3>
<p>Internal packages &amp; templates now use Base UI rather than Radix
UI.</p>
<h2>fumadocs-twoslash@3.2.1</h2>
<h3>Migrate to <code>cnfast</code></h3>
<p>Drop <code>tailwind-merge</code>.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9a269030df"><code>9a26903</code></a>
Version Packages</li>
<li><a
href="3e33f4362f"><code>3e33f43</code></a>
perf(satteri): reduce clones</li>
<li><a
href="3597c9d1e6"><code>3597c9d</code></a>
perf(satteri): persist results</li>
<li><a
href="0f389cf3de"><code>0f389cf</code></a>
feat(satteri): decouple imports/exports from <code>compile()</code></li>
<li><a
href="4611f97d49"><code>4611f97</code></a>
feat(satteri): full rehype-toc functionality</li>
<li><a
href="d095300760"><code>d095300</code></a>
fix(satteri): workaround common issues</li>
<li><a
href="0297e25477"><code>0297e25</code></a>
configure pretrust</li>
<li><a
href="02c242b0da"><code>02c242b</code></a>
chore(satteri): clean code</li>
<li><a
href="3d80b8b242"><code>3d80b8b</code></a>
fix(mdx): ensure satteri integration is optional</li>
<li><a
href="0ec19af868"><code>0ec19af</code></a>
feat(satteri): more tests &amp; move remark-include</li>
<li>Additional commits viewable in <a
href="https://github.com/fuma-nama/fumadocs/compare/fumadocs-twoslash@3.1.15...fumadocs-twoslash@3.3.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `fumadocs-ui` from 16.10.3 to 16.11.1
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ab09f500cb"><code>ab09f50</code></a>
Version Packages (<a
href="https://redirect.github.com/fuma-nama/fumadocs/issues/3405">#3405</a>)</li>
<li><a
href="889a296d06"><code>889a296</code></a>
docs: update stale content</li>
<li><a
href="a5c081d0c6"><code>a5c081d</code></a>
fix: UI inconsistencies</li>
<li><a
href="9a269030df"><code>9a26903</code></a>
Version Packages</li>
<li><a
href="3e33f4362f"><code>3e33f43</code></a>
perf(satteri): reduce clones</li>
<li><a
href="3597c9d1e6"><code>3597c9d</code></a>
perf(satteri): persist results</li>
<li><a
href="0f389cf3de"><code>0f389cf</code></a>
feat(satteri): decouple imports/exports from <code>compile()</code></li>
<li><a
href="4611f97d49"><code>4611f97</code></a>
feat(satteri): full rehype-toc functionality</li>
<li><a
href="d095300760"><code>d095300</code></a>
fix(satteri): workaround common issues</li>
<li><a
href="0297e25477"><code>0297e25</code></a>
configure pretrust</li>
<li>Additional commits viewable in <a
href="https://github.com/fuma-nama/fumadocs/compare/fumadocs-ui@16.10.3...fumadocs@16.11.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `lucide-react` from 1.20.0 to 1.23.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/lucide-icons/lucide/releases">lucide-react's
releases</a>.</em></p>
<blockquote>
<h2>Version 1.23.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix(docs): prevent scrollbar layout shift on icons page by <a
href="https://github.com/g30r93g"><code>@​g30r93g</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4500">lucide-icons/lucide#4500</a></li>
<li>chore(docs): Remove certificates banner by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4504">lucide-icons/lucide#4504</a></li>
<li>ci(repo-journal.yml): GH copilot repo summary by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4505">lucide-icons/lucide#4505</a></li>
<li>ci(repo-journal.yml): Small fix in the workflow by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4508">lucide-icons/lucide#4508</a></li>
<li>ci(repo-journal.yml): Switch to token by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4509">lucide-icons/lucide#4509</a></li>
<li>feat(icons): added <code>paper-bag</code> icon by <a
href="https://github.com/dkast"><code>@​dkast</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4023">lucide-icons/lucide#4023</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/g30r93g"><code>@​g30r93g</code></a> made
their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4500">lucide-icons/lucide#4500</a></li>
<li><a href="https://github.com/dkast"><code>@​dkast</code></a> made
their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4023">lucide-icons/lucide#4023</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lucide-icons/lucide/compare/1.22.0...1.23.0">https://github.com/lucide-icons/lucide/compare/1.22.0...1.23.0</a></p>
<h2>Version 1.22.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(icons): add 6 database variant icons by <a
href="https://github.com/Barakudum"><code>@​Barakudum</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4336">lucide-icons/lucide#4336</a></li>
<li>ci(release.yml): Remove concurrency field to prevent release mess by
<a href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in
<a
href="https://redirect.github.com/lucide-icons/lucide/pull/4485">lucide-icons/lucide#4485</a></li>
<li>fix(docs): fix color input clipping by <a
href="https://github.com/Hsiii"><code>@​Hsiii</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4488">lucide-icons/lucide#4488</a></li>
<li>docs(site): add Deno to installation instructions by <a
href="https://github.com/bartlomieju"><code>@​bartlomieju</code></a> in
<a
href="https://redirect.github.com/lucide-icons/lucide/pull/4486">lucide-icons/lucide#4486</a></li>
<li>chore(deps): bump esbuild from 0.25.12 to 0.28.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4459">lucide-icons/lucide#4459</a></li>
<li>fix(docs): prevent private analytics token from blocking local dev
by <a href="https://github.com/Hsiii"><code>@​Hsiii</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4481">lucide-icons/lucide#4481</a></li>
<li>docs(installation.md): Remove outdate next tag in installation by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4495">lucide-icons/lucide#4495</a></li>
<li>fix(lucide-react-native): Fix context provider export by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4497">lucide-icons/lucide#4497</a></li>
<li>fix(astro): add Astro v7 compatibility by <a
href="https://github.com/iseraph-dev"><code>@​iseraph-dev</code></a> in
<a
href="https://redirect.github.com/lucide-icons/lucide/pull/4491">lucide-icons/lucide#4491</a></li>
<li>fix(icons): changed <code>carrot</code> icon by <a
href="https://github.com/jguddas"><code>@​jguddas</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4010">lucide-icons/lucide#4010</a></li>
<li>fix(icons): changed <code>ungroup</code> icon by <a
href="https://github.com/jguddas"><code>@​jguddas</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/3969">lucide-icons/lucide#3969</a></li>
<li>feat(icons): added <code>phi</code> icon also used as
<code>golden-ratio</code> by <a
href="https://github.com/whoisBugsbunny"><code>@​whoisBugsbunny</code></a>
in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4218">lucide-icons/lucide#4218</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/bartlomieju"><code>@​bartlomieju</code></a>
made their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4486">lucide-icons/lucide#4486</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lucide-icons/lucide/compare/1.21.0...1.22.0">https://github.com/lucide-icons/lucide/compare/1.21.0...1.22.0</a></p>
<h2>Version 1.21.0</h2>
<h2>What's Changed</h2>
<ul>
<li>ci(release.yml): Remove new-version in release flow by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4478">lucide-icons/lucide#4478</a></li>
<li>ci(release.yml): Fix workflow and remove <code>version</code>
scripts in package scripts by <a
href="https://github.com/ericfennis"><code>@​ericfennis</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4479">lucide-icons/lucide#4479</a></li>
<li>fix(docs): rename navigation category label by <a
href="https://github.com/Hsiii"><code>@​Hsiii</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4483">lucide-icons/lucide#4483</a></li>
<li>feat(icons): added <code>broken-bone</code> icon by <a
href="https://github.com/Patolord"><code>@​Patolord</code></a> in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4131">lucide-icons/lucide#4131</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Hsiii"><code>@​Hsiii</code></a> made
their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4483">lucide-icons/lucide#4483</a></li>
<li><a href="https://github.com/Patolord"><code>@​Patolord</code></a>
made their first contribution in <a
href="https://redirect.github.com/lucide-icons/lucide/pull/4131">lucide-icons/lucide#4131</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lucide-icons/lucide/compare/1.20.0...1.21.0">https://github.com/lucide-icons/lucide/compare/1.20.0...1.21.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5ff536e139"><code>5ff536e</code></a>
ci(release.yml): Fix workflow and remove <code>version</code> scripts in
package scripts...</li>
<li>See full diff in <a
href="https://github.com/lucide-icons/lucide/commits/1.23.0/packages/lucide-react">compare
view</a></li>
</ul>
</details>
<br />

Updates `next` from 16.2.6 to 16.2.10
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/next.js/releases">next's
releases</a>.</em></p>
<blockquote>
<h2>v16.2.10</h2>
<p>Contains no changes except publishing <code>@next/swc-wasm-web</code>
which was accidentally not published since 16.2.4.</p>
<h2>v16.2.9</h2>
<p>Empty release to ensure <code>next@latest</code> points at a stable
release. Next.js only allows publishing with Trusted Publishing enabled.
In order to fix NPM dist-tags, we have to release a new version.
Updating dist-tags is not possible with Trusted Publishing.</p>
<h2>v16.2.8</h2>
<p>Release with no changes in an attempt to fix <code>next@latest</code>
pointing at a prerelease version.</p>
<h2>v16.2.7</h2>
<blockquote>
<p>[!NOTE]
This release is backporting bug fixes. It does <strong>not</strong>
include all pending features/changes on canary.</p>
</blockquote>
<h3>Core Changes</h3>
<ul>
<li>Backport documentation fixes for v16.2 (<a
href="https://redirect.github.com/vercel/next.js/issues/93804">#93804</a>)</li>
<li>[backport] Patch <code>playwright-core</code> to resolve
<code>_finishedPromise</code> on <code>requestFailed</code> (<a
href="https://redirect.github.com/vercel/next.js/issues/93920">#93920</a>)</li>
<li>[backport] Fix dev mode hydration failure when page is served from
HTTP cache (<a
href="https://redirect.github.com/vercel/next.js/issues/93492">#93492</a>)</li>
<li>[backport] Fix catch-all <code>router.query</code> corruption with
<code>basePath</code> + <code>rewrites</code> (<a
href="https://redirect.github.com/vercel/next.js/issues/93917">#93917</a>)</li>
<li>[backport] Encode non-ASCII characters in cache tags at construction
(<a
href="https://redirect.github.com/vercel/next.js/issues/93918">#93918</a>)</li>
<li>[backport] Fix server action forwarding loop with middleware
rewrites (<a
href="https://redirect.github.com/vercel/next.js/issues/93919">#93919</a>)</li>
<li>[backport] Turbopack: switch from base40 to base38 hash encoding (<a
href="https://redirect.github.com/vercel/next.js/issues/93932">#93932</a>)</li>
<li>[ci] Disable hanging node 24 typescript tests on 16.2 backport
branch (<a
href="https://redirect.github.com/vercel/next.js/issues/94164">#94164</a>)</li>
<li>[backport] Fix &quot;type: module&quot; in project dir when using
standalone or adapters (<a
href="https://redirect.github.com/vercel/next.js/issues/94050">#94050</a>)</li>
<li>[backport] Propagate adapter preferred regions (<a
href="https://redirect.github.com/vercel/next.js/issues/94200">#94200</a>)</li>
<li>[16.2.x] Don't drop <code>FormData</code> entries (<a
href="https://redirect.github.com/vercel/next.js/issues/94240">#94240</a>)</li>
<li>[backport] feat(turbopack): add LocalPathOrProjectPath PostCSS
config resolution (<a
href="https://redirect.github.com/vercel/next.js/issues/94284">#94284</a>)</li>
</ul>
<h3>Credits</h3>
<p>Huge thanks to <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/icyJoseph"><code>@​icyJoseph</code></a>, <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>, <a
href="https://github.com/mischnic"><code>@​mischnic</code></a>, <a
href="https://github.com/bgw"><code>@​bgw</code></a>, <a
href="https://github.com/timneutkens"><code>@​timneutkens</code></a>,
and <a
href="https://github.com/lukesandberg"><code>@​lukesandberg</code></a>
for helping!</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9dadfd693c"><code>9dadfd6</code></a>
v16.2.10</li>
<li><a
href="534d9c144c"><code>534d9c1</code></a>
[16.2.x] Release pipeline updates (<a
href="https://redirect.github.com/vercel/next.js/issues/95160">#95160</a>)</li>
<li><a
href="98941fc427"><code>98941fc</code></a>
backport: docs fixes 16.2.x (<a
href="https://redirect.github.com/vercel/next.js/issues/94935">#94935</a>)</li>
<li><a
href="6e1a94de7c"><code>6e1a94d</code></a>
[16.2.x][ci]: fix release script to not strip newlines (<a
href="https://redirect.github.com/vercel/next.js/issues/94640">#94640</a>)</li>
<li><a
href="f37fad9405"><code>f37fad9</code></a>
v16.2.9</li>
<li><a
href="d9aaaedfd8"><code>d9aaaed</code></a>
[cd] Allow tagging semver-lower releases as <code>@latest</code> if
<code>@latest</code> po… (<a
href="https://redirect.github.com/vercel/next.js/issues/94627">#94627</a>)</li>
<li><a
href="6f1680448c"><code>6f16804</code></a>
v16.2.8</li>
<li><a
href="0dbc1d5c86"><code>0dbc1d5</code></a>
[16.2.x][cd] Ensure release can be triggered on old branches (<a
href="https://redirect.github.com/vercel/next.js/issues/94598">#94598</a>)</li>
<li><a
href="90e3c811e7"><code>90e3c81</code></a>
[16.2.x] Align Actions dependencies with Canary (<a
href="https://redirect.github.com/vercel/next.js/issues/94339">#94339</a>)</li>
<li><a
href="83f402c69d"><code>83f402c</code></a>
[16.2.x][cd] Stop fetching all tags when searching parent tag (<a
href="https://redirect.github.com/vercel/next.js/issues/94334">#94334</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/next.js/compare/v16.2.6...v16.2.10">compare
view</a></li>
</ul>
</details>
<br />

Updates `react` from 19.2.4 to 19.2.7
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/facebook/react/releases">react's
releases</a>.</em></p>
<blockquote>
<h2>19.2.7 (June 1st, 2026)</h2>
<h2>React Server Components</h2>
<ul>
<li>Fixed missing <code>FormData</code> entries in Server Actions which
regressed in 19.2.6
(<a
href="https://redirect.github.com/facebook/react/pull/36566">#36566</a>
by <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>)</li>
</ul>
<h2>19.2.6 (May 6th, 2026)</h2>
<h2>React Server Components</h2>
<ul>
<li>Type hardening and performance improvements
(<a
href="https://redirect.github.com/facebook/react/pull/36425">#36425</a>
by <a href="https://github.com/eps1lon"><code>@​eps1lon</code></a> and
<a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>)</li>
</ul>
<h2>19.2.5 (April 8th, 2026)</h2>
<h2>React Server Components</h2>
<ul>
<li>Add more cycle protections (<a
href="https://redirect.github.com/facebook/react/pull/36236">#36236</a>
by <a href="https://github.com/eps1lon"><code>@​eps1lon</code></a> and
<a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/react/react/blob/main/CHANGELOG.md">react's
changelog</a>.</em></p>
<blockquote>
<h2>19.2.7 (June 1, 2026)</h2>
<h3>React Server Components</h3>
<ul>
<li>Fixed missing <code>FormData</code> entries in Server Actions which
regressed in 19.2.6 (<a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36566">#36566</a>)</li>
</ul>
<h2>19.2.6 (May 6, 2026)</h2>
<h3>React Server Components</h3>
<ul>
<li>Type hardening and performance improvements (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36425">#36425</a>)</li>
</ul>
<h2>19.2.5 (March 18, 2026)</h2>
<h3>React Server Components</h3>
<ul>
<li>Add more cycle protections (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36236">#36236</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6117d7cca4"><code>6117d7c</code></a>
Version 19.2.7 (<a
href="https://github.com/facebook/react/tree/HEAD/packages/react/issues/36591">#36591</a>)</li>
<li><a
href="eaf3e95ca9"><code>eaf3e95</code></a>
Version 19.2.6</li>
<li><a
href="23f4f9f30d"><code>23f4f9f</code></a>
19.2.5</li>
<li>See full diff in <a
href="https://github.com/facebook/react/commits/v19.2.7/packages/react">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for react since your current version.</p>
</details>
<br />

Updates `@types/react` from 19.2.14 to 19.2.17
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />

Updates `react-dom` from 19.2.4 to 19.2.7
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/facebook/react/releases">react-dom's
releases</a>.</em></p>
<blockquote>
<h2>19.2.7 (June 1st, 2026)</h2>
<h2>React Server Components</h2>
<ul>
<li>Fixed missing <code>FormData</code> entries in Server Actions which
regressed in 19.2.6
(<a
href="https://redirect.github.com/facebook/react/pull/36566">#36566</a>
by <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>)</li>
</ul>
<h2>19.2.6 (May 6th, 2026)</h2>
<h2>React Server Components</h2>
<ul>
<li>Type hardening and performance improvements
(<a
href="https://redirect.github.com/facebook/react/pull/36425">#36425</a>
by <a href="https://github.com/eps1lon"><code>@​eps1lon</code></a> and
<a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>)</li>
</ul>
<h2>19.2.5 (April 8th, 2026)</h2>
<h2>React Server Components</h2>
<ul>
<li>Add more cycle protections (<a
href="https://redirect.github.com/facebook/react/pull/36236">#36236</a>
by <a href="https://github.com/eps1lon"><code>@​eps1lon</code></a> and
<a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/react/react/blob/main/CHANGELOG.md">react-dom's
changelog</a>.</em></p>
<blockquote>
<h2>19.2.7 (June 1, 2026)</h2>
<h3>React Server Components</h3>
<ul>
<li>Fixed missing <code>FormData</code> entries in Server Actions which
regressed in 19.2.6 (<a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36566">#36566</a>)</li>
</ul>
<h2>19.2.6 (May 6, 2026)</h2>
<h3>React Server Components</h3>
<ul>
<li>Type hardening and performance improvements (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36425">#36425</a>)</li>
</ul>
<h2>19.2.5 (March 18, 2026)</h2>
<h3>React Server Components</h3>
<ul>
<li>Add more cycle protections (<a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a> <a
href="https://redirect.github.com/facebook/react/pull/36236">#36236</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6117d7cca4"><code>6117d7c</code></a>
Version 19.2.7 (<a
href="https://github.com/facebook/react/tree/HEAD/packages/react-dom/issues/36591">#36591</a>)</li>
<li><a
href="eaf3e95ca9"><code>eaf3e95</code></a>
Version 19.2.6</li>
<li><a
href="23f4f9f30d"><code>23f4f9f</code></a>
19.2.5</li>
<li>See full diff in <a
href="https://github.com/facebook/react/commits/v19.2.7/packages/react-dom">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for react-dom since your current version.</p>
</details>
<br />

Updates `recharts` from 3.8.1 to 3.9.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/recharts/recharts/releases">recharts's
releases</a>.</em></p>
<blockquote>
<h2>v3.9.2</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: clarify custom labels and ticks need SVG elements by <a
href="https://github.com/ishaanlabs-gg"><code>@​ishaanlabs-gg</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7524">recharts/recharts#7524</a></li>
<li>chore(deps): bump immer from 11.1.8 to 11.1.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/recharts/recharts/pull/7526">recharts/recharts#7526</a></li>
<li>fix(Sankey): avoid exponential depth traversal on dense graphs by <a
href="https://github.com/dm-gthb"><code>@​dm-gthb</code></a> in <a
href="https://redirect.github.com/recharts/recharts/pull/7479">recharts/recharts#7479</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/ishaanlabs-gg"><code>@​ishaanlabs-gg</code></a>
made their first contribution in <a
href="https://redirect.github.com/recharts/recharts/pull/7524">recharts/recharts#7524</a></li>
<li><a href="https://github.com/dm-gthb"><code>@​dm-gthb</code></a> made
their first contribution in <a
href="https://redirect.github.com/recharts/recharts/pull/7479">recharts/recharts#7479</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/recharts/recharts/compare/v3.9.1...v3.9.2">https://github.com/recharts/recharts/compare/v3.9.1...v3.9.2</a></p>
<h2>v3.9.1</h2>
<h2>What's Changed</h2>
<ul>
<li>perf: optimize ScatterChart hover by reducing re-renders from O(n)
to O(1) by <a href="https://github.com/roy7"><code>@​roy7</code></a> in
<a
href="https://redirect.github.com/recharts/recharts/pull/7133">recharts/recharts#7133</a></li>
<li>fix(YAxis): render explicit ticks when a non-literal domain can't
resolve on empty data (<a
href="https://redirect.github.com/recharts/recharts/issues/7362">#7362</a>)
by <a href="https://github.com/nlenepveu"><code>@​nlenepveu</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7393">recharts/recharts#7393</a></li>
<li>fix: avoid Sankey nodes overlapping skipped-depth links by <a
href="https://github.com/pupuking723"><code>@​pupuking723</code></a> in
<a
href="https://redirect.github.com/recharts/recharts/pull/7471">recharts/recharts#7471</a></li>
<li>Add stacked bar chart with horizontal threshold line example by <a
href="https://github.com/nijuse"><code>@​nijuse</code></a> in <a
href="https://redirect.github.com/recharts/recharts/pull/7495">recharts/recharts#7495</a></li>
<li>fix(DefaultLegendContent): omit empty value from legend icon
aria-label by <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a> in
<a
href="https://redirect.github.com/recharts/recharts/pull/7501">recharts/recharts#7501</a></li>
<li>chore(deps): bump immer from 10.2.0 to 11.1.8 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/recharts/recharts/pull/7452">recharts/recharts#7452</a></li>
<li>fix(getNiceTickValues): remove trailing duplicate tick when
allowDecimals=false by <a
href="https://github.com/JSap0914"><code>@​JSap0914</code></a> in <a
href="https://redirect.github.com/recharts/recharts/pull/7482">recharts/recharts#7482</a></li>
<li>Fix/per graphical item formatter prop by <a
href="https://github.com/shreedharbhat98"><code>@​shreedharbhat98</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7287">recharts/recharts#7287</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/pupuking723"><code>@​pupuking723</code></a>
made their first contribution in <a
href="https://redirect.github.com/recharts/recharts/pull/7471">recharts/recharts#7471</a></li>
<li><a href="https://github.com/nijuse"><code>@​nijuse</code></a> made
their first contribution in <a
href="https://redirect.github.com/recharts/recharts/pull/7495">recharts/recharts#7495</a></li>
<li><a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>
made their first contribution in <a
href="https://redirect.github.com/recharts/recharts/pull/7501">recharts/recharts#7501</a></li>
<li><a href="https://github.com/JSap0914"><code>@​JSap0914</code></a>
made their first contribution in <a
href="https://redirect.github.com/recharts/recharts/pull/7482">recharts/recharts#7482</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/recharts/recharts/compare/v3.9.0...v3.9.1">https://github.com/recharts/recharts/compare/v3.9.0...v3.9.1</a></p>
<h2>v3.9.0</h2>
<h2>What's Changed</h2>
<h3>Animations</h3>
<p>3.9 comes with new animations! There are several bug fixes and what's
best, all animations are now fully customizable.</p>
<p>See the animations guide on <a
href="https://recharts.github.io/en-US/guide/animations/">https://recharts.github.io/en-US/guide/animations/</a></p>
<ul>
<li>Animation guide by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7179">recharts/recharts#7179</a></li>
<li>Animation tests by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7255">recharts/recharts#7255</a></li>
<li>New animation props by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7215">recharts/recharts#7215</a></li>
<li>test: cover legacy animation length changes by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7283">recharts/recharts#7283</a></li>
<li>test: add sparse animation path tests for Line component by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7295">recharts/recharts#7295</a></li>
<li>Export and document interpolate function by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7293">recharts/recharts#7293</a></li>
<li>test: enhance line animation tests for ComposedChart and responsive
by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7289">recharts/recharts#7289</a></li>
<li>Manual animations on website by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7483">recharts/recharts#7483</a></li>
<li>Add new example where chart animates by scroll by <a
href="https://github.com/PavelVanecek"><code>@​PavelVanecek</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7484">recharts/recharts#7484</a></li>
<li>fix: preserve single-value line dash gaps during animation by <a
href="https://github.com/puneetdixit200"><code>@​puneetdixit200</code></a>
in <a
href="https://redirect.github.com/recharts/recharts/pull/7405">recharts/recharts#7405</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b3451050c0"><code>b345105</code></a>
3.9.2</li>
<li><a
href="f27779c048"><code>f27779c</code></a>
npm i</li>
<li><a
href="85f9369f40"><code>85f9369</code></a>
chore(deps-dev): bump prettier from 3.8.4 to 3.9.4 (<a
href="https://redirect.github.com/recharts/recharts/issues/7520">#7520</a>)</li>
<li><a
href="52a2a896cf"><code>52a2a89</code></a>
fix(Sankey): avoid exponential depth traversal on dense graphs (<a
href="https://redirect.github.com/recharts/recharts/issues/7479">#7479</a>)</li>
<li><a
href="8a056c1018"><code>8a056c1</code></a>
chore(deps-dev): bump rollup from 4.61.1 to 4.62.2 (<a
href="https://redirect.github.com/recharts/recharts/issues/7527">#7527</a>)</li>
<li><a
href="2af6ec6f0f"><code>2af6ec6</code></a>
chore(deps): bump immer from 11.1.8 to 11.1.9 (<a
href="https://redirect.github.com/recharts/recharts/issues/7526">#7526</a>)</li>
<li><a
href="6f10d53cf6"><code>6f10d53</code></a>
docs: clarify custom labels and ticks need SVG elements (<a
href="https://redirect.github.com/recharts/recharts/issues/7524">#7524</a>)</li>
<li><a
href="c04f1a7678"><code>c04f1a7</code></a>
chore(deps-dev): bump lint-staged from 17.0.7 to 17.0.8 (<a
href="https://redirect.github.com/recharts/recharts/issues/7521">#7521</a>)</li>
<li><a
href="69c7a9630a"><code>69c7a96</code></a>
chore(deps-dev): bump glob from 11.1.0 to 13.0.6 (<a
href="https://redirect.github.com/recharts/recharts/issues/7522">#7522</a>)</li>
<li><a
href="6efaf16a1a"><code>6efaf16</code></a>
chore(deps): bump es-toolkit from 1.47.0 to 1.49.0 (<a
href="https://redirect.github.com/recharts/recharts/issues/7515">#7515</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/recharts/recharts/compare/v3.8.1...v3.9.2">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for recharts since your current version.</p>
</details>
<details>
<summary>Install script changes</summary>
<p>This version modifies <code>prepare</code> script that runs during
installation. Review the package contents before updating.</p>
</details>
<br />

Updates `@tailwindcss/postcss` from 4.2.2 to 4.3.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tailwindlabs/tailwindcss/releases">@​tailwindcss/postcss's
releases</a>.</em></p>
<blockquote>
<h2>v4.3.2</h2>
<h3>Fixed</h3>
<ul>
<li>Support bare spacing values for <code>auto-rows-*</code> and
<code>auto-cols-*</code> utilities (e.g. <code>auto-rows-12</code> and
<code>auto-cols-16</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20229">#20229</a>)</li>
<li>Prevent <code>@tailwindcss/cli</code> in <code>--watch</code> mode
from crashing on Windows when <code>@source</code> points to a directory
that doesn't exist (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20242">#20242</a>)</li>
<li>Prevent <code>@tailwindcss/vite</code> from crashing in Deno v2.8.x
when <code>context.parentURL</code> is not a valid URL (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20245">#20245</a>)</li>
<li>Ensure <code>@tailwindcss/cli</code> in <code>--watch</code> mode
rebuilds when the input CSS file changes in an ignored directory (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20246">#20246</a>)</li>
<li>Allow <code>@variant</code> rules used in <code>addBase(…)</code> to
use custom variants defined later (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20247">#20247</a>)</li>
<li>Prevent <code>@tailwindcss/vite</code> from crashing during HMR when
scanned files or directories are deleted (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20259">#20259</a>)</li>
<li>Generate <code>font-size</code> instead of <code>color</code>
declarations for <code>text-[--spacing(…)]</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20260">#20260</a>)</li>
<li>Prevent <code>@source</code> patterns from scanning unrelated
sibling files and folders (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20263">#20263</a>)</li>
<li>Extract class candidates adjacent to Template Toolkit delimiters
like <code>%]…[%</code> in <code>.tt</code>, <code>.tt2</code>, and
<code>.tx</code> files (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20269">#20269</a>)</li>
<li>Extract class candidates from conditional Maud syntax like
<code>p.text-black[condition]</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20269">#20269</a>)</li>
<li>Prevent <code>@position-try</code> rules from triggering unknown
at-rule warnings when optimizing CSS (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20277">#20277</a>)</li>
<li>Support class suggestions for named opacity modifiers from
<code>--opacity</code> theme values (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20287">#20287</a>)</li>
<li>Prevent type errors in <code>@tailwindcss/postcss</code> when used
with newer PostCSS patch releases (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20289">#20289</a>)</li>
</ul>
<h2>v4.3.1</h2>
<h3>Added</h3>
<ul>
<li>Add <code>--silent</code> option to suppress output in
<code>@tailwindcss/cli</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20100">#20100</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Remove deprecation warnings by using
<code>Module#registerHooks</code> instead of
<code>Module#register</code> on Node 26+ (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20028">#20028</a>)</li>
<li>Canonicalization: don't crash when plugin utilities throw for
unsupported values (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20052">#20052</a>)</li>
<li>Allow <code>@apply</code> to be used with CSS mixins (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19427">#19427</a>)</li>
<li>Ensure <code>not-*</code> correctly negates <code>@container</code>
queries, including <code>style(…)</code> queries (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20059">#20059</a>)</li>
<li>Ensure <code>drop-shadow-*</code> color utilities work with custom
shadow values containing <code>calc(…)</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20080">#20080</a>)</li>
<li>Fix 'Sourcemap is likely to be incorrect' warnings when using
<code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20103">#20103</a>)</li>
<li>Ensure <code>@tailwindcss/webpack</code> can be installed in Rspack
projects without requiring <code>webpack</code> as a peer dependency (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20027">#20027</a>)</li>
<li>Canonicalization: don't suggest invalid <code>calc(…)</code>
expressions (e.g. <code>px-[calc(1rem+0px)]</code> →
<code>px-[calc(1rem+0)]</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20127">#20127</a>)</li>
<li>Canonicalization: avoid suggesting large spacing-scale values for
arbitrary lengths (e.g. <code>left-[99999px]</code> →
<code>left-[99999px]</code>, not <code>left-24999.75</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20130">#20130</a>)</li>
<li>Ensure <code>@tailwindcss/cli</code> in <code>--watch</code> mode
recovers when a tracked dependency is deleted and restored (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20137">#20137</a>)</li>
<li>Ensure standalone <code>@tailwindcss/cli</code> binaries are ignored
when scanning for class candidates (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20139">#20139</a>)</li>
<li>Ensure class candidates are extracted from Twig
<code>addClass(…)</code> and <code>removeClass(…)</code> calls (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20198">#20198</a>)</li>
<li>Don't crash in the Ruby or Vue preprocessors when scanning files
containing invalid UTF-8 bytes (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19588">#19588</a>)</li>
<li>Allow <code>@variant</code> to be used inside <code>addBase</code>
(<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19480">#19480</a>)</li>
<li>Ensure <code>@source</code> globs with symlinks are preserved (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20203">#20203</a>)</li>
<li>Ensure later <code>@source</code> rules can re-include files
excluded by earlier <code>@source not</code> rules (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20203">#20203</a>)</li>
<li>Upgrade: don't migrate empty class rules to invalid
<code>@utility</code> rules (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20205">#20205</a>)</li>
<li>Ensure transitions between <code>inset-shadow-none</code> and other
inset shadows work correctly (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20208">#20208</a>)</li>
<li>Ensure explicitly referenced <code>@source</code> directories are
scanned even when ignored by git (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20214">#20214</a>)</li>
<li>Ensure <code>@source</code> globs ending in <code>**/*</code>
preserve dynamic path segments to avoid scanning too many files (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20217">#20217</a>)</li>
<li>Canonicalization: don't fold <code>calc(…)</code> divisions when the
result would require high precision (e.g.
<code>w-[calc(100%/3.5)]</code> → <code>w-[calc(100%/3.5)]</code>, not
<code>w-[28.571428571428573%]</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20221">#20221</a>)</li>
<li>Serve ESM type declarations to ESM importers of
<code>@tailwindcss/postcss</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20228">#20228</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Generate <code>0</code> instead of <code>calc(var(--spacing) *
0)</code> for spacing utilities like <code>m-0</code> and
<code>left-0</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20196">#20196</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md">@​tailwindcss/postcss's
changelog</a>.</em></p>
<blockquote>
<h2>[4.3.2] - 2026-06-26</h2>
<h3>Fixed</h3>
<ul>
<li>Support bare spacing values for <code>auto-rows-*</code> and
<code>auto-cols-*</code> utilities (e.g. <code>auto-rows-12</code> and
<code>auto-cols-16</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20229">#20229</a>)</li>
<li>Prevent <code>@tailwindcss/cli</code> in <code>--watch</code> mode
from crashing on Windows when <code>@source</code> points to a directory
that doesn't exist (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20242">#20242</a>)</li>
<li>Prevent <code>@tailwindcss/vite</code> from crashing in Deno v2.8.x
when <code>context.parentURL</code> is not a valid URL (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20245">#20245</a>)</li>
<li>Ensure <code>@tailwindcss/cli</code> in <code>--watch</code> mode
rebuilds when the input CSS file changes in an ignored directory (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20246">#20246</a>)</li>
<li>Allow <code>@variant</code> rules used in <code>addBase(…)</code> to
use custom variants defined later (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20247">#20247</a>)</li>
<li>Prevent <code>@tailwindcss/vite</code> from crashing during HMR when
scanned files or directories are deleted (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20259">#20259</a>)</li>
<li>Generate <code>font-size</code> instead of <code>color</code>
declarations for <code>text-[--spacing(…)]</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20260">#20260</a>)</li>
<li>Prevent <code>@source</code> patterns from scanning unrelated
sibling files and folders (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20263">#20263</a>)</li>
<li>Extract class candidates adjacent to Template Toolkit delimiters
like <code>%]…[%</code> in <code>.tt</code>, <code>.tt2</code>, and
<code>.tx</code> files (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20269">#20269</a>)</li>
<li>Extract class candidates from conditional Maud syntax like
<code>p.text-black[condition]</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20269">#20269</a>)</li>
<li>Prevent <code>@position-try</code> rules from triggering unknown
at-rule warnings when optimizing CSS (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20277">#20277</a>)</li>
<li>Support class suggestions for named opacity modifiers from
<code>--opacity</code> theme values (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20287">#20287</a>)</li>
<li>Prevent type errors in <code>@tailwindcss/postcss</code> when used
with newer PostCSS patch releases (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20289">#20289</a>)</li>
</ul>
<h2>[4.3.1] - 2026-06-12</h2>
<h3>Added</h3>
<ul>
<li>Add <code>--silent</code> option to suppress output in
<code>@tailwindcss/cli</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20100">#20100</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Remove deprecation warnings by using
<code>Module#registerHooks</code> instead of
<code>Module#register</code> on Node 26+ (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20028">#20028</a>)</li>
<li>Canonicalization: don't crash when plugin utilities throw for
unsupported values (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20052">#20052</a>)</li>
<li>Allow <code>@apply</code> to be used with CSS mixins (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19427">#19427</a>)</li>
<li>Ensure <code>not-*</code> correctly negates <code>@container</code>
queries, including <code>style(…)</code> queries (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20059">#20059</a>)</li>
<li>Ensure <code>drop-shadow-*</code> color utilities work with custom
shadow values containing <code>calc(…)</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20080">#20080</a>)</li>
<li>Fix 'Sourcemap is likely to be incorrect' warnings when using
<code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20103">#20103</a>)</li>
<li>Ensure <code>@tailwindcss/webpack</code> can be installed in Rspack
projects without requiring <code>webpack</code> as a peer dependency (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20027">#20027</a>)</li>
<li>Canonicalization: don't suggest invalid <code>calc(…)</code>
expressions (e.g. <code>px-[calc(1rem+0px)]</code> →
<code>px-[calc(1rem+0)]</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20127">#20127</a>)</li>
<li>Canonicalization: avoid suggesting large spacing-scale values for
arbitrary lengths (e.g. <code>left-[99999px]</code> →
<code>left-[99999px]</code>, not <code>left-24999.75</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20130">#20130</a>)</li>
<li>Ensure <code>@tailwindcss/cli</code> in <code>--watch</code> mode
recovers when a tracked dependency is deleted and restored (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20137">#20137</a>)</li>
<li>Ensure standalone <code>@tailwindcss/cli</code> binaries are ignored
when scanning for class candidates (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20139">#20139</a>)</li>
<li>Ensure class candidates are extracted from Twig
<code>addClass(…)</code> and <code>removeClass(…)</code> calls (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20198">#20198</a>)</li>
<li>Don't crash in the Ruby or Vue preprocessors when scanning files
containing invalid UTF-8 bytes (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19588">#19588</a>)</li>
<li>Allow <code>@variant</code> to be used inside <code>addBase</code>
(<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19480">#19480</a>)</li>
<li>Ensure <code>@source</code> globs with symlinks are preserved (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20203">#20203</a>)</li>
<li>Ensure later <code>@source</code> rules can re-include files
excluded by earlier <code>@source not</code> rules (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20203">#20203</a>)</li>
<li>Upgrade: don't migrate empty class rules to invalid
<code>@utility</code> rules (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20205">#20205</a>)</li>
<li>Ensure transitions between <code>inset-shadow-none</code> and other
inset shadows work correctly (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20208">#20208</a>)</li>
<li>Ensure explicitly referenced <code>@source</code> directories are
scanned even when ignored by git (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20214">#20214</a>)</li>
<li>Ensure <code>@source</code> globs ending in <code>**/*</code>
preserve dynamic path segments to avoid scanning too many files (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20217">#20217</a>)</li>
<li>Canonicalization: don't fold <code>calc(…)</code> divisions when the
result would require high precision (e.g.
<code>w-[calc(100%/3.5)]</code> → <code>w-[calc(100%/3.5)]</code>, not
<code>w-[28.571428571428573%]</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20221">#20221</a>)</li>
<li>Serve ESM type declarations to ESM importers of
<code>@tailwindcss/postcss</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/20228">#20228</a>)</li>
</ul>
<h3>Changed</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="056a155072"><code>056a155</code></a>
4.3.2 (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/20281">#20281</a>)</li>
<li><a
href="8a14a71010"><code>8a14a71</code></a>
4.3.1 (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/20226">#20226</a>)</li>
<li><a
href="522288ca08"><code>522288c</code></a>
Serve ESM type declarations to ESM importers of
<code>@tailwindcss/postcss</code> (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/20228">#20228</a>)</li>
<li><a
href="8dcdb66e8a"><code>8dcdb66</code></a>
Bump dependencies (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/20095">#20095</a>)</li>
<li><a
href="588bd7371f"><code>588bd73</code></a>
4.3.0 (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/20023">#20023</a>)</li>
<li><a
href="12eb5ae7b6"><code>12eb5ae</code></a>
Cleanup noisy test output (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/20015">#20015</a>)</li>
<li><a
href="4255671c5f"><code>4255671</code></a>
Improve snapshot tests (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/20013">#20013</a>)</li>
<li><a
href="52f94c74bb"><code>52f94c7</code></a>
Improve codebase quality (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/19999">#19999</a>)</li>
<li><a
href="d194d4c3e6"><code>d194d4c</code></a>
docs: fix various typos in comments and documentation (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/19878">#19878</a>)</li>
<li><a
href="bfb5732b0b"><code>bfb5732</code></a>
Fall back to the plugin <code>base</code> when PostCSS has no
<code>from</code> option (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss/issues/19980">#19980</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-postcss">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for <code>@​tailwindcss/postcss</code> since your current
version.</p>
</details>
<br />

Updates `@types/mdx` from 2.0.13 to 2.0.14
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mdx">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/react` from 19.2.14 to 19.2.17
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />

Updates `postcss` from 8.5.15 to 8.5.16
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.16</h2>
<ul>
<li>Fixed <code>Input#origin()</code> position (by <a
href="https://github.com/mizdra"><code>@​mizdra</code></a>).</li>
<li>Fixed <code>raws</code> after rehydrating a JSON AST (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed putting parent-less node in <code>nodes</code> of new node (by
<a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
<li>Fixed computing <code>offset</code> in <code>positionBy()</code> (by
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>rangeBy()</code> on <code>index: 0</code> (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.16</h2>
<ul>
<li>Fixed <code>Input#origin()</code> position (by <a
href="https://github.com/mizdra"><code>@​mizdra</code></a>).</li>
<li>Fixed <code>raws</code> after rehydrating a JSON AST (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed putting parent-less node in <code>nodes</code> of new node (by
<a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
<li>Fixed computing <code>offset</code> in <code>positionBy()</code> (by
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>rangeBy()</code> on <code>index: 0</code> (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="92ccc93ff1"><code>92ccc93</code></a>
Release 8.5.16 version</li>
<li><a
href="818bdd6043"><code>818bdd6</code></a>
Update formatting</li>
<li><a
href="46e451068e"><code>46e4510</code></a>
Fix <code>Input#origin()</code> returning incorrect position (<a
href="https://redirect.github.com/postcss/postcss/issues/2036">#2036</a>)</li>
<li><a
href="34942ce76c"><code>34942ce</code></a>
Fix tests</li>
<li><a
href="d4feed6453"><code>d4feed6</code></a>
Don't clone root-less child nodes in container constructor (<a
href="https://redirect.github.com/postcss/postcss/issues/2097">#2097</a>)</li>
<li><a
href="da323fc8d3"><code>da323fc</code></a>
Revert version update to fix old Node.js on CI</li>
<li><a
href="8863369194"><code>8863369</code></a>
Update dependencies</li>
<li><a
href="3828982213"><code>3828982</code></a>
Preserve node raws when rehydrating a JSON AST (<a
href="https://redirect.github.com/postcss/postcss/issues/2100">#2100</a>)</li>
<li><a
href="d1e80b8303"><code>d1e80b8</code></a>
Fix Node#rangeBy() ignoring index 0 (<a
href="https://redirect.github.com/postcss/postcss/issues/2091">#2091</a>)</li>
<li><a
href="b91e4a6390"><code>b91e4a6</code></a>
Fix Node.js 26 tests</li>
<li>Additional commits viewable in <a
href="https://github.com/postcss/postcss/compare/8.5.15...8.5.16">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a href="https://www.npm...

_Description has been truncated_

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 09:38:37 -04:00
dependabot[bot]
45601d93bc
deps: bump the cargo-minor-patch group across 1 directory with 7 updates (#1909)
Bumps the cargo-minor-patch group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [bytes](https://github.com/tokio-rs/bytes) | `1.12.0` | `1.12.1` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.102` | `1.0.103` |
| [aws-credential-types](https://github.com/smithy-lang/smithy-rs) |
`1.2.14` | `1.3.0` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.3` | `1.23.4` |
| [humantime](https://github.com/chronotope/humantime) | `2.3.0` |
`2.4.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.65` | `1.2.66` |


Updates `bytes` from 1.12.0 to 1.12.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/bytes/releases">bytes's
releases</a>.</em></p>
<blockquote>
<h2>Bytes v1.12.1</h2>
<h1>1.12.1 (July 8th, 2026)</h1>
<h3>Fixed</h3>
<ul>
<li>Properly handle when <code>Box::new</code> panics (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/837">#837</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md">bytes's
changelog</a>.</em></p>
<blockquote>
<h1>1.12.1 (July 8th, 2026)</h1>
<h3>Fixed</h3>
<ul>
<li>Properly handle when <code>Box::new</code> panics (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/837">#837</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="76c0fbb54e"><code>76c0fbb</code></a>
Release bytes v1.12.1 (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/838">#838</a>)</li>
<li><a
href="924c82bf00"><code>924c82b</code></a>
Handle unwinding from Box::new (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/837">#837</a>)</li>
<li>See full diff in <a
href="https://github.com/tokio-rs/bytes/compare/v1.12.0...v1.12.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `anyhow` from 1.0.102 to 1.0.103
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/anyhow/releases">anyhow's
releases</a>.</em></p>
<blockquote>
<h2>1.0.103</h2>
<ul>
<li>Fix Stacked Borrows violation (UB) in
<code>Error::downcast_mut</code> (<a
href="https://redirect.github.com/dtolnay/anyhow/issues/451">#451</a>,
<a
href="https://redirect.github.com/dtolnay/anyhow/issues/452">#452</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5bdb0e24db"><code>5bdb0e2</code></a>
Release 1.0.103</li>
<li><a
href="e621bd35dd"><code>e621bd3</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/anyhow/issues/452">#452</a>
from dtolnay/downcast</li>
<li><a
href="6e8c000690"><code>6e8c000</code></a>
Eliminate pointer-&gt;reference-&gt;pointer during downcast</li>
<li><a
href="67c4abd771"><code>67c4abd</code></a>
Add regression test for issue 451</li>
<li><a
href="917a169320"><code>917a169</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="d9dc3faf78"><code>d9dc3fa</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="841522b2aa"><code>841522b</code></a>
Raise minimum tested compiler to rust 1.85</li>
<li>See full diff in <a
href="https://github.com/dtolnay/anyhow/compare/1.0.102...1.0.103">compare
view</a></li>
</ul>
</details>
<br />

Updates `aws-credential-types` from 1.2.14 to 1.3.0
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/smithy-lang/smithy-rs/commits">compare
view</a></li>
</ul>
</details>
<br />

Updates `aws-smithy-runtime-api` from 1.12.3 to 1.13.0
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/smithy-lang/smithy-rs/commits">compare
view</a></li>
</ul>
</details>
<br />

Updates `uuid` from 1.23.3 to 1.23.4
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/uuid-rs/uuid/releases">uuid's
releases</a>.</em></p>
<blockquote>
<h2>v1.23.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix up name of fuzz script in readme by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/888">uuid-rs/uuid#888</a></li>
<li>document fixes by <a
href="https://github.com/frostyplanet"><code>@​frostyplanet</code></a>
in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/889">uuid-rs/uuid#889</a></li>
<li>Prepare for 1.23.4 release by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/890">uuid-rs/uuid#890</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/frostyplanet"><code>@​frostyplanet</code></a>
made their first contribution in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/889">uuid-rs/uuid#889</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4">https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3296d64a19"><code>3296d64</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/890">#890</a> from
uuid-rs/cargo/v1.23.4</li>
<li><a
href="cba53d0da2"><code>cba53d0</code></a>
prepare for 1.23.4 release</li>
<li><a
href="e347af48aa"><code>e347af4</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/889">#889</a> from
frostyplanet/main</li>
<li><a
href="e9bf55c222"><code>e9bf55c</code></a>
doc: Fix broken link warnings</li>
<li><a
href="5351af40a0"><code>5351af4</code></a>
doc: Enable feature flag label for docs.rs</li>
<li><a
href="1e6a9669e3"><code>1e6a966</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/888">#888</a> from
uuid-rs/KodrAus-patch-1</li>
<li><a
href="c9619f639c"><code>c9619f6</code></a>
fix up name of fuzz script in readme</li>
<li>See full diff in <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `humantime` from 2.3.0 to 2.4.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/chronotope/humantime/releases">humantime's
releases</a>.</em></p>
<blockquote>
<h2>2.4.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat: allow creating Duration in const context by <a
href="https://github.com/ctron"><code>@​ctron</code></a> in <a
href="https://redirect.github.com/chronotope/humantime/pull/70">chronotope/humantime#70</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fc092817fa"><code>fc09281</code></a>
chore: prepare release 2.4.0</li>
<li><a
href="8a022cce55"><code>8a022cc</code></a>
feat: allow creating Duration in const context</li>
<li><a
href="27a4f77519"><code>27a4f77</code></a>
Explicitly set rust-version to 1.60</li>
<li><a
href="acc3c19658"><code>acc3c19</code></a>
ci: upgrade to actions/checkout v7</li>
<li><a
href="3acf96bb88"><code>3acf96b</code></a>
ci: fix workflow formatting</li>
<li>See full diff in <a
href="https://github.com/chronotope/humantime/compare/v2.3.0...v2.4.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `cc` from 1.2.65 to 1.2.66
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/cc-rs/releases">cc's
releases</a>.</em></p>
<blockquote>
<h2>cc-v1.2.66</h2>
<h3>Other</h3>
<ul>
<li>Fix target parsing for aarch64-unknown-linux-pauthtest (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1779">#1779</a>)</li>
<li>Support new QNX targets (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1775">#1775</a>)</li>
<li>Add kache to the supported compiler wrappers (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1770">#1770</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md">cc's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/rust-lang/cc-rs/compare/cc-v1.2.65...cc-v1.2.66">1.2.66</a>
- 2026-07-05</h2>
<h3>Other</h3>
<ul>
<li>Fix target parsing for aarch64-unknown-linux-pauthtest (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1779">#1779</a>)</li>
<li>Support new QNX targets (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1775">#1775</a>)</li>
<li>Add kache to the supported compiler wrappers (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1770">#1770</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9c53ddc649"><code>9c53ddc</code></a>
chore(cc): release v1.2.66 (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1777">#1777</a>)</li>
<li><a
href="f2836c0fc5"><code>f2836c0</code></a>
Fix target parsing (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1779">#1779</a>)</li>
<li><a
href="32996914ec"><code>3299691</code></a>
ci(test-wasm): do not download pre release of wasi-sdk (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1778">#1778</a>)</li>
<li><a
href="bfd1ac98d4"><code>bfd1ac9</code></a>
Support new QNX targets (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1775">#1775</a>)</li>
<li><a
href="1887a73fdd"><code>1887a73</code></a>
Add kache to the supported compiler wrappers (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1770">#1770</a>)</li>
<li><a
href="e4847e1720"><code>e4847e1</code></a>
Bump actions/checkout from 6.0.3 to 7.0.0 (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1767">#1767</a>)</li>
<li>See full diff in <a
href="https://github.com/rust-lang/cc-rs/compare/cc-v1.2.65...cc-v1.2.66">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 09:21:38 -04:00
Rod Boev
42ebbc6cce
fix(evals): default unparseable judge scores below pass threshold (#1892)
## Description

`_parse_judge_response` in `headroom/evals/memory/judge.py` defaulted
the score to `3.0` whenever it couldn't find a parseable `Score:` line
in the judge's raw text. `before_after.py`'s `GroundTruthEvaluator`
treats `judge_score >= 3.0` as "contains ground truth" (`contains_gt =
judge_score >= 3.0`). Because `3.0` is exactly the pass threshold, any
judge response the parser couldn't understand (malformed output, missing
`Score:` line, a refusal, truncated text, etc.) silently counted as a
pass instead of surfacing as a scoring failure, biasing
BFCL/ground-truth eval accuracy upward with no visibility into how often
it happened.

The fix tracks whether a real score was actually parsed out of the
response. If nothing parseable was found, the score now defaults to
`0.0` (a hard fail, below the `>= 3.0` threshold) and a `logger.warning`
is emitted with the raw judge text so the failure is visible instead of
silent.

Refs #1890.

## 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/evals/memory/judge.py`: `_parse_judge_response` now tracks
whether a `Score:` line was successfully parsed; on failure it defaults
to `0.0` instead of `3.0` and logs a warning with the raw response text.
- `tests/test_memory_eval.py`: added
`TestJudge.test_parse_judge_response_unparseable_defaults_to_failing_score`,
asserting an unparseable response scores below the `3.0` pass threshold.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_memory_eval.py -k
judge`)
- [x] Linting passes (`uv run ruff check headroom/evals/memory/judge.py
headroom/evals/runners/before_after.py tests/test_memory_eval.py && uv
run ruff format --check headroom/evals/memory/judge.py
headroom/evals/runners/before_after.py tests/test_memory_eval.py`)
- [ ] Type checking passes (`uv run mypy headroom`) — not run; not part
of this repo's local validation loop for this change
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
tests\test_memory_eval.py .......                                                                                [ 77%]
tests\test_verbosity_learn.py ..                                                                                 [100%]
9 passed

$ uv run ruff check headroom/evals/memory/judge.py headroom/evals/runners/before_after.py tests/test_memory_eval.py && uv run ruff format --check headroom/evals/memory/judge.py headroom/evals/runners/before_after.py tests/test_memory_eval.py
3 files already formatted
```

## Real Behavior Proof

- Environment: Windows 11, Python (uv-managed venv), no LLM provider
calls needed — `_parse_judge_response` is a pure text-parsing function.
- Exact command / steps: checked out the pre-fix version of
`_parse_judge_response` (default `score = 3.0`) and ran the new
regression test against an unparseable response (`"The model's response
looks reasonable overall."`, no `Score:` line). Confirmed it failed with
`assert 3.0 < 3.0`. Restored the fix and reran — passes, with `score ==
0.0`.
- Observed result: pre-fix, an unparseable judge response scored `3.0`
and would have passed `contains_gt = judge_score >= 3.0` in
`before_after.py`. Post-fix, the same input scores `0.0`, fails the
threshold, and logs a warning naming the raw response text.
- Not tested: the live
`create_openai_judge`/`create_anthropic_judge`/`create_litellm_judge`
call paths (require provider API keys) and the end-to-end
`GroundTruthEvaluator.evaluate` flow in `before_after.py` — only the
pure parsing function and its documented contract with the `>= 3.0`
threshold were exercised.

## 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 intentionally left untouched — this repo's release
pipeline generates it from conventional commits.
- No user-facing docs describe the parse-failure default, so no
documentation changes were needed.
- Kept the change minimal and localized to the parsing function; didn't
touch `before_after.py`'s threshold or comments since its `>= 3.0`
semantics for successfully-parsed scores are unchanged and correct.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-08 19:31:25 -07:00
Alex Ander
f663894f60
fix(ccr): preserve Anthropic re-stream shape (#1854)
## Description

Buffered Anthropic CCR re-streaming now preserves response shape instead
of normalizing newer Anthropic/Fable fields away during SSE
reconstruction.

Related upstream traffic checked before opening:

- #1451 added the direct streaming CCR buffered path and already
preserves thinking/signature/citation fields in
`StreamingMixin._response_to_sse`.
- #1825 / #1806 cover unknown Anthropic content block types such as
`server_tool_use`; this PR does not duplicate that fix.
- No open or closed issue/PR search result mentioned `stop_details`,
`signature_delta thinking_delta`, `refusal stop_reason`, `Fable CCR`, or
`re-stream thinking` as this exact gap.

## 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

- Preserve `thinking`, `redacted_thinking`, `signature_delta`,
`citations_delta`, `stop_details`, and verbatim `stop_reason` while
parsing Anthropic SSE in `StreamingCCRHandler`.
- Reuse the shared proxy Anthropic SSE renderer for the legacy
`StreamingCCRHandler` output path so it preserves the same shape as the
direct buffered streaming CCR path.
- Preserve `stop_details` and stop defaulting missing `stop_reason` to
`end_turn` in `StreamingMixin._response_to_sse`.
- Add focused regressions for empty thinking blocks, signatures,
redacted thinking data, `refusal`, and `stop_details`.

## 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 --frozen --extra dev pytest tests/test_ccr_response_handler_extra.py tests/test_sse_thinking_blocks.py -q
18 passed in 0.29s

$ uv run --frozen --extra dev pytest tests/test_proxy/test_anthropic_streaming_ccr_retrieve.py -q
4 passed, 1 warning in 10.25s

$ uv run --frozen --extra dev ruff check headroom/ccr/response_handler.py headroom/proxy/handlers/streaming.py tests/test_ccr_response_handler_extra.py tests/test_sse_thinking_blocks.py
All checks passed!

$ uv run --frozen --extra dev ruff format --check headroom/ccr/response_handler.py headroom/proxy/handlers/streaming.py tests/test_ccr_response_handler_extra.py tests/test_sse_thinking_blocks.py
4 files already formatted
```

## Real Behavior Proof

- Environment: local worktree based on current `origin/main` after `git
fetch origin --prune && git rebase origin/main`.
- Exact command / steps: parse and re-emit a synthetic Anthropic SSE
stream containing an empty `thinking` block, `signature_delta`,
`redacted_thinking.data`, `message_delta.stop_reason = "refusal"`, and
`message_delta.stop_details`.
- Observed result: the reconstructed response and re-emitted SSE retain
the thinking/signature/redacted data plus `refusal` and `stop_details`;
a missing `stop_reason` is no longer rewritten to `end_turn`.
- Not tested: live upstream Fable/Opus traffic against the proxy.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-08 19:17:05 -07:00
Rod Boev
ede085cc11
fix(ccr): preserve thinking blocks in buffered stream re-synthesis (#1897)
## Description

Closes #1876.

When CCR forces `stream: false` upstream (the buffered path for
`headroom_retrieve`), the proxy re-synthesizes an SSE stream for the
client from the buffered JSON response via
`StreamingMixin._response_to_sse`. The reported symptom was
extended-thinking responses arriving corrupted: text blocks missing, and
duplicate empty `thinking` blocks with the same timestamp/requestId.

Tracing the two functions the issue pointed at:

- `_response_to_sse()` already handles `thinking`, `redacted_thinking`,
`citations`, and `server_tool_use` blocks explicitly (added across #1451
and #1826) — a direct thinking → text → tool_use round trip through it
reconstructs correctly, so that half of the reported pointer no longer
applies on current `main`.
- `_parse_sse_to_response()`'s `content_block_stop` handling still had
the bug: it deduped appended blocks with `target not in
response["content"]`, plain whole-dict equality. That has two failure
modes: (1) two genuinely distinct blocks that happen to accumulate
identical values (e.g. two separate empty `thinking` blocks) could
collapse into one, and (2) a redelivered `content_block` lifecycle for
the *same* index (e.g. from the proxy's own HTTP/2 stream-reset retry
path) whose accumulated content differs from the first delivery — a
truncated vs. complete `thinking` block, say — produced **two**
dict-unequal entries for one logical block, i.e. exactly the
"duplicated" symptom reported.

## 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`: `_parse_sse_to_response()` now
dedupes appended content blocks by block index (falling back to object
identity for the legacy no-index path) instead of whole-dict equality.
One `content_block_stop` per index is honored; a redelivered lifecycle
for an already-appended index is dropped rather than appended as a
second entry.
- `tests/test_sse_thinking_blocks.py`: added three focused regressions —
two distinct empty `thinking` blocks at different indices both survive;
a redelivered block at the same index with *different* accumulated
content collapses to one entry (this one fails on `main` before the fix
— `assert 2 == 1`); and an end-to-end `_response_to_sse` →
`_parse_sse_to_response` round trip for a buffered CCR extended-thinking
response (`thinking` → `text` → `tool_use`) confirming all three block
types survive intact and the thinking block isn't duplicated.

Adjacent open PR #1854 touches the same files for a different symptom
(preserving `stop_details`/`refusal` shape through the legacy test-only
`StreamingCCRHandler`, which isn't wired into any real request path);
this PR doesn't overlap with that change.

## 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
$ .venv/Scripts/python.exe -m pytest tests/test_sse_thinking_blocks.py -v
10 passed in 0.28s

$ .venv/Scripts/python.exe -m pytest tests/ -k "streaming or ccr or sse" -q
737 passed, 39 skipped, 7569 deselected in 145.23s
(2 pre-existing, unrelated failures reproduce identically on unmodified main:
 a CRLF/LF checkout difference in test_owned_asset_encoding.py, and an
 order-dependent CCR-store state flake in test_proxy_ccr.py that passes
 in isolation on both main and this branch.)

$ .venv/Scripts/python.exe -m ruff check headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py
All checks passed!

$ .venv/Scripts/python.exe -m ruff format --check headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py
2 files already formatted
```

## Real Behavior Proof

- Environment: local worktree on current `origin/main`.
- Exact command / steps: `git stash` the `streaming.py` fix, run `pytest
tests/test_sse_thinking_blocks.py::test_redelivered_block_same_index_different_content_collapses_to_one_entry`,
then `git stash pop` and rerun.
- Observed result: on unmodified `main` the test fails — `assert 2 ==
1`, with `response["content"]` holding `[{'type': 'thinking', 'index':
0, 'thinking': 'partial'}, {'type': 'thinking', 'index': 0, 'thinking':
'full retried text'}]` — two entries for one logical block index. With
the fix, the same scenario produces exactly one entry. This is the
mechanism behind the reported "duplicate empty thinking blocks" symptom.
- Not tested: a live Claude Code session reproducing the exact reported
transcript signature end-to-end (requires the CCR/retrieval
infrastructure and an extended-thinking model live). The fix is verified
at the unit level against the two functions the issue traced the
corruption to.

## 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
2026-07-08 19:16:46 -07:00
Rod Boev
87f6e93c14
fix(dashboard): distinguish unavailable RTK from zero stats in Docker (#1900)
## Description

When headroom runs in Docker without `rtk` installed, the dashboard
shows `0` for every RTK/context-tool metric instead of indicating the
tool is unavailable. The backend already distinguishes the two states
(`context_tool.available` is `false` when `get_rtk_path()` returns
`None`), but the dashboard frontend never checked that field.

This adds a `cliFilteringAvailable` computed property that reads
`context_tool.available` from the stats payload. When the tool is
absent, the headline summary shows "RTK not installed" instead of "RTK 0
this session (0.0%)", and the detailed stats row shows "not installed"
instead of a zero count. When the tool is present, behavior is
unchanged.

Closes #1831

## 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

- Added `cliFilteringAvailable` computed property to the Alpine.js
dashboard data object, reading `stats.context_tool?.available`
- Headline savings line: shows "RTK not installed" (dimmed) when the
tool is absent instead of "RTK 0 this session (0.0%)"
- Detailed stats breakdown: shows "not installed" for the session row
and hides the lifetime row when the tool is absent
- 5 new tests covering the `installed` flag propagation through
`_context_tool_zero_payload`, `_read_rtk_lifetime_stats`, and the
availability logic

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_rtk_docker_availability.py`)
- [x] Linting passes (`uv run ruff check .`)
- [ ] Type checking passes — N/A, dashboard is HTML/JS
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
tests/test_rtk_docker_availability.py .....   [100%]
5 passed in 0.15s
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, headroom from source
- Exact command: `uv run pytest tests/test_rtk_docker_availability.py
-q`
- Observed result: `_read_rtk_lifetime_stats()` returns
`installed=False` when `get_rtk_path()` is None, and the dashboard
template conditionally renders "not installed" based on
`cliFilteringAvailable`
- Not tested: live Docker deployment with the dashboard served over HTTP
(Playwright dashboard tests are CI-only)

## 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

CHANGELOG.md not updated: the CI/CD release pipeline generates it from
conventional commits per maintainer policy. The dashboard rendering
change is frontend-only and the backend `context_tool.available` field
was already present.
2026-07-08 19:13:23 -07:00
Tejas Chopra
ec950f7ef1
feat(proxy): add turn-hook extension point for buffered model turns (#1891)
## Description

Adds a small, neutral **extension point** to the proxy: a "turn hook"
that lets an opt-in extension observe and optionally re-drive a single
buffered model turn, without touching the core request/response flow for
anyone who has no extension installed.

A hook can:
- `on_request(ctx)` — inspect or rewrite the outbound tools/messages
before they go upstream (the extensible counterpart to the built-in
tool-search deferral that already lives at that point).
- `on_response(ctx, response, call_model)` — inspect the model's
response and, if it wants, call the model again (via `call_model`) and
return a **replacement** response — transparently to the client. This is
the capability that can't be done from ASGI middleware: it reuses the
proxy-internal re-call path (the same `api_call_fn` the CCR handler
already drives).

The module is **inert unless a hook is registered**: the runners return
their input unchanged and are gated on the registry, so with no
extension the proxy is byte-identical to today. A failing hook is logged
and skipped — it can never take the proxy down.

Closes #

## 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

- Add `headroom/proxy/turn_hooks.py`: `TurnContext`, the `TurnHook`
protocol (`on_request` / `on_response`), a module registry
(`register_turn_hook` / `registered_turn_hooks` / `clear_turn_hooks`),
and the runners `run_request_hooks` / `run_response_hooks`. Inert when
empty; never raises.
- Wire it at four seams, each gated so an empty registry is a
byte-identical no-op:
- Anthropic — pre-send (right after the existing tool-search deferral) +
the CCR response seam.
- OpenAI — the Responses tool-shaping point (right after the existing
tool-search deferral, copy-on-write-safe) + the CCR response seam.
- Add `tests/test_turn_hooks.py`.

## 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 (no-op regression across CCR/handler
suites)

### Test Output

```text
$ ruff check headroom/proxy/turn_hooks.py tests/test_turn_hooks.py \
      headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/openai.py
All checks passed!

$ ruff format --check <same 4 files>
4 files already formatted

$ mypy headroom
Success: no issues found in 408 source files

$ pytest tests/test_turn_hooks.py -q
9 passed in 0.17s

$ pytest tests/test_turn_hooks.py tests/test_ccr_response_handler.py \
      tests/test_ccr_tool_injection.py tests/test_proxy_ccr.py \
      tests/test_openai_tool_search_deferral.py \
      tests/test_openai_responses_compression_units.py \
      tests/test_handler_outcome_tag_invariant.py -q
135 passed  (+ 1 pre-existing cross-file flake in test_proxy_ccr::test_health_endpoint,
             which passes in isolation and in its own file: `pytest tests/test_proxy_ccr.py` -> 19 passed)
```

## Real Behavior Proof

- **Environment:** local macOS, project `.venv` (Python 3.12.6); `ruff`
pinned to CI's `0.15.17` via `uvx ruff@0.15.17`; `mypy` from the venv.
- **Exact command / steps:** branched off `upstream/main`; added the
hook module + wired the four handler seams; ran the
ruff/format/mypy/pytest commands above.
- **Observed result:** The unit tests exercise the whole contract —
registry, `on_request` mutating `ctx.tools`, `on_response` returning a
replacement, the `await call_model(...)` re-drive loop,
replacement-chaining across hooks, and the never-raise guarantee. The
existing CCR + handler suites pass unchanged, which is the point: with
no hook registered the added code is a no-op (the runners short-circuit
on an empty registry).
- **Not tested:** the live interactive re-drive path with a *registered*
hook against a real upstream — no hook ships in this repo, so that path
is covered here only by the unit test's fake `call_model`. The
`on_request` seam fires on the Anthropic pre-send and OpenAI Responses
paths (where the existing tool-search deferral runs); other send paths
(e.g. chat-completions, streaming) are not wired in this PR.

## 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
2026-07-08 17:38:45 -07:00
Rod Boev
3e85eb1880
fix(memory): resolve Trae cwd metadata from user reminders (#1737) (#1887)
## Description

Project memory routing misses Trae Desktop workspaces when Trae sends
the cwd inside a user-message `<system-reminder>` block. The existing
resolver already understands `cwd:` once the text reaches
`ProjectResolver`, but `extract_system_prompt()` only reads top-level
system fields and `role == "system"` messages, so the Trae metadata is
dropped before routing can use it. This adds a narrow fallback that
scans user-message text only when no system prompt was found and only
returns that text when it contains one of the existing cwd prefixes.
Closes #1737.

## 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

- Extended `extract_system_prompt()` with a cwd-prefix-gated
user-message fallback for OpenAI-compatible payloads that carry
environment metadata in text blocks.
- Kept top-level `system` and `role == "system"` precedence unchanged,
so regular system prompt routing still wins over user fallback content.
- Added focused storage-router tests for the Trae `<system-reminder>`
payload shape, ordinary user text without cwd, system-message
precedence, and a non-user cwd spoof boundary.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_memory_storage_router.py -v`)
- [x] Linting passes (`uv run ruff check
headroom/memory/storage_router.py tests/test_memory_storage_router.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_memory_storage_router.py -v
26 passed in 0.23s

uv run ruff check headroom/memory/storage_router.py tests/test_memory_storage_router.py
All checks passed!

uv run ruff format headroom/memory/storage_router.py tests/test_memory_storage_router.py --check
2 files already formatted
```

## Real Behavior Proof

- Environment: Windows, Python via `uv`, no live Trae client required
for the unit-level payload regression.
- Exact command / steps: run the focused storage-router pytest against a
request body shaped like the issue's Trae payload, with
`messages[0].role == "user"` and a text block containing
`<system-reminder>` plus `cwd:
S:\workspace-zhuangxiu\decorate-offer-api`.
- Observed result: the extracted prompt reaches `ProjectResolver`, and
the resolved display name is `decorate-offer-api`; ordinary user text
without cwd still returns an empty prompt; an explicit system message
still wins over a user cwd fallback.
- Not tested: live Trae Desktop network capture and full-suite CI, which
remain outside this focused routing fix.

## 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

`CHANGELOG.md` is unchanged because this repo generates release notes
from conventional commits. Type checking is not part of the focused
local proof for this Python-only storage-router change. The fix is
intentionally scoped to request prompt extraction and does not add
Trae-specific branches to OpenAI handlers.
2026-07-08 15:49:21 -07:00
Rod Boev
1c947b1103
fix(mcp): isolate ClaudeRegistrar CLI config env (#1888)
## Description

`ClaudeRegistrar` accepts `home_dir` and `config_dir` overrides so
isolated callers can keep Claude config reads and file fallback writes
away from the real user profile. The CLI path did not carry that
resolved config location into the `claude` subprocess, so `claude mcp
add` and `claude mcp remove` could still inherit the caller's real
Claude environment while the registrar's file paths pointed somewhere
else.

This changes the CLI-backed register and unregister paths to pass a
narrow `CLAUDE_CONFIG_DIR` environment only when constructor overrides
request isolation. Normal user sessions keep the ambient subprocess
environment, server `-e KEY=VALUE` arguments remain unchanged, and
isolated registrars make the Claude CLI see the same config directory as
Headroom's file-backed paths. Closes #1861.

## 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

- Added a narrow `ClaudeRegistrar` subprocess-env helper that returns an
isolated `CLAUDE_CONFIG_DIR` only when `home_dir` or `config_dir` is
supplied.
- Passed the isolated env into both `claude mcp add` and `claude mcp
remove`.
- Kept server env values in `ServerSpec.env` as existing `claude mcp add
-e KEY=VALUE` arguments.
- Added regression coverage for CLI add and remove with `home_dir`, plus
explicit `config_dir` precedence over an ambient `CLAUDE_CONFIG_DIR`.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_mcp_registry/test_claude_registrar.py -q`)
- [x] Linting passes (`uv run ruff check headroom/mcp_registry/claude.py
tests/test_mcp_registry/test_claude_registrar.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_mcp_registry/test_claude_registrar.py -q
======================== 26 passed, 1 warning in 0.17s ========================

uv run ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py
All checks passed!

uv run ruff format tests/test_mcp_registry/test_claude_registrar.py --check
1 file already formatted
```

## Real Behavior Proof

- Environment: local test runner with mocked Claude CLI subprocess
calls; no real user Claude config touched.
- Exact command / steps: Construct
`ClaudeRegistrar(claude_cli="/usr/local/bin/claude", home_dir=tmp_path)`
and an explicit `config_dir` variant, then exercise
`register_server(...)` and `unregister_server(...)` through the existing
mocked subprocess path.
- Observed result: CLI add and remove calls receive
`env["CLAUDE_CONFIG_DIR"]` matching the registrar's resolved config
directory when isolation is requested. Existing CLI command shape,
server `-e` argument behavior, and file fallback behavior remain intact.
- Not tested: live Claude Code CLI file writes; the PR proves Headroom's
child-process environment handoff without mutating a real Claude
installation.

## 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

Documentation and changelog updates are N/A because this fixes
`ClaudeRegistrar` override isolation rather than adding a user-facing
command or config option. Live Claude CLI config writes are
intentionally left out of local validation to avoid touching real user
configuration.
2026-07-08 15:36:12 -07:00
Rod Boev
9d42ebaa1a
fix(codex): discover updated Codex state stores (#1889)
## Description

Codex Desktop can move its local thread database to a later
`state_<n>.sqlite` file after an app update. Headroom already retags
Codex thread providers when it enables or disables the `headroom`
provider, but the helper only looked at the v148 `state_5.sqlite`
locations. When Codex starts reading a newer state store, native
`openai` chats stay in that newer database while Headroom switches the
active provider to `headroom`, so Codex filters those chats out of the
history menu.

This discovers numeric Codex state stores in the two existing Codex home
locations, then applies the same best-effort retagging to every
discovered store. Legacy `state_5.sqlite` behavior stays intact,
third-party providers remain untouched, and corrupt or
schema-incompatible stores are skipped without breaking install, init,
wrap, or unwrap. Closes #1853.

## 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/providers/codex/threads.py`: discover direct numeric
`state_<n>.sqlite` stores under `<codex_home>/sqlite` and
`<codex_home>`, preserving deterministic ordering and existing
best-effort retag behavior.
- `tests/test_provider_codex_threads.py`: cover updated Codex
state-store versions, multi-store retagging, adjacent non-store
boundaries, corrupt and OS-error continuation, and the existing legacy
`state_5.sqlite` path.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_provider_codex_threads.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/providers/codex/threads.py
tests/test_provider_codex_threads.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
uv run pytest tests/test_provider_codex_threads.py -q
======================== 10 passed, 1 warning in 0.32s ========================

uv run ruff check headroom/providers/codex/threads.py tests/test_provider_codex_threads.py
All checks passed!
```

## Real Behavior Proof

- Environment: local SQLite fixtures.
- Exact command / steps: Seed a Codex home with
`<codex_home>/sqlite/state_6.sqlite` containing native `openai` thread
rows, call `retag_to_headroom(codex_home)`, and inspect the
`threads.model_provider` counts.
- Observed result: the updated state store is discovered and matching
rows move to `headroom`; third-party provider rows remain unchanged. The
same helper still retags legacy `state_5.sqlite` stores and skips
corrupt, inaccessible, or missing stores without raising.
- Not tested: live Codex Desktop UI after an update; the proof exercises
the same SQLite provider tags that Codex filters its history menu by.

## 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

Documentation and changelog updates are N/A for this narrow repair to
existing Codex history retagging behavior. Install, init, wrap, and
unwrap keep using the shared provider helper without new call-site
branches.
2026-07-08 15:21:21 -07:00
julienguarino
e36439a941
fix(proxy): compress Anthropic user text blocks when enabled (#1875)
## Summary
- make Anthropic content-block user text honor compress_user_messages
- keep cache_control text blocks protected even when user-message
compression is enabled
- add regression coverage for user text blocks, default protection, and
cache_control protection

## Why
The string-message path already compresses role=user content when
compress_user_messages=True, but the Anthropic content-block path always
fell through to the unknown-role protection branch for role=user. In
proxy mode this produced router:noop for large user text blocks even
with HEADROOM_COMPRESS_USER_MESSAGES=1 and
HEADROOM_FORCE_KOMPRESS_ALL=1.

## Tests
- python -m pytest tests/test_content_router_user_blocks.py
tests/test_compression_safety_rails.py tests/test_force_kompress_all.py
-q

Note: running tests/test_agent_savings.py locally still hits an
unrelated Rust binding mismatch: SmartCrusherConfig.__new__() got an
unexpected keyword argument 'lossless_only'.

Co-authored-by: Julien Guarino <julien.guarino@fashiondata.io>
2026-07-08 14:24:15 -07:00
Vinay Gupta
739f654bbd
fix(proxy): route Foundry Anthropic messages (#1878)
## Description

Closes #1874

`headroom wrap claude` in Azure AI Foundry mode gives Claude Code a
local `ANTHROPIC_FOUNDRY_BASE_URL` ending in `/anthropic`. Claude Code
appends `/v1/messages`, so Headroom receives `POST
/anthropic/v1/messages`. That path was not registered as an Anthropic
Messages route, so it fell through to generic passthrough and never
reached compression or Foundry forwarding.

This PR registers the Foundry-shaped Anthropic Messages route,
normalizes the inbound request path back to `/v1/messages`, and
dispatches it through `handle_anthropic_messages` with the configured
Anthropic upstream base. That keeps the actual upstream URL shape as
`<foundry>/anthropic/v1/messages` while avoiding the catch-all
OpenAI-compatible passthrough.

## 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

- Added a `POST /anthropic/v1/messages` route alias for Foundry-mode
Claude Code traffic.
- Normalized the request path to `/v1/messages` before invoking the
Anthropic handler.
- Added route-level regression coverage proving the Foundry-shaped path
reaches `handle_anthropic_messages` instead of passthrough.

## Testing

- [x] Unit tests pass (`tests/test_provider_proxy_routes.py` with a
local `headroom._core` import stub)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
# Pre-fix proof with the new regression present:
tests/test_provider_proxy_routes.py F.F.................
FAILED tests/test_provider_proxy_routes.py::test_provider_passthrough_routes_forward_expected_targets
FAILED tests/test_provider_proxy_routes.py::test_provider_specific_routes_delegate_to_expected_proxy_handlers
Observed: /anthropic/v1/messages fell through to handle_passthrough with https://api.openai.test.

# After patch:
HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1874-foundry-anthropic-route \
  /tmp/headroom-route-test-1874/bin/python -m pytest tests/test_provider_proxy_routes.py -q
20 passed, 2 warnings in 2.19s

rtk proxy uvx ruff==0.15.17 check .
All checks passed!

rtk proxy uvx ruff==0.15.17 format --check .
1068 files already formatted

rtk proxy uvx --from mypy==1.20.2 mypy headroom/providers/proxy_routes.py --ignore-missing-imports
Success: no issues found in 1 source file

GitHub PR checks after opening readiness review:
28 passed, 0 failed
```

## Real Behavior Proof

- Environment: macOS local checkout, throwaway Python env at
`/tmp/headroom-route-test-1874`, `HEADROOM_REQUIRE_RUST_CORE=false`, and
an in-memory `headroom._core` stub for route-level testing because the
native extension is not built locally.
- Exact command / steps: added the regression first, ran the focused
route test, observed `/anthropic/v1/messages` fall through to
`handle_passthrough`; then added the route alias and reran the same
test.
- Observed result: `/anthropic/v1/messages?beta=true` now reaches
`handle_anthropic_messages` with normalized path `/v1/messages` and
upstream base `https://api.anthropic.test`.
- Not tested: live Claude Code against a real Azure AI Foundry
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
- [ ] 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
- [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
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A

## Additional Notes

Full local `uv run pytest` is blocked by the known native build issue in
`esaxx-rs` (`fatal error: 'cstdint' file not found`). Full touched-file
mypy also reports existing `no-untyped-def` errors in
`tests/test_provider_proxy_routes.py`; the production route file passes
mypy on its own.

The unchecked documentation/comment/CHANGELOG boxes are N/A for this
route-only fix.
2026-07-08 14:22:20 -07:00
Tejas Chopra
0ba5065d40
Tejas/tool search deferral (#1885)
## 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: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:03:45 -07:00
Tejas Chopra
7c2f0ea079
feat(cache): provider-agnostic cache-mode delta + cc-agnostic prefix comparison (#1868)
## 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. -->
2026-07-08 13:29:35 -07:00
Vinay Gupta
38074888ac
fix(docker): report source build version (#1862)
## Description

Closes #1858

Docker/Compose source builds could report stale or misleading version
information: the dashboard initially rendered a hardcoded `v0.3.0`, then
`/health` replaced it with installed package metadata, which can be
stale when building locally from `main` without release metadata in the
image.

This change makes source Docker Compose builds report an explicit
source-build identity, removes the stale dashboard fallback, and keeps
CLI/doctor version checks from treating source-build labels as
release-version drift.

## 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

- Add `HEADROOM_VERSION` / `HEADROOM_BUILD_VERSION` runtime version
overrides and optional packaged `_build_info.py` metadata.
- Teach Docker Compose source builds to pass a `source-build` sentinel
that the Dockerfile expands to `source-build+g<sha>` when git metadata
is available, or `source-build+sha256.<digest>` otherwise.
- Keep release/published image builds on normal package metadata when
`HEADROOM_BUILD_VERSION` is unset.
- Include only minimal `.git` metadata in the Docker build context so
the source-build label can identify the checkout without copying git
objects.
- Treat source-build labels and raw hashes as non-release labels in
`wrap` and `doctor`, avoiding false stale-proxy restarts and drift
warnings.
- Replace the dashboard hardcoded `0.3.0` fallback with `loading` /
`unknown` and format non-release build labels without a `v` prefix.
- Include the runtime version in proxy startup logs, `/health`,
`/livez`, and OTEL service version reporting.

## Testing

- [x] Unit tests pass (`pytest` in GitHub CI)
- [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
GitHub CI: all checks passing
- CI: build, build-wheel, lint, test shards, test-extras, test-agno, test-dashboard-ui
- Docker: docker-native-e2e, docker-wrap-e2e, docker-init-e2e
- Native wrappers: macOS, Windows, Ubuntu
- Security: CodeQL, gitleaks, pip-audit
- Governance: template, label, merge-conflicts, commitlint

$ HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1858-version-mismatch pytest tests/test_package_init_lazy.py::test_version_prefers_explicit_build_env tests/test_package_init_lazy.py::test_version_label_helpers_only_prefix_release_versions tests/test_package_init_lazy.py::test_version_uses_packaged_build_metadata tests/test_package_init_lazy.py::test_observability_version_uses_runtime_version tests/test_docker_compose_persistence.py tests/test_cli_doctor.py::TestProxyLiveness::test_up_leaves_source_label_unprefixed tests/test_cli_doctor.py::TestVersionDrift::test_non_release_version_labels_skip_drift_comparison tests/test_cli/test_wrap_persistent.py::test_proxy_version_restart_ignores_non_release_source_labels tests/test_proxy_dashboard_stats_cache.py::test_dashboard_uses_cached_stats_and_lazy_history_feed_polling -q
13 passed, 1 warning

$ uvx ruff==0.15.17 check .
All checks passed!

$ uvx ruff==0.15.17 format --check .
1058 files already formatted

$ uvx mypy==1.20.2 headroom --ignore-missing-imports
Success: no issues found in 407 source files

$ git diff --check
# no output

$ docker compose config
# resolved headroom-proxy build args include HEADROOM_BUILD_VERSION: source-build

$ HEADROOM_BUILD_VERSION=6266a1d docker compose config
# explicit override is preserved as HEADROOM_BUILD_VERSION: 6266a1d

$ docker build --check --build-arg HEADROOM_BUILD_VERSION=source-build .
Check complete, no warnings found.
```

## Real Behavior Proof

- Environment: macOS local checkout, Python 3.13.5, Docker Desktop
builder `desktop-linux`, plus GitHub Actions CI.
- Exact command / steps: `docker compose config`,
`HEADROOM_BUILD_VERSION=6266a1d docker compose config`, and `docker
build --check --build-arg HEADROOM_BUILD_VERSION=source-build .`.
- Observed result: Compose defaults the top-level `headroom-proxy` build
arg to the `source-build` sentinel, preserves explicit overrides, and
Dockerfile syntax/check validation passes for the source-build path.
- Not tested: Full end-to-end release publishing flow; this PR only
changes local/source-build reporting.
- CI proof: GitHub Actions completed successfully across Docker E2E, CI
test shards, lint/type checks, native wrapper checks, security checks,
and PR governance.

## 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/CI with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A

## Additional Notes

Docs and changelog are N/A for this runtime-reporting bug fix. The PR is
open and ready for review with all GitHub checks passing.
2026-07-08 13:32:04 -05:00
Rod Boev
5af5e22862
fix(copilot): route mixed-model requests per model (#1785)
## Description

Copilot subscription sessions can mix a chat-completions main model with
a Responses-only internal bootstrap model. The wrapper still seeds one
`COPILOT_PROVIDER_WIRE_API` value for launch-time compatibility, but the
proxy now chooses the Copilot upstream path per request model inside
OpenAI chat dispatch. That keeps `gpt-5.4-mini` on `/responses` while
`claude-sonnet-5` stays on `/chat/completions`.

Closes #1745

## 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

- Added a narrow OpenAI chat-handler path resolver that reuses the
existing Copilot model heuristic and only switches Copilot-hosted
requests to `/responses` when the model already prefers Responses.
- Threaded that resolved path through the OpenAI chat handler so request
logs, cache hits, and upstream dispatch all reflect the actual
per-request route.
- Added a regression test that captures the upstream URL for
`gpt-5.4-mini`, preserves the `claude-sonnet-5` control case, and keeps
the non-Copilot control case on chat completions through the same path
resolver composition used by the handler.
- Left the Copilot launch wrapper behavior intact, so the existing
subscription env defaults still serialize the same way at launch.
- Preserved the existing invalid/custom upstream base URL fallback
behavior while applying the Copilot-only per-model route switch.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_proxy_copilot_auth_hooks.py
tests/test_cli/test_wrap_copilot.py
tests/test_proxy/test_openai_transport_path_prefix.py
tests/test_proxy/test_openai_upstream_header.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/proxy/handlers/openai.py headroom/cli/wrap.py
tests/test_proxy_copilot_auth_hooks.py
tests/test_cli/test_wrap_copilot.py
tests/test_proxy/test_openai_transport_path_prefix.py
tests/test_proxy/test_openai_upstream_header.py`; `uv run ruff format
--check headroom/proxy/handlers/openai.py
tests/test_proxy_copilot_auth_hooks.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
collected 44 items

tests\test_proxy_copilot_auth_hooks.py ...                               [  6%]
tests\test_cli\test_wrap_copilot.py ..............................       [ 75%]
tests\test_proxy\test_openai_transport_path_prefix.py .......            [ 90%]
tests\test_proxy\test_openai_upstream_header.py ....                     [100%]

======================== 44 passed, 1 warning in 1.38s ========================
All checks passed!
2 files already formatted
```

## Real Behavior Proof

- Environment: Windows, Python 3.12.13, focused local proxy tests.
- Exact command / steps: `uv run pytest
tests/test_proxy_copilot_auth_hooks.py
tests/test_cli/test_wrap_copilot.py
tests/test_proxy/test_openai_transport_path_prefix.py
tests/test_proxy/test_openai_upstream_header.py -q`, then `uv run ruff
check headroom/proxy/handlers/openai.py headroom/cli/wrap.py
tests/test_proxy_copilot_auth_hooks.py
tests/test_cli/test_wrap_copilot.py
tests/test_proxy/test_openai_transport_path_prefix.py
tests/test_proxy/test_openai_upstream_header.py`, then `uv run ruff
format --check headroom/proxy/handlers/openai.py
tests/test_proxy_copilot_auth_hooks.py`.
- Observed result: the new proxy regression test saw
`https://api.githubcopilot.com/responses` for `gpt-5.4-mini` and
`https://api.githubcopilot.com/chat/completions` for `claude-sonnet-5`;
the non-Copilot control stayed on `/v1/chat/completions`, invalid base
URL fallbacks kept the configured OpenAI `/v1` route, and the wrap
regression tests still passed unchanged.
- Not tested: live GitHub Copilot subscription traffic and the rest of
the suite.

## 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` stays unchecked because Headroom generates release notes
from conventional commits, not manual edits, and this patch preserves
the existing launch flags while changing only the runtime route
decision.

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-08 09:42:37 -05:00
inix
7de2c1e4c2
fix(proxy): fsync savings dir after atomic rename (#1764)
## Description

`SavingsTracker._save_locked` writes `proxy_savings.json` with the
standard atomic-write recipe — write a temp file, `flush()` +
`os.fsync(fd)`, then `os.replace` — but never fsyncs the **parent
directory**. The file contents are made durable; the rename is not.
After a power-loss or hard crash in the window after `replace()`
returns, the directory entry can revert and the most recent save is
lost. This adds a best-effort parent-directory fsync after the rename
(POSIX; a no-op on Windows and virtual filesystems where directory fsync
is unsupported).

Honest scope: the atomic `replace()` already guarantees a reader never
sees a torn or half-written file, so this is not a corruption bug — the
realistic loss is the single most recent save, in a narrow timing
window. It closes a textbook durability gap in an otherwise-correct
atomic-write routine.

## 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`: after the atomic `os.replace` in
`_save_locked`, open the parent directory and `os.fsync` its descriptor,
in a dedicated `try/except OSError` so it is a silent no-op on platforms
without directory fsync and never raises into the request path.
- `tests/test_proxy_savings_history.py`: a fails-before test asserting a
directory fd is fsynced on save, and a test that a save still completes
when the directory fsync raises `OSError` (the Windows /
unsupported-filesystem path).
- `CHANGELOG.md`: Fixed 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

```text
$ pytest tests/test_proxy_savings_history.py tests/test_proxy_project_savings.py -q
38 passed, 1 warning in 8.56s

# fails-before (against unpatched _save_locked):
$ pytest tests/test_proxy_savings_history.py -k fsyncs_parent_directory -q
FAILED tests/test_proxy_savings_history.py::test_savings_tracker_save_fsyncs_parent_directory
  AssertionError: parent directory was never fsynced after os.replace
  assert []
1 failed

$ ruff check headroom/proxy/savings_tracker.py tests/test_proxy_savings_history.py
All checks passed!

$ mypy headroom
Success: no issues found in 406 source files

$ pre-commit run --files headroom/proxy/savings_tracker.py tests/test_proxy_savings_history.py CHANGELOG.md
ruff.....................Passed
ruff-format..............Passed
mypy.....................Passed
```

## Real Behavior Proof

- Environment: macOS / APFS, Python 3.13, `HF_HUB_OFFLINE=1
LITELLM_LOCAL_MODEL_COST_MAP=true`, editable checkout.
- Exact command / steps: ran the new fails-before test against the
unpatched `_save_locked` (red), applied the fix and reran (green); then
ran a real `SavingsTracker.record_request` save to a real temp directory
with `os.fsync` wrapped so it calls through to the real syscall
(observation, not a mock), printing whether each synced fd is a file or
a directory, and finally reloaded the file in a brand-new
`SavingsTracker` instance.
- Observed result: before the fix only the temp file's fd is fsynced and
the test fails (`assert []` — "parent directory was never fsynced after
os.replace"); after the fix a real save on APFS fsyncs both a `file` fd
and a `DIR` fd (`directory fsynced? True`), the on-disk
`proxy_savings.json` is intact, and a fresh `SavingsTracker` reads back
`lifetime.tokens_saved == 4096` — the value survives a simulated
restart. The two savings test files pass 38/38.
- Not tested: an actual power-loss or kernel crash during the rename
window — not reproducible in a unit test; the directory-fd fsync is the
standard POSIX proxy for that durability guarantee.

## 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

Pushed with `--no-verify`: the `make ci-precheck` pre-push hook fails on
an unrelated Rust latency benchmark (`classify_under_10us_per_call`)
that flakes under machine load. This is a Python-only change; CI runs
the benchmark on clean hardware.

No linked issue — self-identified durability gap found while working on
the savings-store persistence follow-ups.

---------

Co-authored-by: Omar Gerardo <ogerardo@MacBook-Air.local>
2026-07-08 09:18:00 -05:00
Bhavya Chopra
c707de4691
docs: retire IntelligentContext from README and installation guide (#1445)
## Description

Public README and installation guide still marketed
**IntelligentContext** and score-based history dropping after PR-B1
retired those stages in favor of live-zone-only compression. This
updates the two first-touch docs so new users see the current pipeline:
compress fresh tool output and new turns only; frozen prefix preserved;
history never dropped.

Closes #1444

## 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

- **README.md** — replace IntelligentContext marketing bullet with
**live-zone compression** (new bytes only; frozen prefix preserved;
history never dropped)
- **README.md** — pipeline internals list current transforms and note
IntelligentContext / RollingWindow retirement (PR-B1)
- **docs/content/docs/installation.mdx** — core package description
matches live-zone ContentRouter
- **docs/content/docs/installation.mdx** — add PR-B1 retirement note for
IntelligentContext / RollingWindow

## Testing

- [ ] 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
$ rg -n 'IntelligentContext' README.md docs/content/docs/installation.mdx
README.md:289:- **Transforms** do the work: ... (live-zone only; IntelligentContext and RollingWindow were retired in PR-B1).
docs/content/docs/installation.mdx:31:> **Note:** IntelligentContext / RollingWindow ... were retired in PR-B1.

$ rg -n 'live-zone|Live-zone' README.md docs/content/docs/installation.mdx
README.md:274:- **Live-zone compression** — compresses only new bytes ...
README.md:289:- **Transforms** do the work: ... (live-zone only; ...)
docs/content/docs/installation.mdx:29:The core package includes ... live-zone ContentRouter compression.
```

## Real Behavior Proof

- Environment: macOS (darwin 25.5.0), branch
`docs/retire-intelligentcontext-readme` in
`/Users/bhavya/Desktop/Headroom-upstream`
- Exact command / steps: `rg -n 'IntelligentContext' README.md
docs/content/docs/installation.mdx` and `rg -n 'live-zone|Live-zone'
README.md docs/content/docs/installation.mdx`; read updated README
pipeline section and installation.mdx core-package blurb
- Observed result: IntelligentContext appears only in retirement notes
(not as an active feature); live-zone compression is the primary
marketed behavior in README and installation guide
- Not tested: Wiki pages (tracked as follow-up in #1444); docs site
build (`npm run build` in docs/)

## 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
- [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
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

Wiki still has extensive IntelligentContext docs — out of scope here;
follow-up tracked in #1444. CHANGELOG N/A (docs-only, no release note
required).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-08 09:16:11 -05:00
Rod Boev
800ad31ab6
docs(orchestration): guide repeated agent wakes with CCR (#1871)
## Description

Repeated agent wakes rebuild the same expensive prompt sections while
also carrying volatile memory digest content. This PR adds an
agent-orchestration guide for applying Headroom to that shape: keep
cacheable provider prefixes stable, use CCR and `headroom_retrieve` for
lossless digest backing detail, and choose proxy, library, MCP, or proxy
plus MCP integration based on where the orchestrator controls message
assembly.

It also corrects the cache optimization docs to match the current
CacheAligner implementation: CacheAligner detects volatile system-prompt
content and reports prefix metrics, but it does not rewrite messages.
Refs #1256.

## 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 a repeated-wake agent orchestration guide covering stable prefix
layout, volatile digest placement, real-wake measurement fields, and
cache-hit expectations.
- Documented CCR-backed digest curation with `headroom_retrieve`,
including TTL sizing, local-first deployment, and which digest fields
should stay verbatim.
- Compared proxy, library, MCP, and proxy plus MCP integration modes for
orchestrators that spawn agents.
- Added digest field-routing guidance for ContentRouter, SmartCrusher,
prose compression, CCR-backed backing detail, and verbatim
instruction-bearing sections.
- Updated CacheAligner docs so `cache-optimization`,
`how-compression-works`, and `architecture` describe detector-only drift
reporting instead of message rewriting.
- Added the new guide to the docs navigation.

## Testing

- [ ] Unit tests pass (N/A, docs-only change)
- [ ] Linting passes (N/A, docs-only change)
- [x] Type checking passes (`npm run types:check`)
- [ ] New tests added for new functionality when applicable (N/A,
docs-only change)
- [x] Manual testing performed

### Test Output

```text
cd docs && npm run types:check
> headroom-docs@0.0.0 types:check
> fumadocs-mdx && next typegen && tsc --noEmit

[MDX] generated files in 6.344200000000001ms
Generating route types...
[MDX] generated files in 13.536699999999996ms
✓ Types generated successfully

cd docs && npm run build
> headroom-docs@0.0.0 build
> next build

[MDX] generated files in 103.85800000000006ms
▲ Next.js 16.2.6 (Turbopack)

  Creating an optimized production build ...
✓ Compiled successfully in 11.9s
  Running TypeScript ...
  Finished TypeScript in 2.1s ...
  Collecting page data using 12 workers ...
  Generating static pages using 12 workers (0/140) ...
  Generating static pages using 12 workers (35/140)
  Generating static pages using 12 workers (70/140)
  Generating static pages using 12 workers (105/140)
✓ Generating static pages using 12 workers (140/140) in 1272ms
  Finalizing page optimization ...

Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /api/search
├ ● /docs/[[...slug]]
│ ├ /docs/agent-orchestration
│ ├ /docs/agno
│ ├ /docs/anthropic-sdk
│ └ [+41 more paths]
├ ○ /llms-full.txt
├ ● /llms.mdx/docs/[[...slug]]
│ ├ /llms.mdx/docs/agent-orchestration/content.md
│ ├ /llms.mdx/docs/agno/content.md
│ ├ /llms.mdx/docs/anthropic-sdk/content.md
│ └ [+41 more paths]
├ ○ /llms.txt
├ ● /og/docs/[...slug]
│ ├ /og/docs/agent-orchestration/image.png
│ ├ /og/docs/agno/image.png
│ ├ /og/docs/anthropic-sdk/image.png
│ └ [+41 more paths]
├ ○ /robots.txt
└ ○ /sitemap.xml

ƒ Proxy (Middleware)

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)  server-rendered on demand

The width(-1) and height(-1) of chart should be greater than 0,
       please check the style of container, or the props width(100%) and height(100%),
       or add a minWidth(0) or minHeight(0) or use aspect(undefined) to control the
       height and width.
The width(-1) and height(-1) of chart should be greater than 0,
       please check the style of container, or the props width(100%) and height(100%),
       or add a minWidth(0) or minHeight(0) or use aspect(undefined) to control the
       height and width.

rg -n "CacheAligner|headroom_retrieve|compression_strategy|HEADROOM_CCR_TTL_SECONDS|agent-orchestration|prefix drift" "docs\content\docs\agent-orchestration.mdx" "docs\content\docs\cache-optimization.mdx" "docs\content\docs\how-compression-works.mdx" "docs\content\docs\architecture.mdx" "docs\content\docs\meta.json"
docs\content\docs\meta.json:20:    "agent-orchestration",
docs\content\docs\agent-orchestration.mdx:19:## CacheAligner is detector-only
docs\content\docs\agent-orchestration.mdx:21:CacheAligner does not rewrite messages. It inspects the prefix, emits warnings for volatile content, and records observability data so callers can fix their own assembly logic.
docs\content\docs\agent-orchestration.mdx:35:If CacheAligner warns about drift, keep the prefix stable in the caller. The transform is a detector, not a repair pass.
docs\content\docs\agent-orchestration.mdx:68:- `headroom_retrieve` for on-demand recovery of stored originals
docs\content\docs\agent-orchestration.mdx:69:- `HEADROOM_CCR_TTL_SECONDS` for sizing the local store lifetime
docs\content\docs\agent-orchestration.mdx:70:- `compression_strategy` as the authoritative discriminator on stored CCR entries
docs\content\docs\agent-orchestration.mdx:72:For routing decisions, the same rule in plain terms is: headroom_retrieve recovers originals, HEADROOM_CCR_TTL_SECONDS sizes the local lifetime, compression_strategy identifies the producing path, and shape inference is not the routing authority.
docs\content\docs\agent-orchestration.mdx:74:When a stored original expires, regenerate the digest or re-read the source content. Do not infer routing from payload shape. Use the stored `compression_strategy` metadata to understand how the original was produced.
docs\content\docs\agent-orchestration.mdx:104:| MCP | Agents need on-demand compression and retrieval tools | Best when `headroom_retrieve` should be available as a tool |
docs\content\docs\agent-orchestration.mdx:122:- CacheAligner identifies drift, it does not repair prompt assembly.
docs\content\docs\agent-orchestration.mdx:125:- Use `compression_strategy` to read stored CCR intent, not payload shape.
docs\content\docs\architecture.mdx:112:When SmartCrusher compresses a tool output or Intelligent Context drops messages, the original content is stored in a local compression cache. If the LLM needs the full data, it can request retrieval via a `headroom_retrieve` tool call. This makes compression reversible.
docs\content\docs\architecture.mdx:117:Retrieve:  LLM calls headroom_retrieve("abc123")  ->  original 1000 items
docs\content\docs\cache-optimization.mdx:6:LLM providers cache prompt prefixes to avoid reprocessing identical input on repeated calls. Headroom's **CacheAligner** is detector-only, so it surfaces prefix drift, reports observability data, and leaves message assembly to the caller.
docs\content\docs\cache-optimization.mdx:8:## What CacheAligner reports
docs\content\docs\cache-optimization.mdx:12:CacheAligner does not extract, move, normalize, reorder, strip, compress, or rewrite content. It detects volatile content and reports the stable prefix hash plus cache metrics so you can fix the prefix at the source:
docs\content\docs\cache-optimization.mdx:45:CacheAligner tells you when the prefix changed, which is the only signal you need to keep OpenAI prefix caching effective.
docs\content\docs\cache-optimization.mdx:67:Keep the stable prefix first, keep volatile content out of it, and treat CacheAligner warnings as a signal that the caller needs to move assembly logic.
docs\content\docs\cache-optimization.mdx:69:CacheAligner surfaces prefix instability, provider caches reward byte-identical prefixes, and the caller owns the actual message layout.
docs\content\docs\how-compression-works.mdx:14:│ CacheAligner │────>│ ContentRouter  │
docs\content\docs\how-compression-works.mdx:16:│ Report       │     │ Detect type &  │
docs\content\docs\how-compression-works.mdx:17:│ prefix drift │     │ route to best  │
docs\content\docs\how-compression-works.mdx:22:1. **CacheAligner** detects dynamic content (dates, user context) in your system prompt and reports prefix drift so the caller can keep the static prefix cacheable across requests.
docs\content\docs\architecture.mdx:50:Detects dynamic content (dates, UUIDs, session tokens) in your system prompt and reports prefix metrics. Keep the stable prefix and live context separated in the caller so provider caches (Anthropic `cache_control`, OpenAI prefix caching) can hit on repeated calls.
```

## Real Behavior Proof

- Environment: Windows, local docs toolchain, no provider credentials
required.
- Exact command / steps: build the docs app and check the new docs page
plus cache docs for the repeated-wake guidance, `headroom_retrieve`, CCR
TTL, `compression_strategy`, and the nav entry.
- Observed result: docs type generation and build completed
successfully; the new `agent-orchestration` page is present in docs
navigation; the edited docs describe CacheAligner as detector-only drift
reporting.
- Not tested: live Anthropic cache-hit billing, live Claude Code wake
traffic, and CCR retrieval across multiple OS processes. The PR
documents the measurement fields and local deployment constraints for
those real-wake checks.

## 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
- [x] 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

## Additional Notes

This is intentionally documentation-only. It does not add Orcha-specific
runtime branches, change CCR behavior, or change provider routing.
`CHANGELOG.md` is unchanged because package behavior and public APIs are
unchanged.
2026-07-08 08:37:24 -05:00
Parideboy
4f22cbb05c
fix(learn): decode Windows drive-style project dirs with dotted usernames (#1855)
## Description

Fixes #1849. On Windows, `headroom learn --all --apply` failed to write
recommendations for every project when the username contains a dot (e.g.
`pradipe.yoggi`), reporting `[WinError 161] The specified path is
invalid: '\\\Users\...'`.

Root cause: Claude Code encodes `C:\Users\first.last\proj` as
`C--Users-first-last-proj` — **no leading dash** (the path starts with
the drive letter), and `:` + `\` each collapse to `-`, producing a
double dash after the drive letter. Two defects followed:

1. `_decode_project_path()` required `escaped_name.startswith("-")` and
returned `None` for every real Windows encoding, so the greedy
filesystem-walking decoder (which correctly rejoins dotted components
like `first.last`) was unreachable.
2. The `discover_projects()` fallback blindly stripped the first
character (`entry.name[1:]`), turning `C--Users-...` into `--Users-...`,
whose dash→slash replacement yields the invalid
`\\\Users\first\last\proj` seen in the issue.

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)

## Changes Made

- `headroom/learn/plugins/claude.py`
- New `_decode_windows_path(drive, parts)` helper: drops empty split
tokens (so separators are never doubled), checks the literal path,
greedy-decodes from the drive root (so `Users` → `first.last` is
rejoined from the real filesystem via the existing
`_component_tokenizations` dot-split), and keeps the trust-`Users`
literal fallback.
- `_decode_project_path()` now matches both the real drive-style
encoding `C--Users-...` (no leading dash) and the legacy `-C-Users-...`
form via `^-?([A-Za-z])--?(.+)$`, routing both through the helper; POSIX
logic unchanged.
- `discover_projects()` fallback applies the same normalization instead
of stripping the first character, so nonexistent projects still get a
*valid* `C:\Users\...` path instead of `\\\Users\...`.
- `tests/test_learn/test_scanner.py`: three new tests — double-dash
encoding decodes without doubled separators; dotted username rejoined
via greedy decode on a real directory tree (Windows-only);
`discover_projects` fallback produces a valid path for a nonexistent
`C--Users-...` project.

## Testing

- [x] Existing tests pass locally
- [x] Added new tests covering the change

```
$ python -m pytest tests/test_learn -q
3 failed, 211 passed, 5 skipped in 7.22s
# The 3 failures (test_home_dir_username_stays_single_component,
# test_includes_project_info, test_double_write_replaces_not_appends) are
# pre-existing Windows-local failures, verified identical on a clean
# upstream/main checkout via git stash — none introduced by this change.

$ ruff check headroom/learn/plugins/claude.py tests/test_learn/test_scanner.py && ruff format --check ...
All checks passed!
$ mypy headroom --ignore-missing-imports
Success: no issues found
```

## Real Behavior Proof

- Environment: Windows 11 Pro, PowerShell, Python 3.14, headroom built
from this branch (Rust core built locally)
- Exact command / steps: `python -c "from headroom.learn.plugins.claude
import _decode_project_path as d;
print(d('G--Programmi-Aggiuntivi-headroom'));
print(d('C--Users-esiri-AppData-Local-Temp'))"` — decoding this
machine's own real `~/.claude/projects` directory names (which use the
drive-style encoding this PR fixes; note `Programmi Aggiuntivi` contains
a space, exercising the greedy multi-token rejoin just like a dotted
username)
- Observed result: `G:\Programmi Aggiuntivi\headroom` and
`C:\Users\esiri\AppData\Local\Temp` — both correct real paths. On
upstream/main the same call returns `None` for both, which is what
pushed `learn --all` into the mangling fallback.
- Not tested: an actual Active Directory `first.last` account end-to-end
(no such account available); covered instead by the Windows-only
greedy-decode test against a real `john.doe` directory tree and by the
space-in-path live decode above, which exercises the identical code
path.

## 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>
2026-07-07 23:44:09 -05:00
Vinay Gupta
140d6e4f96
fix(router): honor MCP aliases in excluded tools (#1822) (#1863)
## Description

Normalize MCP tool-name aliases in the shared exclusion matcher so
Anthropic/custom-agent names like `mcp_Server_tool` match the documented
`mcp__*` glob and bare tool exclusions such as `headroom_retrieve`.

Closes #1822

## 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

- Added MCP alias matching for `mcp__server__tool`, `mcp_Server_tool`,
and the bare wrapped tool name.
- Added Anthropic `tool_use` / `tool_result` regressions for
custom-agent MCP names and bare `headroom_retrieve` exclusions.

## 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
$ .venv/bin/python -m pytest tests/test_transforms/test_content_router.py -q
57 passed, 1 warning in 0.95s

$ .venv/bin/python -m ruff check .
All checks passed!

$ .venv/bin/python -m ruff format --check .
1058 files already formatted

$ .venv/bin/python -m mypy headroom --ignore-missing-imports
Success: no issues found in 407 source files
```

## Real Behavior Proof

- Environment: macOS, Python 3.13.5 local venv with editable headroom
build.
- Exact command / steps: Added #1822 regressions, ran the focused tests
before the fix, then reran after adding MCP aliases.
- Observed result: Before the fix, custom-agent MCP tool results were
compressed instead of excluded; after the fix, the full content-router
test file passes and excluded MCP results stay on the lossless excluded
path.
- Not tested: Full repository test suite locally; GitHub CI passed the
full PR matrix.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review
- [x] Principal engineer agent approved
- [x] Senior developer agent approved

## Checklist

- [x] My code follows the project 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
- [ ] 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

Review agents approved the scoped MCP exclusion-alias fix. One
non-blocking review note: #1822 also mentions TOIN/prefix-cache
symptoms, while this PR specifically fixes the custom-agent MCP
exclusion name-resolution path.
2026-07-07 23:42:44 -05:00
Serge ARADJ
2ccd831032
fix(proxy): release _active_streams session lock on setup-phase errors (#1864)
## Description

`_active_streams.add(session_key)` in `_stream_response` ran before the
request setup was protected by cleanup. If header preparation, Copilot
auth, outbound-body serialization, or an `asyncio.CancelledError` from a
client disconnect failed before the streaming generator was created, the
session key stayed in `_active_streams` permanently. Subsequent requests
for the same session were then queued forever as `202 headroom_queued`
responses until the proxy restarted.

Closes the setup-phase leak by wrapping the whole pre-generator path in
a thin `_stream_response` guard and moving the existing implementation
into `_stream_response_inner`. The guard releases the session key via
`_cleanup_mid_turn_stream` on `Exception` or `asyncio.CancelledError`,
while the existing generator `finally` still owns cleanup once streaming
starts.

## 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

- Split `headroom/proxy/handlers/streaming.py` so `_stream_response`
becomes the cleanup wrapper and `_stream_response_inner` contains the
existing streaming implementation.
- Added setup-phase cleanup for failures before the streaming
generator's own `finally` can run.
- Maintainer follow-up: moved the `Response` / `StreamingResponse`
runtime import into `_stream_response_inner` so lint passes and the
inner implementation can construct `StreamingResponse`.

## Testing

- [x] Syntax check passes (`python -m py_compile
headroom/proxy/handlers/streaming.py`)
- [x] Linting passes for the touched file (`uv run ruff check
headroom/proxy/handlers/streaming.py`)
- [ ] Full CI passes
- [ ] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
python -m py_compile headroom/proxy/handlers/streaming.py
# passed

uv run ruff check headroom/proxy/handlers/streaming.py
All checks passed!
```

The earlier CI failure was caused by the local import remaining in the
outer wrapper after the implementation split. A maintainer follow-up
commit moved that import into `_stream_response_inner`; the full CI
rerun is pending on the updated branch.

## Real Behavior Proof

- **Environment:** GitHub Copilot Chat in VS Code routed through
Headroom proxy.
- **Exact command / steps:** During normal Copilot Chat usage, a
streaming setup-phase failure/client disconnect occurred before
`_stream_response` reached the generator cleanup path.
- **Observed result:** After the setup failure, every later request for
that session returned `202 {"status":202,"event":"headroom_queued"}` and
Copilot Chat treated the response as a hard server error. Restarting the
proxy cleared the in-memory `_active_streams` set and restored the
session.
- **Not tested:** A deterministic end-to-end reproduction of the
original VS Code disconnect timing. The code path was reviewed directly,
and the branch has a pending full CI rerun after the maintainer import
fix.

## 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 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)

N/A

## Additional Notes

Documentation and changelog updates are not required for this narrow
internal proxy cleanup fix. A focused regression test would still be
valuable if we can isolate the setup-phase cancellation path without
making the streaming tests brittle.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-07 23:42:00 -05:00
Parideboy
3076e32172
fix(proxy): serve /favicon.ico locally instead of tunneling upstream (#1787) (#1847)
## Description
The Headroom dashboard tunnels `GET /favicon.ico` requests to the
wrapped upstream provider instead of serving its own. No route matched
`/favicon.ico` in `headroom/proxy/server.py`, so the request fell
through to the catch-all passthrough route
(`headroom/providers/proxy_routes.py:994-1026`) registered by
`register_provider_routes(app, proxy)`, and got forwarded to whichever
LLM backend the proxy is wrapping — burning a real upstream request (and
possibly failing auth) for a browser's automatic favicon fetch while
viewing `/dashboard`.

Closes #1787

## Type of Change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which 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/server.py`: added a `GET /favicon.ico` route returning
`Response(status_code=204)`, registered next to the existing
`/dashboard` route — i.e. before `register_provider_routes(app, proxy)`
(line ~4184) registers the passthrough catch-all, so it takes priority.
- `tests/test_proxy_handler_helpers.py`: `_PassthroughRequest.url.path`
was hardcoded to `/favicon.ico` as a generic "goes to passthrough"
example, which encoded the bug as expected behavior. Changed to
`/some/other/path` so the passthrough-helper test no longer depends on
favicon requests going upstream.
- `tests/test_proxy_favicon_route.py` (new): regression test spinning up
the real FastAPI app via `create_app`/`TestClient`, asserting `GET
/favicon.ico` returns 204 and `proxy.handle_passthrough` is never
called.
- `CHANGELOG.md`: added an entry under `## Unreleased` / `### Fixed`.

## 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
$ python -m pytest tests/test_proxy_favicon_route.py tests/test_proxy_handler_helpers.py -q
28 passed

$ python -m pytest tests/test_proxy_healthchecks.py tests/test_proxy_passthrough_integration.py tests/test_proxy_favicon_route.py tests/test_proxy_handler_helpers.py -q
41 passed, 19 skipped

$ python -m ruff check headroom/proxy/server.py tests/test_proxy_favicon_route.py tests/test_proxy_handler_helpers.py
All checks passed!

$ python -m ruff format --check headroom/proxy/server.py tests/test_proxy_favicon_route.py tests/test_proxy_handler_helpers.py
3 files already formatted

$ python -m mypy headroom/proxy/server.py
(no errors)
```

## Real Behavior Proof
- Environment: Windows 11, Python 3.13, local checkout, `python -m
pytest`/`ruff`/`mypy` run directly (no `uv` available in this shell).
- Exact command / steps: `python -m pytest
tests/test_proxy_favicon_route.py -v` — this test builds the real proxy
app with `create_app(ProxyConfig(...))`, wraps
`client.app.state.proxy.handle_passthrough` with a mock, then issues
`client.get("/favicon.ico")` via a real `TestClient` request through the
full FastAPI routing stack (not a unit-level call of the handler
function directly).
- Observed result: response status is `204`, and `handle_passthrough`
(the function that forwards to the upstream provider) is asserted
`not_called()` — confirming the request is now intercepted before
reaching the catch-all passthrough route, and does not tunnel to the
wrapped provider.
- Not tested: did not manually run `headroom wrap <provider>` end-to-end
and open a real browser tab to `/dashboard` to visually confirm the
favicon icon in the tab (the fix returns 204/no-icon rather than a real
bundled `.ico` — browsers handle this fine, but the visual "no more
broken/upstream favicon request" experience wasn't screenshotted). The
FastAPI-level test above exercises the actual routing/dispatch path this
bug lived in.

## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist
- [x] My code follows the style guidelines of this project
- [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 — no
user-facing docs describe dashboard route internals beyond CHANGELOG)
- [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 CHANGELOG.md where applicable

## Screenshots (if applicable)
N/A — server-side route change, no UI change.

## Additional Notes
Deliberately kept the fix minimal: no `StaticFiles` mount or general
static-asset serving system was added, since a single favicon route
doesn't warrant that abstraction. No real `.ico` binary asset was
bundled either — a `204 No Content` response is sufficient for browsers
and avoids maintaining a binary asset in the repo; this can be upgraded
to serve a real branded icon later if desired.

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-07 23:36:10 -05:00
Parideboy
1573f1fd07
fix: use rtk native Cursor hook instead of injecting .cursorrules (#756) (#1846)
## Description

`headroom wrap cursor` unconditionally injected an `rtk`-usage
instructions block into `.cursorrules`. rtk itself supports a native
hook for Cursor (`rtk init --agent cursor`) — the same registration
mechanism headroom already uses for Claude Code — which rewrites shell
commands transparently with zero custom-instructions text needed.
Headroom never tried that path for Cursor, so users got a redundant
`.cursorrules` file duplicating guidance the native hook already
provides silently.

A follow-up commit hardens the switch: `register_agent_hooks` returns
`True` on rtk exit 0, but some rtk builds exit 0 without writing
`~/.cursor/hooks.json`. headroom now trusts the on-disk hook file, not
the exit code, before skipping the `.cursorrules` fallback.

Closes #756

## 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/rtk/installer.py`: generalized `register_claude_hooks` into
`register_agent_hooks(rtk_path, *, agent="claude")`, which passes
`--agent <agent>` to `rtk init` for non-Claude agents.
`register_claude_hooks` kept as a thin wrapper for backward
compatibility. Added `RTK_NATIVE_HOOK_AGENTS` documenting which agents
rtk supports a native hook for.
- `headroom/cli/wrap.py`: `wrap cursor` now calls
`register_agent_hooks(rtk_path, agent="cursor")` first, and only skips
the `.cursorrules` fallback when `~/.cursor/hooks.json` is actually on
disk; otherwise it falls back to `_inject_rtk_instructions(...)`.
- Tests: `tests/test_rtk_installer.py` and
`tests/test_cli/test_wrap_bridge.py` cover the native-hook path, the
on-disk verification, and the `.cursorrules` fallback.
- `CHANGELOG.md`: added an entry under `## Unreleased` / `### Fixed`.

## 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
$ python -m ruff format --check headroom/ tests/ e2e/
953 files already formatted

$ python -m ruff check headroom/cli/wrap.py headroom/rtk/installer.py tests/test_cli/test_wrap_bridge.py tests/test_rtk_installer.py
All checks passed!

$ python -m pytest tests/test_cli/test_wrap_bridge.py -k cursor -q
3 passed
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, local checkout; `python -m
pytest` / `ruff` run directly.
- Exact command / steps: `python -m pytest
tests/test_cli/test_wrap_bridge.py -k cursor -q` — the first test mocks
`register_agent_hooks` to write `~/.cursor/hooks.json` and asserts
`.cursorrules` is NOT created; the second mocks it to write nothing and
asserts `.cursorrules` IS created with the `headroom:rtk-instructions`
marker; the third exercises the explicit registration-failure fallback.
- Observed result: `3 passed`. Native-hook path skips `.cursorrules`
only when the hook file exists on disk; every other outcome falls back
to `.cursorrules`, so Cursor always gets RTK guidance.
- Not tested: real `rtk` binary writing `~/.cursor/hooks.json`
end-to-end — that path is covered by the `docker-wrap-e2e` CI job, not
locally.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A — CLI-only change.

## Additional Notes

Scope: rtk's native-hook-capable agents include `claude`, `cursor`,
`windsurf`, `cline`, `kilocode`, `antigravity`, `pi`, `hermes`, but only
`cursor` and `claude` have a corresponding `headroom wrap` subcommand
today, so this fix only changes `wrap cursor` behavior.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:31:54 -05:00
gglucass
cfcd40f8ac
agent_savings: don't crash the proxy on an unknown savings profile (#1830)
## Description

`get_agent_savings_profile()` now falls back to the default profile
(`agent-90`) with a logged warning when given an unrecognized name,
instead of raising `ValueError`.

The function is resolved during proxy **startup**
(`proxy_pipeline_kwargs` -> `create_app` -> `HeadroomProxy.__init__`),
so raising on an unknown name kills the proxy before it opens its port —
the user ends up with **no proxy at all**, not a degraded one. This
fires on client/runtime version skew: the Headroom desktop app sets
`HEADROOM_SAVINGS_PROFILE=coding` (added in 0.30.0); when a user's
0.30.0 boot validation times out the app falls back to the 0.28.0
runtime, whose profile set is only `{agent-90, balanced}`, and the proxy
then crashes on startup with `ValueError: unknown savings profile
'coding'; expected one of: agent-90, balanced`. Observed across multiple
hosts on the current desktop release. A soft config knob should degrade,
not be fatal.

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/agent_savings.py`: `get_agent_savings_profile()` returns the
default profile (`agent-90`) with a `logger.warning` instead of raising
`ValueError` on an unknown name. Added a module logger.
- `tests/test_agent_savings.py`: replaced the old "raises ValueError"
test with one asserting fallback-to-default plus the warning.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ pytest tests/test_agent_savings.py -q
tests/test_agent_savings.py ...............................              [100%]
============================== 31 passed in 2.08s ==============================

$ ruff check headroom/agent_savings.py tests/test_agent_savings.py
All checks passed!

$ mypy headroom/agent_savings.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: macOS, CPython 3.10.18, headroom-ai from this branch
(`fix/savings-profile-fallback`).
- Exact command / steps: on `main`,
`get_agent_savings_profile("coding")` on a runtime whose `_PROFILES`
lacks `coding` raises `ValueError`, which propagates out of `create_app`
and the proxy exits 1 before binding its port (reproduced in the field:
proxy subprocess "exited with status 1 before opening port 6768", full
traceback ending in this `ValueError`).
- Observed result: with this change the same call returns the `agent-90`
profile and logs `unknown savings profile 'coding'; falling back to
'agent-90' (known: agent-90, balanced)`; the proxy starts normally.
- Not tested: end-to-end desktop upgrade/fallback flow (that path lives
in the desktop app; the desktop side is separately version-gating the
env var).

## 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

- Docs/CHANGELOG N/A: internal behavior hardening, no user-facing API or
config change.
- No linked issue number — surfaced via Sentry (proxy exits before
opening its port on runtime/profile skew). Happy to add one if you'd
like it tracked as an issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:28:23 -05:00
Dustin Burke
4d433592de
Install using uv tool globally (#1829)
Resolves headroomlabs-ai/headroom#768

## Description

Explain how to install using `uv tool` as a global tool. This should be
the preferred option for installation so that headroom is setup as a
global tool within a self-contained virtual env and the binary on the
user's path.

That way, a wrapped coding agent can correctly invoke headroom within
the virtual env to avoid python package import issues.

For example, `~/.claude.json`:

```json
  "mcpServers": {
    "headroom": {
      "type": "stdio",
      "command": "/Users/USERNAME/.local/bin/headroom",
      "args": [
        "mcp",
        "serve"
      ],
      "env": {}
    },
```

uses the command path based on `command -v headroom`.


## 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

- Updated README.md

## 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

```sh
uv tool install "headroom-ai[all]"
```

```sh
command -v headroom
/Users/dustin/.local/bin/headroom
```

```sh
headroom wrap claude                                                                                                                                                                                          
  ╔═══════════════════════════════════════════════╗
  ║            HEADROOM WRAP: CLAUDE              ║
  ╚═══════════════════════════════════════════════╝

  Starting Headroom proxy on port 8787...
  Logs: /Users/dustin/.headroom/logs/proxy.log
  Proxy ready on http://127.0.0.1:8787
  Dashboard:    http://127.0.0.1:8787/dashboard
  Setting up rtk...
  Code graph: indexed (tokensave)

  Launching Claude Code (API routed through Headroom)...
  ANTHROPIC_BASE_URL=http://127.0.0.1:8787
  Remote Control: Claude Code may hide the Remote Control menu while ANTHROPIC_BASE_URL points at a custom endpoint (the wrapped Claude session's ANTHROPIC_BASE_URL); launch Claude without Headroom for sessions that need this feature.

  ENABLE_TOOL_SEARCH=true (on-demand tool loading kept on; issue #746)
```

## Real Behavior Proof

- Environment: See below
- Exact command / steps: See test output section above
- Observed result: Headroom installed as expected, Claude coding agent
successfully had headroom MCP available
- Not tested: Coding agents other than Claude.  OS other than Mac.


```sh
uname -a                                                                                                                                                                                                      
Darwin mac.lan 25.5.0 Darwin Kernel Version 25.5.0: Mon Apr 27 20:41:26 PDT 2026; root:xnu-12377.121.6~2/RELEASE_ARM64_T8132 arm64

claude --version                                                                                                                                                                                              2.1.201 (Claude Code)

headroom --version                                                                                                                                                                                           
headroom, version 0.30.0
```

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [ ] My code follows the project's style guidelines
- [x] I have performed a self-review
- [ ] 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
- [ ] 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)

N/A

## Additional Notes

<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->
2026-07-07 23:27:56 -05:00
Rod Boev
0f606b6281
fix(cache): avoid fallback session collisions (#1827)
## Description

Cache-mode session tracking currently collapses unrelated conversations
when they share a large static first system prompt. The fallback
session-id hash ignores later system messages entirely, so dynamic
per-conversation context can get cut out of the key and two different
sessions reuse the same `PrefixCacheTracker`. This hashes the full
ordered system-text payload instead, while leaving explicit
`x-headroom-session-id` overrides untouched. Refs #1808.

## 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

- Collected all system-text content when building the fallback cache
session id.
- Stopped truncating fallback session-id input to the first 500
characters of the first system message.
- Added a regression that proves two conversations with different later
system context no longer collide.
- Added a preservation test that appending only non-system turns keeps
the same fallback session id.
- Applied the pinned Ruff formatter to three pre-existing files on the
current base so the repo-wide lint job passes unchanged semantics.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_cache/test_prefix_tracker.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/cache/prefix_tracker.py
tests/test_cache/test_prefix_tracker.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
uv run pytest tests/test_cache/test_prefix_tracker.py -q
40 passed, 1 warning in 0.15s

uv run ruff check headroom/cache/prefix_tracker.py tests/test_cache/test_prefix_tracker.py
All checks passed!

uv run ruff check .
All checks passed!

uv run ruff format --check .
1046 files already formatted
```

## Real Behavior Proof

- Environment: Windows, project `uv` environment, focused cache-tracker
regression.
- Exact command / steps: run `tests/test_cache/test_prefix_tracker.py`
on `origin/main` with the new collision regression present, then rerun
the same file on this branch.
- Observed result: base returns the same session id for two
conversations that differ only in a later system message and fails
`assert id_a != id_b`; head passes the focused file and keeps the
fallback session id stable when only non-system turns are appended.
- Not tested: live proxy traffic through a real agentic client.

## 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

This is only the session-collision half of #1808. The
duplicate-response-header fix stays separate so this PR can reference
the issue without claiming the whole bug report is resolved. The extra
formatting-only diff comes from the current base failing the pinned
full-repo Ruff format check.
2026-07-07 23:26:36 -05:00
Rod Boev
4ac54934cb
fix(streaming): preserve server_tool_use sse blocks (#1826)
## Description

Buffered Anthropic responses currently fail late when they contain a
`server_tool_use` block. `_response_to_sse()` raises after the upstream
response is already fully buffered, so callers wait through the whole
generation and then receive a 502 instead of the completed response.
This adds explicit `server_tool_use` support in the buffered-to-SSE
replay path, while keeping the existing rejection for truly unsupported
Anthropic block types. Closes #1806.

## 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

- Added a `server_tool_use` branch in the Anthropic buffered-response
SSE conversion loop.
- Emitted the full `server_tool_use` block in `content_block_start`
instead of raising during replay.
- Added a focused regression that proves buffered `server_tool_use`
blocks convert to SSE and round-trip with the block type intact.
- Kept the existing reject-unknown test so unsupported future block
types still fail loudly.
- Applied the pinned Ruff formatter to three pre-existing files on the
current base so the repo-wide lint job passes unchanged semantics.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_sse_thinking_blocks.py
-q`)
- [x] Linting passes (`uv run ruff check
headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
uv run pytest tests/test_sse_thinking_blocks.py -q
7 passed, 1 warning in 0.19s

uv run ruff check headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py
All checks passed!

uv run ruff check .
All checks passed!

uv run ruff format --check .
1046 files already formatted
```

## Real Behavior Proof

- Environment: Windows, project `uv` environment, focused handler-level
regression.
- Exact command / steps: run `tests/test_sse_thinking_blocks.py` on
`origin/main` with the new `server_tool_use` regression present, then
rerun the same file on this branch.
- Observed result: base raises `Unsupported Anthropic content block type
for SSE conversion: 'server_tool_use'`; head passes the focused file and
preserves the `server_tool_use` block type through buffered SSE
reconstruction, while the existing reject-unknown test still passes.
- Not tested: live proxy traffic against Anthropic server-side tools.

## 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

No changelog entry is needed for this internal handler fix. The issue
suggested a broader accept-all fallback, but this PR stays intentionally
narrower: it handles the proven `server_tool_use` case and keeps the
existing rejection for truly unsupported Anthropic block types. The
extra formatting-only diff comes from the current base failing the
pinned full-repo Ruff format check.
2026-07-07 23:25:58 -05:00
Rod Boev
53a465b121
fix(proxy): subtract cache write premiums from net savings (#1800)
## Description

Cache stats already calculate both prompt-cache read savings and
cache-write premium cost, but the exported `net_savings_usd` field used
gross read savings alone. That made cache-heavy token-mode workloads
look profitable even when extra cache writes offset or exceeded the read
discount. This updates existing cache cost accounting so provider and
total `net_savings_usd` subtract write premiums while keeping gross
savings and write premium fields visible. Refs #327.

The scope follows doublefx's controlled measurement in
https://github.com/headroomlabs-ai/headroom/issues/327#issuecomment-4683604089,
which showed token-mode compression increasing cache write volume and
billed cost while dashboard token savings looked positive.

## 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

- Subtract cache write premiums from provider-level cache
`net_savings_usd`.
- Subtract aggregate cache write premiums from total cache
`net_savings_usd`.
- Keep gross `savings_usd` and `write_premium_usd` visible for dashboard
and telemetry consumers.
- Add focused regressions for provider net, total net, and
zero-write-premium preservation.
- Update the dashboard cache TTL fixture to match the corrected net
value.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_proxy_cache_ttl_metrics.py
tests/test_dashboard_cache_ttl_playwright.py
tests/test_proxy_dashboard_stats_cache.py -q`)
- [x] Linting passes (`uv run ruff check headroom/proxy/cost.py
tests/test_proxy_cache_ttl_metrics.py
tests/test_dashboard_cache_ttl_playwright.py
tests/test_proxy_dashboard_stats_cache.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_proxy_cache_ttl_metrics.py tests/test_dashboard_cache_ttl_playwright.py tests/test_proxy_dashboard_stats_cache.py -q
28 passed, 2 skipped, 1 warning in 32.75s

uv run pytest tests/test_proxy_cache_ttl_metrics.py -q -k keeps_net_equal_without_write_premium
1 passed, 16 deselected in 0.15s

uv run ruff check headroom/proxy/cost.py tests/test_proxy_cache_ttl_metrics.py tests/test_dashboard_cache_ttl_playwright.py tests/test_proxy_dashboard_stats_cache.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows, Python through the project `uv` environment.
- Exact command / steps: run the cache net-savings regressions against
base and head.
- Observed result: base reports provider net as `0.0036` instead of
`0.0021` and total net as `0.0046` instead of `0.0031`; head passes the
focused cache metrics suite and preserves `net_savings_usd ==
savings_usd` when there is no write premium.
- Not tested: broader cache-hit-rate tuning, prompt-cache policy
changes, and live provider billing.

## 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

No changelog entry is needed because this corrects existing stats fields
rather than adding a new command or control. Type checking was not part
of the focused local validation for this Python-only fix. Dashboard
Playwright coverage is CI-owned locally; the import-gated file was
included in the focused pytest command and skipped because Playwright is
not installed in this environment.
2026-07-07 23:24:41 -05:00
Rod Boev
931eed879d
fix(mcp): surface dead proxy state (#1786)
## Description

When the configured Headroom proxy is down, the MCP server can still
start cleanly and return successful-looking no-op compression or zeroed
stats. That hides the real failure from the client and makes it look
like Headroom is working while compression has stopped. This change
makes proxy-backed MCP tool paths surface unreachable-proxy state
explicitly instead of silently degrading.

Closes #881

## 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 unreachable configured proxy state before returning
proxy-backed MCP tool results.
- Report proxy-unreachable status for compression and stats instead of
presenting no-op output as healthy.
- Preserve local MCP behavior when proxy checking is disabled or a
local-only tool path is intended.
- Keep the short `/livez` health probe isolated from the shared proxy
client used by retrieval and stats calls.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_server.py
tests/test_provider_registry.py -q`)
- [x] Linting passes (`uv run ruff check headroom/ccr/mcp_server.py
headroom/providers/registry.py tests/test_ccr_mcp_server.py
tests/test_provider_registry.py`; `uv run ruff format --check
headroom/ccr/mcp_server.py headroom/providers/registry.py
tests/test_ccr_mcp_server.py tests/test_provider_registry.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
collected 26 items

tests\test_ccr_mcp_server.py ...s..........                              [ 53%]
tests\test_provider_registry.py ............                             [100%]

======================== 25 passed, 1 skipped in 6.64s ========================
All checks passed!
4 files already formatted
```

## Real Behavior Proof

- Environment: Windows, focused local MCP tests.
- Exact command / steps: Run `uv run pytest
.tmp\headroom_t45_regression.py -q` in the base and head worktrees, then
run `uv run pytest tests/test_ccr_mcp_server.py
tests/test_provider_registry.py -q`, `uv run ruff check
headroom/ccr/mcp_server.py headroom/providers/registry.py
tests/test_ccr_mcp_server.py tests/test_provider_registry.py`, and `uv
run ruff format --check headroom/ccr/mcp_server.py
headroom/providers/registry.py tests/test_ccr_mcp_server.py
tests/test_provider_registry.py` in the head worktree.
- Observed result: `base: KeyError: 'proxy'` on the new
proxy-unreachable assertions, `head: .tmp\headroom_t45_regression.py
.... [100%]`, broader head suite `25 passed, 1 skipped in 6.64s`, and
the proxy health probe regression preserved the shared proxy client used
by retrieval and stats.
- Not tested: The reporter's bundled macOS runtime, live Claude Desktop
MCP logs, and the full test suite.

## 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
- [ ] 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

Documentation and changelog are left unchecked for now; the behavior
change is an error-surfacing fix for existing MCP tools and Headroom
generates changelog entries from conventional commits.
2026-07-07 23:18:57 -05:00
Rod Boev
0f553a8ebb
fix(proxy): preserve streaming passthrough beta headers (#1783)
## Description

Anthropic-compatible custom upstreams can reject streaming passthrough
requests when Headroom expands the client's `anthropic-beta` header with
sticky session tokens. The request body is still forwarded
byte-faithfully, but the header no longer matches the direct request
that succeeds against the same upstream. This change keeps sticky beta
learning intact while preserving the direct client beta header for the
custom-upstream streaming passthrough path that owns the 503.

Closes #1724

## 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

- Preserves client `anthropic-beta` headers on Vertex
`:streamRawPredict` and custom Anthropic API URL streaming passthrough
requests.
- Keeps sticky beta tracking and adjacent sticky-header behavior for
non-hazard paths.
- Adds focused regression coverage that captures outgoing streaming
headers and preserves existing byte-faithful body checks.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_proxy_byte_faithful_forwarding.py
tests/test_anthropic_beta_session_sticky.py -q`)
- [x] Linting passes (`uvx ruff==0.15.17 check .` and `uvx ruff==0.15.17
format --check .`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
pytest base: exit_code=1, stdout excerpt: AssertionError: assert 'sticky-beta-2024-01-01,claude-code-20250219' == 'claude-code-20250219'
pytest head: exit_code=0, stdout excerpt: 64 passed, 1 warning in 3.57s
ruff: exit_code=0, stdout excerpt: All checks passed! / 1044 files already formatted
```

## Real Behavior Proof

- Environment: Windows, focused local proxy tests through the headless
runner.
- Exact command / steps: Pre-seed sticky beta state, send streaming
Vertex `:streamRawPredict` and `/v1/messages` requests through custom
upstream routing with `anthropic-beta: claude-code-20250219`, and
capture the outgoing request headers. Run the same focused pytest
command on the base checkout, then on the fixed checkout. Run pinned
Ruff 0.15.17 check and format validation against the final branch.
- Observed result: The base checkout expands the streaming
custom-upstream beta header, and the fixed checkout preserves the direct
client beta header for both streaming routes while adjacent
non-streaming custom-upstream requests still carry the sticky union.
- Not tested: The reporter's live MaaS upstream and the full test suite.

## 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

Documentation is left unchecked because the fix preserves the existing
passthrough contract rather than adding a new user-facing option. The
changelog box is left unchecked because Headroom generates changelog
entries from conventional commits.
2026-07-07 23:16:59 -05:00
Rod Boev
be51008c70
fix(toin): publish skip compression recommendations (#1782)
## Description

TOIN already learns when a tool-output slice should skip compression,
but the published recommendation artifact drops that signal. A high
full-retrieval row can therefore still publish an ordinary compressor
strategy even though TOIN marked it as skip-worthy. This change carries
`skip_compression_recommended` into `recommendations.toml`, keeps Rust
parsing backward compatible for older files, and makes skip rows publish
a skip-oriented strategy hint instead of misleading compressor guidance.

Refs #1775

## 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

- Publishes `skip_compression_recommended` in generated recommendation
rows.
- Uses retrieval-aware strategy output for rows TOIN already marked as
skip-worthy.
- Extends the Rust recommendation schema with a backward-compatible
default for older TOML files.
- Adds focused publish and schema coverage for skip and non-skip rows.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_toin_publish.py -q`)
- [x] Linting passes (`uv run ruff check headroom/cli/toin_publish.py
headroom/telemetry/toin.py tests/test_toin_publish.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
- [ ] I have made corresponding changes to the documentation

### Test Output

```text
uv run pytest tests/test_toin_publish.py -q: 8 passed
uv run ruff check headroom/cli/toin_publish.py headroom/telemetry/toin.py tests/test_toin_publish.py: passed
cargo fmt --all -- --check: passed
cargo check -p headroom-core: passed
cargo test -p headroom-core --lib transforms::recommendations: 6 passed
cargo clippy --workspace -- -D warnings: passed
```

## Real Behavior Proof

- Environment: Windows for Python validation through the headless
runner; Rust validation via focused local cargo commands where
available.
- Exact command / steps: `uv run pytest tests/test_toin_publish.py -q`,
`uv run ruff check headroom/cli/toin_publish.py
headroom/telemetry/toin.py tests/test_toin_publish.py`, `cargo fmt --all
-- --check`, `cargo check -p headroom-core`, `cargo test -p
headroom-core --lib transforms::recommendations`, and `cargo clippy
--workspace -- -D warnings`.
- Observed result: Skip-worthy rows carry `skip_compression_recommended
= true` and a skip strategy hint; normal rows carry `false` and preserve
their ordinary strategy.
- Not tested: Live runtime dispatcher skip behavior and full Rust
workspace tests.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective 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

This PR fixes the published recommendation artifact. Runtime dispatcher
enforcement remains a separate follow-up because it needs a dedicated
consumer proof matrix. Documentation and changelog are left unchecked
because this changes generated recommendation data and Headroom's
changelog is generated from conventional commits.
2026-07-07 23:14:54 -05:00
Rod Boev
9cbdba4dc1
fix(ccr): make expired retrieve misses terminal (#1781)
## Description

Expired CCR hashes currently come back through `headroom_retrieve` as
the same generic missing-content error used for typos and never-stored
hashes. That leaves agents with no terminal signal, so they can retry a
dead hash instead of rerunning the source command or rereading the
source file. This change uses the cache store's existing TTL status
metadata before the MCP retrieval path loses that distinction, then
returns expired-hash guidance only when the local store proves the entry
existed and expired.

Closes #1776

## 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

- Uses CCR store status metadata to distinguish expired local hashes
from never-stored hashes in the MCP retrieval path.
- Keeps proxy fallback and successful local retrieval behavior
unchanged.
- Adds focused regression coverage for expired stored hashes, the
status-to-retrieve TTL boundary, proxy fallback preservation, and
missing-hash negative space.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_server.py -q`)
- [x] Linting passes (`uv run ruff check headroom/ccr/mcp_server.py
tests/test_ccr_mcp_server.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
Base pytest:
FAILED tests\test_ccr_mcp_server.py::test_mcp_retrieve_expired_hash_returns_terminal_guidance
E   KeyError: 'status'

Head pytest:
tests\test_ccr_mcp_server.py ...s..........                              [100%]
13 passed, 1 skipped in 0.35s

Ruff:
All checks passed!
```

## Real Behavior Proof

- Environment: Windows, focused local pytest through the headless
runner.
- Exact command / steps: Store a CCR entry with a short TTL, advance
beyond expiry, call `HeadroomMCPServer._retrieve_content(hash)`, force a
second entry to cross TTL between status inspection and `retrieve()`,
stub a proxy-backed retrieval for local misses, then call the same
method with a never-stored hash and no proxy hit.
- Observed result: The already-expired hash and the hash that expires
during retrieval both return terminal expired guidance with `status:
expired`; missing and expired local hashes still return proxy data when
the proxy fallback succeeds; a never-stored hash with no proxy hit still
returns the generic missing-hash error and no expired status.
- Not tested: Full suite, live agent retry behavior, and live external
proxy-backed retrieval.

## 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

Documentation and changelog are left unchecked because this is a narrow
MCP error-shape fix and Headroom's changelog is generated from
conventional commits.
2026-07-07 23:12:53 -05:00
Tejas Chopra
285808b90e
fix(proxy/openai): translate max_tokens -> max_completion_tokens on chat path (#1774)
## Description

GPT-5 / o-series chat models reject the legacy `max_tokens` —
`AI_APICallError: Unsupported parameter: 'max_tokens' is not supported
with this model. Use 'max_completion_tokens' instead.` — while
gpt-4o/4.1 accept `max_completion_tokens` too. openai-compatible clients
(opencode via `@ai-sdk/openai-compatible`, older SDKs) still send
`max_tokens`, so requests for GPT-5 models fail at the proxy's OpenAI
upstream. This is a blocker for any such client pointed at a GPT-5 model
through Headroom.

The proxy already owns the outbound `/v1/chat/completions` body (it
rewrites `messages` to compress them), so translate the token param
there: rename `max_tokens` → `max_completion_tokens` when the newer form
isn't already set, then drop the rejected legacy key. One-way, safe for
current OpenAI models; no-op when the client already sends
`max_completion_tokens`. The Responses path (`max_output_tokens`) is
unaffected.

Closes #

## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made
- New `_normalize_openai_max_tokens(body)` helper + call in
`handle_openai_chat` after body finalization, before upstream forward.

## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added

### Test Output
```text
tests/test_openai_max_completion_tokens.py .... 6 passed
ruff check ... All checks passed!
mypy headroom/proxy/handlers/openai.py ... Success: no issues found
```

## Real Behavior Proof
- Environment: local worktree, Python 3.12.
- Exact command / steps: reproduced live — opencode
(`@ai-sdk/openai-compatible` → Headroom proxy) targeting
`gpt-5.3-chat-latest` failed with `Unsupported parameter: 'max_tokens'
... Use 'max_completion_tokens'` in the DEBUG stream log. The shim
renames the param on the outbound body.
- Observed result: unit tests confirm the rename/drop/no-op cases.
- Not tested: full live opencode completion (its headless `run` stalls
for unrelated reasons in this env — separate from this param fix).

## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review

## Additional Notes
Discovered while debugging why opencode wouldn't run through the proxy:
three layered blockers — (1) missing `models` map in the injected
provider config [PR #1716], (2) no `apiKey` in the injected config /
HTTP path doesn't inject `OPENAI_API_KEY` like the WS path does, (3)
this `max_tokens` vs `max_completion_tokens` mismatch. This PR addresses
(3).
2026-07-07 23:08:11 -05:00
KET ⚡
37a12dd833
[codex] docs: add pipeline extension recipe (#1758)
## Description

Headroom already supports `headroom.pipeline_extension`, but
request-normalization pattern was not documented. That leaves users
guessing how to fix upstream quirks such as `content: null` tool-call
payloads.

Closes #1758

## Type of Change

- [x] Documentation update

## Changes Made

- Added a `Pipeline Extensions` section to `configuration.mdx`.
- Documented the `PRE_SEND` hook as the right place for request cleanup.
- Included a minimal `NormalizeNullContent` example and entry-point
registration.
- Mentioned `x-headroom-base-url` as the per-request routing override.

## Testing

- [ ] 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
Docs-only verification:
- Reviewed docs diff for API names, hook names, and placement.
- Confirmed example uses public `headroom.pipeline` contract and matches existing header-routing terminology.
```

## Real Behavior Proof

- Environment: GitHub PR diff review for docs-only extension recipe.
- Exact command / steps: Compared new configuration docs against public
pipeline-extension and per-request routing interfaces already exposed by
Headroom.
- Observed result: Docs now show concrete request-cleanup extension
pattern without requiring a fork.
- Not tested: Live extension package execution in this verification
pass.

## 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
- [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
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

Co-authored-by: Your Name <you@example.com>
2026-07-07 23:06:53 -05:00
Parideboy
84ecca770e
docs(readme): update lean-ctx comparison row (#1711)
## Description

The README "Compared to" table listed lean-ctx as `Scope: CLI commands,
MCP tools, editor rules · Deploy: CLI wrapper · MCP · Reversible: No`.
The lean-ctx maintainer reported in the issue that the project ships
five reversibility mechanisms (`ctx_expand`, `ctx_retrieve`, proxy CCR
tee store with file-path handles, in-band `<lc_expand:HASH>` markers,
and a `GET /v1/references/{id}` HTTP endpoint), plus a wire-level
transparent proxy, a `compress(messages, model)` Py/TS SDK, and
middleware hooks (LiteLLM, Vercel AI SDK). This PR updates the row to
the wording suggested in the issue so the comparison stays accurate.

Fixes #1675

## 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

- `README.md` ("Compared to" table): lean-ctx row updated to `Scope:
Tool output, files, shell, history · Deploy: Proxy · library ·
middleware · MCP · CLI · Local: Yes · Reversible: Yes`.

## Testing

- [ ] 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
$ grep -n "lean-ctx" README.md | head -1
447:| [lean-ctx](https://github.com/yvgude/lean-ctx)                               | Tool output, files, shell, history             | Proxy · library · middleware · MCP · CLI | Yes | Yes    |
```

## Real Behavior Proof

- Environment: Windows 11, local checkout at `upstream/main` (9fbd47ba).
- Exact command / steps: reviewed the reversibility docs and code
referenced in the issue (lean-ctx
`docs/comparisons/vs-headroom.md#reversibility`,
`rust/src/proxy/ccr.rs`); edited the single table row; rendered the
Markdown table locally to confirm column alignment.
- Observed result: row now matches lean-ctx's documented scope, deploy
surfaces, and reversibility; table renders with the same five columns as
the surrounding rows.
- Not tested: independent runtime verification of every lean-ctx
recovery path (claims are per the lean-ctx maintainer's issue report and
linked docs/source).

## 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>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-07 23:06:04 -05:00
KET ⚡
a7721b2f38
[codex] docs: add Codex install note (#1757)
## Description

README lacked a practical note for Codex and other MCP clients that
cannot reliably inherit a shell PATH. That makes `command = "headroom"`
brittle for uv-installed setups.

Closes #1757

## Type of Change

- [x] Documentation update

## Changes Made

- Added a short Codex/global-install section to README.
- Documented `uv tool install "headroom-ai[all]"` and `command -v
headroom`.
- Showed the absolute-path MCP config pattern.

## Testing

- [ ] 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
Docs-only verification:
- Reviewed README diff for command syntax and placement.
- Cross-checked the documented flow against the existing uv install pattern and absolute-path MCP config example.
```

## Real Behavior Proof

- Environment: GitHub PR diff review for docs-only install guidance.
- Exact command / steps: Compared new README note against documented
`uv` tool install flow and absolute-path MCP launch pattern.
- Observed result: Docs now give Codex/MCP users a stable binary-path
setup instead of relying on ambient PATH inheritance.
- Not tested: Fresh uv install on a clean machine in this verification
pass.

## 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
- [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
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

Co-authored-by: Your Name <you@example.com>
2026-07-07 12:51:22 -05:00
KET ⚡
9c203ddbcc
[codex] docs: remove retired IntelligentContext copy (#1756)
## Description

Public README and installation guide still described retired
IntelligentContext / RollingWindow as active features. Pipeline now uses
live-zone compression only.

Closes #1756

## Type of Change

- [x] Documentation update

## Changes Made

- Reworded README feature bullets to describe live-zone compression and
live-zone pipeline stages.
- Updated installation guide copy to match current core package
behavior.

## Testing

- [ ] 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
Docs-only verification:
- Reviewed PR diff in GitHub Files changed.
- Confirmed touched README / install copy no longer advertises retired IntelligentContext or RollingWindow behavior.
```

## Real Behavior Proof

- Environment: GitHub PR diff review for docs-only change.
- Exact command / steps: Compared updated README and installation docs
text against current live-zone compression behavior described elsewhere
in repo.
- Observed result: Public docs no longer claim retired
IntelligentContext / RollingWindow paths are active.
- Not tested: Runtime commands; docs-only 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
- [ ] 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
- [ ] 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
2026-07-07 12:51:02 -05:00
KET ⚡
a031055a2d
docs(readme): correct lean-ctx comparison row (#1754)
## Description

Correct the `lean-ctx` row in the README comparison table.

The upstream project documents reversible recovery paths and broader
deployment surfaces than the table previously reflected.

Closes #1754

## Type of Change

- [x] Documentation update

## Changes Made

- `README.md`: updated `lean-ctx` comparison row to match current
documented capabilities and reversible behavior.

## Testing

- [ ] 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
Docs-only verification:
- Reviewed rendered README diff for the comparison row.
- Cross-checked updated row against current public lean-ctx docs covering deployment surfaces and reversible behavior.
```

## Real Behavior Proof

- Environment: GitHub PR diff review for docs-only comparison-table
update.
- Exact command / steps: Compared new README row wording against current
public lean-ctx documentation.
- Observed result: Comparison row now matches documented capabilities
instead of understating recovery and deployment support.
- Not tested: Runtime commands; docs-only 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
- [ ] 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
- [ ] 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

Co-authored-by: Your Name <you@example.com>
2026-07-07 12:50:33 -05:00
Zhenjia ZHOU
b38315cf72
fix(code-compressor): CJK-aware relevance-query symbol matching (#1747)
## Description

`CodeAwareCompressor` gives a code symbol a relevance "context boost"
when the query names it. The query tokenizer in
`_analyze_symbol_importance` used an ASCII-only delimiter class, so a
CJK query (no spaces, CJK punctuation) collapsed into one blob and never
matched an ASCII symbol name; the substring fallback was also gated
behind `len(name) > 3`, dropping short ASCII names glued to CJK.

This extracts the query tokenization + matching into two pure helpers,
adds CJK/full-width punctuation as delimiters, and relaxes the `len>3`
guard only for CJK queries. ASCII/English behavior is byte-identical.
`code_compressor` is pure-Python (no Rust twin, no parity fixtures).

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/transforms/code_compressor.py`: add `_query_context_tokens`
(CJK/full-width punctuation + ideographic space as delimiters) and
`_symbol_in_context` (substring `len>3` guard relaxed only for CJK
queries), used by `_analyze_symbol_importance`.
- `tests/test_transforms/test_code_compressor_cjk.py`: pure-function
tests (CJK isolation, short-name relaxation, English-unchanged, empty).

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff`)
- [x] Type checking passes (`mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed (see Real Behavior Proof)

### Test Output

```text
$ .venv/bin/python -m pytest tests/test_transforms/test_code_compressor_cjk.py
6 passed

$ .venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py
35 passed, 39 skipped   # no regression (skips need the [code] tree-sitter extra)

$ ruff check / mypy headroom/transforms/code_compressor.py   # clean
```

## Real Behavior Proof

- Environment: macOS (Darwin), Python in a uv venv, branch
`feat/code-compressor-cjk-relevance` off `main`.
- Exact command / steps: called the extracted helpers directly on CJK
and ASCII queries.
- Observed result: `_query_context_tokens("请重点保留(parse_config)的解析配置")`
isolates `parse_config` as its own token (before: the whole query was
one blob, so the exact-match boost never fired);
`_symbol_in_context("db", ...)` now matches a short ASCII name glued to
a CJK query (before: dropped by the `len>3` guard). English is unchanged
— for `"keep the database helper"`, `_symbol_in_context("db", ...)`
still returns `False` (no spurious short substring match). All 6 new
tests pass; the existing 35 `code_compressor` tests are unchanged.
- Not tested: end-to-end `compress()` (needs the `[code]` tree-sitter
extra); the fix is at the pure query-matching layer and is verified
there.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation — N/A
(internal compressor behavior)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A: internal relevance-scoring
fix, no user-facing surface change

## Additional Notes

- Scope note: a pure-CJK query that names the function only by a Chinese
description (no ASCII token anywhere) still cannot match an ASCII symbol
name — cross-script query matching remains out of scope.
2026-07-07 12:49:26 -05:00
Rohan Richard
5194bdc5a6
fix(content-detector): detect and compress space-separated JSON objects (#1742)
## Description

Headroom's `detect_content_type()` only recognizes content starting with
`[` as a `JSON array. Many web search tools (SerpAPI, Tavily, custom
backends) return space-separated JSON objects instead of a real array
like follows

```json
{"title": "Result 1", "url": "..."} {"title": "Result 2", "url": "..."} {"title": "Result 3", "url": "..."}
```

That shape is detected as `PLAIN_TEXT` (confidence 0.5), so SmartCrusher
never processes it and web-search results compress 0%.

Closes #1741

## 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
- [x] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `content_detector.py`: `_try_detect_json` now recognizes a run of ≥2
whitespace-separated (space- or newline-separated) JSON objects and
returns `JSON_ARRAY` with `metadata["concatenated"] = True`. The router
already falls back to the Python regex detector when the native detector
returns `PLAIN_TEXT` (`content_router.py`), so this fixes routing on the
default backend too.
- `content_detector.py`: added `normalize_concatenated_json()` (and a
`_decode_concatenated_json()` helper) that rewrites the space-separated
shape into a canonical `[{…}, {…}]` array string.
- `smart_crusher.py`: `SmartCrusher.crush()` normalizes concatenated
JSON to a real array before handing it to the Rust crusher, so it
actually compresses.
- The change is deliberately conservative: a single object stays
unclaimed (`_try_detect_json('{"id": 1}')` → `None`), and any non-JSON
token between objects disqualifies the run. Existing `[`-array detection
is unchanged.
- Added tests and a CHANGELOG entry.

## Testing

- [x] Unit tests pass (`pytest`) — affected suites (full suite has
network-dependent ML tests that can't run offline; see note)
- [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
$ ruff check .
All checks passed!

$ pytest tests/test_transforms_content_detection.py -q
............                                                             [100%]
12 passed

$ pytest tests/test_transforms_content_router.py \
         tests/test_smart_crusher_toin_attachment.py \
         tests/test_transforms_tabular.py -q
96 passed, 2 skipped
# + SmartCrusher passthrough tests in test_text_compressors.py: 2 passed
```

## Real Behavior Proof

- Environment: macOS 26.5, Python 3.12.11, editable source build (`uv
pip install -e .`) with the Rust `_core` compiled locally; default
detection backend (native Rust → Python-regex fallback on PLAIN_TEXT).
- Exact command / steps: ran a 100-object space-separated `web_search`
payload through `detect_content_type()` and
`ContentRouter().compress()`, before and after the patch (repro below).
- Observed result: detection flips `PLAIN_TEXT` (conf 0.5) →
`JSON_ARRAY` (conf 1.0) and SmartCrusher compression goes from 0.0% to
34.2% (10369 → 6819 bytes) on the identical payload.
- Not tested: the native Rust *detector* path in isolation (the fix
relies on the existing documented Python-regex fallback for
`PLAIN_TEXT`); separators other than whitespace
(comma-separated-without-brackets is intentionally not claimed).

Before:
```
detected  : ContentType.PLAIN_TEXT  conf 0.5
strategy  : CompressionStrategy.SMART_CRUSHER
orig bytes: 10369
comp bytes: 10369
reduction : 0.0%
```
After:
```
detected  : ContentType.JSON_ARRAY  conf 1.0
strategy  : CompressionStrategy.SMART_CRUSHER
orig bytes: 10369
comp bytes: 6819
reduction : 34.2%
```

## 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
- [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

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-07 12:48:24 -05:00
Parideboy
46d5d685d9
fix(proxy): bound HF tokenizer load and offload token counting off event loop (#1738)
## Description

Fixes #1701.

On Windows, `headroom proxy --anthropic-api-url
https://api.deepseek.com/anthropic` froze: the first `/v1/messages`
request took ~610s (`optimization_latency_ms=609972`) with only
router/lifecycle markers, and afterwards the whole server was a zombie —
`/livez`, `/readyz` and `/health` hung until the process was killed.
`HEADROOM_DETECT_BACKEND=python` was already set, so this was not the
#575/#845 native-detect deadlock.

Root cause: DeepSeek model names route to the HuggingFace tokenizer
backend (`MODEL_PATTERNS` in `headroom/tokenizers/registry.py`).
`HuggingFaceTokenizer` loads lazily, so the registry's construction-time
fallback never fires; the first `count_messages` calls
`AutoTokenizer.from_pretrained(..., trust_remote_code=True)` — unbounded
network downloads/retries — and this ran **synchronously inside the
async Anthropic messages handler** (`get_tokenizer(model)` +
`tokenizer.count_messages(messages)`), outside the 30s
`_run_compression_in_executor` bound. huggingface_hub retry chains on a
restricted network easily reach ~10 minutes, blocking the entire asyncio
event loop; subsequent on-loop counting kept it pinned. tiktoken got a
bounded eager load for the same bug class long ago (#956); the HF
backend never did.

## Type of Change

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

## Changes Made

- `headroom/tokenizers/huggingface.py`: `_load_tokenizer` now tries the
local HF cache first (`local_files_only=True`, no network), then bounds
the network load with `HEADROOM_HF_TOKENIZER_LOAD_TIMEOUT_SECS` (default
10s; `0` disables network loads) on a daemon thread. Timeouts/failures
return `None` (cached by `lru_cache`, so the hub is probed at most once
per process per tokenizer) and `count_messages` fails open to char-based
estimation via the existing `_use_fallback()` path.
- `headroom/proxy/handlers/anthropic.py`: new
`AnthropicHandlerMixin._count_tokens_offloaded(model, messages)` runs
`get_tokenizer` + `count_messages` on the compression executor bounded
by `COMPRESSION_TIMEOUT_SECONDS`, failing open to
`EstimatingTokenCounter` (downgrade logged once per model). Used in
`handle_anthropic_messages` (the issue's hot path, both count sites) and
`handle_anthropic_batch_create`; the batch path's inline
`anthropic_pipeline.apply()` is now offloaded via
`_run_compression_in_executor` (mirrors the #1612 image-compression
offload).
- `headroom/proxy/handlers/batch.py`: the two remaining inline
`openai_pipeline.apply()` calls (`handle_google_batch_create`,
`_compress_batch_jsonl`) are offloaded the same way; existing `except`
blocks keep the pass-through fail-open semantics.
- Tests: `tests/test_huggingface_tokenizer_timeout.py` (cache-first,
bounded timeout, failure caching, timeout=0, fail-open estimation),
`tests/test_tokenizer_count_offload.py` (wiring guards, runs on
`headroom-compress` worker, event loop stays responsive during slow
tokenizer work, fail-open), plus `_run_compression_in_executor` stub on
the batch test double.

## Testing

- [x] All existing tests pass
- [x] Added new tests for the changes
- [ ] Manual testing performed

```
$ python -m pytest tests/test_huggingface_tokenizer_timeout.py tests/test_tokenizer_count_offload.py tests/test_image_compression_offload.py tests/test_gemini_compression_offload.py tests/test_tokenizers tests/test_proxy_handlers_batch.py -q
50 passed

$ ruff check .          # No issues found
$ ruff format --check . # 1043 files already formatted
$ mypy headroom --ignore-missing-imports  # 0 errors
```

## Real Behavior Proof

- Environment: Windows 11 Pro (10.0.26200), Python 3.13, local checkout
of this branch with the Rust core built.
- Exact command / steps: `python -m pytest
tests/test_tokenizer_count_offload.py -q` — includes
`test_count_tokens_offloaded_keeps_loop_responsive`, which reproduces
the issue's mechanism: a tokenizer whose `count_messages` blocks
(stand-in for the unbounded `AutoTokenizer.from_pretrained` network
load) while an asyncio ticker measures event-loop liveness. Also `python
-m pytest tests/test_huggingface_tokenizer_timeout.py -q` with a
`from_pretrained` stub that sleeps 60s and
`HEADROOM_HF_TOKENIZER_LOAD_TIMEOUT_SECS=0.2`.
- Observed result: with the fix, the slow count runs on a
`headroom-compress` worker thread and the loop keeps ticking (`ticks >=
5`; inline it yields ~0 — the zombie). The 60s-hung HF load unblocks at
the 0.2s timeout, falls back to estimation, and the second call returns
instantly (failure cached, no re-probe). All 10 new tests pass.
- Not tested: live reproduction against `api.deepseek.com` from a
network where HF hub downloads stall (the reporter's exact environment);
actual HF vocab download timing on a healthy network.

## 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>
2026-07-07 12:46:26 -05:00
Parideboy
c7665ca088
fix(transforms): pass through ragged tables instead of misaligning columns (#1713)
## Description

Issue #1652 reports the proxy's compression layer surfacing an
"impossible mixed" status line — a row combining fields from two
different rows of a version-status table (Docker row `0.42.4 → 0.43.0
update available` blended with WSL row `0.42.4 → 0.42.4 up-to-date`).
The reporter's follow-up refined the claim: the stored canonical content
was intact, but the compression path presents a lossier view that
invites exactly this misattribution.

There is a concrete mechanism for that in the tabular bridge:
`parse_tabular` (`headroom/transforms/tabular_ingest.py`) hands parsed
rows to `to_records`, which **silently pads/truncates every row to the
header width**. For ragged tables — rows whose cell count differs from
the header row, exactly what mixed-shape status tables like the
reporter's produce (`✓` and `-` placeholder cells change the token count
per row) — this shifts values under the wrong column before SmartCrusher
compaction. The compressed output can then state column/value pairings
the original never contained.

Fix: `parse_tabular` now rejects ragged tables (any row width ≠ header
width) and returns `None`, so the content passes through verbatim, per
the issue's requirement that a lossy summary "must not create impossible
mixed facts". Aligned tables compress exactly as before. The Rust
`log_template` Drain miner was also examined; its template rendering
only emits tokens that are constant across all rows of a run, so no
defect was found there and it is left untouched.

Fixes #1652

## Type of Change

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

## Changes Made

- `headroom/transforms/tabular_ingest.py`: `parse_tabular` returns
`None` when any parsed row's cell count differs from the header count,
instead of letting `to_records` pad/truncate rows into the wrong
columns. `TabularCompressor.compress` then takes its existing
pass-through branch (`was_modified=False`).
- `tests/test_transforms_tabular.py`: three new tests — ragged
fixed-width table rejected (reproducing the issue's rtk version-status
shape), ragged markdown table rejected, and end-to-end
`TabularCompressor.compress` pass-through of a ragged table.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ python -m pytest tests/test_transforms_tabular.py -q
39 passed

$ ruff check headroom/transforms/tabular_ingest.py tests/test_transforms_tabular.py
All checks passed!
$ ruff format --check headroom/transforms/tabular_ingest.py tests/test_transforms_tabular.py
2 files already formatted
$ mypy headroom --ignore-missing-imports
Success: no issues found (note-level messages only)
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, local checkout branched from
`upstream/main` (9fbd47ba), Rust core built locally.
- Exact command / steps: constructed the issue's table shape (Docker row
with 4 cells, WSL row with 6 cells under 4 headers) and ran it through
`TabularCompressor().compress()` before and after the change; ran the
full `tests/test_transforms_tabular.py` suite.
- Observed result: before — `to_records` turned the WSL row into
`{'tool': 'rtk', 'installed': '✓', 'latest': '0.42.4', 'status':
'0.42.4'}`: the `up-to-date` status is dropped and a version number
lands under `status` — precisely the misattributed-fact class from the
issue. After — `parse_tabular` returns `None`, `compress` returns the
original text unmodified (`was_modified=False`, byte-identical
pass-through), and all 39 tests pass (36 pre-existing + 3 new).
- Not tested: the reporter's exact end-to-end session (OMP → headroom
proxy on 8787 → sticky-router on 4140); the Rust BuildOutput
`log_template` path, which was reviewed and found to only emit
run-constant tokens.

## 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>
2026-07-07 12:45:46 -05:00
Parideboy
0dd24ecfb5
docs: add pipeline-extension recipe and x-headroom-base-url routing docs (#1712)
## Description

Issue #1697 asked for two things: (1) a documented recipe for writing a
request-normalization `headroom.pipeline_extension` for quirky upstream
providers (the reporter's provider rejects OpenAI-spec `content: null` +
`tool_calls` assistant messages, and they solved it with a `PRE_SEND`
extension they could only discover by reading source), and (2) shipping
the `x-headroom-base-url` per-request upstream override. The header
support already exists on main (`headroom/proxy/handlers/openai.py`
honors it in the dedicated chat/responses handlers and passthrough) and
will ship with the next release-please release, so this PR delivers the
missing piece: documentation for both.

Adds `docs/content/docs/pipeline-extensions.mdx` covering the
entry-point contract (`headroom.pipeline_extension`, `PipelineStage`,
fail-open dispatch, `discover_pipeline_extensions` /
`pipeline_extensions` config), a complete copy-pasteable
`NullContentNormalizer` recipe with `pyproject.toml` entry-point
registration, and a section on per-request upstream routing with
`x-headroom-base-url` (including the `HEADROOM_STRIP_INTERNAL_HEADERS`
interaction).

Fixes #1697

## 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

- New page `docs/content/docs/pipeline-extensions.mdx`: lifecycle-stage
table, request-normalization extension recipe (class + entry-point
registration + discovery/fail-open semantics), and `x-headroom-base-url`
per-request routing section with a `curl` example.
- `docs/content/docs/meta.json`: added `pipeline-extensions` to the nav
after `configuration`.

## Testing

- [ ] 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
$ python -c "import json; json.load(open('docs/content/docs/meta.json')); print('META_OK')"
META_OK

$ python -c "
from headroom.pipeline import ENTRY_POINT_GROUP, PipelineStage
print(ENTRY_POINT_GROUP, PipelineStage.PRE_SEND)
"
headroom.pipeline_extension PipelineStage.PRE_SEND
```

## Real Behavior Proof

- Environment: Windows 11, local checkout at `upstream/main` (9fbd47ba),
Python 3.13.
- Exact command / steps: verified every documented claim against source
— `headroom/pipeline.py` (entry-point group, `PipelineStage.PRE_SEND`,
fail-open dispatch in `PipelineExtensionManager.emit`, class
auto-instantiation in `discover_pipeline_extensions`),
`headroom/proxy/server.py`
(`PipelineExtensionManager(discover=config.discover_pipeline_extensions)`
at proxy startup), `headroom/proxy/models.py` (`pipeline_extensions` /
`discover_pipeline_extensions` config fields),
`headroom/proxy/handlers/openai.py` (`x-headroom-base-url` constant and
`_resolve_openai_upstream`); validated `meta.json` still parses.
- Observed result: docs match the code contract on main; nav entry
renders between Configuration and Filesystem Contract.
- Not tested: full docs-site (Next.js) production build; end-to-end run
of the example extension against a live quirky provider (the recipe
mirrors the pattern the issue reporter confirmed working).

## 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>
2026-07-07 12:45:16 -05:00
Parideboy
d6e0710228
fix(install): pass sc.exe create as raw command line so binPath= quoting survives (#1654) (#1702)
## Description

`headroom install apply --preset persistent-service` fails on Windows
with `sc.exe` error 1639 ("invalid start= field"). The service install
built the `sc.exe create` invocation as an argv list whose `binPath=`
token embedded both spaces and inner double quotes (`cmd.exe /c
"…run-headroom.cmd"`). Python's `subprocess.list2cmdline` then wrapped
that whole token in outer quotes, so the command line `sc.exe` actually
received tokenized as `'binPath= cmd.exe /c "…"'` and `'start= auto'` —
single glued tokens — instead of the documented `binPath=` `<value>`
`start=` `<value>` separate-token pairs. `sc.exe` rejects that with
1639.

This PR builds the exact command line as a pre-quoted string and passes
it to `subprocess.run` directly; on Windows a string argument goes
verbatim to `CreateProcess`, bypassing `list2cmdline` entirely. The
`sc.exe failure` / `start` / `stop` / `delete` calls keep the argv-list
form since none of their tokens embed quotes.

Fixes #1654

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)

## Changes Made

- `headroom/install/supervisors.py`: the Windows `SERVICE` branch of
`install_supervisor` now builds the `sc.exe create` command as a single
pre-quoted string — `sc.exe create <name> binPath= "cmd.exe /c
\"<run-headroom.cmd>\"" start= auto` — and passes it to `subprocess.run`
as a string instead of an argv list.
- `tests/test_install/test_supervisors.py`: updated the Windows-service
assertion to expect the new command-line string (regression test for
#1654), verifying the backslash-escaped inner quotes and `start= auto`
as a separate trailing pair.

## Testing

- [x] Unit tests pass (`tests/test_install/test_supervisors.py`)
- [x] Lint/type gates pass (`ruff check`, `ruff format --check`, `mypy`)

```
$ python -m pytest tests/test_install/ -q
94 passed, 1 failed, 1 skipped
# the 1 failure is tests/test_install/test_runtime.py::test_runtime_start_lock_blocks_another_process,
# which fails identically on a clean upstream/main checkout on this machine (pre-existing local env flake,
# unrelated to this change)

$ ruff check headroom/install/supervisors.py tests/test_install/test_supervisors.py
All checks passed!
$ ruff format --check headroom/install/supervisors.py tests/test_install/test_supervisors.py
2 files already formatted
$ mypy headroom --ignore-missing-imports   # exit 0, notes only
```

## Real Behavior Proof

- Environment: Windows 11 Pro 10.0.26200, Python 3.13, local checkout of
this branch.
- Exact command / steps: Tokenized both the old (argv-list →
`list2cmdline`) and new (pre-quoted string) command lines with
`shell32.CommandLineToArgvW` — the same parsing `sc.exe` applies to its
received command line — using the exact path from the issue report. Also
ran the new string form through `subprocess.run` against the real
`sc.exe` (non-elevated).
- Observed result: Old form tokenizes to `['sc.exe', 'create',
'headroom-default', 'binPath= cmd.exe /c
"C:\\Users\\Adron\\...\\run-headroom.cmd"', 'start= auto']` —
`binPath=`/`start=` glued to their values, which `sc.exe` rejects with
1639. New form tokenizes to `['sc.exe', 'create', 'headroom-default',
'binPath=', 'cmd.exe /c "C:\\Users\\Adron\\...\\run-headroom.cmd"',
'start=', 'auto']` — exactly the documented `sc create` token shape.
Running the new string against real `sc.exe` non-elevated proceeds past
argument parsing to `OpenSCManager FAILED 5: Access is denied` (the
expected no-admin outcome per the issue reporter's own non-admin run),
with no 1639 syntax error.
- Not tested: Full elevated end-to-end `headroom install apply --preset
persistent-service` service creation + service start on an Administrator
shell (no elevated session available in this environment); behavior on
non-English locales other than the tokenization-level verification
above.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

**Follow-up candidate (out of scope here)**: the issue also notes that a
failed install removes `~/.headroom/deploy/<profile>/` artifacts,
hampering post-mortem debugging — worth a separate issue/PR to preserve
or relocate failed-install artifacts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 12:43:57 -05:00