Commit graph

2590 commits

Author SHA1 Message Date
JD Davis
b23175a3d3
Merge branch 'main' into fix/learn-codex-cli-failure 2026-08-14 12:16:08 -05:00
dependabot[bot]
bbe901319d
deps: bump tokio-tungstenite from 0.24.0 to 0.30.0 (#2967)
Bumps [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite)
from 0.24.0 to 0.30.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/snapview/tokio-tungstenite/blob/master/CHANGELOG.md">tokio-tungstenite's
changelog</a>.</em></p>
<blockquote>
<h1>0.30.0</h1>
<ul>
<li>Update <code>tungstenite</code> to <code>0.30.0</code>. See <a
href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md"><code>tungstenite</code>
release</a>.</li>
</ul>
<h1>0.29.0</h1>
<ul>
<li>Update <code>tungstenite</code> to <code>0.29.0</code>. See <a
href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md"><code>tungstenite</code>
release</a>.</li>
</ul>
<h1>0.28.0</h1>
<ul>
<li>Update <code>tungstenite</code> to <code>0.28.0</code>. See <a
href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md"><code>tungstenite</code>
release</a>.</li>
</ul>
<h1>0.27.0</h1>
<ul>
<li>See <a
href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0270">performance
updates in <code>tungstenite-rs</code></a>.</li>
</ul>
<h1>0.26.2</h1>
<ul>
<li>Update <code>tungstenite</code>, see <a
href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0262">changes
here</a>.</li>
</ul>
<h1>0.26.1</h1>
<ul>
<li>Update <code>tungstenite</code> to address an issue that might cause
UB in certain cases.</li>
</ul>
<h1>0.26.0</h1>
<ul>
<li>Update <code>tungstenite</code> to <code>0.26.0</code> (<a
href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0260">breaking
changes</a>).</li>
</ul>
<h1>0.25.0</h1>
<ul>
<li>Update <code>tungstenite</code> to <code>0.25.0</code> (<a
href="https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0250">important
updates!</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4994a07803"><code>4994a07</code></a>
Bump version</li>
<li><a
href="753ca72690"><code>753ca72</code></a>
Document cancel safety of reading from WebSocketStream (<a
href="https://redirect.github.com/snapview/tokio-tungstenite/issues/378">#378</a>)</li>
<li><a
href="751d7e2bc2"><code>751d7e2</code></a>
Update version number listed in Readme (<a
href="https://redirect.github.com/snapview/tokio-tungstenite/issues/375">#375</a>)</li>
<li><a
href="57fc3d0276"><code>57fc3d0</code></a>
docs(CHANGELOG.md): fix <code>tungstenite</code> versions (<a
href="https://redirect.github.com/snapview/tokio-tungstenite/issues/374">#374</a>)</li>
<li><a
href="7930ff2f82"><code>7930ff2</code></a>
Bump version</li>
<li><a
href="38d04656fe"><code>38d0465</code></a>
Update Readme (<a
href="https://redirect.github.com/snapview/tokio-tungstenite/issues/369">#369</a>)</li>
<li><a
href="35d110c24c"><code>35d110c</code></a>
Implement into_inner to get the underlying stream (<a
href="https://redirect.github.com/snapview/tokio-tungstenite/issues/367">#367</a>)</li>
<li><a
href="f3ae75d1de"><code>f3ae75d</code></a>
Update <code>tungstenite</code> version and fix bugs</li>
<li><a
href="25b544e43f"><code>25b544e</code></a>
Allow getting a reference to the shared inner stream (<a
href="https://redirect.github.com/snapview/tokio-tungstenite/issues/363">#363</a>)</li>
<li><a
href="e855f9eb8c"><code>e855f9e</code></a>
Fix errors in the examples caused by <code>Utf8Error</code></li>
<li>Additional commits viewable in <a
href="https://github.com/snapview/tokio-tungstenite/compare/v0.24.0...v0.30.0">compare
view</a></li>
</ul>
</details>
<br />

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-14 10:50:15 -05:00
dependabot[bot]
888a9f4e14
deps: bump the cargo-minor-patch group across 1 directory with 4 updates (#2964)
Bumps the cargo-minor-patch group with 4 updates in the / directory:
[aws-config](https://github.com/smithy-lang/smithy-rs),
[rusqlite](https://github.com/rusqlite/rusqlite),
[async-trait](https://github.com/dtolnay/async-trait) and
[cc](https://github.com/rust-lang/cc-rs).

Updates `aws-config` from 1.10.0 to 1.10.1
<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 `rusqlite` from 0.40.1 to 0.40.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rusqlite/rusqlite/releases">rusqlite's
releases</a>.</em></p>
<blockquote>
<h2>0.40.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Lower MSRV to 1.88.0</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/rusqlite/rusqlite/compare/v0.40.1...v0.40.2">https://github.com/rusqlite/rusqlite/compare/v0.40.1...v0.40.2</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e88f112bef"><code>e88f112</code></a>
Prepare release</li>
<li><a
href="d11c76e7d7"><code>d11c76e</code></a>
Update main.yml</li>
<li><a
href="c922ca5b71"><code>c922ca5</code></a>
Lower MSRV to 1.88.0</li>
<li>See full diff in <a
href="https://github.com/rusqlite/rusqlite/compare/v0.40.1...v0.40.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `async-trait` from 0.1.91 to 0.1.92
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/async-trait/releases">async-trait's
releases</a>.</em></p>
<blockquote>
<h2>0.1.92</h2>
<ul>
<li>Resolve double_must_use clippy lint in generated code (<a
href="https://redirect.github.com/dtolnay/async-trait/issues/303">#303</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="82e7e9edd6"><code>82e7e9e</code></a>
Release 0.1.92</li>
<li><a
href="9a35cb87f9"><code>9a35cb8</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/async-trait/issues/303">#303</a>
from dtolnay/mustuse</li>
<li><a
href="875ceecb10"><code>875ceec</code></a>
Resolve double_must_use clippy lint</li>
<li><a
href="62993a57bc"><code>62993a5</code></a>
Raise minimum tested compiler to rust 1.88</li>
<li>See full diff in <a
href="https://github.com/dtolnay/async-trait/compare/0.1.91...0.1.92">compare
view</a></li>
</ul>
</details>
<br />

Updates `cc` from 1.4.1 to 1.4.2
<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.4.2</h2>
<h3>Fixed</h3>
<ul>
<li>Infer NEON, not VFPv4, from <code>neon</code> in the target name (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1843">#1843</a>)</li>
<li>do not emit <code>-mno-omit-leaf-frame-pointer</code> if unsupported
(<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1845">#1845</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.4.1...cc-v1.4.2">1.4.2</a>
- 2026-08-08</h2>
<h3>Fixed</h3>
<ul>
<li>Infer NEON, not VFPv4, from <code>neon</code> in the target name (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1843">#1843</a>)</li>
<li>do not emit <code>-mno-omit-leaf-frame-pointer</code> if unsupported
(<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1845">#1845</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a91e05ec40"><code>a91e05e</code></a>
chore(cc): release v1.4.2 (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1846">#1846</a>)</li>
<li><a
href="0e91755354"><code>0e91755</code></a>
fix: Infer NEON, not VFPv4, from <code>neon</code> in the target name
(<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1843">#1843</a>)</li>
<li><a
href="c20feddb4f"><code>c20fedd</code></a>
do not emit -mno-omit-leaf-frame-pointer if unsupported (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1845">#1845</a>)</li>
<li>See full diff in <a
href="https://github.com/rust-lang/cc-rs/compare/cc-v1.4.1...cc-v1.4.2">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-14 07:25:43 -05:00
JD Davis
2d88e31a40
fix(claude): reject conflicting auth before proxy startup (#2993)
## Description

Fixes #1443.

Claude Code rejects an effective configuration containing both
ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN before any request reaches
Headroom. The existing wrapper started the proxy and mutated project
settings before Claude surfaced its generic Invalid API key message,
leaving users to guess which credential came from their shell, global
settings, or project settings.

Headroom does not own either credential, and both represent legitimate
but different auth/billing modes, so automatically deleting one would be
destructive. This PR detects the contradiction before any proxy/config
mutation and tells the user which source contains each key without
exposing credential values.

## Changes Made

- Add a pure Claude auth-conflict classifier with explicit
settings-layer precedence.
- Cover user settings, project .claude/settings.json, project
.claude/settings.local.json, and shell environment.
- Treat higher-precedence empty values as clearing inherited
credentials.
- Abort wrap claude before proxy registration/startup when both keys
remain effective.
- Add a headroom doctor failure with the same source-aware,
value-redacted remediation.
- Preserve both user credentials and require an explicit choice between
API-key billing and token/gateway auth.

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

## Testing

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

### Test Output

```text
151 Claude runtime, wrap, doctor, Remote Control, and MCP dependency-contract tests passed
ruff check and format checks passed
git diff --check passed
```

Branch contains current main, including the MCP v1 cap and the five
just-merged blocker PRs.

## Real Behavior Proof

- Environment: isolated local worktree on current `main` with Claude
wrapper and doctor fixtures.
- Exact command / steps: exercised conflicting and non-conflicting
shell, user, project, and local-project credential layers through the
focused wrap and doctor test suites.
- Observed result: conflicting effective credentials fail before proxy
startup or settings mutation, report only credential sources, and never
expose values.
- Not tested: a live Claude Code login with production credentials;
credential precedence and side-effect boundaries are covered by
fixtures.

## Runtime Rollout Safety

- Rollout-managed feature(s): Claude authentication-conflict preflight.
- Minimum rollout channel: normal patch release.
- Stable/default behavior changed: only configurations with both
effective credentials now stop early with actionable diagnostics.
- Kill switch / disable path: remove or clear either conflicting
credential in its reported source.
- Unsafe override required: none; Headroom deliberately does not choose
or delete a user credential.
- Qualification impact: Claude wrap, doctor, Remote Control, and MCP
dependency-contract tests must remain green.
- Rollback path: human revert restores the previous late Claude Code
rejection; no persisted migration is involved.

## Review Readiness

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

## Safety

No credential value is returned by the classifier, printed by wrap, or
emitted in doctor JSON. The preflight runs before
_register_proxy_client, proxy startup, MCP registration, or settings
writes.
2026-08-13 23:01:59 -05:00
Jerrett Davis
db7e74187a fix(learn): surface Codex analysis failures 2026-08-13 22:47:15 -05:00
JD Davis
aa811fa91f
ci: allow generated dependency commit bodies (#3012)
## Description

Disable commitlint's per-line body length limit because Dependabot
generates grouped-update commit bodies with dependency/link lines whose
length varies with group contents. PR #2964 currently fails only because
one generated line is 274 characters long.

Closes # N/A

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

- Disabled `body-max-line-length` in `.commitlintrc.json`.
- Kept Conventional Commit type, subject, and all other configured
validation rules enforced.

## 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
PR #2964 Dependabot commit (49 lines; maximum line length 274)
exit code: 0

bogus: should fail
type must be one of [build, chore, ci, docs, deps, feat, fix, parity, perf, refactor, revert, style, test] [type-enum]
exit code: 1

fix:
subject may not be empty [subject-empty]
exit code: 1

git diff --check
exit code: 0
```

## Real Behavior Proof

- Environment: Windows PowerShell; Node.js 22; `@commitlint/cli` and
`@commitlint/config-conventional` 19.8.1.
- Exact command / steps: Fetched the current PR #2964 commit message
through the GitHub API and piped the complete message into commitlint
using this branch's `.commitlintrc.json`; then ran negative type and
subject cases.
- Observed result: The exact grouped Dependabot commit passed; an
unapproved type and empty subject remained rejected.
- Not tested: Python/Rust unit tests and runtime behavior; this change
only modifies commit-message validation configuration.

## Runtime Rollout Safety

- Rollout-managed feature(s): N/A; CI configuration only.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: Commit bodies may contain lines of
any length; all other commitlint rules remain active.
- Kill switch / disable path: Revert this commit or restore a numeric
`body-max-line-length` limit.
- Unsafe override required: No.
- Qualification impact: Generated Dependabot group descriptions no
longer fail CI due solely to a long dependency/link line.
- Rollback path: Revert this commit.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A; this change has no user interface.

## Additional Notes

- The comment and documentation checklist items are not applicable to
this one-line commitlint configuration change.
- No automated test file was added; the exact positive and negative
commitlint cases were run manually as shown above.
- Full application tests were not run because no application code or
runtime behavior changed.
- Keep this PR unmerged pending maintainer review.
2026-08-13 22:31:47 -05:00
JD Davis
7e3128057c
ci: allow Dependabot deps commits (#3009)
## Description

Allow the `deps:` Conventional Commit type emitted by Dependabot.
Dependabot PRs currently fail the CI `commitlint` job because `deps` is
not included in the repository's configured `type-enum`.

Closes # N/A

## 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 `deps` to the allowed commit types in `.commitlintrc.json`.
- Existing and future Dependabot commits using `deps: ...` can pass the
commit-message policy.

## 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
deps: bump ruff from 0.15.22 to 0.16.2
exit code: 0

bogus: should fail
type must be one of [build, chore, ci, docs, deps, feat, fix, parity, perf, refactor, revert, style, test] [type-enum]
exit code: 1

git diff --check
exit code: 0
```

## Real Behavior Proof

- Environment: Windows PowerShell; Node.js 22; `@commitlint/cli` and
`@commitlint/config-conventional` 19.8.1.
- Exact command / steps: Ran commitlint with `.commitlintrc.json`
against a real failing Dependabot subject, then against an unapproved
`bogus:` type.
- Observed result: The `deps:` subject passed; the unapproved type
remained rejected by `type-enum`.
- Not tested: Python/Rust unit tests and runtime behavior; this change
only modifies commit-message validation configuration.

## Runtime Rollout Safety

- Rollout-managed feature(s): N/A; CI configuration only.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: Commitlint now accepts the `deps`
type.
- Kill switch / disable path: Revert this commit or remove `deps` from
`type-enum`.
- Unsafe override required: No.
- Qualification impact: Dependabot PR commit messages no longer fail
solely because their type is `deps`.
- Rollback path: Revert this commit.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A; this change has no user interface.

## Additional Notes

- The comment and documentation checklist items are not applicable to
this one-line commitlint configuration change.
- No automated test file was added; the exact positive and negative
commitlint cases were run manually as shown above.
- Full application tests were not run because no application code or
runtime behavior changed.
- Keep this PR unmerged pending maintainer review.
2026-08-13 21:45:16 -05:00
Copilot
8ea87e7804
fix: tool_search_tool_regex deferred and falsely resolved on direct-Anthropic path (#2971)
## Description

Direct Anthropic users could receive `400 Tool reference
'tool_search_tool_regex' not found in available tools` when Claude Code
sent a typeless `tool_search_tool_regex` entry. Headroom treated it as
an ordinary deferrable tool, injected a typed search tool with the same
name, and later mistook that typed search mechanism for a valid target
of the stale `tool_reference`.

This change prevents the duplicate injection and repairs
already-poisoned transcripts without stripping valid references to
ordinary deferred tools. It addresses the first-party Anthropic
regression reported in [PR #2539's
follow-up](https://github.com/headroomlabs-ai/headroom/pull/2539#issuecomment-5280259642)
and complements the history repair from #2805.

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

- recognize typeless, case-insensitive `tool_search_tool_*` names as an
existing client tool-search surface and skip Headroom's duplicate
injection
- exclude typed Anthropic search mechanisms from the set of valid
`tool_reference` targets
- preserve valid regular deferred-tool references and the normal
deferral path for similar non-reserved names
- add a first-party Anthropic handler regression that proves the
outbound tools remain unchanged and stale search bookkeeping is removed
- rebase onto #2996, which prevents the native detector from hanging the
full CI test shard

## 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/Scripts/python.exe -m pytest \
  tests/test_issue_746_tool_search.py \
  tests/test_anthropic_stage_timings.py \
  tests/test_cache_control_ttl_order.py \
  tests/test_cache_ttl_preserved.py \
  tests/test_proxy/test_tool_search_repair_after_turn_hooks.py \
  tests/test_transforms/test_detect_fallback_1123.py \
  tests/test_transforms_content_detection.py \
  tests/test_transforms_content_router.py \
  -q --disable-warnings --maxfail=1
170 passed, 1 warning in 10.27s

.venv/Scripts/ruff.exe check .
All checks passed!

.venv/Scripts/ruff.exe format --check .
1411 files already formatted

pre-commit run mypy --all-files
Success: no issues found in 519 source files

git diff --check
(no output)
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.3, first-party Anthropic handler
test with `HEADROOM_TOOL_SEARCH` at its default enabled setting
- Exact command / steps: run the same helper-level payload against the
pre-fix base and this branch, then run
`test_anthropic_direct_path_repairs_typeless_tool_search_regression`
through `handle_anthropic_messages()` with 20 ordinary tools, one
typeless `tool_search_tool_regex`, and a stale self-reference
- Observed result: before the fix, Headroom injected a second typed
search tool, deferred the typeless client tool, and removed 0 stale
blocks; on this branch, it skips duplicate injection, preserves the
client tools array, and removes the paired `server_tool_use` and
`tool_search_tool_result` blocks before forwarding
- Not tested: a live request against a paid Anthropic account; the
production handler's outbound body is captured before the network
boundary instead

## Runtime Rollout Safety

- Rollout-managed feature(s): Anthropic server-side tool-search deferral
(`HEADROOM_TOOL_SEARCH`)
- Minimum rollout channel: standard CI; narrow corrective change to an
existing default-on path
- Stable/default behavior changed: yes; reserved typeless client search
tools now suppress duplicate injection, and typed search mechanisms no
longer satisfy deferred-tool references
- Kill switch / disable path: set `HEADROOM_TOOL_SEARCH=0` to disable
new injection; history repair remains unconditional so existing poisoned
sessions can recover
- Unsafe override required: no
- Qualification impact: no new rollout surface or configuration; focused
handler, helper, cache-control, and hook-order regressions cover the
affected path
- Rollback path: revert this PR; operators can set
`HEADROOM_TOOL_SEARCH=0` while rolling back

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A — proxy request transformation only.

## Additional Notes

- Documentation is not changed because this fixes internal request
classification and transcript repair without adding a user-facing option
or workflow.
- Anthropic documents `tool_search_tool_regex` / `tool_search_tool_bm25`
as server search mechanisms; deferred definitions, rather than the
search mechanism itself, are the valid `tool_reference` targets.
- Rebased onto #2996, which fixes the unrelated native-detector hang
that timed out shard 4 on the prior merge commit.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-13 21:15:37 -05:00
JD Davis
8a1d38bc5d
fix(proxy): complete stateless Responses and buffered CCR lifecycle (#2997)
## Description

Consolidates the related OpenAI Responses ZDR/stateless continuation and
buffered CCR response-lifecycle corrections on current main. It
preserves client storage policy, makes Headroom-owned continuations
stateless across HTTP and WebSocket, and prevents buffered streaming
paths from committing a false HTTP 200 before the real upstream outcome
is known.

Closes #2675

## 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 explicit and omitted Responses `store` policy instead of
forcing provider storage or disabling memory tools.
- Replays normalized input, replayable outputs, encrypted reasoning
content, and Headroom function outputs without `previous_response_id`.
- Applies the same stateless continuation policy to HTTP and WebSocket.
- Prevents transparent memory execution after client-visible WebSocket
output.
- Delays buffered CCR ASGI status/headers until the operation resolves
for Anthropic Messages and OpenAI Responses.
- Preserves real 429/5xx status and retry headers.
- Converts malformed non-JSON/non-SSE upstream 200 replies to a
sanitized 502 protocol error.
- Preserves valid JSON-to-SSE synthesis and existing SSE adaptation.
- Removes unreachable task cleanup left behind after replacing the old
keepalive polling loop with a direct awaited operation.

## 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
118 passed across the changed HTTP/WS ZDR, lifecycle, and both-provider CCR suites
11526 tests collected with no collection errors
ruff check .: All checks passed
ruff format --check .: 1411 files already formatted
mypy headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/openai.py:
Success: no issues found in 2 source files
```

Exact-head CI is entirely green on
`cbc2739c0c`.

## Real Behavior Proof

- Environment: macOS arm64/Python 3.13 locally; GitHub-hosted Ubuntu
matrix pending.
- Exact command / steps: exercise `store=false` Responses memory calls
over HTTP and WebSocket; exercise buffered Anthropic and Responses
requests returning successful JSON/SSE, delayed 429 responses,
exceptions, and malformed successful bodies; invoke returned ASGI
responses and inspect emitted status, headers, and body order.
- Observed result: stateless continuations omit provider response IDs
and retain `store=false`; no ASGI start event is emitted before the
buffered outcome; real failures preserve status/headers; malformed 200
responses become sanitized 502 errors.
- Not tested: live ZDR tenant and live Anthropic/OpenAI upstream
credentials are unavailable in repository CI; wire contracts are
exercised through deterministic upstream doubles.

## Runtime Rollout Safety

- Rollout-managed feature(s): Responses memory continuation and buffered
CCR handling.
- Minimum rollout channel: normal patch release after full CI
qualification.
- Stable/default behavior changed: memory continuation no longer
requires provider storage; buffered CCR waits before committing response
status.
- Kill switch / disable path: disable memory/CCR using existing proxy
configuration (`--no-ccr` for CCR); ordinary non-buffered paths are
unchanged.
- Unsafe override required: none.
- Qualification impact: full Python matrix plus focused HTTP/WS
lifecycle suites must pass; patch coverage must not rely on unreachable
cleanup.
- Rollback path: human revert of this PR restores prior
continuation/buffering behavior; no persisted data migration is
introduced.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review — exact-head CI is entirely
green

## 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 — inline
protocol/lifecycle documentation; no separate user guide required
- [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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable; proxy protocol behavior.

## Additional Notes

Human review only. No merge or auto-merge is configured. This supersedes
narrower #2995 and incorporates the complete intent of #2705, #2959, and
#2968 without falsely closing those PRs. It does not claim the broader
event-level streaming-splice guarantees requested by #1877. Refreshed
from main after #2996; the MCP cap `mcp>=1.28.1,<2.0.0` is preserved.
2026-08-13 21:12:38 -05:00
JD Davis
a708c0571e
fix(ci): prevent native detector from hanging test shards (#2996)
## Description

CI shard 4 was not merely slow: after thousands of fast tests it parked
indefinitely inside `headroom._core.detect_content_type` at 0% CPU. The
router watchdogged only the first native call and then permanently
trusted direct calls via `_detect_native_verified`. Earlier suite
activity can change ORT/native state after that first success, making a
later call deadlock until GitHub cancels the job.

This keeps every native call bounded by the existing watchdog, activates
the process-wide pure-Python circuit breaker after a timeout, restores
the test-job ceiling to 30 minutes, and removes a separate wall-clock
scheduler assertion that generated false shard-1 failures despite the
structural regression guards passing.

No issue is auto-closed by this infrastructure repair.

## Type of Change

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

## Changes Made

- Removed the unsafe process-lifetime `_detect_native_verified` fast
path.
- Kept every native detection call behind the existing bounded watchdog.
- Preserved the process-wide fallback circuit breaker so only the first
wedged call consumes the watchdog budget.
- Added a success-then-hang regression test.
- Isolated native circuit-breaker state in fallback exception tests.
- Restored the CI test timeout from the temporary 90-minute diagnostic
ceiling to 30 minutes.
- Replaced the Codex scheduler's noise-sensitive p99/p50 assertion with
its meaningful absolute regression ceiling while retaining source-level
guards against the removed semaphore and nested executor.
- Corrected import order and formatting defects inherited from current
main so the synthetic merge commit passes repository-wide lint.

## 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
Exact local shard-4 command with coverage:
2723 passed, 172 skipped, 8661 deselected in 108.40s

Focused detector/router suite:
62 passed

Codex scheduler suite:
3 passed, 1 skipped

ruff check .
All checks passed!

ruff format --check .
1411 files already formatted

mypy headroom/transforms/content_router.py
Success: no issues found in 1 source file
```

Exact-head GitHub CI on `28f284c7a1` is
entirely green. Test jobs 1–4, test-extras, test-agno, build, wheel,
lint, CodeQL, dependency audit, secret scan, smoke, governance, and
conflict checks all passed. Remaining skips are path-filtered jobs not
applicable to this diff.

## Real Behavior Proof

- Environment: macOS arm64/Python 3.13 locally; GitHub-hosted
Ubuntu/Python 3.12 using the production CI workflow and prebuilt wheel.
- Exact command / steps: reproduced `pytest tests scripts/tests --splits
4 --group 4 ...` hanging in native detection; sampled the parked
process; reran with `pytest-timeout` to locate `_rust_detect`; applied
the correction; reran the exact shard locally and all four CI shards
remotely.
- Observed result: local shard 4 completed in 1:48. GitHub shard 4's
pytest step completed in 5:45 and its full job in 8:06 under the
restored 30-minute ceiling. All four shards passed on the same head.
- Not tested: deliberately wedging a real production ORT runtime outside
the deterministic mocked regression; the watchdog behavior is covered
with a native-call fake that succeeds once and then never returns.

## Runtime Rollout Safety

- Rollout-managed feature(s): native content detection watchdog and
fallback only.
- Minimum rollout channel: normal patch release; no staged feature flag
required.
- Stable/default behavior changed: every native detection call remains
watchdog-bounded instead of only the first successful call.
- Kill switch / disable path: `HEADROOM_DETECT_BACKEND=python` bypasses
native detection; `HEADROOM_DETECT_TIMEOUT_SECS` controls the watchdog
budget.
- Unsafe override required: none.
- Qualification impact: full Python CI matrix must remain green; exact
shard-4 completion is the primary qualification evidence.
- Rollback path: human revert of this PR if bounded calls cause an
unexpected regression; setting the Python backend provides an immediate
operational fallback without code rollback.

## 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 — inline
lifecycle documentation and PR operational notes; no user-facing docs
change is needed
- [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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable; no UI change.

## Additional Notes

Human review only. No merge or auto-merge action has been configured.
The branch includes current main and preserves the MCP SDK compatibility
cap `mcp>=1.28.1,<2.0.0`.
2026-08-13 20:47:55 -05:00
Tejas Chopra
3145242645
Unify savings attribution across stats, perf, metrics, and dashboard (#2976)
## Summary

Adds a small provider-neutral savings attribution seam. Named sources
can attach realized or projected token/USD deltas to a request without
changing headline arithmetic or introducing private-package inventory
into OSS.

Also fixes the Anthropic buffered lifecycle so normal successful
responses run response hooks, applies stream-safety filtering, includes
tool savings in per-model perf totals, and surfaces the same breakdown
in request logs, `/stats`, `headroom perf`, Prometheus, OTEL, and the
dashboard.

## Why

Request-local savings were split between canonical token deltas,
process-global extension counters, and tool-only tags. This made correct
headline totals possible while losing attribution in perf, recent
requests, metrics, and the dashboard. Normal Anthropic responses also
skipped response hooks unless CCR ran.

## Validation

- 74 focused tests passed: turn hooks, OpenAI hook lifecycle, outcome
funnel, perf formats, and tool-search repair
- Ruff passes on all changed Python files
- Existing compression-observability suite: 11 passed; 2 tokenizer-cache
tests require network access to fetch the tiktoken vocabulary

## Compatibility

No named private packages or private inventory are encoded in OSS.
Existing hooks remain source-compatible because all new TurnContext
fields are optional.

---------

Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-13 17:13:23 -07:00
JD Davis
1aa701adaa
fix(vscode): persist compatible Claude modes and route Copilot CAPI (#2986)
## Description

Fixes #2492, #2028, and #2827.

Claude daemon workers consume project settings rather than reliably
inheriting wrapper environment state, while the Claude VS Code webview
cannot render deferred-tool response blocks. Separately, recent Copilot
Chat versions use the whole CAPI override for generation; the legacy
proxy override alone only sends model discovery through Headroom.

This PR carries both integrations through to the actual consumers
instead of only changing their launch-time surface configuration.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Build / CI

## Changes Made

- Persist the resolved Claude ENABLE_TOOL_SEARCH value into project
settings for daemon workers and restore it transactionally after wrap
exits.
- Use compatibility-safe Foundry and Claude VS Code defaults while
preserving explicit user choices.
- Configure both Copilot overrideProxyUrl and overrideCapiUrl in the
reversible managed VS Code settings block.
- Route Copilot unprefixed POST /chat/completions and HTTP /responses
requests through the real compression handlers.
- Keep /responses out of the Codex WebSocket aliases because Copilot and
Codex use different WebSocket wire protocols.
- Extend wrap E2E assertions for both the Claude webview mode and
Copilot CAPI routing.

## Testing

- [x] 127 combined Claude, Copilot, route-integration, and MCP
dependency-contract tests pass.
- [x] Ruff check passes on all changed Python files.
- [x] Ruff format check passes.
- [x] Python compilation and git diff --check pass.

## Runtime Safety

Standalone Claude CLI defaults remain unchanged. Explicit Claude
tool-search values retain precedence, and project settings are restored
through the existing cleanup path. Copilot model/session helper
endpoints continue through generic passthrough, while only validated
HTTP generation paths receive explicit compression routes. Existing
Codex WebSocket behavior is unchanged.

## Review Readiness

- [x] Current main and MCP v1 compatibility retained
- [x] Worker-facing Claude persistence covered
- [x] Reversible Copilot and Claude settings behavior covered
- [x] Copilot generation routes covered at registration and proxy
integration layers
- [x] Ready for review
2026-08-13 15:06:41 -05:00
JD Davis
eafdf11a2c
fix(docker): ship Bedrock auth and current registry (#2982)
## Description

Fixes #1551 and #1692.

Every published Headroom Docker image now installs the existing
`bedrock` extra, so `--backend bedrock` can authenticate with temporary
STS, SSO, and credential-process credentials instead of failing because
`botocore` is absent.

Public Docker instructions now consistently use
`ghcr.io/headroomlabs-ai/headroom`. Several still pointed at the old
personal package, which is frozen at 0.27.0 and caused users to report
that no latest image existed.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [x] Documentation update
- [x] Build / CI

## Changes Made

- Add `bedrock` to the standalone Dockerfile default extras.
- Add `bedrock` to all nine root/code/slim/nonroot bake targets.
- Replace obsolete personal GHCR references in README, llms.txt, Compose
guidance, testing guidance, and wiki docs.
- Add release contract tests for Bedrock dependencies and the current
organization registry.

## Testing

- [x] Focused Docker release and Bedrock preflight tests pass.
- [x] Full updater suites pass: 69 tests.
- [x] `uv run ruff check tests/test_release_workflows.py`
- [x] `docker buildx bake --print`
- [x] `git diff --check`

## Real Behavior Proof

Before this change, every published bake target installed only `proxy`
or `proxy,code`, so `AWS_SESSION_TOKEN` selected an unavailable botocore
path. Public copy-paste commands also referenced
`ghcr.io/chopratejas/headroom`, which the existing migration code and
changelog identify as frozen at 0.27.0.

After this change, all nine parsed bake targets install `bedrock`; the
regression resolves that package extra and confirms `boto3` plus
`botocore`. Every public Docker instruction covered by the contract
names `ghcr.io/headroomlabs-ai/headroom`.

## Runtime Rollout Safety

This changes image contents and documentation only; proxy routing and
non-Docker installs are unchanged. Static AWS credentials remain
unaffected. Existing manifests using the deprecated image continue to be
migrated by the established install-state logic. Rollback is a
Docker/bake extras and documentation revert.

## Review Readiness

- [x] Two related Docker blockers batched in one PR
- [x] Regression coverage included
- [x] No unrelated lockfile changes
- [x] Ready for review
2026-08-13 15:06:21 -05:00
JD Davis
ddd2a259ec
fix(install): consolidate Windows fallback and cleanup safety (#2980)
## Description

Consolidates two fully reviewed installation-safety fixes whose original
PRs can no longer merge under current branch protection: Windows
persistent-service deployments need a supported Task Scheduler fallback,
and legacy context-tool cleanup must never delete user-owned
RTK/lean-ctx artifacts.

Closes #2552
Closes #2817

Supersedes #2600 and #2828 while preserving their authors' commits and
review-driven corrections.

## Type of Change

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

## Changes Made

- Convert Windows `persistent-service` plans to the supported
`persistent-task` supervisor and make the fallback explicit in CLI
output.
- Restrict context-tool cleanup to artifacts proven to live under
Headroom's managed directory.
- Recognize wrapped, relative, and platform-specific managed commands
without accepting prefixed/path-boundary lookalikes.
- Scope cleanup completion state correctly across projects and alternate
agent homes.
- Stamp cleanup complete only after all managed remnants are settled.
- Preserve the original focused regression suites and behavior-proof
artifact.

## Testing

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

### Test Output

```text
$ uv run pytest -q tests/test_install/test_planner.py tests/test_install/test_supervisors.py tests/test_cli/test_install_cli.py tests/test_context_tool_cleanup.py tests/test_cli/test_unwrap_claude.py
135 passed in 0.45s

$ uv run ruff check <changed Python and test files>
All checks passed!

$ uv run ruff format --check <changed Python and test files>
8 files already formatted
```

## Real Behavior Proof

- Environment: macOS arm64 for consolidated current-main validation; the
Windows fallback source PR was independently validated on Windows and
includes its captured verification artifact.
- Exact command / steps: run the planner, supervisor, install CLI,
cleanup provenance, and unwrap suites on the rebased combined branch.
- Observed result: 135/135 focused tests pass. Windows service requests
resolve to `persistent-task`; cleanup rejects user-owned and path-prefix
lookalikes while removing managed artifacts.
- Not tested: a fresh privileged Windows host deployment in this local
pass; #2600's accepted review contains the Windows-specific proof.

## Runtime Rollout Safety

- Rollout-managed feature(s): Install supervisor selection and one-time
legacy cleanup.
- Minimum rollout channel: Stable/default; both prevent currently
destructive or nonfunctional install paths.
- Stable/default behavior changed: Windows service requests use Task
Scheduler; cleanup requires managed provenance.
- Kill switch / disable path: Select `persistent-task` explicitly;
cleanup remains bounded by its completion stamp and provenance checks.
- Unsafe override required: No.
- Qualification impact: Windows native install and wrap/unwrap cleanup
suites.
- Rollback path: Revert this PR, restoring the two pre-fix behaviors.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

The Windows verification artifact from #2600 is retained at
`.github/pr-images/issue-2552-windows-fallback-verification.png`.

## Additional Notes

This is intentionally an installation-safety batch rather than two
replacement PRs. Original commit authorship is preserved, and the
combined diff was applied cleanly to current `main` after #2832 and
#1628 landed.

---------

Co-authored-by: Inference1 <68734681+Inference1@users.noreply.github.com>
Co-authored-by: Dennis Alexis Valin Dittrich <dd+github@dr-dittrich.de>
2026-08-13 15:05:45 -05:00
JD Davis
a3fe5cb65b
fix(onnx): enforce Rust API-24 runtime compatibility (#2979)
## Description

Rust fastembed enables ORT C API 24, but the Python dependency allowed
ONNX Runtime 1.23.2. Entering ort's initializer with that library
deadlocks permanently instead of returning an error. Align dependency
resolution where compatible wheels exist and preflight native detection
where they do not.

Closes #2960

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

- Require ONNX Runtime 1.24+ for Python 3.11+ in the proxy and voice
extras.
- Keep the available pre-1.24 runtime on Python 3.10 for Python ONNX
consumers.
- Refuse to auto-pin an incompatible runtime into the Rust extension.
- Bypass native detection immediately when API 24 is unavailable,
preserving Python fallback without a five-second watchdog delay or stuck
native thread.
- Add dependency, pinning, override, and router regression coverage.

## Testing

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

### Test Output

```text
$ uv run pytest -q tests/test_transforms/test_ort_dylib.py tests/test_onnx_dependency_contract.py tests/test_onnx_runtime.py tests/test_transforms/test_content_router.py
88 passed in 9.31s

$ uv run ruff check headroom/_ort.py headroom/transforms/content_router.py tests/test_transforms/test_ort_dylib.py tests/test_onnx_dependency_contract.py
All checks passed!
```

## Real Behavior Proof

- Environment: macOS arm64; Python 3.13.14 and uv-managed Python
3.10.20.
- Exact command / steps: run the issue's direct
`headroom._core.detect_content_type` call in a subprocess with a
12-second timeout on Python 3.13; run `_detect_content` on Python 3.10
after resolving the proxy extra.
- Observed result: Python 3.13 resolves ORT 1.26.0 and native detection
returns `json_array`; Python 3.10 resolves ORT 1.23.2, leaves
`ORT_DYLIB_PATH` unset, reports compatibility false, and immediately
returns the Python `json_array` fallback.
- Not tested: Linux-specific shared-object execution locally; CI's
existing Linux Rust job already preflights ORT 1.24+ and exercises
native tests.

## Runtime Rollout Safety

- Rollout-managed feature(s): Native Rust content detection.
- Minimum rollout channel: Stable/default; this is a deadlock prevention
guard.
- Stable/default behavior changed: Python 3.11+ installs a compatible
ORT; Python 3.10 skips incompatible native detection.
- Kill switch / disable path: `HEADROOM_DETECT_BACKEND=python` remains
available; an explicit `ORT_DYLIB_PATH` remains an operator override.
- Unsafe override required: No.
- Qualification impact: Native detection stays enabled only with
API-24-compatible ORT.
- Rollback path: Revert this PR, which restores the old watchdog-only
degradation.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable.

## Additional Notes

The large lockfile diff is dependency resolution: Python 3.10 keeps ORT
1.23.2 while 3.11+ resolves 1.26.0. The functional Python change is
intentionally small and keeps explicit `ORT_DYLIB_PATH` overrides
working.
2026-08-13 15:05:41 -05:00
Abhay Singh
7de35739c6
fix(proxy/anthropic): repair headroom_retrieve history references the tools array cannot support (#2876)
## Description

#2805 / #2807 established the mechanism: Claude Code replays one
transcript across requests that carry different `tools` arrays, and
Anthropic validates every history reference against the array of the
request at hand. #2807 fixed it for tool-search blocks by repairing
history (`strip_unsupported_tool_search_blocks`) rather than trying to
predict the client's tool set.

The same mechanism applies to CCR's `headroom_retrieve`, and it is
tool-agnostic. A passthrough side-request (the prompt-type Stop hook
evaluator, `/compact`) that the proxy forwards without declaring
`headroom_retrieve` still carries a historical `tool_use` naming it, and
Anthropic 400s on the dangling reference. The injection-side fixes
(#2766 / #2533) decide *when to re-declare the tool*; this makes the 400
*structurally impossible* where the tool is intentionally absent. It is
belt-and-braces with them, not a replacement.

The fix adds the symmetric repair next to #2807's. When the outbound
`tools` array does not declare `headroom_retrieve`, it replaces each
`headroom_retrieve` `tool_use` and its paired `tool_result` with a text
block, so no dangling reference survives.

It **neutralizes** (replaces in place) rather than **drops**, which is
the one deliberate difference from #2807: CCR's `tool_use` lives in an
assistant turn and its `tool_result` in the next user turn, i.e. two
different messages. Dropping a whole message could leave two same-role
messages adjacent and break Anthropic's strict user/assistant
alternation, turning one 400 into another. Replacing blocks in place
keeps every message and role intact, and preserves the retrieved text
the model already saw. #2807's server-tool blocks both live in the same
assistant turn, so dropping was safe there.

It runs after CCR tool injection, so on the main loop -- where the tool
IS injected (a present marker) -- it neutralizes nothing and the
prompt-cache prefix is untouched, mirroring #2807's placement and
sequencing.

Fixes #2814

## 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/helpers.py`: added
`strip_unsupported_ccr_retrieve_blocks(messages, tools)` (and a small
`_ccr_result_as_text` helper). No-ops (returning the original object by
identity) when `headroom_retrieve` is declared or no such history
exists; otherwise neutralizes the `tool_use` and its paired
`tool_result` to text.
- `headroom/proxy/handlers/anthropic.py`: call the repair right after
the tool-search history repair (which is after CCR tool injection),
guarded on it actually changing anything, tagged
`router:ccr_retrieve_repair:Nblocks`.
- `tests/test_ccr_retrieve_history_repair.py`: 5 unit tests (no-op when
declared, no-op without retrieve history, neutralize + preserve result
text + keep alternation, leave foreign tool_use untouched, placeholder
when the result has no text).

## 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
tests/test_ccr_retrieve_history_repair.py  5 passed

# Broader CCR / tool-search / handler suites (unchanged behavior):
tests/test_ccr_retrieve_history_repair.py tests/test_proxy/test_anthropic_ccr_deferred_injection.py
tests/test_issue_746_tool_search.py                                      71 passed

# uvx ruff@0.15.17 check  -> All checks passed!
# uvx mypy@1.20.2 headroom/proxy/helpers.py -> Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, project venv, pytest 9.1.1,
ruff 0.15.17 and mypy 1.20.2 via uvx.
- Exact command / steps: confirmed the injection point
(`apply_session_sticky_ccr_tool`) and the tool-search repair placement
in `handlers/anthropic.py`, confirmed `body["tools"]` reflects the CCR
injection before the repair call site (`body["tools"] = tools` is
written well upstream and the adjacent tool-search repair already relies
on it), then drove the helper over a transcript with a
`headroom_retrieve` tool_use + paired tool_result: with the tool
declared it returns the original object unchanged; with the tool absent
it neutralizes both blocks, preserves the result text, and keeps the
message roles/count identical.
- Observed result: a forwarded request that would 400 with "Tool
reference 'headroom_retrieve' not found in available tools" now carries
text blocks in place of the retrieve `tool_use`/`tool_result`, so there
is no reference for Anthropic to reject, and user/assistant alternation
is preserved. The main loop (tool present) is a no-op.
- Not tested: a live multi-turn Claude Code session hitting a
Stop-hook/`/compact` side-request against a real provider (no live
provider here). The repair is a pure function verified directly over the
exact block shapes Anthropic validates, and it mirrors the
already-merged tool-search repair's mechanism and wiring.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have 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 did **not** edit `CHANGELOG.md`: it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

The issue reporter noted their own logs show the tool-search variant of
this 400 (61 across 11 days) but zero `headroom_retrieve` occurrences,
because they run `HEADROOM_LOSSLESS=1` which disables CCR entirely. This
PR fixes the CCR variant of the same, proven, tool-agnostic mechanism
rather than a fresh CCR repro. The neutralize-vs-drop choice is the one
place I departed from #2807, for the alternation reason above; if you
would rather it drop (accepting the alternation handling that implies),
I am happy to switch it.

---------

Co-authored-by: Jerrett Davis <mxjerrett@gmail.com>
2026-08-13 15:05:07 -05:00
JD Davis
6077e5a149
fix(mcp): restore SDK v1 compatibility cap (#2978)
## Description

PR #2963 widened the MCP dependency to 2.x while Headroom's live MCP
server still uses the v1 low-level `Server.list_tools()` and
`Server.call_tool()` decorators. Fresh installs therefore crash before
serving tools. Restore the v1 cap until the explicit SDK 2.x port in
#2658 lands, and pin that compatibility contract with a regression test.

Closes #2977

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

- Restore `mcp>=1.28.1,<2.0.0` in the `proxy` and `mcp` extras.
- Regenerate `uv.lock`, resolving MCP 1.28.1 and removing the
incompatible 2.x transitive set.
- Add a dependency-contract test covering both shipping extras.

## Testing

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

### Test Output

```text
$ uv run pytest -q tests/test_mcp_dependency_contract.py tests/test_ccr_mcp_server.py tests/test_cli/test_mcp.py
41 passed in 0.56s

$ uv run ruff check tests/test_mcp_dependency_contract.py
All checks passed!

$ uv run ruff format --check tests/test_mcp_dependency_contract.py
1 file already formatted
```

## Real Behavior Proof

- Environment: macOS arm64, Python 3.13.14, uv-managed project
environment.
- Exact command / steps: resolve the `mcp` extra, inspect the installed
SDK version and v1 decorators, then instantiate
`HeadroomMCPServer(check_proxy=False)`.
- Observed result: `1.28.1 True True`; server construction returns a v1
`Server` successfully.
- Not tested: full stdio exchange against every external MCP client;
existing MCP unit and CLI suites cover server setup and handlers.

## Runtime Rollout Safety

- Rollout-managed feature(s): None; dependency resolution guard.
- Minimum rollout channel: Stable/default.
- Stable/default behavior changed: Fresh installs stop resolving the
incompatible MCP SDK 2.x release.
- Kill switch / disable path: Revert the dependency cap after #2658
lands.
- Unsafe override required: No.
- Qualification impact: MCP extras and proxy installs remain on the
maintained MCP 1.x line.
- Rollback path: Revert this PR; not recommended until the v2 server
port is merged and tested.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable.

## Additional Notes

The documentation change is the inline dependency rationale next to the
cap. The long-term migration remains #2658; this PR deliberately does
not mix that breaking SDK port into the release-blocker rollback.
2026-08-13 12:35:03 -05:00
石岳峰
b7f342c153
fix(wrap): verify proxy deps before mutating Codex config (#1628)
## Description

\`headroom wrap codex\` now verifies that optional proxy dependencies
(\`headroom-ai[proxy]\`) are installed before mutating Codex
\`config.toml\`. If the check fails, the command exits with the same
error message as \`headroom proxy\` and leaves Codex config untouched.

Fixes #1614 (Bug 1: config mutated before proxy dependency check).

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

- Extract \`ensure_proxy_dependencies()\` in \`headroom/cli/proxy.py\`
(shared with \`headroom proxy\`)
- Call it at the start of \`wrap codex\` when \`not no_proxy\`, before
config snapshot/injection
- Add regression tests for prepare-only abort, \`--no-proxy\` skip, and
import failure messaging

## Testing

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

### Test Output

\`\`\`bash
pytest
tests/test_cli/test_wrap_codex.py::test_wrap_codex_aborts_before_mutating_config_when_proxy_deps_missing
\

tests/test_cli/test_wrap_codex.py::test_wrap_codex_skips_proxy_dependency_check_with_no_proxy
\

tests/test_cli/test_wrap_codex.py::test_ensure_proxy_dependencies_exits_when_server_import_fails
-q
# 3 passed
ruff check headroom/cli/wrap.py headroom/cli/proxy.py
tests/test_cli/test_wrap_codex.py
ruff format --check headroom/cli/wrap.py headroom/cli/proxy.py
tests/test_cli/test_wrap_codex.py
\`\`\`

## Real Behavior Proof

Environment: Linux (Ubuntu), Python 3.12, local checkout with
\`PYTHONPATH\` pointed at patched sources.

Exact command / steps:
1. Created a temp \`~/.codex/config.toml\` with \`model_provider =
"openai"\`.
2. Patched \`headroom.cli.wrap.ensure_proxy_dependencies\` to raise
\`SystemExit(1)\` (simulating missing \`[proxy]\` extra).
3. Ran \`headroom wrap codex --prepare-only --no-serena --port 8787\`.

Observed result: exit code 1; \`config.toml\` unchanged; no
\`config.toml.headroom-backup\` created; no \`[mcp_servers.headroom]\`
block written.

Also verified: \`headroom wrap codex --prepare-only --no-proxy ...\`
does not invoke the dependency check.

Not tested: Windows-specific proxy selector behavior (covered separately
in #1655).

## 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 did not edit CHANGELOG.md; release notes are generated
automatically

---------

Co-authored-by: syf2211 <syf2211@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-13 11:52:22 -05:00
Ben Younes
9fde127534
fix(proxy): relocate stray system-role messages to the top-level system param (#765) (#1357)
## Description

On requests large enough to trigger compression, the proxy emitted an
upstream Anthropic request whose `messages[0]` had `role: "system"`.
Anthropic's Messages API rejects any `system` role inside `messages[]`:

```
400 invalid_request_error: "messages.0: use the top-level 'system' parameter for the initial system prompt"
```

The original request correctly carries its system prompt in the
top-level `system` parameter; a compression/transform/pipeline step
relocates the harness system block into `messages[0]`, so the request
fails outright (intermittent only because it requires a context large
enough to compress).

This adds a wire-contract guard in the Anthropic forwarder: as the
**last** step before sending upstream (after every transform, memory
injection, tool sort, and pipeline extension, covering both the Bedrock
and direct paths), any stray `role="system"` message is relocated out of
`messages[]` and merged back into the top-level `system` parameter.
Content order is preserved (existing system first, relocated content
after) and block-level `cache_control` survives. The guard is a no-op on
the common path (no system-role entry → inputs pass through unchanged).

Closes #765

## 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/helpers.py`: new pure helper
`relocate_system_messages_to_top_level(messages, system) ->
(clean_messages, new_system, changed)` plus `_system_message_to_blocks`.
Handles `system` being `None`/`str`/`list`, never drops content,
preserves order and content blocks.
- `headroom/proxy/handlers/anthropic.py`: invoke the guard just before
the byte-faithful forward block; on relocation, update
`body["messages"]`/`body["system"]`, mark the body mutated
(`system_role_relocated`) so the byte-faithful forwarder re-serializes,
and log a warning.
- `tests/test_proxy_handler_helpers.py`: 3 unit tests (relocate stray
system into top-level, append-to-existing-system order, no-op without a
system entry).
- `CHANGELOG.md`: Bug Fixes 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
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py -q
29 passed in 4.95s

# Regression on the forward path (byte-faithful forwarding, system-prompt immutability, cache stability):
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py tests/test_proxy_byte_faithful_forwarding.py tests/test_proxy_system_prompt_immutable.py tests/test_proxy_anthropic_cache_stability.py -q
90 passed, 15 warnings in 29.72s

$ uv run ruff check headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py tests/test_proxy_handler_helpers.py
All checks passed!

$ uv run ruff format --check ...   # 3 files already formatted
$ uv run mypy headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py
Success: no issues found in 2 source files
```

## Test verification (RED → GREEN)

The new tests exercise the guard directly and import the new helper at
module top, so reverting the production fix makes them fail at
collection.

**RED — production fix reverted (helper removed):**
```text
ImportError while importing test module 'tests/test_proxy_handler_helpers.py'.
E   ImportError: cannot import name 'relocate_system_messages_to_top_level' from 'headroom.proxy.helpers'
=========================== 1 error in 0.41s ===============================
```

**GREEN — production fix applied:**
```text
tests/test_proxy_handler_helpers.py ...                                  [100%]
======================= 3 passed, 26 deselected in 1.50s =======================
```

## Real Behavior Proof

- Environment: Python 3.13, `uv run` in this repo, branch
`fix/issue-765`.
- Exact command / steps: ran the guard on a body in the exact #765
failure shape — `system: None` and a `role="system"` harness block at
`messages[0]`:
- Observed result:
  ```text
  BEFORE: messages[0].role = system (Anthropic 400 trigger)
  changed       = True
  AFTER roles   = ['user', 'assistant']
system param = [{"type": "text", "text": "You are Claude Code.
<system-reminder>...</system-reminder>"}]
OK: no role=system in messages[]; system content preserved in top-level
param
  ```
The illegal `role="system"` entry is removed from `messages[]` and its
content lands in the top-level `system` parameter — exactly the body
Anthropic accepts.
- Not tested: a full live 250k+-token Claude Code session against the
real Anthropic API (needs a large live context + API key); the fix is
validated at the request-shaping boundary the 400 is raised on.

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

The guard intentionally fires at the forwarder boundary rather than in
any single transform: the issue's captures show the relocation can
originate from the compression path, and pipeline extensions / hooks can
also mutate `messages` late. Enforcing Anthropic's wire contract once,
at the point the body is serialized upstream, fixes the 400 regardless
of which step introduced the stray entry and matches the architecture
invariant "never produce a `system`-role entry within `messages[]`".

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-13 11:52:09 -05:00
Sergei Baikin
6576ef639c
fix(openclaw-plugin): circuit breaker + per-request timeout for proxy resilience (#639)
## Description

This change adds bounded timeout and circuit-breaker behavior so
OpenClaw can degrade safely when Headroom or the upstream stream stalls,
while returning structured proxy errors instead of hanging.

Closes #638 by improving OpenClaw/proxy resilience when the Headroom
proxy stalls or Anthropic resets a stream. The PR adds proxy-side
handling for `httpx.RemoteProtocolError`, returns structured 502
responses for otherwise unhandled proxy middleware errors, and adds
OpenClaw plugin timeout/circuit-breaker fallback behavior.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [x] Documentation
- [ ] Refactor
- [x] Tests only

## Changes Made

- Added OpenClaw plugin per-request compression timeout and circuit
breaker fallback.
- Cleared timeout timers after successful or failed compression so
successful calls do not leave pending timers.
- Added a focused Vitest regression for timeout cleanup.
- Added `contracts.tools` for `headroom_retrieve` without whole-file
manifest reformatting.
- Added proxy handling for mid-stream `httpx.RemoteProtocolError` and
structured 502 fallback behavior.
- Documented the new OpenClaw resilience configuration fields.

## Testing

- [x] Unit tests
- [x] Integration-style proxy tests
- [x] Typecheck/build
- [ ] Manual testing

### Test Output

```text
cd plugins/openclaw && npm test
Test Files 6 passed (6), Tests 55 passed (55)

cd plugins/openclaw && npm run typecheck
passed

cd plugins/openclaw && npm run build
Build success

UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --with pytest --with pytest-asyncio --with fastapi --with httpx --with uvicorn --with h2 python -m pytest tests/test_proxy_streaming_resilience.py -q
24 passed in 2.16s
```

## Real Behavior Proof

- Environment: Windows 11, Node/npm from local plugin worktree, Python
3.13.3, focused local worktree for PR #639.
- Exact command / steps: Installed plugin dependencies, ran OpenClaw
plugin tests/typecheck/build, and ran the proxy streaming resilience
suite with required async/FastAPI/httpx extras.
- Observed result: Plugin tests, typecheck, build, and proxy resilience
tests all passed.
- Not tested: Live OpenClaw gateway session in this pass; original
reporter previously verified patched files in a container and OpenClaw
degraded/recovered cleanly.

## Review Readiness

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

---------

Co-authored-by: Sergei Baikin <sergei.baikin@fotograf.de>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-13 11:52:01 -05:00
Abhay Singh
82526191a1
fix(cli/install): resolve the deployment profile instead of dead-ending on default (#2832)
## Description

`headroom init` installs its persistent deployment under a non-`default`
profile name (`init-user` for a global-scope install), but every
`headroom install <lifecycle>` subcommand hardcodes `--profile default`.
The docs show those commands without `--profile`, so on a machine set up
by `headroom init` every documented lifecycle command fails while the
real deployment is running fine:

```console
$ headroom install status
Error: No deployment profile named 'default' is installed.

$ headroom install status --profile init-user
Status:  running
Healthy: yes
```

The error named neither the installed profile nor the `--profile` flag,
so there was nothing to lead the user to `init-user`, which exists only
as an internal constant.

When the requested profile is not installed, `_require_manifest` now
resolves the real target instead of dead-ending on a name the user never
chose:

1. an explicit `HEADROOM_DEPLOYMENT_PROFILE` (which the runtime already
exports) wins;
2. otherwise, when `--profile` was left at its `default` default and
exactly one deployment is installed, that one is used;
3. when it still cannot decide, the error lists the installed profiles
and points at `--profile`.

This changes only the not-found path. An installed `default` still loads
exactly as before, and an explicit typo'd `--profile` still fails, now
with a helpful list.

Fixes #2811

## 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/cli/install.py` (`_require_manifest`): on a manifest miss,
resolve via `HEADROOM_DEPLOYMENT_PROFILE`, then a single installed
deployment when the request is the bare `default`, and otherwise raise
an error that lists installed profiles and points at `--profile`.
Imported `list_manifests` (already present in `headroom.install.state`)
for the enumeration.
- `tests/test_cli/test_install_cli.py`: added
`test_require_manifest_resolves_single_profile_when_default_missing`,
`test_require_manifest_honors_env_profile`, and
`test_require_manifest_lists_installed_profiles_when_ambiguous`.

## 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
# Before/after on the exact reported scenario (one installed profile "init-user"):
# ORIGINAL: _require_manifest("default")  -> raises "No deployment profile named 'default' is installed."
# FIXED:    _require_manifest("default")  -> resolves to "init-user"

# Pass-after, install suites:
tests/test_cli/test_install_cli.py         33 passed
tests/test_install/                        174 passed, 1 skipped, 1 pre-existing failure
# the 1 failure is tests/test_install/test_native_installers.py::
#   test_powershell_native_installer_supports_persistent_docker_lifecycle, which
#   runs scripts/install.ps1 and fails identically on clean main with these changes
#   stashed (an environment-specific PowerShell exit, unrelated to this diff).

# uvx ruff@0.15.17 check  -> All checks passed!
# uvx mypy@1.20.2 headroom/cli/install.py -> Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, project venv, pytest 9.1.1,
ruff 0.15.17 and mypy 1.20.2 via uvx.
- Exact command / steps: read `detect`/`_require_manifest` and the
lifecycle command options (`--profile default` at
install.py:720/748/763/775/789/818/830) to confirm the mismatch with
`init.py`'s `_GLOBAL_PROFILE = "init-user"`, then demonstrated
before/after by monkeypatching `load_manifest`/`list_manifests`: on the
original code `_require_manifest("default")` raises "No deployment
profile named 'default' is installed."; with the fix it returns the
single installed manifest (`init-user`). Fail-before via `git stash push
headroom/cli/install.py` and a direct call; pass-after with `git stash
pop` and the install suites (33 passed in the CLI file, 174 passed in
test_install with one pre-existing environment failure).
- Observed result: a bare lifecycle command on an init'd machine now
targets the running deployment instead of failing, matching the
`--profile init-user` command the issue reporter confirmed works. An
explicit `HEADROOM_DEPLOYMENT_PROFILE` selects the target, and an
ambiguous multi-profile machine gets an error naming the installed
profiles and the `--profile` flag.
- Not tested: a full end-to-end `headroom init` then `headroom install
status` on a fresh host (that flow spawns a real deployment and
supervisor). The resolution logic is a pure function verified directly,
and the manifest loading it calls is existing, tested code.

## 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 did **not** edit `CHANGELOG.md`: it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

The resolution deliberately only triggers on the not-found path and only
auto-selects when a single deployment is installed or an explicit env
profile names one, so it never silently picks the wrong deployment on a
multi-profile host. The docs that show the bare commands
(`docs/content/docs/persistent-installs.mdx`,
`wiki/persistent-installs.md`, `wiki/cli.md`) become correct again
without needing a `--profile` on every line.
2026-08-13 11:51:34 -05:00
Parideboy
f1c34d336c
fix(proxy/anthropic): don't buffer a CCR stream when passthrough discards the stream flip (#2953)
## Description

Fixes #2952. Since `dc163bcd` (#2254), a Claude Code session with
extended thinking on dies from the turn where the first signed
`thinking` block enters history:

```
API Error: API returned an empty or malformed response (HTTP 200) — check for a proxy or gateway intercepting the request
```

`select_outbound_body` forwards the client's original bytes whenever the
body carries a signed `thinking` / `redacted_thinking` block, and it
decides that **before** it looks at `body_mutated` — so every edit the
handler made is discarded. The buffered-CCR path depends on exactly such
an edit: it sets `body["stream"] = False` (`anthropic.py:3073-3078`) so
the reply arrives as one JSON document it can scan for
`headroom_retrieve` calls. With the flip discarded, upstream streams,
`response.json()` fails, SSE resynthesis is skipped, and the client is
handed a 200 it cannot read.

From the reporter's `proxy.log`, the turn that breaks — note
`body_bytes` equals the inbound `content_length` byte for byte, and
`source=passthrough` despite two recorded mutations:

```
CCR: stream:true request has headroom_retrieve available; using buffered stream:false upstream request
event=outbound_request forwarder=anthropic_messages body_bytes=132017 body_mutated=true
  mutation_reasons=structural_diff_vs_original,ccr_streaming_retrieve_buffered_non_stream source=passthrough
PERF ... msgs=6 tok_saved=3575 tool_saved=16064 tok_out=0 total_ms=9623
```

This is a different failure from #2251 (a 400 from Anthropic). Signed
thinking blocks still leave as original bytes here, so that fix is
untouched.

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

Primary fix, `headroom/proxy/handlers/anthropic.py`:

- Gate `buffered_stream_ccr` on the outbound body actually being ours to
change, via a new `outbound_body_is_client_bytes()` predicate that
mirrors the passthrough branch. Those turns take the plain streaming
path instead, which is the coherent outcome: the injected retrieve tool
is itself a discarded mutation there, so the model was never going to
see it. One INFO line records the choice.

Three follow-on defenses, each of which independently kept the failure
alive or invisible:

- A non-JSON 200 on the buffered path now logs at WARNING (it was DEBUG,
which is why nothing in `proxy.log` looked wrong) and the upstream SSE
is relayed to the client verbatim, instead of falling through to a plain
`Response` that `_BufferedCCRResponse` can only turn into a bare `event:
error` once its 1 s keepalive has committed headers.
- The semantic cache no longer stores a body that did not parse as JSON,
and drops the stored `content-type` on the hit path. The cache key has
no `stream` component, so a cached SSE body was replayed to buffered
callers for the full 3600 s TTL — that replay is the request the
reporter actually saw the error on (a 2 ms `PERF ... transforms=none`
cache hit).
- `select_outbound_body` now reports the mutations passthrough discarded
(`dropped_mutations` / `dropped_mutation_reasons`), and
`log_outbound_request` logs them as
`event=outbound_body_mutations_dropped` at WARNING. Without it, `PERF`
reports savings and tool injections that never reached the wire and
nothing contradicts it.

`prepare_outbound_body_bytes` keeps its two-value shape, so existing
callers are unchanged.

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

New `tests/test_ccr_buffered_stream_signed_thinking.py` covers the gate
(signed-thinking history takes the streaming path and still leaves as
`stream: true`; the same request without thinking blocks still takes the
buffered path with `stream: false`), the SSE relay, and the cache
guards. The relay case is parametrized on upstream latency because the
failure only reaches its worst form past the 1 s keepalive, where a
plain `Response` has no `body_iterator` left to forward.

Verified red before green — with the source changes stashed and the
tests in place:

```text
FAILED tests/test_ccr_buffered_stream_signed_thinking.py::test_signed_thinking_history_skips_the_buffered_ccr_path[True-True]
FAILED tests/test_ccr_buffered_stream_signed_thinking.py::test_buffered_ccr_relays_an_unexpected_sse_reply_and_does_not_cache_it[prompt]
FAILED tests/test_ccr_buffered_stream_signed_thinking.py::test_buffered_ccr_relays_an_unexpected_sse_reply_and_does_not_cache_it[past-keepalive]
FAILED tests/test_ccr_buffered_stream_signed_thinking.py::test_cache_hit_never_replays_a_foreign_content_type
========================= 4 failed, 1 passed in 8.88s =========================
```

With the fix applied:

```text
$ python -m pytest tests/test_proxy_byte_faithful_forwarding.py tests/test_ccr_buffered_stream_signed_thinking.py -q
55 passed in 11.40s

$ python -m pytest tests/test_compression_cache.py tests/test_ccr_inline_resolve_handlers.py \
    tests/test_ccr_sqlite_backend.py tests/test_anthropic_stage_timings.py \
    tests/test_backend_nonstreaming_cache_metrics.py tests/test_cache_mode_cold_start.py \
    tests/test_cache_breakpoint_diagnostics.py -q
87 passed, 1 skipped in 17.57s

$ python -m ruff check .
All checks passed!

$ python -m mypy headroom --ignore-missing-imports --python-version 3.13
Found 12 errors in 3 files (checked 517 source files)
# all 12 pre-existing in headroom/ccr/mcp_server.py and headroom/memory/mcp_server.py
# (local mcp package version); none in the four files this PR touches
```

## Real Behavior Proof

- Environment: headroom 0.35.0-dev source checkout, Python 3.13.11,
Windows 11, Claude Code CLI 2.1.228 against api.anthropic.com
(claude-opus-5), proxy run as `headroom proxy --port 8787 --memory
--code-aware --mode token`
- Exact command / steps: reproduced from the reporter's
`~/.headroom/logs/proxy.log` — request `hr_1786551079_000005` shows
`source=passthrough` with `body_bytes` identical to the inbound
`content_length` while `mutation_reasons` contains
`ccr_streaming_retrieve_buffered_non_stream`, then `tok_out=0`; request
`hr_1786551089_000006` is the 2 ms `transforms=none` semantic-cache hit
that returned the poisoned SSE body to a caller asking for JSON. The
preceding turn (`...000004`, no thinking block yet) was
`source=canonical` with `tok_out=341`. Then: pytest suites above, with
the red/green stash comparison
- Observed result: with the gate in place the thinking-bearing turn goes
down `_stream_response` and the forwarded body still says `"stream":
true`, matching the bytes passthrough will send; a buffered turn whose
upstream answers with SSE reaches the client as a stream and writes
nothing to the semantic cache; a cache entry can no longer hand a caller
a content-type from a differently-shaped request
- Not tested: a live end-to-end Claude Code session against Anthropic
with the patched proxy (the reproduction here is the reporter's proxy
log plus handler-level tests); `/v1/responses`, which has the same
`stream`-flip pattern (`openai.py:5479-5487`) but carries `input` rather
than `messages`, so `has_signed_thinking_blocks` never fires there and I
left it alone

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

Deliberately out of scope, worth a separate issue: option (b) of #2251 —
a canonical re-serialization that preserves signed blocks byte-for-byte
so compression and tool injection survive a thinking-bearing history
rather than being silently dropped. #2251 reports a 400 even on a no-op
re-encode whose only transform was `tool_search_deferral`, which hints
the signature covers `tools` too, so getting it wrong would re-break
every multi-turn thinking session. That needs validating against the
live API, not guessing. Until then the new WARNING at least makes the
dropped work visible.

Also unfixed by design: the savings accounting itself. A passthrough
turn still books `tok_saved` / `tool_saved` in `PERF` for bytes that
never shipped; correcting the numbers is a wider change than this bug
needs.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-08-13 11:46:55 -05:00
Parideboy
2d1e96b85c
fix(memory): sanitize entity_refs to prevent dict-shaped entries crashing search (#2951)
## Description

Closes #2947

`entity_refs` is annotated `list[str]` everywhere, but nothing enforced
that at runtime. `LocalBackend.save_memory`'s `entities` argument is
filled straight from LLM-supplied `memory_save` tool input
(`headroom/memory/system.py:575` into `memory_handler.py:1242`), so a
caller can pass the typed `{"entity": ..., "entity_type": ...}` shape,
which is the format `extracted_entities` expects, into it by mistake.
Those dicts were then persisted verbatim into `entity_refs`, both in the
`memories` table and in the duplicated copy the vector index keeps for
post-filtering.

Every later `search_memories` call does
`set().update(memory.entity_refs)` while collecting entities for graph
expansion. Hashing a dict raises `TypeError: unhashable type: 'dict'`,
and because that happens inside the vector-result loop rather than
per-item, **one** poisoned row aborted the **entire** search. The
proxy's memory handler catches the exception and returns no memories, so
recall went quietly dark rather than failing loudly, and the bad row
kept re-appearing in top-k for related queries, so it stayed dark. The
issue reporter hit this in production: 4 bad rows disabled memory search
for a whole project for a day, with nothing visible to the end user
beyond a swallowed warning in `proxy.log`.

The same root cause has two more crash modes, both confirmed below:
`AttributeError: 'dict' object has no attribute 'lower'` during graph
linking on the save path, and the same error in the `entities` search
filter (`ref.lower()`).

The fix adds one helper and applies it at both ends of the data flow.
Dicts are **unwrapped to their `entity` name** rather than dropped, so
rows that are already corrupted keep contributing to graph expansion
instead of silently losing their entities.

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

- **New helper `normalize_entity_refs()` in
`headroom/memory/models.py`.** Coerces a raw entity-reference list into
the `list[str]` it claims to be: strings pass through, dicts are
unwrapped via their `entity` (or `name`) key, and anything with no
recoverable name is dropped rather than stringified, since a ref like
`"{'entity_type': 'project'}"` would only pollute the graph. Order is
preserved and duplicate names are collapsed.
- **Write path, to stop new corruption at the door.**
`LocalBackend.save_memory` normalizes `entities` before it reaches
`entity_refs` and graph linking. `LocalBackend.search_memories`
normalizes the `entities` *filter* argument too, since it arrives from
the same untrusted tool input (`memory_handler.py:1320`).
- **Read path, to heal rows that were written before this fix.** Applied
at the three deserialization boundaries, so no data migration is needed
and corrupted rows normalize themselves the next time they are loaded:
`Memory.from_dict` (`headroom/memory/models.py`),
`SQLiteMemoryStore._row_to_memory`
(`headroom/memory/adapters/sqlite.py`), and the vector indexes' own
`entity_refs` copies used for post-filtering, `VectorMetadata.from_json`
(`headroom/memory/adapters/sqlite_vector.py`) and
`IndexedMemoryMetadata.from_dict` (`headroom/memory/adapters/hnsw.py`).
- **Defensive normalization on emitted results.** `search_memories` and
`text_search` normalize the refs they return as `related_entities`, so a
backend that produces `Memory` objects by some path not covered above
still cannot take a whole query down, and callers never receive a dict
where they expect an entity name.

**Note on scope versus the patch proposed in the issue.** The issue
proposed normalizing in two places (`save_memory` plus the
`set().update()` line). I widened it slightly because that pair leaves
three related failures live: the `entities` filter still crashes on
`ref.lower()`, `related_entities` still hands dicts back to the caller,
and, most importantly, already-poisoned rows stay poisoned in storage.
Normalizing at the deserialization boundaries fixes all three at once
and is what makes existing corrupted databases recover on their own.

**Behavior change worth flagging.** `entity_refs` is now de-duplicated
(case-sensitively) on both save and load. Refs were already treated as a
set for graph expansion, so this is semantically a no-op, but it is a
visible difference if anything asserts on exact list contents.

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

New file `tests/test_memory/test_entity_ref_sanitization.py` adds 10
tests covering the helper, both write paths, all three deserialization
boundaries, and the three crash modes.

### Test Output

```text
$ python -m pytest tests/test_memory/test_entity_ref_sanitization.py -q
..........                                                               [100%]
10 passed, 17 warnings in 0.34s
```

Full memory suite, plus a before/after comparison of the failure set to
prove no regressions:

```text
$ python -m pytest tests/test_memory/ -q
13 failed, 576 passed, 3 skipped, 1072 warnings, 25 errors in 44.74s

# the same run with the source changes stashed (baseline on upstream/main @ 941c25d3):
13 failed, 566 passed, 3 skipped, 1055 warnings, 25 errors in 44.92s

# diff of the failing/erroring test IDs, before versus after:
$ diff baseline.txt after.txt && echo "NO NEW FAILURES vs baseline"
NO NEW FAILURES vs baseline
```

576 passed equals the 566 baseline plus the 10 new tests. The 13
failures and 25 errors are pre-existing on `upstream/main` and unrelated
to this change: they are Windows-only temp-directory cleanup failures in
this local environment.

```text
E   PermissionError: [WinError 32] The process cannot access the file because it is being
    used by another process: 'C:\Users\...\Temp\tmp_qnqtami\test.db'
```

Adjacent suites that construct `Memory` objects:

```text
$ python -m pytest tests/test_memory_system.py tests/test_memory_eval.py tests/test_critical_gaps.py -q
158 passed, 1 skipped, 514 warnings in 20.58s
```

Lint and format on the changed files:

```text
$ ruff check headroom/memory tests/test_memory/test_entity_ref_sanitization.py
All checks passed!

$ ruff format --check headroom/memory tests/test_memory/test_entity_ref_sanitization.py
48 files already formatted
```

`mypy headroom --ignore-missing-imports --python-version 3.13` reports
12 errors, all pre-existing on `upstream/main` and all in files this PR
does not touch (`headroom/ccr/mcp_server.py`,
`headroom/memory/mcp_server.py`, `headroom/release_version.py`; they
come from a local MCP SDK version mismatch). Zero errors in any changed
file.

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.11, pytest 9.1.1, ruff 0.15.17,
local `headroom._core` built. Branched from `upstream/main` at
`941c25d3`, the same branch point the issue reports.
- Exact command / steps: Ran a standalone script (not a mock-only test)
driving `LocalBackend.search_memories` and `LocalBackend.save_memory`
with `entity_refs=[{"entity": "Project X", "entity_type": "project"}]`,
first against unmodified `941c25d3` and then against this branch. Three
scenarios: vector search with graph expansion, search with an `entities`
filter, and a save carrying dict-shaped `entities`.
- Observed result: on unmodified `941c25d3` all three crashed, printing
`SEARCH: TypeError: unhashable type: 'dict'`, `FILTER: TypeError:
unhashable type: 'dict'`, and `SAVE: AttributeError: 'dict' object has
no attribute 'lower'`. With this branch applied all three succeed:
search returns both the poisoned and the clean memory with
`related_entities == ["Project X"]`, the filter matches the recovered
name, and the save persists `entity_refs == ["Project X"]`. Those three
scenarios are now the regression tests in
`test_entity_ref_sanitization.py`.
- Not tested: no live end-to-end run through the MCP `memory_save` tool
against a real LLM, and no test against a real pre-existing SQLite
database containing dict-shaped rows. The healing-on-load path is
covered at the deserialization functions (`Memory.from_dict`,
`VectorMetadata.from_json`, `IndexedMemoryMetadata.from_dict`) rather
than through an actual corrupted `.db` file. The non-local backends
(`mem0`, `direct_mem0`, `qdrant-neo4j`, `cognee`) were not exercised;
this PR only changes the local backend and the shared models and
adapters.

## 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 did **not** edit `CHANGELOG.md`, it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

Documentation was not changed because `normalize_entity_refs()` is an
internal helper and no public API or user-facing behavior changes;
`entity_refs` still behaves exactly as its existing `list[str]` contract
always documented.

Credit for the diagnosis, the root-cause analysis, and the original
repro goes to @apacheco-RT in #2947, who could not open a PR directly
because GitHub blocks Enterprise Managed User accounts from forking
outside their enterprise.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-08-13 11:46:44 -05:00
Parideboy
6147883d5e
fix(wrap): stop the Serena pre-index stalling the launch path for 300s (#2945)
## Description

`headroom wrap <agent>` could sit silently for a full 300 seconds before
the agent launched, and leaked one orphaned process every time it did.

`_setup_serena_mcp` runs `serena project index` synchronously on the
launch path, with `capture_output=True`, an inherited stdin and
`timeout=300`. When a project has no `.serena/project.yml`, Serena
auto-creates one — and that auto-creation asks one `[y/N]` question per
additionally-detected language server. Three things then combine:

1. stdin was inherited, so Serena believed it could prompt.
2. stdout was captured, so the question never reached the terminal.
3. the call was synchronous, so the agent waited out the entire timeout.

The user saw no prompt, no progress and no error — only a wrapper that
appeared to hang. The pre-index could never succeed in that state, so
the 300 seconds bought nothing.

On top of that, `subprocess.run` kills only its direct child on timeout.
`uvx` is a launcher that execs the real `serena` executable as a
grandchild, which was never signalled: it reparented to PID 1 and
survived indefinitely. Same class of bug as #615 and #880.

Closes #2938

## Type of Change

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

## Changes Made

- `_serena_project_skip_reason` (`headroom/cli/wrap.py`) now returns a
skip reason when `.serena/project.yml` is absent, so the pre-index does
not run in the one state where it cannot succeed.
- `_index_serena_project` passes `stdin=subprocess.DEVNULL`, so a
subprocess that decides to prompt gets EOF and exits in about a second
instead of blocking behind a captured pipe. This is deliberately kept as
a second line of defence even though the skip above already avoids the
known prompt.
- `_index_serena_project` now spawns via `subprocess.Popen` in its own
process group (`start_new_session=True` on POSIX,
`CREATE_NEW_PROCESS_GROUP` on Windows) instead of `run(...)`, so the
whole tree can be signalled.
- New `_kill_serena_index_tree` helper kills that tree on timeout —
`killpg(..., SIGKILL)` on POSIX, `taskkill /F /T /PID` on Windows — then
reaps the child and closes the capture pipes. Best-effort throughout; it
never raises.
- Corrected two comments that asserted the opposite of the observed
behaviour ("a failure or timeout here never blocks the wrap", "neither
blocks the wrap"). Both were accurate about intent and wrong about
effect.
- Added `_SERENA_INDEX_TIMEOUT` (still 300) and a line announcing the
pre-index, so a legitimately long index no longer looks like a hang.
- Tests in `tests/test_cli/test_wrap_serena_boost.py` rewritten for the
`Popen` path and extended to cover the DEVNULL stdin, the process-group
flag, the timeout tree-kill, the new skip reason, and the
`_setup_serena_mcp` wiring on both a fresh project and one that already
has `project.yml`.

### Behaviour change worth a reviewer's attention

**On a project with no `.serena/project.yml`, the pre-index no longer
runs at all.** That is the first wrap of any project, so this is the
common case.

I went this way rather than fixing the prompt because there is no way to
fix it from Headroom's side without re-introducing something the project
deliberately removed. Serena's `project index` command has no
non-interactive switch: `ProjectCommands._create_project` calls
`ProjectConfig.autogenerate(..., interactive=True)` with `interactive`
hardcoded. The only path that skips the prompt is passing
`--ls/--language` explicitly, which means Headroom guessing the
project's languages again — exactly the hand-maintained
extension-to-language map that was removed in #2674, with a comment in
this same function explaining why Serena should own that job.

The cost of skipping is small and self-correcting. Serena's MCP server
(`serena start-mcp-server --project-from-cwd`) generates `project.yml`
itself, non-interactively, on first start, and indexes lazily on demand
— which is the fallback the existing docstring already relied on. So the
first wrap now launches immediately with lazy indexing, and every wrap
after that pre-indexes for real. Previously the first wrap cost 300
seconds *and* still produced no index, so nothing of value is lost.

Happy to switch to passing `--ls` instead if maintainers would rather
keep the pre-index on the first wrap and accept a language map; the
other two changes stand either way.

## 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_cli/test_wrap_serena_boost.py tests/test_cli/test_serena_migrate.py -q
collected 29 items

tests\test_cli\test_wrap_serena_boost.py .............s...........       [ 86%]
tests\test_cli\test_serena_migrate.py ....                               [100%]

======================== 28 passed, 1 skipped in 0.54s ========================

$ python -m ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_serena_boost.py
All checks passed!

$ python -m ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_serena_boost.py
2 files already formatted

$ python -m mypy headroom/cli/wrap.py --ignore-missing-imports --python-version 3.13 --follow-imports=silent
Success: no issues found in 1 source file
```

The single skip is `test_kill_tree_signals_the_group_on_posix`, which is
platform-gated; the Windows counterpart ran. I develop on Windows, so
the POSIX `killpg` branch is covered by unit test only — the end-to-end
tree-kill proof below is the Windows `taskkill` branch.

## Real Behavior Proof

- Environment: Windows 11 Pro 26200, Python 3.13.11, headroom checkout
at 941c25d3 plus this branch, uvx resolving `serena-agent` from PyPI,
throwaway project with bash + TypeScript + Python sources and no
`.serena/`
- Exact command / steps: spawned the real `uvx --from serena-agent
serena project index` against that project twice — once with stdin
readable and never closed (a parent-held pipe, the faithful stand-in for
the idle terminal the old code inherited; `communicate()` cannot be used
here because with `input=None` it closes the child's stdin immediately
and hands it the EOF the real bug never delivers), once with
`stdin=subprocess.DEVNULL`. Then spawned it twice more, stopping it with
a plain `proc.kill()` (the old timeout behaviour) versus the new
`_kill_serena_index_tree`, counting survivors with `Get-CimInstance
Win32_Process` filtered on the command line.
- Observed result: stall reproduced and fixed — `A/open-stdin: STILL
BLOCKED after 30.0s (timeout hit)` versus `B/DEVNULL: exit=1
elapsed=1.1s` with stderr tail `Project configuration auto-generation
failed after 0.000 seconds / Error: EOF when reading a line`. Leak
reproduced and fixed — after spawning a 5-process tree, `after
proc.kill(): still alive: [34468, 35044, 35808]` versus `after
_kill_serena_index_tree: still alive: []`. The 30-second bound in the
first experiment stands in for the shipped 300; the point is that the
child never returns on its own.
- Not tested: the POSIX `killpg`/`start_new_session` branch end-to-end
(no Linux or macOS host available, so it is unit-tested only — and it is
the branch the issue reporter observed failing); a full `headroom wrap
opencode` launch end-to-end, since this machine has no working local
proxy (`tests/test_cli/test_wrap_opencode.py` fails identically with and
without this branch for that reason); and the second-wrap pre-index
success path against a live Serena run, which is covered by unit test
instead.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

- The documentation checkbox is unchecked because no user-facing doc
describes the pre-index; the behaviour is explained in source
docstrings, which this PR rewrites.
- `_kill_serena_index_tree` is deliberately total: every step is wrapped
so cleaning up an already-dead child cannot turn a timeout into a crash
on the launch path. There is a test for that.
- #2754 (use a pre-installed Serena) would remove the `uvx` layer this
leak depends on, but not the prompt itself — the stdin guard here is
still needed after that lands.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-08-13 11:46:30 -05:00
Ashish Patel
41dab2d099
fix(ccr): verify a scanned marker's hash before advertising it (#2908)
## Description

`CCRToolInjector.scan_for_markers()` decides whether a compression
marker is Headroom's own by *shape* alone — any bracket marker carrying
a 24-hex hash counts, per the generic fallback pattern
(`\[.*?compressed.*?hash=([a-f0-9]{24})\]`). Other context tools emit
exactly that shape. Once a foreign hash is scanned,
`has_compressed_content` flips true and the retrieve tool + "Available
hashes" system instruction get injected for a hash this proxy never
stored — the model calls `headroom_retrieve`, gets a guaranteed miss,
and re-does work it already had. Two wasted turns per adopted foreign
hash.

Closes #2836

## Type of Change

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

## Changes Made

- `headroom/ccr/tool_injection.py`: added
`CCRToolInjector.verify_ownership()` — filters `detected_hashes` down to
hashes the compression store actually recognizes, via the same
`store.exists()` check the retrieve endpoint itself performs. Added a
small `_HashOwnershipStore` Protocol (structural typing, not a hard
dependency on the concrete `CompressionStore` class) and a
`compression_store` constructor field for dependency injection/testing.
`scan_for_markers()` itself is untouched — kept store-independent (pure
regex) rather than baking the check into the scan loop, since that
approach broke 24 existing tests that correctly test "does this shape
match" in isolation.
- `headroom/proxy/handlers/anthropic.py`,
`headroom/proxy/handlers/openai.py`: call `injector.verify_ownership()`
right after `scan_for_markers()` — the two real per-request call sites.
- `verify_ownership()` is also called inside `process_request()` (the
convenience wrapper `batch.py`'s Google path uses), so that path is
covered without a separate call site edit.
- `tests/test_ccr_tool_injection.py`: new `TestVerifyOwnership` class (6
tests) — the exact issue repro, a real-hash-survives case, mixed
own/foreign hashes, explicit store override, store-exception safety
(must not raise), and no-op-on-empty-hashes.
- `tests/test_proxy_anthropic_cache_stability.py`: 3 pre-existing tests
needed updating for the new (correct) behavior — two `_FakeInjector`
test doubles needed a `verify_ownership()` stub added, and one real
end-to-end test needed a genuine store entry seeded (via
`explicit_hash`) for the hash its hand-typed marker references, instead
of asserting on an unverified shape-only match.

## Testing

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

### Test Output

```text
$ .venv/Scripts/python -m pytest tests/test_ccr_marker_policy.py tests/test_ccr_tool_always_on.py tests/test_ccr_tool_injection.py tests/test_proxy/test_ccr_frozen_prefix_coupling.py tests/test_proxy_anthropic_cache_stability.py tests/test_proxy_handlers_batch.py tests/test_proxy_handler_helpers.py -q
151 passed in 15.87s

$ .venv/Scripts/python -m pytest tests/test_proxy_ccr.py tests/test_proxy_openai_responses_stream_ccr.py tests/test_anthropic_ccr_workspace_unbound.py tests/test_compression_store.py tests/test_no_ccr_lossy.py tests/test_proxy_handlers_batch.py -q
121 passed in 20.73s

$ .venv/Scripts/ruff check . && .venv/Scripts/ruff format --check .   # touched files only
All checks passed / already formatted
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.14.5, local venv
- Exact command / steps: ran the issue's exact 3-line repro
(`CCRToolInjector.scan_for_markers()` on the foreign marker text, then
`verify_ownership()`) before and after the fix; separately verified a
genuinely-Headroom-stored hash (via `store.store(...,
explicit_hash=...)`) still survives verification and still drives
injection
- Observed result: before the fix (scan only, no verify step exists yet)
`has_compressed_content` is `True` for the foreign marker — matches the
bug report exactly. After adding `verify_ownership()`: foreign marker →
`detected_hashes == []`, `has_compressed_content is False`; real stored
hash → `detected_hashes == [real_hash]`, `has_compressed_content is
True`.
- Not tested: have not driven this through a live two-context-tool proxy
session (e.g. Headroom alongside another CCR-shaped tool in the same
conversation) — verified at the unit/integration level (the exact repro
plus the real proxy handler call sites via
`test_proxy_anthropic_cache_stability.py`'s end-to-end `TestClient`
tests), not via a live multi-tool session.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation (N/A —
internal CCR safety behavior, no user-facing docs reference the
marker-adoption mechanism)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable (release-please
generates this automatically from commit messages)

## Additional Notes

Design note on why `verify_ownership()` is a separate step rather than
baked into `scan_for_markers()`: my first attempt did exactly that and
broke 24 tests across `test_ccr_tool_injection.py`,
`test_ccr_marker_policy.py`, `test_proxy_anthropic_cache_stability.py`,
and `test_proxy_handler_helpers.py` — all of them legitimately testing
"does the regex detect this marker shape" independent of any store
state. Keeping the scan pure and adding an explicit, separately-testable
verification step kept that test surface intact while still closing the
real gap at the three places that actually decide whether to advertise
the retrieve tool.
2026-08-13 11:46:21 -05:00
Pragadeesh
d76fce04a3
fix(proxy): adapt 200 SSE upstream replies on buffered /v1/responses instead of 502 (#2622)
## Description

The buffered HTTP `/v1/responses` path (`_buffered_ccr_operation` in
`headroom/proxy/handlers/openai.py`) assumed every upstream reply to a
`stream: false` request is JSON. Some OpenAI-compatible upstreams answer
with a valid `200 text/event-stream` body carrying Responses API events.
`response.json()` raised `JSONDecodeError`, which is not in the narrow
usage-extraction catch (`KeyError, TypeError, AttributeError`), so it
escaped to the outer handler and the **successful** upstream reply was
converted into a generic `502 proxy_error` — the client loses the
response and typically retries, duplicating paid calls.

The fix classifies the upstream reply at the ingestion boundary by its
declared `Content-Type` (the SSE spec's own discriminator) instead of
parsing by expectation:

- **200 SSE with a terminal `response.completed` event** → the complete
response object is reassembled from that event
(`_openai_responses_from_sse`, the inverse of the existing
`_openai_responses_to_sse`) and swapped in as a synthesized
`application/json` response *before any parsing happens*. Everything
downstream — usage extraction, CCR retrieval handling, memory-tool
handling — runs unmodified.
- **200 SSE without a recognizable terminal event** → the successful
upstream body is forwarded to the client unchanged (sanitized headers)
rather than fabricating a 502. Adapt only when the adaptation is
provably faithful; otherwise pass through.
- **Everything else** (normal JSON replies, non-200s) → byte-identical
pre-existing behavior.

Deliberately *not* done: widening the `except` clause (would leave
`resp_json` unbound and break the downstream pipeline) and body sniffing
(the declared media type is trusted; a mislabeled body keeps today's
behavior).

Closes #2613

## Type of Change

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

## Changes Made

- `headroom/proxy/handlers/openai.py`: new module-level helper
`_openai_responses_from_sse()` (SSE-spec framing: blank-line event
separation, multi-line `data:` joining, `\r` tolerance, at most one
stripped space, `[DONE]` skipped; returns the terminal event's
`response` object or `None`), placed next to its inverse
`_openai_responses_to_sse()`.
- `headroom/proxy/handlers/openai.py::_buffered_ccr_operation()`:
content-type dispatch for 200 replies immediately after the upstream
response (and after wire-debug capture, so debug logs keep the true
upstream bytes) — adapt SSE→JSON when a terminal event exists, pass
through unchanged when it doesn't.
- `tests/test_openai_codex_routing.py`: two new handler-level tests (see
below).

## 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
# Both new tests watched failing BEFORE the fix with the exact issue signature:
#   ERROR headroom.proxy:openai.py [req-1] OpenAI responses request failed: JSONDecodeError: Expecting value: line 1 column 1 (char 0)
#   assert 502 == 200

$ pytest tests/test_openai_codex_routing.py -q
24 passed in 2.08s

$ pytest tests/test_openai_codex_routing.py tests/test_ccr_response_handler_openai_responses.py tests/test_codex_responses_passthrough_bytes.py -q
38 passed, 1 warning in 13.80s

$ pytest tests/test_output_shaper_responses.py tests/test_codex_responses_waste_signals.py tests/test_codex_openai_contract_parity.py tests/test_openai_beta_session_sticky.py tests/test_openai_max_completion_tokens.py tests/test_openai_response_cache_key.py tests/test_litellm_openai_passthrough.py -q
61 passed, 1 warning

$ ruff check headroom/proxy/handlers/openai.py tests/test_openai_codex_routing.py
All checks passed!

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

New tests:

- `test_handle_openai_responses_non_stream_adapts_sse_upstream` — 200
SSE with `response.completed` → client gets 200 `application/json` with
the reassembled response.
-
`test_handle_openai_responses_non_stream_passes_through_unparseable_sse`
— 200 SSE with no terminal event → client gets 200 with the body
unchanged, never a 502.

## Real Behavior Proof

- Environment: macOS 26.5 (arm64), Python 3.12 via `uv`, headroom from
source (editable install). Local fake OpenAI-compatible upstream
(`http.server`) that answers every `POST` with `200 text/event-stream`
containing a `response.completed` event and `data: [DONE]` — the
upstream behavior reported in the issue. Proxy started with
`OPENAI_TARGET_API_URL=http://127.0.0.1:9302 headroom proxy --port
<port>`.
- Exact command / steps: same-session A/B against real proxy processes —
identical upstream and identical request, only the checked-out revision
changed:

  ```bash
curl -s -w "\nHTTP_STATUS=%{http_code} CONTENT_TYPE=%{content_type}\n" \
    -X POST http://127.0.0.1:<port>/v1/responses \
-H "content-type: application/json" -H "authorization: Bearer sk-test" \
    -d '{"model":"gpt-5.4","stream":false,"input":"hello"}'
  ```

- Observed result: unpatched `main` converts the successful upstream
reply into the issue's 502; this branch returns the complete response as
JSON. Full captures:

  **Before (unpatched `main`, port 8794):**

  ```
{"error":{"message":"An error occurred while processing your request.
Please try again.","type":"server_error","code":"proxy_error"}}
  HTTP_STATUS=502
  ```

  **After (this branch, port 8795):**

  ```
{"id": "resp_sse_repro", "object": "response", "status": "completed",
"model": "gpt-5.4", "output": [{"type": "message", "id": "msg_1",
"role": "assistant", "content": [{"type": "output_text", "text": "hello
from sse upstream"}]}], "usage": {"input_tokens": 2, "output_tokens":
1}}
  HTTP_STATUS=200 CONTENT_TYPE=application/json
  ```

- Not tested: a wild third-party SSE-answering upstream (the repro uses
a local stub shaped per the issue report); the buffered-stream-CCR
variant of this path against a live upstream (unit-tested only);
Windows.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

- Documentation checklist item is N/A — internal proxy behavior fix, no
documented surface changes.
- Known residual (pre-existing, out of this issue's scope): a
**non-200** upstream reply with a non-JSON body (an SSE error stream, a
gateway HTML error page) still follows the old `JSONDecodeError → 502`
path, blurring a meaningful upstream error into a generic 502. This PR
deliberately adapts only declared-SSE **200** replies, where reassembly
from `response.completed` is provably faithful. Happy to file the
non-200 case as a follow-up issue if maintainers want it tracked.
2026-08-13 11:46:12 -05:00
Abhay Singh
d6d121e399
fix(ccr): re-inject headroom_retrieve when history references it on the sessionless path (#2440) (#2533)
## Description

Fixes #2440. `apply_session_sticky_ccr_tool` bypasses the
`SessionCcrTracker` when `session_id` is `None` (WS / pre-session paths)
and drives injection purely off the per-turn
`has_compressed_content_this_turn` flag:

```python
if not session_id:
    if not has_compressed_content_this_turn:
        ...  # skip: tool NOT re-declared
        return tools_out, False
    ...
```

If an earlier turn emitted a `headroom_retrieve` tool_use into history
but the current turn produced no fresh compression marker, the tool
definition is not re-declared in `tools`, while the forwarded history
still references it. The provider then rejects the whole request:

```
API Error: 400 Tool reference 'headroom_retrieve' not found in available tools.
```

Without a session the tracker can't remember the earlier turn's CCR, so
this is unique to the sessionless path.

## Fix

Add `history_references_ccr_tool(messages)` which detects an existing
`headroom_retrieve` call in the forwarded messages — both the Anthropic
assistant `tool_use` content block and the OpenAI assistant
`tool_calls[].function.name` shapes, fully null-guarded. On the
sessionless path, injection now fires when
`has_compressed_content_this_turn` **or** history already references the
tool, so the definition is re-declared and the request validates. The
decision is logged as a new `inject_history_reference` outcome. Both
handlers pass the signal computed from `optimized_messages` (the bytes
actually forwarded). Behavior with a real `session_id` (the sticky
tracker path) is unchanged.

## 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/helpers.py`: add `history_references_ccr_tool`; add a
`history_has_ccr_reference` parameter to `apply_session_sticky_ccr_tool`
and OR it into the sessionless injection decision.
- `headroom/proxy/tool_injection_logging.py`: add the
`inject_history_reference` decision literal.
- `headroom/proxy/handlers/anthropic.py`,
`headroom/proxy/handlers/openai.py`: pass
`history_references_ccr_tool(optimized_messages)` into the sticky-tool
call.
- `tests/test_ccr_tool_always_on.py`: regressions for the detector (both
provider shapes + malformed inputs) and for sessionless re-injection
when history references the tool.

## 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_ccr_tool_always_on.py -q
14 passed

# with just the `or history_has_ccr_reference` condition reverted, the new
# sessionless re-injection test fails (tool not injected -> would 400)

$ uvx ruff@0.15.17 check headroom/proxy/helpers.py headroom/proxy/tool_injection_logging.py headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/openai.py tests/test_ccr_tool_always_on.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/helpers.py headroom/proxy/tool_injection_logging.py
Success: no issues found in 2 source files
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, project venv (`uv sync --extra
proxy`), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`, pytest in the venv.
- Exact command / steps: called `history_references_ccr_tool` on
Anthropic `tool_use` and OpenAI `tool_calls` histories (plus
null/non-list shapes), and
`apply_session_sticky_ccr_tool(session_id=None,
has_compressed_content_this_turn=False,
history_has_ccr_reference=True)`; then temporarily reverted only the `or
history_has_ccr_reference` condition and re-ran the regression.
- Observed result: the detector returns `True` for both provider shapes
and `False`/no-crash for malformed input; with the fix the sessionless
call injects the tool (`was_injected=True`, tool present) even with no
fresh compression; with the condition reverted the same call returns
`was_injected=False` (the tool is dropped — exactly the 400 path). Ran
against the actual module via `tests/test_ccr_tool_always_on.py`.
- Not tested: a live sessionless multi-turn WS request reproducing the
upstream 400 end to end.

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

Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-13 11:45:51 -05:00
dependabot[bot]
b30f339d69
deps: bump criterion from 0.5.1 to 0.8.2 (#2965)
Bumps [criterion](https://github.com/criterion-rs/criterion.rs) from
0.5.1 to 0.8.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/criterion-rs/criterion.rs/releases">criterion's
releases</a>.</em></p>
<blockquote>
<h2>criterion-plot-v0.8.2</h2>
<h3>Other</h3>
<ul>
<li>Update Readme</li>
</ul>
<h2>criterion-v0.8.2</h2>
<h3>Fixed</h3>
<ul>
<li>don't build alloca on unsupported targets</li>
</ul>
<h3>Other</h3>
<ul>
<li><em>(deps)</em> bump crate-ci/typos from 1.40.0 to 1.43.0</li>
<li>Fix panic with uniform iteration durations in benchmarks</li>
<li>Update Readme</li>
<li>Exclude development scripts from published package</li>
</ul>
<h2>criterion-plot-v0.8.1</h2>
<h3>Fixed</h3>
<ul>
<li>Typo</li>
</ul>
<h2>criterion-v0.8.1</h2>
<h3>Fixed</h3>
<ul>
<li>Homepage link</li>
</ul>
<h3>Other</h3>
<ul>
<li><em>(deps)</em> bump crate-ci/typos from 1.23.5 to 1.40.0</li>
<li><em>(deps)</em> bump jontze/action-mdbook from 3 to 4</li>
<li><em>(deps)</em> bump actions/checkout from 4 to 6</li>
</ul>
<h2>criterion-plot-v0.8.0</h2>
<p>No release notes provided.</p>
<h2>criterion-v0.8.0</h2>
<h3>BREAKING</h3>
<ul>
<li>Drop async-std support</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Bump MSRV to 1.86, stable to 1.91.1</li>
</ul>
<h3>Added</h3>
<ul>
<li>Add ability to plot throughput on summary page.</li>
<li>Add support for reporting throughput in elements and bytes -
<code>Throughput::ElementsAndBytes</code> allows the text summary to
report throughput in both units simultaneously.</li>
<li>Add alloca-based memory layout randomisation to mitigate memory
effects on measurements.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/criterion-rs/criterion.rs/blob/master/CHANGELOG.md">criterion's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/criterion-rs/criterion.rs/compare/criterion-v0.8.1...criterion-v0.8.2">0.8.2</a>
- 2026-02-04</h2>
<h3>Fixed</h3>
<ul>
<li>don't build alloca on unsupported targets</li>
</ul>
<h3>Other</h3>
<ul>
<li><em>(deps)</em> bump crate-ci/typos from 1.40.0 to 1.43.0</li>
<li>Fix panic with uniform iteration durations in benchmarks</li>
<li>Update Readme</li>
<li>Exclude development scripts from published package</li>
</ul>
<h2><a
href="https://github.com/criterion-rs/criterion.rs/compare/criterion-v0.8.0...criterion-v0.8.1">0.8.1</a>
- 2025-12-07</h2>
<h3>Fixed</h3>
<ul>
<li>Homepage link</li>
</ul>
<h3>Other</h3>
<ul>
<li><em>(deps)</em> bump crate-ci/typos from 1.23.5 to 1.40.0</li>
<li><em>(deps)</em> bump jontze/action-mdbook from 3 to 4</li>
<li><em>(deps)</em> bump actions/checkout from 4 to 6</li>
</ul>
<h2><a
href="https://github.com/criterion-rs/criterion.rs/compare/criterion-v0.7.0...criterion-v0.8.0">0.8.0</a>
- 2025-11-29</h2>
<h3>BREAKING</h3>
<ul>
<li>Drop async-std support</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Bump MSRV to 1.86, stable to 1.91.1</li>
</ul>
<h3>Added</h3>
<ul>
<li>Add ability to plot throughput on summary page.</li>
<li>Add support for reporting throughput in elements and bytes -
<code>Throughput::ElementsAndBytes</code> allows the text summary to
report throughput in both units simultaneously.</li>
<li>Add alloca-based memory layout randomisation to mitigate memory
effects on measurements.</li>
<li>Add doc comment to benchmark runner in criterion_group macro
(removes linter warnings)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix plotting NaN bug</li>
</ul>
<h3>Other</h3>
<ul>
<li>Remove Master API Docs links temporarily while we restore the docs
publishing.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7f0d745532"><code>7f0d745</code></a>
chore: release v0.8.2</li>
<li><a
href="4a467ce964"><code>4a467ce</code></a>
chore(deps): bump crate-ci/typos from 1.40.0 to 1.43.0</li>
<li><a
href="b277a75145"><code>b277a75</code></a>
Fix panic with uniform iteration durations in benchmarks</li>
<li><a
href="828af1450d"><code>828af14</code></a>
fix: don't build alloca on unsupported targets</li>
<li><a
href="b01316b76e"><code>b01316b</code></a>
Update Readme</li>
<li><a
href="4c02a3b4e5"><code>4c02a3b</code></a>
Exclude development scripts from published package</li>
<li><a
href="e4e06dfdc3"><code>e4e06df</code></a>
chore: release v0.8.1</li>
<li><a
href="aa548b9f58"><code>aa548b9</code></a>
fix: Homepage link</li>
<li><a
href="950c3b727a"><code>950c3b7</code></a>
fix: Typo</li>
<li><a
href="7e3e50c369"><code>7e3e50c</code></a>
chore(deps): bump crate-ci/typos from 1.23.5 to 1.40.0</li>
<li>Additional commits viewable in <a
href="https://github.com/criterion-rs/criterion.rs/compare/0.5.1...criterion-v0.8.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=criterion&package-manager=cargo&previous-version=0.5.1&new-version=0.8.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 08:54:47 -05:00
dependabot[bot]
d6fb5365f6
deps: update mcp requirement from <2.0.0,>=1.28.1 to >=1.28.1,<3.0.0 (#2963)
Updates the requirements on
[mcp](https://github.com/modelcontextprotocol/python-sdk) to permit the
latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/modelcontextprotocol/python-sdk/releases">mcp's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.0</h2>
<h1>MCP Python SDK v2 Stable Release</h1>
<p>This is v2.0.0, the stable v2 release of the MCP Python SDK. It
supports the 2026-07-28 revision of the Model Context Protocol and
serves every earlier revision from the same server. <code>pip install
mcp</code> now installs 2.x.</p>
<pre lang="bash"><code>pip install &quot;mcp[cli]&quot;
# or
uv add &quot;mcp[cli]&quot;
</code></pre>
<h3>Documentation Rewrite</h3>
<p>The <a
href="https://py.sdk.modelcontextprotocol.io/">documentation</a> has the
full tutorial and API reference. Coming from v1? <a
href="https://py.sdk.modelcontextprotocol.io/whats-new/">What's new in
v2</a> is the tour of what changed and why, and the <a
href="https://py.sdk.modelcontextprotocol.io/migration/">migration
guide</a> lists every breaking change with before-and-after code.</p>
<h3>V1 Maintenance mode</h3>
<p><strong>v1.x is in maintenance mode and will only receive security
fixes from now on</strong> The 1.x line lives on the <a
href="https://github.com/modelcontextprotocol/python-sdk/tree/v1.x"><code>v1.x</code>
branch</a>, continues to receive critical bug fixes and security
patches, and is documented at <a
href="https://py.sdk.modelcontextprotocol.io/v1/">https://py.sdk.modelcontextprotocol.io/v1/</a>.
If your project is not ready to migrate, keep a <code>&lt;2</code> upper
bound on your requirement (for example
<code>mcp&gt;=1.28,&lt;2</code>).</p>
<h2>Highlights</h2>
<h3>One SDK, both protocol eras</h3>
<p>v2 speaks the 2026-07-28 revision (stateless requests with no
handshake, <code>server/discover</code>,
<code>subscriptions/listen</code>, multi-round-trip requests) and still
serves every 2025-era client from the same <code>MCPServer</code>, over
Streamable HTTP and stdio, with nothing to configure.
<code>Client(target)</code> negotiates the version automatically.</p>
<h3><code>FastMCP</code> is now <code>MCPServer</code>, and there is a
first-class <code>Client</code></h3>
<p>The decorator API is unchanged; the low-level <code>Server</code> is
rebuilt around a shared dispatcher engine, and one <code>Client</code>
object replaces v1's
transport-plus-<code>ClientSession</code>-plus-<code>initialize()</code>
layering. It connects to a URL, a stdio subprocess, a custom transport,
or straight to a server object in memory for tests.</p>
<h3>Multi-round-trip requests and resolver dependency injection</h3>
<p>At 2026-07-28 the server can no longer call the client, so tools
return the question instead. A <code>Resolve(fn)</code> parameter is
filled by your function invisibly to the model and can put a question to
the user; one tool body serves both eras.</p>
<h3>Extension APIs, OpenTelemetry, and a standalone types package</h3>
<p>Servers and clients compose protocol extensions through pluggable
extension APIs (MCP Apps built in); OpenTelemetry tracing ships on by
default; every protocol type is its own package, <code>mcp-types</code>
(imported as <code>mcp_types</code>), published in lock-step with
<code>mcp</code>.</p>
<h3>Hardened stdio and auth</h3>
<p>stdio servers keep handler subprocesses and stray prints off the
wire, and stdout is diverted to stderr while serving. OAuth adds RFC
9207 issuer validation, the SEP-990 identity-assertion flow, and the
client-credentials extension.</p>
<h2>Coming from a v2 pre-release</h2>
<p>Since the last release candidate: the per-version wire packages are
private (<code>mcp_types._v*</code>), <code>mcp.types</code> is a
permanent alias for <code>mcp_types</code>, the auth registration
request model is split from the registered-client record, cancelled
requests are no longer answered, and log notifications are gated on the
per-request log-level opt-in at 2026-07-28. Since the betas:
<code>Client(cache=False)</code> is now <code>cache=None</code> with
<code>CacheConfig()</code> the default; <code>Context.client_id</code>,
<code>RFC7523OAuthClientProvider</code>, and
<code>OAuthClientProvider(timeout=)</code> are removed; the
client-credentials providers take <code>scope=</code>;
<code>message_handler</code> receives notifications and exceptions only;
<code>FileResource(is_binary=)</code> becomes <code>encoding</code>;
<code>MCP_*</code> env vars are gone with
<code>pydantic-settings</code>; Streamable HTTP servers reject bodies
over 4 MiB with HTTP 413. The migration guide covers all of it.</p>
<h2>Known gaps</h2>
<p>The tasks extension (SEP-2663) is not part of this release. On the
client, the DPoP proof binding (SEP-1932) and the workload-identity
<code>jwt-bearer</code> grant are not implemented; both are additive and
can land in 2.x.</p>
<h2>Feedback</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6f69a3758e"><code>6f69a37</code></a>
Present v2 as the stable release across the README, docs, and policies
(<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3178">#3178</a>)</li>
<li><a
href="78e6fbb7e4"><code>78e6fbb</code></a>
Serve v2 docs at the site root, with permanent per-major paths (<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3176">#3176</a>)</li>
<li><a
href="af06330a31"><code>af06330</code></a>
Remove unused StreamableHTTPTransport.get_session_id() (<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3205">#3205</a>)</li>
<li><a
href="68ca87e20b"><code>68ca87e</code></a>
Document the two-line release process for stable v2 (<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3179">#3179</a>)</li>
<li><a
href="c9c431b71a"><code>c9c431b</code></a>
Expose the middleware chain on MCPServer and stop sending unrequested
change ...</li>
<li><a
href="528e366558"><code>528e366</code></a>
Fail fast on server-to-client requests in JSON-response mode instead of
hangi...</li>
<li><a
href="27f5cc7a46"><code>27f5cc7</code></a>
Remove unused mcpserver.exceptions.ValidationError (<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3199">#3199</a>)</li>
<li><a
href="89c5e700f2"><code>89c5e70</code></a>
Gate log notifications on the per-request log-level opt-in at 2026-07-28
(<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3198">#3198</a>)</li>
<li><a
href="b61ce388dd"><code>b61ce38</code></a>
docs: fix off-by-one hl_lines in apps.md (<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3196">#3196</a>)</li>
<li><a
href="b7c9a916d6"><code>b7c9a91</code></a>
Add mcp.types as a permanent alias for mcp_types (<a
href="https://redirect.github.com/modelcontextprotocol/python-sdk/issues/3190">#3190</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/modelcontextprotocol/python-sdk/compare/v1.28.1...v2.0.0">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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-13 08:48:33 -05:00
JD Davis
e269afb935
fix(ci): unjam release and Docker publishing (#2958)
## Description

Fixes two release-automation defects exposed by the 0.35.0 release:

1. Release Please grouped the single package into a PR titled `chore:
release main`, which could not be matched back to the `headroom-ai`
component/version and therefore never emitted the release event that
starts PyPI publishing.
2. Docker manifest jobs downloaded digest artifacts with overlapping
variant globs. For example, `digests-code-*` also selected code-nonroot,
code-slim, and code-slim-nonroot artifacts, yielding eight markers where
exactly two were required.

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

- Route the single root package through Release Please's normal
versioned-PR path.
- Preserve both `${component}` and `${version}` in generated release PR
titles.
- Download Docker amd64 and arm64 digest artifacts by exact name instead
of an overlapping variant glob.
- Add regression assertions for both Release Please title matching and
Docker artifact isolation.

## 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_release_workflows.py -q
44 passed in 2.29s

pre-commit hooks:
Sync plugin versions........................Passed
Verify Ruff version alignment...............Passed
check for merge conflicts...................Passed
ruff (legacy alias).........................Passed
ruff format.................................Passed
mypy........................................Passed
```

## Real Behavior Proof

- Environment: GitHub Actions release runs 31653073583 and 31664975515,
`main` at the 0.35.0 release merge (`93f2d7a2`).
- Exact command / steps: Inspected the Release Please rerun and each
failed Docker manifest job; enumerated the digest artifacts downloaded
by their configured patterns.
- Observed result: Release Please logged `There are untagged, merged
release PRs outstanding - aborting`. Docker's `slim`, `code`, and
`code-slim` manifest cells found 4, 8, and 4 digest markers respectively
instead of 2 because their prefix globs included related variants. The
new Docker workflow requests `digests-<variant>-amd64` and
`digests-<variant>-arm64` by exact name.
- Not tested: A synthetic release was not published because registry
versions/tags are irreversible. Both configuration invariants are
covered by regression tests, and GitHub's PR workflow validation runs
against this branch.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A — workflow configuration and regression-test changes only.

## Additional Notes

- Documentation is N/A: these are internal release workflow corrections
with no user-facing command or API changes.
- The 0.35.0 Python release recovery is proceeding separately through
the existing wheel smoke-test and PyPI publication gates.
- The already-started 0.35.0 Docker run used the old workflow from tag
commit `93f2d7a2`; this PR prevents the artifact collision in subsequent
Docker runs.
2026-08-12 23:43:04 -05:00
JD Davis
3077ac81e8
feat: add deterministic runtime rollout controls (#1490)
## Description

Establish one centrally resolved, observable, deterministic, versioned
runtime rollout-control mechanism for Headroom. Runtime rollout controls
which behaviors an already-built artifact may expose; it does not select
or qualify a Headroom release/version.

## Type of Change

- [x] New feature (non-breaking change that adds functionality)
- [x] Bug fix (non-breaking change that fixes rollout enforcement
regressions)
- [x] Documentation update
- [x] Code refactoring (no functional changes)

## Changes Made

- Added `RolloutChannel`, `HEADROOM_ROLLOUT_CHANNEL`,
`--rollout-channel`, and a versioned immutable `RolloutSnapshot` shared
by Python configuration boundaries.
- Added schema/policy versions, canonical registry and snapshot SHA-256
identities, per-feature decision reasons, disable precedence, unsafe
qualification poisoning, strict CLI validation, and fail-closed
environment handling.
- Added `headroom rollout status --json`, Python `/stats.rollout`, and
Rust `/rollout/status` runtime provenance.
- Added equivalent Rust snapshot semantics and shared Python/Rust policy
vectors while retaining language-specific feature registries.
- Enforced rollout policy at alternate Python server composition roots
so `HEADROOM_READ_MATURATION=1` cannot bypass its beta gate.
- Preserved typed rollout snapshots across multi-worker serialization
with schema, policy, registry, snapshot-digest, type, and feature-name
validation.
- Made loopback runtime output-shaper updates replace the immutable
snapshot atomically for request readers, retain explicit request/disable
provenance, preserve channel and kill-switch precedence, invalidate
cached stats, and return the effective rollout decision.
- Made `headroom learn --verbosity --apply` report a channel-blocked
update instead of claiming the shaper is live.
- Made explicit CLI feature flags fail loudly when their current channel
blocks them.
- Made persistent interceptor installation select canary automatically,
or reject an explicitly insufficient channel unless the break-glass
override is set.
- Updated architecture, proxy, rollout, learn, and output-shaper
documentation with required channels and hot-reload semantics.

## Testing

- [x] Unit tests pass
- [x] Linting passes (`ruff check .` and `ruff format --check .`)
- [x] Type checking passes (`mypy headroom --ignore-missing-imports`)
- [x] New regression tests added for every corrected behavior
- [x] Rust tests and production-target Clippy pass
- [x] Documentation build passes

### Test Output

```text
Focused rollout coverage suite
57 passed; headroom.rollout + rollout CLI: 98% coverage

Affected proxy/rollout/transform/governance suites
222 passed; 0 failed

Final changed regression suites
100 passed; 0 failed

Cross-module hot-reload isolation regression
6 passed; 0 failed

cargo test -p headroom-core -p headroom-proxy --quiet
headroom-core: 924 passed; 1 ignored
headroom-proxy and integration suites: all passed

cargo clippy -p headroom-core -p headroom-proxy --lib --bins -- -D warnings
cargo fmt --all -- --check
ruff check .
ruff format --check .
mypy headroom --ignore-missing-imports
git diff --check
All passed

cd docs && npm run build
Compiled successfully; 164 static pages generated
```

The unsharded Windows-only CI selection exposed unrelated baseline
failures, principally the existing `sqlite:///C:\\...` URL parser
producing an invalid `\\C:\\...` path. At commit `8e793a80`, all 52
completed GitHub checks passed; the only other conclusions are expected
skips and superseded governance jobs.

## Real Behavior Proof

- **Environment:** Windows checkout on Python 3.13.3 and the current
Rust workspace, based on upstream `main` at `93f2d7a2`.
- **Exact command / steps:** Exercised canary and beta feature requests
through CLI status, Python `/stats.rollout`, Rust `/rollout/status`,
multi-worker payload round trips, loopback `/admin/runtime-env`, real
proxy request shaping before/after hot reload, installer manifest
generation, and shared Python/Rust policy vectors.
- **Observed result:** Stable blocks unstable requests; disable wins
over explicit/default/legacy/unsafe paths; unsafe state reports
`qualification_eligible=false`; worker handoff rejects tampering;
running output shaping changes only when the effective beta policy
permits it; explicit blocked flags fail with actionable diagnostics.
- **Not tested:** Live production traffic requiring provider
credentials, or future artifact qualification/promotion automation
(intentionally out of scope).

## Runtime Rollout Safety

- **Rollout-managed features:** Python `tool_result_interceptors`,
`proxy_output_shaper`, `read_maturation`; Rust `native_bedrock`,
`openai_responses_streaming`, `canary_probe`.
- **Minimum rollout channel:** Registry-defined per feature; process
default is `stable`.
- **Stable/default behavior changed:** No unstable feature becomes
enabled by default. Explicit blocked CLI flags now fail instead of
silently doing nothing.
- **Kill switch / disable path:**
`HEADROOM_DISABLE_FEATURES=<comma-separated feature names>`; explicit
disable has highest precedence, including over the unsafe override.
- **Unsafe override required:** No.
`HEADROOM_UNSAFE_ALLOW_UNSTABLE_FEATURES=1` is break-glass only and
makes qualification evidence ineligible.
- **Qualification impact:** Adds machine-readable policy/snapshot
identities and eligibility; does not implement qualification itself.
- **Rollback path:** Set the named disable list for operational
rollback, lower the channel, or revert this PR.

## Review Readiness

- [x] I have performed a full diff 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 hard-to-understand areas
- [x] I have made corresponding documentation changes
- [x] My changes generate no new warnings
- [x] I added tests that reproduce and prevent every regression fixed
during review
- [x] New and existing affected tests pass locally
- [x] I did **not** edit `CHANGELOG.md`; release-please generates it
from the Conventional Commit PR title

## Additional Notes

Out of scope: artifact candidates, benchmark orchestration,
qualification manifests/gates, promotion automation, release branches,
publication guards, and release-risk classification. Those workflows can
consume the rollout registry digest, runtime snapshot digest, decision
reasons, and qualification eligibility through supported black-box
interfaces.

---------

Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
Co-authored-by: JD Davis <jd@JDH-AIR-00.local>
2026-08-12 23:16:54 -05:00
Tejas Chopra
93f2d7a2da
chore: release main (#2792)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.35.0</summary>

##
[0.35.0](https://github.com/headroomlabs-ai/headroom/compare/v0.34.0...v0.35.0)
(2026-08-12)


### Features

* **beacon:** allowlist the routing summary key
([#2818](https://github.com/headroomlabs-ai/headroom/issues/2818))
([7940c05](7940c05ebf))
* **beacon:** hourly R2 compaction, per-strategy savings, and a stack
that reports
([#2853](https://github.com/headroomlabs-ai/headroom/issues/2853))
([e0870ef](e0870ef931))
* **cli,pricing:** add CLI extension seam and prompt-cache TTL pricing
([#2802](https://github.com/headroomlabs-ai/headroom/issues/2802))
([6ec3e34](6ec3e3478a))


### Bug Fixes

* **anthropic:** strip first-party tool search on custom upstreams
([#2539](https://github.com/headroomlabs-ai/headroom/issues/2539))
([7f6950b](7f6950be34))
* **backends/anyllm:** convert Anthropic tools and tool_choice to OpenAI
shape
([0d6866b](0d6866b91a))
* **backends/anyllm:** stream tool_use blocks and map finish_reason on
the streaming path
([e4904e2](e4904e23a6))
* **backends/litellm:** None-guard core token counts in OpenAI usage
block ([#2324](https://github.com/headroomlabs-ai/headroom/issues/2324))
([12f9f58](12f9f58cb3))
* **beacon:** report all-layers savings, not context-compression only
([#2796](https://github.com/headroomlabs-ai/headroom/issues/2796))
([e9a24f3](e9a24f3ec1))
* **beacon:** split session failures by status code
([#2815](https://github.com/headroomlabs-ai/headroom/issues/2815))
([2954e37](2954e37048))
* **cache:** bound compression cache bookkeeping
([0ae948c](0ae948c151))
* **cache:** enforce Anthropic's 1h-before-5m cache_control ordering
before forwarding
([#2941](https://github.com/headroomlabs-ai/headroom/issues/2941))
([3752458](3752458022))
* **cache:** mirror client cache_control positions instead of
single-marker consolidation
([def3d76](def3d76e5a))
* **cache:** stabilize Anthropic block-growing lineages
([#2917](https://github.com/headroomlabs-ai/headroom/issues/2917))
([1a04c95](1a04c957f5))
* **ccr:** avoid injecting tool on chat streaming
([d0c1f5b](d0c1f5b8ad))
* **ccr:** preserve exact SQLite TTL boundary
([#2669](https://github.com/headroomlabs-ai/headroom/issues/2669))
([d0a86d4](d0a86d409f))
* **ccr:** report embedded hashes from compress endpoint
([#717](https://github.com/headroomlabs-ai/headroom/issues/717))
([685ebe4](685ebe457d))
* **ccr:** resolve &lt;&lt;ccr:...&gt;&gt; markers inline when no
retrieve-tool path exists
([#2512](https://github.com/headroomlabs-ai/headroom/issues/2512))
([ce8ce83](ce8ce8313f))
* **ccr:** tolerate null/malformed OpenAI data in response handling
([#2467](https://github.com/headroomlabs-ai/headroom/issues/2467))
([e583e08](e583e082d8))
* **ci:** publish latest from the root Docker manifest
([#2252](https://github.com/headroomlabs-ai/headroom/issues/2252))
([5568d73](5568d738af))
* **claude:** stop forcing tool search on Foundry
([#2477](https://github.com/headroomlabs-ai/headroom/issues/2477))
([7981396](798139608c))
* **cli/update:** let install ownership win over bare /.dockerenv so
venv installs self-update
([#2830](https://github.com/headroomlabs-ai/headroom/issues/2830))
([7092b53](7092b53c46))
* **codex:** route alpha search through the Codex backend
([#2538](https://github.com/headroomlabs-ai/headroom/issues/2538))
([a540eb2](a540eb2c61))
* **content-router:** protect custom-tag blocks before mixed-content
section split
([d7bc1e2](d7bc1e275f))
* **deps:** bump h2 to 4.4.1 for CVE-2026-71554
([#2839](https://github.com/headroomlabs-ai/headroom/issues/2839))
([564e0a8](564e0a8d0f))
* **deps:** enforce audited transitive dependency floors
([#2791](https://github.com/headroomlabs-ai/headroom/issues/2791))
([64e2039](64e203931b))
* **doctor:** flag `ollama launch claude` proxy bypass instead of
misdirecting
([#2566](https://github.com/headroomlabs-ai/headroom/issues/2566))
([7f24d69](7f24d695ee))
* emit SSE ping before message_start on Bedrock streaming path (issue
[#902](https://github.com/headroomlabs-ai/headroom/issues/902))
([#1080](https://github.com/headroomlabs-ai/headroom/issues/1080))
([4dab254](4dab254d52))
* **gemini:** resolve native CCR retrieval calls
([#2253](https://github.com/headroomlabs-ai/headroom/issues/2253))
([2483f57](2483f57002))
* **health:** label kompress as degraded/optional when not yet loaded
([#2865](https://github.com/headroomlabs-ai/headroom/issues/2865))
([8949371](89493714d2))
* **image:** decouple routing types from trained_router so importing the
compressor doesn't import torch
([#2513](https://github.com/headroomlabs-ai/headroom/issues/2513))
([#2537](https://github.com/headroomlabs-ai/headroom/issues/2537))
([d7cf981](d7cf981093))
* **install/windows:** register persistent-task from S4U hidden XML
([#2453](https://github.com/headroomlabs-ai/headroom/issues/2453))
([#2459](https://github.com/headroomlabs-ai/headroom/issues/2459))
([1edaeb8](1edaeb8b76))
* **install:** don't crash the PowerShell installer when $PROFILE is
unset ([#2469](https://github.com/headroomlabs-ai/headroom/issues/2469))
([fc5c4e2](fc5c4e239c))
* **install:** trust Docker bridge for dashboard metadata
([e044139](e044139001))
* **install:** use --userns=keep-id under Podman so bind-mount writes
don't fail
([#2846](https://github.com/headroomlabs-ai/headroom/issues/2846))
([3488f8d](3488f8d4b5))
* **learn/gemini:** stop double-counting session tokens
([#2230](https://github.com/headroomlabs-ai/headroom/issues/2230))
([29d8a5e](29d8a5e563))
* **learn/grok:** detect a Windows absolute project path
([#2283](https://github.com/headroomlabs-ai/headroom/issues/2283))
([e240df2](e240df2b69))
* **learn:** stop classifying a successful exit code 0 as an error
([#2289](https://github.com/headroomlabs-ai/headroom/issues/2289))
([a24fe7d](a24fe7dcbf))
* **litellm:** add async_post_call_success_hook to HeadroomCallback
([#1322](https://github.com/headroomlabs-ai/headroom/issues/1322))
([3107994](3107994aed))
* **litellm:** don't forward a caller key the target cannot accept
([#2883](https://github.com/headroomlabs-ai/headroom/issues/2883))
([2f2950a](2f2950a626))
* **memory:** bound the TrafficLearner pending-pattern accumulator
(memory leak)
([#2579](https://github.com/headroomlabs-ai/headroom/issues/2579))
([1f5feff](1f5fefffd3))
* **memory:** close DirectMem0 resources
([6596182](65961827cf))
* **memory:** close MCP backend on shutdown
([4bd8ecd](4bd8ecd1e3))
* **memory:** don't crash inline memory extraction on a non-object
&lt;memory&gt; block
([#2470](https://github.com/headroomlabs-ai/headroom/issues/2470))
([e00c6ff](e00c6ff81c))
* **memory:** keep vector metadata in sync
([#2295](https://github.com/headroomlabs-ai/headroom/issues/2295))
([c471800](c471800e8e))
* **memory:** make explicit-project and user store keys
collision-resistant
([#2231](https://github.com/headroomlabs-ai/headroom/issues/2231))
([f840d5f](f840d5f2fe))
* **memory:** skip &lt;system-reminder&gt; blocks when building the
retrieval query
([#2195](https://github.com/headroomlabs-ai/headroom/issues/2195))
([#2541](https://github.com/headroomlabs-ai/headroom/issues/2541))
([4e5a67a](4e5a67a342))
* **memory:** sync FTS5 and vector indexes on CLI
delete/edit/prune/purge
([fd4628d](fd4628d821))
* **oauth2:** make repository lint checks pass
([c85abf7](c85abf7a87))
* **observability:** aggregate tool savings in OTEL
([#2936](https://github.com/headroomlabs-ai/headroom/issues/2936))
([941c25d](941c25d31e))
* **onnx:** stop ONNX thread pools from spinning idle cores
([#2495](https://github.com/headroomlabs-ai/headroom/issues/2495))
([#2540](https://github.com/headroomlabs-ai/headroom/issues/2540))
([5c561bd](5c561bd913))
* **openai:** skip Responses tool-search deferral for clients that
cannot execute it
([#2696](https://github.com/headroomlabs-ai/headroom/issues/2696))
([54ea28d](54ea28d983))
* **opencode:** ship the transport hook-shim so wheel installs route
Node child traffic
([702dbc5](702dbc5902))
* **providers/anthropic:** don't crash token estimation on null
tool_calls
([#2472](https://github.com/headroomlabs-ai/headroom/issues/2472))
([08466f3](08466f3cae))
* **providers/openai:** bound tiktoken vocab loads with the guarded
loader
([#2554](https://github.com/headroomlabs-ai/headroom/issues/2554))
([0805e8e](0805e8e410))
* **proxy/anthropic:** inject headroom_retrieve whenever a CCR marker is
present, not only for new markers
([#2848](https://github.com/headroomlabs-ai/headroom/issues/2848))
([3808f60](3808f60ca6))
* **proxy/anthropic:** None-guard usage token counts on the direct
buffered path
([#2434](https://github.com/headroomlabs-ai/headroom/issues/2434))
([2b5ee7c](2b5ee7cde8))
* **proxy/anthropic:** run tool-search history repair after turn hooks
([c6f9948](c6f99482e1))
* **proxy/batch:** don't crash an OpenAI batch on a valid-JSON
non-object line
([#2316](https://github.com/headroomlabs-ai/headroom/issues/2316))
([1f2c681](1f2c681c0b))
* **proxy/bedrock:** report uncached input tokens from backend usage,
not the live-zone count
([#2318](https://github.com/headroomlabs-ai/headroom/issues/2318))
([c19e412](c19e412b33))
* **proxy/gemini:** keep streaming-parity baseline so eligible_pct can't
exceed 100
([#2824](https://github.com/headroomlabs-ai/headroom/issues/2824))
([b97c7c6](b97c7c6e99))
* **proxy/metrics:** cap client-supplied model label cardinality
([#2480](https://github.com/headroomlabs-ai/headroom/issues/2480))
([e24a7e6](e24a7e66b9))
* **proxy/metrics:** escape label values in the Prometheus export
([#2463](https://github.com/headroomlabs-ai/headroom/issues/2463))
([6a53861](6a53861063))
* **proxy/openai:** don't crash the Responses memory tool loops on null
arguments
([#2273](https://github.com/headroomlabs-ai/headroom/issues/2273))
([a30db2c](a30db2cae4))
* **proxy/openai:** feed Codex WS traffic into the traffic learner
([#2334](https://github.com/headroomlabs-ai/headroom/issues/2334))
([f669149](f669149769))
* **proxy/openai:** run response hooks on Responses, and bill their
re-drives
([#2872](https://github.com/headroomlabs-ai/headroom/issues/2872))
([675d13f](675d13f08d))
* **proxy:** allow settings routes for trusted gateway/dashboard clients
([#2491](https://github.com/headroomlabs-ai/headroom/issues/2491))
([a5b0a8f](a5b0a8f4cc))
* **proxy:** cache litellm model resolution to stop repeated Provider
List spam
([99f07e7](99f07e7bbd))
* **proxy:** cancel periodic TOIN task on shutdown
([739fdef](739fdef423))
* **proxy:** close the upstream stream when a streaming body is never
consumed
([0951663](0951663562))
* **proxy:** compress cache-mode cold starts and tag prefix-mismatch
passthrough
([#2365](https://github.com/headroomlabs-ai/headroom/issues/2365))
([aaeba0a](aaeba0a319))
* **proxy:** emit request log timestamps in UTC
([620028f](620028fa18))
* **proxy:** enable tool search by default and repair poisoned
transcripts
([#2807](https://github.com/headroomlabs-ai/headroom/issues/2807))
([0237cbf](0237cbffbb))
* **proxy:** gate mid-turn message coalescing to Claude Code clients
([#1643](https://github.com/headroomlabs-ai/headroom/issues/1643))
([a4bd2e6](a4bd2e62a5))
* **proxy:** give each Codex /v1/responses WS turn a unique request_id
([#2164](https://github.com/headroomlabs-ai/headroom/issues/2164))
([d02df10](d02df10758))
* **proxy:** graceful shutdown and reliable Ctrl+C exit
([#621](https://github.com/headroomlabs-ai/headroom/issues/621))
([17cdb18](17cdb185bc))
* **proxy:** guard telemetry and TOIN endpoints
([cde1513](cde1513c91))
* **proxy:** include tool_search_deferral savings in the savings ledger
([12149f7](12149f7446))
* **proxy:** pass through cross-region prefixed Bedrock model IDs
directly
([#2330](https://github.com/headroomlabs-ai/headroom/issues/2330))
([64cb46e](64cb46e24b))
* **proxy:** port session-sticky beta headers to the Rust proxy
([#2381](https://github.com/headroomlabs-ai/headroom/issues/2381))
([f6398a6](f6398a6476))
* **proxy:** preserve merged session and quarantine contracts
([#2943](https://github.com/headroomlabs-ai/headroom/issues/2943))
([039cd24](039cd2431a))
* **proxy:** preserve signed Anthropic thinking blocks on outbound
re-serialize
([#2254](https://github.com/headroomlabs-ai/headroom/issues/2254))
([dc163bc](dc163bcd1c))
* **proxy:** stop discarding compressed Codex WS later-frame payloads
([#2823](https://github.com/headroomlabs-ai/headroom/issues/2823))
([4ec416d](4ec416df88))
* **proxy:** time-cap the compression timeout-debt quarantine
([#2360](https://github.com/headroomlabs-ai/headroom/issues/2360))
([#2412](https://github.com/headroomlabs-ai/headroom/issues/2412))
([c5a08d2](c5a08d22e0))
* **proxy:** unwrap Hermes tool_call bridge in tool name map
([#2717](https://github.com/headroomlabs-ai/headroom/issues/2717))
([a97b824](a97b82413b))
* publish headroom-opencode in release workflow
([#2372](https://github.com/headroomlabs-ai/headroom/issues/2372))
([7859154](78591545ce))
* **settings:** accept documented HEADROOM_* env names as settings keys
([#2833](https://github.com/headroomlabs-ai/headroom/issues/2833))
([de9e052](de9e0523da))
* **subscription:** dedup transcript usage by message id
([#2340](https://github.com/headroomlabs-ai/headroom/issues/2340) token
inflation)
([#2408](https://github.com/headroomlabs-ai/headroom/issues/2408))
([74275b7](74275b7c3e))
* **toin:** bound private query and pattern retention
([8cd1380](8cd138039e))
* **tokenizer:** coerce non-string tool_call fields before counting
([#2801](https://github.com/headroomlabs-ai/headroom/issues/2801))
([b6f9877](b6f9877c78))
* **tokenizer:** price CJK in the Rust fixed-ratio estimator (Python
parity)
([#2260](https://github.com/headroomlabs-ai/headroom/issues/2260))
([6840153](6840153473))
* **transforms/adaptive-sizer:** honor max_k on small-input fast path
([#2319](https://github.com/headroomlabs-ai/headroom/issues/2319))
([8a90523](8a90523209))
* **transforms/smart_crusher:** don't crash on a tool call with a null
function
([#2232](https://github.com/headroomlabs-ai/headroom/issues/2232))
([3bb02f8](3bb02f8f75))
* Vertex model pricing shows $0.00 for versioned model names and
vertex:anthropic provider
([#2517](https://github.com/headroomlabs-ai/headroom/issues/2517))
([eb5b5e4](eb5b5e4198))
* **wrap/claude:** keep --1m effective when an explicit --model is
passed through
([c093bf1](c093bf11eb))
* **wrap/opencode:** verify the opencode binary before mutating config
([ae38486](ae384862a4))
* **wrap/serena:** install Serena from the serena-agent PyPI wheel, not
the git source
([d7b25ae](d7b25ae3bb))
* **wrap:** honor Copilot OAuth wire-api override and model default
([#2387](https://github.com/headroomlabs-ai/headroom/issues/2387))
([1db6d88](1db6d88ab4))
* **wrap:** serialize shared proxy startup
([#2946](https://github.com/headroomlabs-ai/headroom/issues/2946))
([e540d64](e540d64feb))
* **wrap:** stop the launch cwd from shadowing the installed package in
the proxy subprocess
([#2843](https://github.com/headroomlabs-ai/headroom/issues/2843))
([c49be26](c49be269a1))


### Performance Improvements

* cut hot-path latency 27% (token-count memo, startup preloads, JSON
scan memo)
([#2838](https://github.com/headroomlabs-ai/headroom/issues/2838))
([53af90d](53af90d68c))
* **proxy:** bound upstream calls and hot-path costs
([#2852](https://github.com/headroomlabs-ai/headroom/issues/2852))
([f624d3a](f624d3a00a))
* **subscription:** skip transcripts older than the window in
compute_window_tokens
([#2861](https://github.com/headroomlabs-ai/headroom/issues/2861))
([91d6bf3](91d6bf33cd))


### Dependencies

* bump brace-expansion from 5.0.7 to 5.0.9 in /docs
([#2751](https://github.com/headroomlabs-ai/headroom/issues/2751))
([56ee57b](56ee57be98))
* bump bytesize from 1.3.3 to 2.4.2
([#2286](https://github.com/headroomlabs-ai/headroom/issues/2286))
([6448545](6448545a7f))
* bump hf-hub from 0.4.3 to 0.5.0
([#2285](https://github.com/headroomlabs-ai/headroom/issues/2285))
([4925bf6](4925bf6a82))
* bump next from 16.2.10 to 16.3.0 in /docs
([#2750](https://github.com/headroomlabs-ai/headroom/issues/2750))
([0fd0b99](0fd0b996a4))
* bump postcss from 8.5.19 to 8.5.25 in /plugins/openclaw
([#2749](https://github.com/headroomlabs-ai/headroom/issues/2749))
([cd60ee9](cd60ee9ae8))
* bump postcss from 8.5.19 to 8.5.25 in /plugins/opencode
([#2748](https://github.com/headroomlabs-ai/headroom/issues/2748))
([ff4e016](ff4e0167bb))
* bump postcss from 8.5.19 to 8.5.25 in /sdk/typescript
([#2747](https://github.com/headroomlabs-ai/headroom/issues/2747))
([267c2bd](267c2bdcb5))
* bump postcss from 8.5.19 to 8.5.26 in /docs
([#2881](https://github.com/headroomlabs-ai/headroom/issues/2881))
([e6e5826](e6e5826423))
* bump ruff from 0.15.17 to 0.15.22 in the pip-minor-patch group
([#2501](https://github.com/headroomlabs-ai/headroom/issues/2501))
([ecf130d](ecf130d3ac))
* bump rusqlite from 0.32.1 to 0.40.1
([#2287](https://github.com/headroomlabs-ai/headroom/issues/2287))
([522faa1](522faa1a59))
* bump the cargo-minor-patch group across 1 directory with 22 updates
([#2916](https://github.com/headroomlabs-ai/headroom/issues/2916))
([148d860](148d8605e2))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: JD Davis <mxjerrett@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-12 19:02:51 -05:00
dependabot[bot]
148d8605e2
deps: bump the cargo-minor-patch group across 1 directory with 22 updates (#2916)
Bumps the cargo-minor-patch group with 21 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [serde_json](https://github.com/serde-rs/json) | `1.0.150` | `1.0.151`
|
| [thiserror](https://github.com/dtolnay/thiserror) | `2.0.18` |
`2.0.19` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.103` | `1.0.104` |
| [clap](https://github.com/clap-rs/clap) | `4.6.2` | `4.6.6` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.52.3` | `1.53.1` |
| [pyo3](https://github.com/pyo3/pyo3) | `0.29.0` | `0.29.2` |
| [aws-config](https://github.com/smithy-lang/smithy-rs) | `1.9.0` |
`1.10.1` |
| [aws-smithy-runtime-api](https://github.com/smithy-lang/smithy-rs) |
`1.13.0` | `1.14.0` |
| [unidiff](https://github.com/messense/unidiff-rs) | `0.4.0` | `0.4.1`
|
| [aho-corasick](https://github.com/BurntSushi/aho-corasick) | `1.1.4` |
`1.1.5` |
| [toml](https://github.com/toml-rs/toml) | `1.1.3+spec-1.1.0` |
`1.1.4+spec-1.1.0` |
| [blake3](https://github.com/BLAKE3-team/BLAKE3) | `1.8.5` | `1.8.6` |
| [http](https://github.com/hyperium/http) | `1.4.2` | `1.5.0` |
| [futures](https://github.com/rust-lang/futures-rs) | `0.3.32` |
`0.3.33` |
| [hyper](https://github.com/hyperium/hyper) | `1.10.1` | `1.11.0` |
| [bytesize](https://github.com/bytesize-rs/bytesize) | `2.4.2` |
`2.7.0` |
| [tokio-util](https://github.com/tokio-rs/tokio) | `0.7.18` | `0.7.19`
|
| [lru](https://github.com/jeromefroe/lru-rs) | `0.18.1` | `0.18.2` |
| [async-trait](https://github.com/dtolnay/async-trait) | `0.1.89` |
`0.1.91` |
| [tokio-stream](https://github.com/tokio-rs/tokio) | `0.1.18` |
`0.1.19` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.67` | `1.4.1` |


Updates `serde_json` from 1.0.150 to 1.0.151
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.151</h2>
<ul>
<li>Add RawValue::from_string_unchecked (<a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>,
thanks <a
href="https://github.com/WonderLawrence"><code>@​WonderLawrence</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="de8500740c"><code>de85007</code></a>
Release 1.0.151</li>
<li><a
href="3b2b3c5f28"><code>3b2b3c5</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>
from WonderLawrence/rawvalue-from-string-unchecked</li>
<li><a
href="0406d96860"><code>0406d96</code></a>
Debug-assert well-formedness and no-whitespace in
from_string_unchecked</li>
<li><a
href="cf16f75d81"><code>cf16f75</code></a>
Add RawValue::from_string_unchecked</li>
<li><a
href="827a315bf2"><code>827a315</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="cea36a5c01"><code>cea36a5</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li>See full diff in <a
href="https://github.com/serde-rs/json/compare/v1.0.150...v1.0.151">compare
view</a></li>
</ul>
</details>
<br />

Updates `thiserror` from 2.0.18 to 2.0.19
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/thiserror/releases">thiserror's
releases</a>.</em></p>
<blockquote>
<h2>2.0.19</h2>
<ul>
<li>Update to syn 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e13a785433"><code>e13a785</code></a>
Release 2.0.19</li>
<li><a
href="0a0e76cc0f"><code>0a0e76c</code></a>
Update to syn 3</li>
<li><a
href="ec42ea7085"><code>ec42ea7</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="4178c4a0e1"><code>4178c4a</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="7214e0e833"><code>7214e0e</code></a>
Ignore items_after_statements pedantic clippy lint in test</li>
<li><a
href="febcc0381f"><code>febcc03</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/thiserror/issues/451">#451</a>
from vip892766gma/maint/20260521171412</li>
<li><a
href="c50e38779d"><code>c50e387</code></a>
chore: improve thiserror maintenance path</li>
<li><a
href="d4a2507576"><code>d4a2507</code></a>
Raise minimum tested compiler to rust 1.85</li>
<li><a
href="99e8a6cd6a"><code>99e8a6c</code></a>
Unpin CI miri toolchain</li>
<li><a
href="9ac165c400"><code>9ac165c</code></a>
Pin CI miri to nightly-2026-02-11</li>
<li>Additional commits viewable in <a
href="https://github.com/dtolnay/thiserror/compare/2.0.18...2.0.19">compare
view</a></li>
</ul>
</details>
<br />

Updates `anyhow` from 1.0.103 to 1.0.104
<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.104</h2>
<ul>
<li>Update <code>syn</code> dev-dependency to version 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1dbe1862aa"><code>1dbe186</code></a>
Release 1.0.104</li>
<li><a
href="f6479f8e5e"><code>f6479f8</code></a>
Update to syn 3</li>
<li>See full diff in <a
href="https://github.com/dtolnay/anyhow/compare/1.0.103...1.0.104">compare
view</a></li>
</ul>
</details>
<br />

Updates `clap` from 4.6.2 to 4.6.6
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/releases">clap's
releases</a>.</em></p>
<blockquote>
<h2>v4.6.6</h2>
<h2>[4.6.6] - 2026-08-06</h2>
<h3>Features</h3>
<ul>
<li>Add <code>Command::get_overridden_usage</code></li>
</ul>
<h2>v4.6.5</h2>
<h2>[4.6.5] - 2026-07-31</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Correctly mark which <code>value_names</code> are
optional with <code>num_args</code></li>
</ul>
<h2>v4.6.4</h2>
<h2>[4.6.4] - 2026-07-21</h2>
<h3>Internal</h3>
<ul>
<li>Update to syn v3</li>
</ul>
<h2>v4.6.3</h2>
<h2>[4.6.3] - 2026-07-20</h2>
<h3>Fixes</h3>
<ul>
<li><em>(derive)</em> Allow <code>&quot;literal&quot;.function()</code>
as attribute values</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/blob/master/CHANGELOG.md">clap's
changelog</a>.</em></p>
<blockquote>
<h2>[4.6.6] - 2026-08-06</h2>
<h3>Features</h3>
<ul>
<li>Add <code>Command::get_overridden_usage</code></li>
</ul>
<h2>[4.6.5] - 2026-07-31</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Correctly mark which <code>value_names</code> are
optional with <code>num_args</code></li>
</ul>
<h2>[4.6.4] - 2026-07-21</h2>
<h3>Internal</h3>
<ul>
<li>Update to syn v3</li>
</ul>
<h2>[4.6.3] - 2026-07-20</h2>
<h3>Fixes</h3>
<ul>
<li><em>(derive)</em> Allow <code>&quot;literal&quot;.function()</code>
as attribute values</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="348cff3fc4"><code>348cff3</code></a>
chore: Release</li>
<li><a
href="d4783779f8"><code>d478377</code></a>
docs: Update changelog</li>
<li><a
href="04b9fbb83a"><code>04b9fbb</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/6414">#6414</a>
from koopatroopa787/fix-bash-completion-bracket-glob</li>
<li><a
href="70752392a8"><code>7075239</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/6422">#6422</a>
from BaumiCoder/fix-fish-indentations</li>
<li><a
href="f90a96636a"><code>f90a966</code></a>
fix(complete): Use spaces for indentation in fish</li>
<li><a
href="dd4997ba2d"><code>dd4997b</code></a>
fix(complete): Don't glob-expand bash positionals</li>
<li><a
href="8387c812c4"><code>8387c81</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/6399">#6399</a>
from clap-rs/renovate/crate-ci-typos-1.x</li>
<li><a
href="8141e110ec"><code>8141e11</code></a>
chore(deps): Update compatible (dev) (<a
href="https://redirect.github.com/clap-rs/clap/issues/6398">#6398</a>)</li>
<li><a
href="8a6bd4e43e"><code>8a6bd4e</code></a>
chore(deps): Update pre-commit hook crate-ci/typos to v1.47.0</li>
<li><a
href="71a7213d6e"><code>71a7213</code></a>
chore(deps): Update Rust Stable to v1.96 (<a
href="https://redirect.github.com/clap-rs/clap/issues/6396">#6396</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/clap-rs/clap/compare/clap_complete-v4.6.2...clap_complete-v4.6.6">compare
view</a></li>
</ul>
</details>
<br />

Updates `tokio` from 1.52.3 to 1.53.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/tokio/releases">tokio's
releases</a>.</em></p>
<blockquote>
<h2>Tokio v1.53.1</h2>
<h1>1.53.1 (July 20th, 2026)</h1>
<h3>Fixed</h3>
<ul>
<li>signal: restore MSRV by removing <code>OnceLock::wait</code> from
the Windows handler (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8300">#8300</a>)</li>
</ul>
<h3>Fixed (unstable)</h3>
<ul>
<li>time: fix alt timer cancellation and insertion race (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8252">#8252</a>)</li>
</ul>
<h3>Documented</h3>
<ul>
<li>runtime: remove dead link definition in Runtime::block_on (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8301">#8301</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/tokio-rs/tokio/issues/8252">#8252</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/8252">tokio-rs/tokio#8252</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8300">#8300</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/8300">tokio-rs/tokio#8300</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8301">#8301</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/8301">tokio-rs/tokio#8301</a></p>
<h2>Tokio v1.53.0</h2>
<h1>1.53.0 (July 17th, 2026)</h1>
<h3>Added</h3>
<ul>
<li>fs: implement <code>From&lt;OwnedFd&gt;</code> and
<code>From&lt;OwnedHandle&gt;</code> for <code>File</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8266">#8266</a>)</li>
<li>metrics: add task schedule latency metric (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7986">#7986</a>)</li>
<li>net: add <code>SocketAddr</code> methods to Unix sockets (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8144">#8144</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>io: add <code>#[inline]</code> to IO trait impls for in-memory types
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8242">#8242</a>)</li>
<li>net: implement UCred::pid on FreeBSD (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8086">#8086</a>)</li>
<li>net: support Nuttx target os (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8259">#8259</a>)</li>
<li>signal: refactor global variables on Windows (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8231">#8231</a>)</li>
<li>sync: <code>mpsc::{Receiver,UnboundedReceiver}</code> now drops
waker on drop, even if there are still senders (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8095">#8095</a>)</li>
<li>taskdump: support taskdumps on s390x (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8192">#8192</a>)</li>
<li>time: add <code>#[track_caller]</code> to <code>timeout_at()</code>
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8077">#8077</a>)</li>
<li>time: consolidate mutex locks on spurious poll (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8124">#8124</a>)</li>
<li>time: defer waker clone on spurious poll (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8107">#8107</a>)</li>
<li>time: move lazy-registration state into <code>Sleep</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8132">#8132</a>)</li>
<li>tracing: remove unnecessary span clone (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8126">#8126</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>io: do not treat zero-length reads as EOF in <code>Chain</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8251">#8251</a>)</li>
<li>net: use getpeereid for QNX peer credentials (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8270">#8270</a>)</li>
<li>runtime: avoid illegal state in <code>FastRand</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8078">#8078</a>)</li>
<li>sync: wake mpsc receiver when a queued <code>reserve[_many]</code>
returns permits (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8260">#8260</a>)</li>
<li>taskdump: skip double wake on
<code>Trace::capture</code>/<code>Trace::trace_with</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8043">#8043</a>)</li>
<li>time: avoid stack overflow in runtime constructor (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8093">#8093</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="75fef53d0a"><code>75fef53</code></a>
chore: prepare Tokio v1.53.1 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8303">#8303</a>)</li>
<li><a
href="ae9d011213"><code>ae9d011</code></a>
signal: restore MSRV by removing OnceLock::wait from the Windows handler
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8300">#8300</a>)</li>
<li><a
href="eb4988dc2e"><code>eb4988d</code></a>
time: fix the loom test of the race between cancellation/insertion (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8302">#8302</a>)</li>
<li><a
href="91d3b4c0bc"><code>91d3b4c</code></a>
time: fix alt timer cancellation and insertion race (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8252">#8252</a>)</li>
<li><a
href="a46338401b"><code>a463384</code></a>
runtime: remove dead link definition in <code>Runtime::block_on</code>
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8301">#8301</a>)</li>
<li><a
href="be689a35f5"><code>be689a3</code></a>
chore: prepare Tokio v1.53.0 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8294">#8294</a>)</li>
<li><a
href="50f76c71ec"><code>50f76c7</code></a>
chore: prepare tokio-macros v2.7.1 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8295">#8295</a>)</li>
<li><a
href="f61fccad3c"><code>f61fcca</code></a>
Merge 'tokio-1.52.4' into 'master' (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8290">#8290</a>)</li>
<li><a
href="efdba5fcf0"><code>efdba5f</code></a>
chore: prepare Tokio v1.52.4 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8289">#8289</a>)</li>
<li><a
href="b0ba02e755"><code>b0ba02e</code></a>
Merge 'tokio-1.51.4' into 'tokio-1.52.x' (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8288">#8288</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tokio-rs/tokio/compare/tokio-1.52.3...tokio-1.53.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `pyo3` from 0.29.0 to 0.29.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pyo3/pyo3/releases">pyo3's
releases</a>.</em></p>
<blockquote>
<h2>PyO3 0.29.2</h2>
<p>This patch fixes a regression in PyO3 0.29.1 which broke PyPy 3.11
compatibility (<code>#[pyclass]</code> types would crash PyPy on
instance deletion).</p>
<p>A few further fixes have also landed with similar themes to PyO3
0.29.1: fixes to minor reference counting bugs, rough edges which would
cause crashes, issues which would cause failed builds, and fixes to
<code>experimental-inspect</code> type stub generation.</p>
<p>For a full list of the exact fixes please consult the CHANGELOG.</p>
<p>Thank you to the following contributors for the improvements:</p>
<p><a
href="https://github.com/davidhewitt"><code>@​davidhewitt</code></a>
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
<a href="https://github.com/ImFeH2"><code>@​ImFeH2</code></a>
<a
href="https://github.com/musicinmybrain"><code>@​musicinmybrain</code></a>
<a href="https://github.com/Tpt"><code>@​Tpt</code></a>
<a
href="https://github.com/WaterWhisperer"><code>@​WaterWhisperer</code></a></p>
<h2>PyO3 0.29.1</h2>
<p>This patch is a stack of fixes for PyO3 0.29. Particular themes
include:</p>
<ul>
<li>Fixes addressing build failures with newer interpreters such as
GraalPy 3.13 and CPython 3.15</li>
<li>Fixes resolving potential memory leaks inside
<code>#[pyclass]</code> implementation internals</li>
<li>Fixes to the new <code>experimental-inspect</code> type stub
machinery</li>
</ul>
<p>For a full list of the fixes in these above themes and more, please
consult the CHANGELOG.</p>
<p>Thank you to the following contributors for the improvements:</p>
<p><a href="https://github.com/alex"><code>@​alex</code></a>
<a
href="https://github.com/bschoenmaeckers"><code>@​bschoenmaeckers</code></a>
<a href="https://github.com/chirizxc"><code>@​chirizxc</code></a>
<a href="https://github.com/davidhewitt"><code>@​davidhewitt</code></a>
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
<a href="https://github.com/exg"><code>@​exg</code></a>
<a href="https://github.com/ImFeH2"><code>@​ImFeH2</code></a>
<a
href="https://github.com/IvanIsCoding"><code>@​IvanIsCoding</code></a>
<a href="https://github.com/jonasdedden"><code>@​jonasdedden</code></a>
<a
href="https://github.com/MatthieuDartiailh"><code>@​MatthieuDartiailh</code></a>
<a href="https://github.com/msimacek"><code>@​msimacek</code></a>
<a href="https://github.com/ngoldbaum"><code>@​ngoldbaum</code></a>
<a href="https://github.com/Person-93"><code>@​Person-93</code></a>
<a href="https://github.com/ratazzi"><code>@​ratazzi</code></a>
<a href="https://github.com/rewitt94"><code>@​rewitt94</code></a>
<a
href="https://github.com/scott-griffiths"><code>@​scott-griffiths</code></a>
<a href="https://github.com/ShiroKSH"><code>@​ShiroKSH</code></a>
<a href="https://github.com/tobni"><code>@​tobni</code></a>
<a href="https://github.com/Tpt"><code>@​Tpt</code></a>
<a
href="https://github.com/WaterWhisperer"><code>@​WaterWhisperer</code></a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md">pyo3's
changelog</a>.</em></p>
<blockquote>
<h2>[0.29.2] - 2026-08-05</h2>
<h3>Packaging</h3>
<ul>
<li>Add <code>PYO3_USE_RAW_DYLIB=0</code> opt-out of
<code>raw-dylib</code> linking for Windows. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6185">#6185</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix PyO3 0.29 regression with failure to link under Cygwin / MSYS2.
<a href="https://redirect.github.com/PyO3/pyo3/pull/6185">#6185</a></li>
<li>Fix stubs generation for field getters (<code>#[pyo3(get)]</code>)
when <code>IntoPyObject</code> is only implemented on references of the
field type. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6276">#6276</a></li>
<li>Fix <code>#[classmethod]</code> magic methods receiving the instance
instead of its type when invoked through a type slot. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6283">#6283</a></li>
<li>Fix <code>pyo3_build_config::add_libpython_rpath_link_args</code>
emitting Unix-style rpath linker arguments on Windows and Cygwin. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6284">#6284</a></li>
<li>Fix PyO3 0.29.1 regression on PyPy causing crashes when deallocating
<code>#[pyclass]</code> instances. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6294">#6294</a></li>
<li>Fix missing trailing nul in Python 3.9 <code>#[pyclass]</code>
docstrings. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6296">#6296</a></li>
<li>Fix reference count leak of <code>#[classattr]</code> values created
from <code>fn</code> items. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6297">#6297</a></li>
</ul>
<h2>[0.29.1] - 2026-08-02</h2>
<h3>Changed</h3>
<ul>
<li>Use the inline definition of <code>Py_TYPE</code> in the unlimited
API on 3.14+ <a
href="https://redirect.github.com/PyO3/pyo3/pull/6179">#6179</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix incorrect pointer arithmetic in FFI definitions
<code>PyObject_GET_WEAKREFS_LISTPTR</code> and
<code>PyHeapType_GET_MEMBERS</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6145">#6145</a></li>
<li>Fix compilation error with <code>nightly</code> feature on PyPy and
GraalPy due to <code>!Ungil</code> implementations for FFI types not
available on those platforms. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6146">#6146</a></li>
<li>Fix <code>append_to_inittab</code> and
<code>PyInit_&lt;module&gt;</code> internal module definition corruption
on 32-bit and big-endian platforms on Python 3.15+. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6150">#6150</a></li>
<li>Fix return value of <code>PyClassGuardMutSuper::as_super</code>
being scoped to the full guard lifetime, now the <code>&amp;mut</code>
borrow of the <code>as_super()</code> call. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6181">#6181</a></li>
<li>Fix builds for free-threaded interpreters older than 3.15 erroring
with &quot;cannot set a minimum Python version&quot; when an
<code>abi3t-py3*</code> feature is enabled and the configuration comes
from <code>PYO3_CONFIG_FILE</code>, sysconfigdata or cross-compilation
defaults. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6192">#6192</a></li>
<li>Fix a memory leak when deallocating <code>#[pyclass(dict)]</code>
instances with a populated <code>__dict__</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6198">#6198</a></li>
<li>Fix an abort inside a <code>#[pyclass]</code>'s GC traversal when
the traversal is stopped early. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6206">#6206</a></li>
<li>Fix reference cycles through the <code>__dict__</code> of a
<code>#[pyclass(dict)]</code> never being collected. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6206">#6206</a></li>
<li>Fix building on GraalPy 3.13. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6208">#6208</a></li>
<li>Fix reference count leak of references to <code>#[pyclass]</code>
type objects held by their instances on instance deallocation. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6224">#6224</a></li>
<li>Fix FFI definitions <code>PyByteArray_GET_SIZE</code>,
<code>PyList_GET_SIZE</code>, and <code>PySet_GET_SIZE</code> to use an
atomic load for free-threaded Python. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6230">#6230</a></li>
<li>Fix a memory leak on Python 3.11 and 3.12 where creating an instance
of a <code>#[pyclass(dict)]</code> class leaked one empty dict per
instance. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6234">#6234</a></li>
<li>Fix <code>experimental-inspect</code> type stubs to emit the
arguments of many magic methods as positional-only to match runtime
behavior, rather than positional-or-keyword. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6239">#6239</a></li>
<li>Fix <code>experimental-inspect</code> type stubs to emit the Python
name rather than the Rust name for <code>#[pyfunction(name =
&quot;...&quot;)]</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6254">#6254</a></li>
<li>Fix <code>experimental-inspect</code> generating invalid internal
JSON when <code>#[pymodule]</code> members are gated by
<code>#[cfg]</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6255">#6255</a></li>
<li>Fix <code>__inplace_concat__</code> and
<code>__inplace_repeat__</code> overriding <code>__concat__</code> and
<code>__repeat__</code> when both were defined. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6260">#6260</a></li>
<li>Fix conversion of out-of-range <code>time::Duration</code> values to
return <code>OverflowError</code> instead of panicking. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6266">#6266</a></li>
<li>Fix <code>experimental-inspect</code> type stubs padding blank lines
inside indented docstrings. <a
href="https://redirect.github.com/PyO3/pyo3/pull/6270">#6270</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a70d17f898"><code>a70d17f</code></a>
release: 0.29.2</li>
<li><a
href="bd00e11864"><code>bd00e11</code></a>
fix backports.zoneinfo for uv install</li>
<li><a
href="7cbd144fd9"><code>7cbd144</code></a>
fix double-decref in PyPy in instance dealloc (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6294">#6294</a>)</li>
<li><a
href="e57fb6f5ac"><code>e57fb6f</code></a>
fix missing trailing nul on Python 3.9 <code>#[pyclass]</code>
docstrings (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6296">#6296</a>)</li>
<li><a
href="d83693c7d7"><code>d83693c</code></a>
fix refcount leak in <code>initialize_tp_dict</code> (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6297">#6297</a>)</li>
<li><a
href="48ebbd86a6"><code>48ebbd8</code></a>
fix: skip libpython rpath args on Windows and Cygwin (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6284">#6284</a>)</li>
<li><a
href="bd73377d4f"><code>bd73377</code></a>
fix: pass class to classmethod magic methods (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6283">#6283</a>)</li>
<li><a
href="af8d149449"><code>af8d149</code></a>
Restore pyo3-introspection license files (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6289">#6289</a>)</li>
<li><a
href="0120c59140"><code>0120c59</code></a>
unblock CI via a uv constraint (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6295">#6295</a>)</li>
<li><a
href="3161efcb22"><code>3161efc</code></a>
build(deps): bump CodSpeedHQ/action from 4 to 5.0.1 (<a
href="https://redirect.github.com/pyo3/pyo3/issues/6286">#6286</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pyo3/pyo3/compare/v0.29.0...v0.29.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `aws-config` from 1.9.0 to 1.10.1
<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.13.0 to 1.14.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 `unidiff` from 0.4.0 to 0.4.1
<details>
<summary>Commits</summary>
<ul>
<li><a
href="61776005a3"><code>6177600</code></a>
Bump version to 0.4.1</li>
<li><a
href="e079b35791"><code>e079b35</code></a>
Fix hunk body lines being misparsed as file headers (<a
href="https://redirect.github.com/messense/unidiff-rs/issues/10">#10</a>)</li>
<li>See full diff in <a
href="https://github.com/messense/unidiff-rs/compare/v0.4.0...v0.4.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `aho-corasick` from 1.1.4 to 1.1.5
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5178060ce7"><code>5178060</code></a>
1.1.5</li>
<li><a
href="b68c8d507a"><code>b68c8d5</code></a>
api: check for overflow in <code>Match::offset</code> too</li>
<li><a
href="c82178696b"><code>c821786</code></a>
build(deps): bump actions/checkout in the actions group (<a
href="https://redirect.github.com/BurntSushi/aho-corasick/issues/171">#171</a>)</li>
<li><a
href="8209bb9d61"><code>8209bb9</code></a>
Hash-pin all actions, drop persisted credentials (<a
href="https://redirect.github.com/BurntSushi/aho-corasick/issues/170">#170</a>)</li>
<li><a
href="0f3f5da9bd"><code>0f3f5da</code></a>
api: document a couple panicking preconditions</li>
<li><a
href="e88e1fce8f"><code>e88e1fc</code></a>
benchmarks: bump dependencies</li>
<li><a
href="88e4966516"><code>88e4966</code></a>
ci: use older version of <code>log</code> on pinned build</li>
<li>See full diff in <a
href="https://github.com/BurntSushi/aho-corasick/compare/1.1.4...1.1.5">compare
view</a></li>
</ul>
</details>
<br />

Updates `toml` from 1.1.3+spec-1.1.0 to 1.1.4+spec-1.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="beee9fe5a9"><code>beee9fe</code></a>
chore: Release</li>
<li><a
href="16e2ac1598"><code>16e2ac1</code></a>
docs: Update changelog</li>
<li><a
href="89f55411d4"><code>89f5541</code></a>
fix(toml): preserve datetimes when deserializing Value (<a
href="https://redirect.github.com/toml-rs/toml/issues/1194">#1194</a>)</li>
<li><a
href="534039ccff"><code>534039c</code></a>
fix(serde): Deserialize Value datetimes into typed targets</li>
<li><a
href="6e45cef5d5"><code>6e45cef</code></a>
test(serde): Reproduce Value datetime deserialization error</li>
<li><a
href="4ec099fed5"><code>4ec099f</code></a>
chore: Release</li>
<li><a
href="5a47a5180e"><code>5a47a51</code></a>
docs: Update changelog</li>
<li><a
href="da0911f7e7"><code>da0911f</code></a>
perf(parser): Reduce over allocation by better tokens/byte ratio (<a
href="https://redirect.github.com/toml-rs/toml/issues/1193">#1193</a>)</li>
<li><a
href="26eb1571f2"><code>26eb157</code></a>
perf(parser): Reduce over allocation by better tokens/byte ratio</li>
<li><a
href="ca4c7bf420"><code>ca4c7bf</code></a>
chore(deps): Update Prek to v0.4.11 (<a
href="https://redirect.github.com/toml-rs/toml/issues/1191">#1191</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/toml-rs/toml/compare/toml-v1.1.3...toml-v1.1.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `blake3` from 1.8.5 to 1.8.6
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/BLAKE3-team/BLAKE3/releases">blake3's
releases</a>.</em></p>
<blockquote>
<h2>1.8.6</h2>
<p>version 1.8.6</p>
<p>Changes since 1.8.5:</p>
<ul>
<li><code>update_mmap</code> and <code>update_mmap_rayon</code> (and by
extension <code>b3sum</code>) now
use <code>seek</code> rather than <code>metadata</code> to get the
length of a file/mapping,
and they tolerate <code>mmap</code> failures. That means
<code>b3sum</code> will now memory
map e.g. Linux block devices, which support mapping despite reporting
length 0 in <code>metadata</code>. Hashing <code>NUL</code> files on
Windows also works now,
where previously it was an error unless you used <code>--no-mmap</code>
or <code>&lt;</code>.
This change was originally proposed by <a
href="https://github.com/nabijaczleweli"><code>@​nabijaczleweli</code></a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="77b257eee7"><code>77b257e</code></a>
version 1.8.6</li>
<li><a
href="83b1746e7c"><code>83b1746</code></a>
use <code>seek</code> instead of <code>metadata</code> to establish mmap
length (<a
href="https://redirect.github.com/BLAKE3-team/BLAKE3/issues/570">#570</a>)</li>
<li><a
href="9eac279fd7"><code>9eac279</code></a>
use vswhere to find Visual Studio in CI</li>
<li><a
href="fc3d0e98e8"><code>fc3d0e9</code></a>
Fix path to Visual Studio toolchain in CI</li>
<li><a
href="8aa5145039"><code>8aa5145</code></a>
a few more colons</li>
<li><a
href="6bb977357b"><code>6bb9773</code></a>
use cargo:: build script syntax</li>
<li><a
href="91f7308e12"><code>91f7308</code></a>
fix Mode docs</li>
<li><a
href="f3913d9531"><code>f3913d9</code></a>
typo fixes</li>
<li><a
href="f51e8226a2"><code>f51e822</code></a>
tweak release instructions</li>
<li>See full diff in <a
href="https://github.com/BLAKE3-team/BLAKE3/compare/1.8.5...1.8.6">compare
view</a></li>
</ul>
</details>
<br />

Updates `http` from 1.4.2 to 1.5.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/hyperium/http/releases">http's
releases</a>.</em></p>
<blockquote>
<h2>v1.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(method): add QUERY method by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/http/pull/798">hyperium/http#798</a></li>
<li>fix(uri): allow empty paths in uri::Builder by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/http/pull/853">hyperium/http#853</a></li>
<li>perf(header,uri): faster value validation, URI parse/format, map
inserts by <a
href="https://github.com/geeknoid"><code>@​geeknoid</code></a> in <a
href="https://redirect.github.com/hyperium/http/pull/852">hyperium/http#852</a></li>
<li>fix(uri): enforce max length in PathAndQuery by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/http/pull/856">hyperium/http#856</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/geeknoid"><code>@​geeknoid</code></a>
made their first contribution in <a
href="https://redirect.github.com/hyperium/http/pull/852">hyperium/http#852</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/hyperium/http/compare/v1.4.2...v1.5.0">https://github.com/hyperium/http/compare/v1.4.2...v1.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/hyperium/http/blob/master/CHANGELOG.md">http's
changelog</a>.</em></p>
<blockquote>
<h1>1.5.0 (July 29, 2026)</h1>
<ul>
<li>Add <code>Method::QUERY</code> constant for the new QUERY method
defined in RFC 10008.</li>
<li>Fix <code>uri::Builder::path_and_query()</code> to allow empty
strings to mean no path.</li>
<li>Fix <code>uri::PathAndQuery</code> parsing to enforce URI max
length.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="16fc9a7b84"><code>16fc9a7</code></a>
v1.5.0</li>
<li><a
href="e559023f67"><code>e559023</code></a>
fix(uri): enforce max length in PathAndQuery (<a
href="https://redirect.github.com/hyperium/http/issues/856">#856</a>)</li>
<li><a
href="2178e175c4"><code>2178e17</code></a>
perf(header,uri): faster value validation, URI parse/format, map inserts
(<a
href="https://redirect.github.com/hyperium/http/issues/852">#852</a>)</li>
<li><a
href="03c8cd7fae"><code>03c8cd7</code></a>
fix(uri): allow empty paths in uri::Builder (<a
href="https://redirect.github.com/hyperium/http/issues/853">#853</a>)</li>
<li><a
href="bb8705b25c"><code>bb8705b</code></a>
feat(method): add QUERY method (<a
href="https://redirect.github.com/hyperium/http/issues/798">#798</a>)</li>
<li>See full diff in <a
href="https://github.com/hyperium/http/compare/v1.4.2...v1.5.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `futures` from 0.3.32 to 0.3.33
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/futures-rs/releases">futures's
releases</a>.</em></p>
<blockquote>
<h2>0.3.33</h2>
<ul>
<li>Fix <code>ReadLine</code>'s soundness issue regarding to exception
safety. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3020">#3020</a>)</li>
<li>Fix unsound <code>Send</code> impl for <code>IterPinRef</code> and
<code>Iter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3003">#3003</a>)</li>
<li>Fix stacked borrows violation in <code>compat01as03</code>
implementation. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3012">#3012</a>)</li>
<li>Fix memory leak in <code>FuturesUnordered::IntoIter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3005">#3005</a>)</li>
<li>Add <code>portable-atomic-alloc</code> feature and use it in
<code>FuturesUnordered</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3007">#3007</a>)</li>
<li>Re-export <code>alloc::task::Wake</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3010">#3010</a>)</li>
<li>Update <code>spin</code> to 0.12. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3014">#3014</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/futures-rs/blob/main/CHANGELOG.md">futures's
changelog</a>.</em></p>
<blockquote>
<h1>0.3.33 - 2026-07-18</h1>
<ul>
<li>Fix <code>ReadLine</code>'s soundness issue regarding to exception
safety. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3020">#3020</a>)</li>
<li>Fix unsound <code>Send</code> impl for <code>IterPinRef</code> and
<code>Iter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3003">#3003</a>)</li>
<li>Fix stacked borrows violation in <code>compat01as03</code>
implementation. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3012">#3012</a>)</li>
<li>Fix memory leak in <code>FuturesUnordered::IntoIter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3005">#3005</a>)</li>
<li>Add <code>portable-atomic-alloc</code> feature and use it in
<code>FuturesUnordered</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3007">#3007</a>)</li>
<li>Re-export <code>alloc::task::Wake</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3010">#3010</a>)</li>
<li>Update <code>spin</code> to 0.12. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3014">#3014</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="89cc254cb8"><code>89cc254</code></a>
Release 0.3.33</li>
<li><a
href="cd9f5befe6"><code>cd9f5be</code></a>
ci: Update release workflow</li>
<li><a
href="d79a499c5f"><code>d79a499</code></a>
Resolve rustdoc::broken_intra_doc_links warning</li>
<li><a
href="95bbcf83be"><code>95bbcf8</code></a>
Resolve rustdoc ambiguous link error</li>
<li><a
href="303c1658dc"><code>303c165</code></a>
Resolve rustdoc::redundant_explicit_links warning</li>
<li><a
href="f34e3f5b9d"><code>f34e3f5</code></a>
ci: Cleanup</li>
<li><a
href="66591a2427"><code>66591a2</code></a>
Enable Miri for more tests</li>
<li><a
href="ab1072fec1"><code>ab1072f</code></a>
Simplify target_has_atomic cfg in utility crates</li>
<li><a
href="cf5d23b68b"><code>cf5d23b</code></a>
Fix unsound compat01as03 implementation (fixes <a
href="https://redirect.github.com/rust-lang/futures-rs/issues/2514">#2514</a>)
(<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3012">#3012</a>)</li>
<li><a
href="8ae794faef"><code>8ae794f</code></a>
Add portable-atomic-alloc feature and use it in FuturesUnordered (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3007">#3007</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33">compare
view</a></li>
</ul>
</details>
<br />

Updates `futures-util` from 0.3.32 to 0.3.33
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/futures-rs/releases">futures-util's
releases</a>.</em></p>
<blockquote>
<h2>0.3.33</h2>
<ul>
<li>Fix <code>ReadLine</code>'s soundness issue regarding to exception
safety. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3020">#3020</a>)</li>
<li>Fix unsound <code>Send</code> impl for <code>IterPinRef</code> and
<code>Iter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3003">#3003</a>)</li>
<li>Fix stacked borrows violation in <code>compat01as03</code>
implementation. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3012">#3012</a>)</li>
<li>Fix memory leak in <code>FuturesUnordered::IntoIter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3005">#3005</a>)</li>
<li>Add <code>portable-atomic-alloc</code> feature and use it in
<code>FuturesUnordered</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3007">#3007</a>)</li>
<li>Re-export <code>alloc::task::Wake</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3010">#3010</a>)</li>
<li>Update <code>spin</code> to 0.12. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3014">#3014</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/futures-rs/blob/main/CHANGELOG.md">futures-util's
changelog</a>.</em></p>
<blockquote>
<h1>0.3.33 - 2026-07-18</h1>
<ul>
<li>Fix <code>ReadLine</code>'s soundness issue regarding to exception
safety. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3020">#3020</a>)</li>
<li>Fix unsound <code>Send</code> impl for <code>IterPinRef</code> and
<code>Iter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3003">#3003</a>)</li>
<li>Fix stacked borrows violation in <code>compat01as03</code>
implementation. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3012">#3012</a>)</li>
<li>Fix memory leak in <code>FuturesUnordered::IntoIter</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3005">#3005</a>)</li>
<li>Add <code>portable-atomic-alloc</code> feature and use it in
<code>FuturesUnordered</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3007">#3007</a>)</li>
<li>Re-export <code>alloc::task::Wake</code>. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3010">#3010</a>)</li>
<li>Update <code>spin</code> to 0.12. (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3014">#3014</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="89cc254cb8"><code>89cc254</code></a>
Release 0.3.33</li>
<li><a
href="cd9f5befe6"><code>cd9f5be</code></a>
ci: Update release workflow</li>
<li><a
href="d79a499c5f"><code>d79a499</code></a>
Resolve rustdoc::broken_intra_doc_links warning</li>
<li><a
href="95bbcf83be"><code>95bbcf8</code></a>
Resolve rustdoc ambiguous link error</li>
<li><a
href="303c1658dc"><code>303c165</code></a>
Resolve rustdoc::redundant_explicit_links warning</li>
<li><a
href="f34e3f5b9d"><code>f34e3f5</code></a>
ci: Cleanup</li>
<li><a
href="66591a2427"><code>66591a2</code></a>
Enable Miri for more tests</li>
<li><a
href="ab1072fec1"><code>ab1072f</code></a>
Simplify target_has_atomic cfg in utility crates</li>
<li><a
href="cf5d23b68b"><code>cf5d23b</code></a>
Fix unsound compat01as03 implementation (fixes <a
href="https://redirect.github.com/rust-lang/futures-rs/issues/2514">#2514</a>)
(<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3012">#3012</a>)</li>
<li><a
href="8ae794faef"><code>8ae794f</code></a>
Add portable-atomic-alloc feature and use it in FuturesUnordered (<a
href="https://redirect.github.com/rust-lang/futures-rs/issues/3007">#3007</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/futures-rs/compare/0.3.32...0.3.33">compare
view</a></li>
</ul>
</details>
<br />

Updates `hyper` from 1.10.1 to 1.11.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/hyperium/hyper/releases">hyper's
releases</a>.</em></p>
<blockquote>
<h2>v1.11.0</h2>
<h2>Features</h2>
<ul>
<li><strong>rt:</strong> add
<code>ReadBufCursor::initialized_unfilled()</code> method (<a
href="https://redirect.github.com/hyperium/hyper/issues/4115">#4115</a>)
(<a
href="ccc1e850dc">ccc1e850</a>)</li>
</ul>
<h2>Bug Fixes</h2>
<ul>
<li><strong>http1:</strong>
<ul>
<li>discard content-length header when received before transfer-encoding
(<a
href="https://redirect.github.com/hyperium/hyper/issues/4124">#4124</a>)
(<a
href="540fff9180">540fff91</a>,
closes <a
href="https://redirect.github.com/hyperium/hyper/issues/4123">#4123</a>)</li>
<li>use append for repeat trailer values in encoder (<a
href="https://redirect.github.com/hyperium/hyper/issues/4118">#4118</a>)
(<a
href="de1483d7db">de1483d7</a>)</li>
<li>allow up to max_headers trailers (<a
href="https://redirect.github.com/hyperium/hyper/issues/4108">#4108</a>)
(<a
href="f584091ac0">f584091a</a>)</li>
<li>use append for repeat trailers (<a
href="https://redirect.github.com/hyperium/hyper/issues/4107">#4107</a>)
(<a
href="876effe10f">876effe1</a>)</li>
<li>flush buffered data before shutdown (<a
href="https://redirect.github.com/hyperium/hyper/issues/4018">#4018</a>)
(<a
href="72046cc72e">72046cc7</a>,
closes <a
href="https://redirect.github.com/hyperium/hyper/issues/4022">#4022</a>)</li>
<li>more strictly enforce max_buf_size when parsing (<a
href="https://redirect.github.com/hyperium/hyper/issues/4093">#4093</a>)
(<a
href="90ede30747">90ede307</a>,
closes <a
href="https://redirect.github.com/hyperium/hyper/issues/4081">#4081</a>)</li>
</ul>
</li>
<li><strong>http2:</strong> avoid buffering <code>Upgraded</code> writes
without send capacity (<a
href="https://redirect.github.com/hyperium/hyper/issues/4102">#4102</a>)
(<a
href="aecf5abfbc">aecf5abf</a>)</li>
</ul>
<h2>All PRs</h2>
<ul>
<li>removing <code>cast_lossless</code> lint by <a
href="https://github.com/josetorrs"><code>@​josetorrs</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4087">hyperium/hyper#4087</a></li>
<li>Fix the borrow_as_ptr lint by <a
href="https://github.com/xd009642"><code>@​xd009642</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4082">hyperium/hyper#4082</a></li>
<li>removing <code>empty_structs_with_brackets</code> lint by <a
href="https://github.com/josetorrs"><code>@​josetorrs</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4088">hyperium/hyper#4088</a></li>
<li>style(proto): removing <code>explicit_iter_loop</code> lint by <a
href="https://github.com/josetorrs"><code>@​josetorrs</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4089">hyperium/hyper#4089</a></li>
<li>Fix the ptr_as_ptr lint by <a
href="https://github.com/nakaryo716"><code>@​nakaryo716</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4091">hyperium/hyper#4091</a></li>
<li>style(lib): remove <code>manual_assert_eq</code> lint by <a
href="https://github.com/josetorrs"><code>@​josetorrs</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4090">hyperium/hyper#4090</a></li>
<li>style(http2): allow an instance of large_enum_variant, deny
otherwise by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4092">hyperium/hyper#4092</a></li>
<li>Removing 'undocumented_unsafe_blocks' lint allowance by <a
href="https://github.com/Lori-Shu"><code>@​Lori-Shu</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4083">hyperium/hyper#4083</a></li>
<li>style(http2): use an enum instead of bool in
strip_connection_headers() by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4094">hyperium/hyper#4094</a></li>
<li>fix(http1): more strictly enforce max_buf_size when parsing by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4093">hyperium/hyper#4093</a></li>
<li>h1 servers can shutdown connections with pending buffered data on
filled sockets by <a
href="https://github.com/deven96"><code>@​deven96</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4018">hyperium/hyper#4018</a></li>
<li>refactor(http1): remove ref_option lint by <a
href="https://github.com/nakaryo716"><code>@​nakaryo716</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4101">hyperium/hyper#4101</a></li>
<li>style(lib): remove <code>unnecessary_semicolon</code> lint by <a
href="https://github.com/josetorrs"><code>@​josetorrs</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4103">hyperium/hyper#4103</a></li>
<li>style(lib): remove <code>uninlined_format_args</code> lint by <a
href="https://github.com/josetorrs"><code>@​josetorrs</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4104">hyperium/hyper#4104</a></li>
<li>style(lib): remove <code>semicolon_if_nothing_returned</code> lint
by <a href="https://github.com/MonkieeBoi"><code>@​MonkieeBoi</code></a>
in <a
href="https://redirect.github.com/hyperium/hyper/pull/4106">hyperium/hyper#4106</a></li>
<li>style(lib): remove <code> single_match_else</code> lint by <a
href="https://github.com/yunz-dev"><code>@​yunz-dev</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4105">hyperium/hyper#4105</a></li>
<li>style(lib): remove <code>default_trait_access</code> lint by <a
href="https://github.com/nakaryo716"><code>@​nakaryo716</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4111">hyperium/hyper#4111</a></li>
<li>fix(http1): use append for repeat trailers by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4107">hyperium/hyper#4107</a></li>
<li>fix(http1): allow up to max_headers trailers by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4108">hyperium/hyper#4108</a></li>
<li>fix(http2): avoid buffering <code>Upgraded</code> writes without
send capacity by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4102">hyperium/hyper#4102</a></li>
<li>style(headers): small refactor to remove <code>question_mark</code>
lint by <a
href="https://github.com/josetorrs"><code>@​josetorrs</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4116">hyperium/hyper#4116</a></li>
<li>fix(h1): append duplicate trailer values when encoding (match <a
href="https://redirect.github.com/hyperium/hyper/issues/4107">#4107</a>)
by <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4118">hyperium/hyper#4118</a></li>
<li>style(proto): fix <code>decimal_literal_representation</code> lint
by <a href="https://github.com/josetorrs"><code>@​josetorrs</code></a>
in <a
href="https://redirect.github.com/hyperium/hyper/pull/4117">hyperium/hyper#4117</a></li>
<li>docs(governance): define Advisor role by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4098">hyperium/hyper#4098</a></li>
<li>docs(maintainers): move some collaborators to emeriti by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4125">hyperium/hyper#4125</a></li>
<li>feat: add initialized_unfilled to ReadBufCursor by <a
href="https://github.com/abh1nav10"><code>@​abh1nav10</code></a> in <a
href="https://redirect.github.com/hyperium/hyper/pull/4115">hyperium/hyper#4115</a></li>
<li>fix(http1): discard content-length header when received before
transfer-encoding by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/hyper/pull/4124">hyperium/hyper#4124</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/josetorrs"><code>@​josetorrs</code></a>
made their first contribution in <a
href="https://redirect.github.com/hyperium/hyper/pull/4087">hyperium/hyper#4087</a></li>
<li><a
href="https://github.com/nakaryo716"><code>@​nakaryo716</code></a> made
their first contribution in <a
href="https://redirect.github.com/hyperium/hyper/pull/4091">hyperium/hyper#4091</a></li>
<li><a href="https://github.com/deven96"><code>@​deven96</code></a> made
their first contribution in <a
href="https://redirect.github.com/hyperium/hyper/pull/4018">hyperium/hyper#4018</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/hyperium/hyper/blob/master/CHANGELOG.md">hyper's
changelog</a>.</em></p>
<blockquote>
<h2>v1.11.0 (2026-07-20)</h2>
<h4>Bug Fixes</h4>
<ul>
<li><strong>http1:</strong>
<ul>
<li>discard content-length header when received before transfer-encoding
(<a
href="https://redirect.github.com/hyperium/hyper/issues/4124">#4124</a>)
(<a
href="540fff9180">540fff91</a>,
closes <a
href="https://redirect.github.com/hyperium/hyper/issues/4123">#4123</a>)</li>
<li>use append for repeat trailer values in encoder (<a
href="https://redirect.github.com/hyperium/hyper/issues/4118">#4118</a>)
(<a
href="de1483d7db">de1483d7</a>)</li>
<li>allow up to max_headers trailers (<a
href="https://redirect.github.com/hyperium/hyper/issues/4108">#4108</a>)
(<a
href="f584091ac0">f584091a</a>)</li>
<li>use append for repeat trailers (<a
href="https://redirect.github.com/hyperium/hyper/issues/4107">#4107</a>)
(<a
href="876effe10f">876effe1</a>)</li>
<li>flush buffered data before shutdown (<a
href="https://redirect.github.com/hyperium/hyper/issues/4018">#4018</a>)
(<a
href="72046cc72e">72046cc7</a>,
closes <a
href="https://redirect.github.com/hyperium/hyper/issues/4022">#4022</a>)</li>
<li>more strictly enforce max_buf_size when parsing (<a
href="https://redirect.github.com/hyperium/hyper/issues/4093">#4093</a>)
(<a
href="90ede30747">90ede307</a>,
closes <a
href="https://redirect.github.com/hyperium/hyper/issues/4081">#4081</a>)</li>
</ul>
</li>
<li><strong>http2:</strong> avoid buffering <code>Upgraded</code> writes
without send capacity (<a
href="https://redirect.github.com/hyperium/hyper/issues/4102">#4102</a>)
(<a
href="aecf5abfbc">aecf5abf</a>)</li>
</ul>
<h4>Features</h4>
<ul>
<li><strong>rt:</strong> add
<code>ReadBufCursor::initialized_unfilled()</code> method (<a
href="https://redirect.github.com/hyperium/hyper/issues/4115">#4115</a>)
(<a
href="ccc1e850dc">ccc1e850</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="67ace6484d"><code>67ace64</code></a>
v1.11.0</li>
<li><a
href="540fff9180"><code>540fff9</code></a>
fix(http1): discard content-length header when received before
transfer-encod...</li>
<li><a
href="ccc1e850dc"><code>ccc1e85</code></a>
feat(rt): add <code>ReadBufCursor::initialized_unfilled()</code> method
(<a
href="https://redirect.github.com/hyperium/hyper/issues/4115">#4115</a>)</li>
<li><a
href="0ea8bc2772"><code>0ea8bc2</code></a>
docs(maintainers): move some collaborators to emeriti (<a
href="https://redirect.github.com/hyperium/hyper/issues/4125">#4125</a>)</li>
<li><a
href="2fc06fc772"><code>2fc06fc</code></a>
docs(governance): define Advisor role (<a
href="https://redirect.github.com/hyperium/hyper/issues/4098">#4098</a>)</li>
<li><a
href="e0d14d19a0"><code>e0d14d1</code></a>
style(proto): explicitly allow
<code>decimal_literal_representation</code> lint (<a
href="https://redirect.github.com/hyperium/hyper/issues/4117">#4117</a>)</li>
<li><a
href="de1483d7db"><code>de1483d</code></a>
fix(http1): use append for repeat trailer values in encoder (<a
href="https://redirect.github.com/hyperium/hyper/issues/4118">#4118</a>)</li>
<li><a
href="08c3416279"><code>08c3416</code></a>
style(headers): small refactor to remove question_mark lint (<a
href="https://redirect.github.com/hyperium/hyper/issues/4116">#4116</a>)</li>
<li><a
href="aecf5abfbc"><code>aecf5ab</code></a>
fix(http2): avoid buffering <code>Upgraded</code> writes without send
capacity (<a
href="https://redirect.github.com/hyperium/hyper/issues/4102">#4102</a>)</li>
<li><a
href="f584091ac0"><code>f584091</code></a>
fix(http1): allow up to max_headers trailers (<a
href="https://redirect.github.com/hyperium/hyper/issues/4108">#4108</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/hyperium/hyper/compare/v1.10.1...v1.11.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `bytesize` from 2.4.2 to 2.7.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/bytesize-rs/bytesize/releases">bytesize's
releases</a>.</em></p>
<blockquote>
<h2>bytesize: v2.7.0</h2>
<ul>
<li>Remove no-alloc support because it removed
<code>ByteSize::display()</code> when default features were
disabled.</li>
</ul>
<h2>bytesize: v2.6.0</h2>
<ul>
<li>Add display styles for IEC and SI bit units.</li>
</ul>
<p>(yanked)</p>
<h2>bytesize: v2.5.0</h2>
<ul>
<li>Honor precision when a width is set with formatting args.</li>
<li>Add <code>#[no_alloc]</code> support.</li>
</ul>
<p>(yanked)</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bytesize-rs/bytesize/blob/master/CHANGELOG.md">bytesize's
changelog</a>.</em></p>
<blockquote>
<h2>2.7.0</h2>
<ul>
<li>Remove no-alloc support because it removed
<code>ByteSize::display()</code> when default features were
disabled.</li>
</ul>
<h2>2.6.0</h2>
<ul>
<li>Add display styles for IEC and SI bit units.</li>
</ul>
<h2>2.5.0</h2>
<ul>
<li>Honor precision when a width is set with formatting args.</li>
<li>Add <code>#[no_alloc]</code> support.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>...

_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-08-12 16:35:19 -05:00
Parideboy
3752458022
fix(cache): enforce Anthropic's 1h-before-5m cache_control ordering before forwarding (#2941)
## Description

Anthropic evaluates prompt-cache breakpoints in **one pass over the
whole request** — `tools`, then `system`, then `messages` — and rejects
the request outright when a `ttl='1h'` breakpoint appears after a
5-minute one. A bare `{"type": "ephemeral"}` marker counts as 5 minutes,
so this is easy to trip without any `ttl` field being visibly wrong:

```
API Error: 400 messages.15.content.1.cache_control.ttl: a ttl='1h' cache_control block must not
come after a ttl='5m' cache_control block. Note that blocks are processed in the following order:
tools, system, messages.
```

Headroom rewrites `cache_control` markers in several independent places,
each looking at one section, and nothing checked the invariant that
spans them. The failure mode is a dead turn, not a silent cost
regression.

Two paths can leave the forwarded body illegal today:

1. **Replayed 1h marker in a 5m request.** Claude Code picks its TTL
lane per request, not per session: the main loop asks for 1h and sends
the `extended-cache-ttl` beta header, while a side question (`/btw` in
the report) goes out in the 5m lane with bare markers and no beta
header. Headroom replays part of the previous turn's forwarded bytes
into `messages` to keep the prefix stable, and those bytes still carry
`ttl: "1h"`. `tools`/`system` at 5m, `messages` at 1h — 400.
2. **Tools breakpoint downgraded.** `inject_tool_search_deferral`
re-places the *last* marker it stripped, so a bare marker on a later
deferred tool overwrites a `ttl='1h'` one. The tools prefix goes
upstream at 5m while message breakpoints are still 1h — 400. Reported
separately as #2767.

The rule spans `tools`/`system`/`messages`, so no individual transform
is in a position to check it. The fix is a guard at the last seam before
the body goes on the wire, plus the one Python/Rust divergence that
manufactures the violation upstream of it.

Closes #2939

## 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/helpers.py`: new `enforce_cache_control_ttl_order`,
plus `cache_control_ttl_lane` / `cache_control_ttl_lanes` /
`walk_cache_control`. The walk visits markers in Anthropic's documented
order and matches the traversal `count_cache_breakpoints` already
performs, so the two cannot disagree about what counts as a breakpoint.
TTL ranking is ported verbatim from the Rust `TtlOrderingWalk::observe`:
absent or `"5m"` is short, `"1h"` is long, any other value is left alone
rather than guessed at. Two repairs:
- **Lane containment** — when the client sent no 1h marker of its own,
strip `ttl` from any 1h marker that leaked in. That request never sent
the `extended-cache-ttl` beta header, so it could not have written a 1h
entry anyway; nothing is lost. Other marker fields (`scope`, …) are
preserved.
- **Ordering** — when the client did ask for 1h, promote every 5m marker
preceding the last 1h one. Demoting would also make the request legal
but would discard 1h caching the client is explicitly paying for, which
is the regression #2375 / #2382 / #2651 were filed to stop. A violation
seen on the way out means headroom downgraded or introduced a marker, so
promoting restores what the client's own (legal) request asked for at
that position.
- Copy-on-write: a legal body is returned by identity, so the hot path
pays only a walk over at most a handful of markers. Kill switch
`HEADROOM_CACHE_CONTROL_TTL_GUARD=0`, matching the
`HEADROOM_TOOL_SEARCH=0` convention.
- `headroom/proxy/handlers/anthropic.py`:
- Capture the client's TTL lane from the inbound snapshot, before any
transform runs. This cannot be inferred from the session or from config
— the lane is a per-request property of Claude Code, which is the whole
reason the `/btw` case exists.
- Call the guard immediately before `log_cache_breakpoints`, i.e. after
every transform, the tool sort, the deferral, CCR injection and the
pipeline extensions. Mark the body mutated when a repair fires, and log
a WARNING carrying the repair kind, the counts and the offending
sections — the diagnostic the next report of this class will need.
- `_sort_tools_deterministically` now skips the sort when any tool
carries `cache_control`, logging `event=tool_sort_skipped
reason=marker_present`. A breakpoint on a tool means "cache through
here", so reordering changes what is inside the cached prefix and can
move a 1h-marked tool behind a 5m-marked one. The Rust proxy already
refuses for exactly this reason (`any_tool_has_cache_control` in
`crates/headroom-proxy/src/compression/live_zone_anthropic.rs:651`); the
Python path never got the same guard. Putting the check in
`_sort_tools_deterministically` rather than `_tools_for_forwarding`
covers all call sites including the batch path.
- `tests/test_cache_control_ttl_order.py`: new, 24 cases.

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

The new tests validate against an independent reimplementation of
Anthropic's rule rather than against the guard's own walk, so a bug in
the walk cannot make the assertions pass. Coverage: lane classification
(bare marker is 5m, unknown TTLs are `other`); containment of a replayed
1h marker including preservation of non-`ttl` fields; promotion across
`tools`→`messages`, `system`→`messages` and within `messages`; markers
nested in `tool_result` sub-blocks; only markers before the *last* 1h
one are rewritten, so a legal 1h-then-5m ordering is left alone; legal
bodies returned by `is` identity; unknown `ttl` untouched; kill switch;
the tool sort skipping on marked tools and still sorting unmarked ones,
with one test pinning that the sort *would* have created a violation
without the guard; and an end-to-end regression running
`inject_tool_search_deferral` then the guard on the #2767 shape.

### Test Output

```text
$ pytest tests/test_cache_control_ttl_order.py -q
24 passed in 0.77s

$ pytest tests/test_cache_ttl_preserved.py tests/test_cache_control_move_bust.py \
         tests/test_cache_breakpoint_diagnostics.py tests/test_issue_746_tool_search.py -q
86 passed in 1.36s

$ pytest tests/test_cache/ tests/test_proxy/ -q
16 failed, 489 passed, 2 skipped in 91.41s

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

$ ruff format --check headroom/ tests/test_cache_control_ttl_order.py
1 file would be reformatted, 520 files already formatted

$ mypy --python-version 3.13 headroom --ignore-missing-imports
Found 12 errors in 3 files (checked 517 source files)
```

The three non-green results above are all pre-existing on a clean
`upstream/main` in this environment, verified by stashing the changes
and re-running:

- The 16 failures are all in
`tests/test_cache/test_client_integration.py` and are a Windows
temp-path problem in this sandbox (`OSError: [WinError 123] ...
'\\C:\\Users\\...\\Temp'`), not a code failure. They fail identically
with the branch stashed.
- `ruff format --check` flags `headroom/testing/README.md`, a docs code
block untouched by this PR.
- The mypy errors are in `headroom/memory/mcp_server.py` and
`headroom/release_version.py`; none are in the files this PR changes.
`--python-version 3.13` is needed locally because the pinned
`python_version = "3.10"` makes mypy reject the installed numpy stubs
before it checks anything.

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.11, headroom at this branch's
head, run against the real `headroom.proxy` forwarding helpers. No
Anthropic API key is available in this environment, so Anthropic's
validator is reimplemented locally from its documented rule and its own
error string; the request bodies are produced by the real code path
(`_sort_tools_deterministically` then `inject_tool_search_deferral` then
the guard), not hand-written.
- Exact command / steps: build two request shapes — (A) a 5m-lane
request whose `messages` carries a replayed `ttl:"1h"` marker, the
`/btw` case; (B) 13 tools with markers on two deferred tools plus 1h
message breakpoints, the #2767 case — push each through the forwarding
helpers twice, once with `HEADROOM_CACHE_CONTROL_TTL_GUARD=0` and once
with the guard at its default, and validate the resulting body.
- Observed result: both scenarios are rejected with the issue's exact
400 when the guard is off, and both are legal with it on. Scenario A is
repaired by lane containment (the leaked 1h ttl is stripped), scenario B
by promotion (the downgraded tools breakpoint goes back to 1h). Full
output:

```text
########## Scenario A: /btw side question replays a 1h marker into a 5m request ##########

===== BEFORE (HEADROOM_CACHE_CONTROL_TTL_GUARD=0) =====
  tools.0                      5m
  system.0                     5m
  messages.1.content.0         1h
  RESULT: API Error: 400 messages.1.content.0.cache_control.ttl: a ttl='1h' cache_control block
  must not come after a ttl='5m' cache_control block. Note that blocks are processed in the
  following order: tools, system, messages.

===== AFTER (default) =====
WARNING event=cache_control_ttl_order request_id=repro-2939 repair=lane_containment demoted=1
leaked_from_section=messages; the client sent no 1h marker, so a replayed 1h breakpoint would
have been rejected upstream
  tools.0                      5m
  system.0                     5m
  messages.1.content.0         5m
  RESULT: 200 OK (request satisfies the ordering rule)

########## Scenario B: tool-search deferral downgrades the tools breakpoint ##########

===== BEFORE (HEADROOM_CACHE_CONTROL_TTL_GUARD=0) =====
INFO event=tool_sort_skipped reason=marker_present tool_count=13 marked=2
  tools.1                      5m
  messages.1.content.0         1h
  RESULT: API Error: 400 messages.1.content.0.cache_control.ttl: a ttl='1h' cache_control block
  must not come after a ttl='5m' cache_control block. Note that blocks are processed in the
  following order: tools, system, messages.

===== AFTER (default) =====
INFO event=tool_sort_skipped reason=marker_present tool_count=13 marked=2
WARNING event=cache_control_ttl_order request_id=repro-2939 repair=promote_to_1h promoted=1
first_short_section=tools first_long_section=messages; a 5m breakpoint preceded a 1h one, which
Anthropic rejects outright
  tools.1                      1h
  messages.1.content.0         1h
  RESULT: 200 OK (request satisfies the ordering rule)
```

- Not tested: no live call to `api.anthropic.com` — no credentials in
this environment — so the 400/200 above come from a local
reimplementation of the rule, not from the API itself. The reporter's
original `/btw` flow was not reproduced end to end through `headroom
wrap claude`. Cache-hit-rate impact of promoting a 5m marker to 1h was
not measured against real traffic; the reasoning for promoting over
demoting is argued above, not benchmarked. Nothing on the Rust side was
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
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

Documentation was not updated: the new env var is a kill switch for an
internal correctness guard with no user-facing behaviour when things are
working, matching how `HEADROOM_KOMPRESS_BACKGROUND_WARM` is handled.
Happy to add a line to the env-var reference if maintainers prefer.

**One trade-off worth a maintainer's eye.** `affinity_tools` feeds a
`segment_fingerprint` used for prefix-tracker affinity. Skipping the
sort makes that fingerprint depend on the client's tool order. Clients
that mark tools must already keep a stable order for their own prefix
cache to work, so this should be safe, but it is stated rather than
assumed.

**Deliberately out of scope, flagged rather than dropped:**

- The sibling hole in `inject_tool_search_deferral`: when
`resident_has_cache_control` is already true at 5m, a dropped 1h marker
is discarded outright. That is a cost regression rather than a 400, and
it sits in the same handful of lines that the open PR #2771 rewrites, so
touching it here would conflict. Better raised on #2767.
- `TtlOrderingWalk` in `crates/headroom-core/src/cache_control.rs` is
instantiated separately per field list, so it only ever sees violations
*within* `messages`, `system` or `tools` — never across them — and it
only warns. Its module doc justifies warn-only with "Anthropic itself
accepts both orderings (just with potentially-suboptimal cache
eviction)". #2939 and #2767 both show that premise is now stale.
Changing Rust behaviour is a separate blast radius.
- `cold_prefix._cache_control_ttls` never scans `tools[]`, so a client
whose only 1h marker rides on `tools` is read as 300s. Separate bug,
separate PR.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-08-12 16:32:12 -05:00
Tejas Chopra
e540d64feb
fix(wrap): serialize shared proxy startup (#2946)
## Description
Serialize concurrent `headroom wrap` startup so separate agents can
safely share one local proxy.

## Type of Change
- [x] Bug fix

## Changes Made
- Added a per-port cross-process startup lock.
- Re-checks proxy health/configuration after waiting for the lock.
- Preserves reference-counted cleanup and Copilot subscription
isolation.
- Preserves `_ensure_proxy`'s introspectable keyword signature on the
locking wrapper.

## Testing
- 88 wrap/persistent/detach tests pass locally.
- Focused lock-boundary tests pass.
- Signature inspection exposes `learn` and the existing keyword-only
options.
- Ruff, format, compile, and diff checks pass.
- Remaining CI failures are unrelated existing shard or
external-download failures.

## Real Behavior Proof
Two wraps that start during proxy cold start now serialize: the second
waits, observes the first healthy listener, and reuses it instead of
spawning a competing listener.

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

---------

Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
Co-authored-by: Jerrett Davis <mxjerrett@gmail.com>
2026-08-12 12:50:09 -07:00
JD Davis
039cd2431a
fix(proxy): preserve merged session and quarantine contracts (#2943)
## Description

Forward-fixes two integration contracts exposed while auditing the large
August 12 merge batch on `main`.

The Codex WebSocket request-ID hardening correctly gave every emitted
dashboard/feed row a unique ID, but it also changed the human-readable
`PERF` prefix from the stable WebSocket session ID to that per-emission
ID. That broke operator correlation and the contract documented by the
original merge. This PR separates storage identity from log correlation:
rows remain unique, while `PERF` lines remain grouped under the session
ID.

The same audit found two tokenizer quarantine tests still modeling the
pre-time-cap behavior. Timeout debt no longer activates quarantine after
its deadline expires. The tests now establish a live deadline and
therefore continue to exercise the intended fail-open branch without
weakening the production guard.

## Changes

- Add an optional `RequestOutcome.perf_request_id` correlation field,
defaulting to the existing `request_id` behavior for all current
callers.
- Set that field to the stable session ID for both per-turn and residual
Codex WebSocket emissions.
- Strengthen the lifecycle regression test to prove the unique feed-row
ID is not used as the `PERF` prefix.
- Update tokenizer quarantine tests to model an active, time-capped
quarantine.

This is a forward fix; it does not revert the unique WebSocket request
IDs or the time-capped quarantine behavior.

## Merge-batch audit context

- Audited 48 squash merges from
`12149f7446` through
`941c25d31e`.
- Reviewed repeated-touch hotspots in the OpenAI/Anthropic handlers and
proxy server.
- Confirmed the batch has no deleted or renamed paths and no
revert/supersession commits.
- Reproduced the deterministic failures on the merged head and confirmed
the corresponding pre-batch tokenizer tests passed; the new WebSocket
assertion was introduced by the batch and exposed the dropped
correlation contract.

## Validation

- `ruff check` — passed
- `ruff format --check` — passed
- `mypy headroom` — passed (517 source files)
- Focused proxy/outcome/tokenizer suite — 99 passed
- CI-equivalent Python suite in four `pytest-split` shards — 10,797
passed, 603 skipped
- `cargo fmt --all -- --check` — passed
- `cargo clippy --workspace -- -D warnings` — passed
- New beta-sticky integration suite — 9 passed
- Simulator-backed proxy E2E — 7 passed
- Real local Headroom process against a real local FastAPI upstream:
  - `/livez` healthy
- `/readyz` ready (Kompress correctly optional/degraded in minimal
passthrough mode)
  - OpenAI `/v1/chat/completions` round trip passed
  - Anthropic `/v1/messages` round trip passed
- Local `act` exercised the push workflow's change-detection job and
entered the real lint job. `act` required local no-op cache/setup shims
because its runner action post-hooks/pip pairing are incompatible with
this macOS Docker environment; the actual Ruff/mypy validations above
were run natively and passed.

## Risk

Low. The new field is optional and preserves existing behavior by
default. Only Codex WebSocket outcome emitters opt into a separate log
correlation ID; dashboard/feed row identity remains unchanged.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 12:27:05 -05:00
Tejas Chopra
941c25d31e
fix(observability): aggregate tool savings in OTEL (#2936)
OTEL proxy savings now aggregate compression and tool-schema deferral
savings. Adds a separate tool-schema component counter, forwards the
value through PrometheusMetrics, updates documentation, and adds focused
regression coverage. 16 focused tests passed; Ruff, compileall, and diff
checks are clean.

Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-11 22:54:55 -07:00
Abhay Singh
d7cf981093
fix(image): decouple routing types from trained_router so importing the compressor doesn't import torch (#2513) (#2537)
## Description

Addresses the secondary crash in #2513. `image/compressor.py` did `from
.trained_router import Technique` at module scope, and
`image/onnx_router.py` imported `ImageSignals` / `RouteDecision` /
`Technique` from `trained_router` the same way. `trained_router` imports
`torch` and `transformers` at module scope, so merely importing the
image compressor eagerly pulled in the heavy ML stack.

On Python 3.13+ that eager import crashed the first image request with:

```
AttributeError: module 'torch' has no attribute 'compiler'
```

because `transformers` touches `torch.compiler` during its own import,
before torch has finished initializing inside the proxy process.

## Fix

Move the dependency-free routing types — the `Technique` enum and the
`ImageSignals` / `RouteDecision` dataclasses — into a new
`headroom/image/image_types.py` (no torch / transformers / onnx
imports). `compressor.py` and `onnx_router.py` import them from there;
`trained_router` re-exports them so existing `from .trained_router
import Technique` imports keep working. Importing the compressor or the
ONNX router no longer imports `trained_router`, so the
torch/transformers stack is only loaded when the PyTorch router is
actually used (lazily, inside `_get_router`).

## 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/image/image_types.py` (new): `Technique`, `ImageSignals`,
`RouteDecision` — pure enum/dataclasses.
- `headroom/image/trained_router.py`: import and re-export those types
from `image_types` (drop the local definitions and the now-unused
`dataclass` / `Enum` imports).
- `headroom/image/compressor.py`, `headroom/image/onnx_router.py`:
import the routing types from `image_types`.
- `tests/test_image_types_torch_decoupling.py` (new): subprocess checks
that importing the compressor / ONNX router does not import
`trained_router`, that `image_types` imports no torch, and that all
three re-export paths resolve to the same objects.

## 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_image_types_torch_decoupling.py -q
4 passed

# with the compressor import reverted, the "does not import trained_router"
# check fails

$ uvx ruff@0.15.17 check headroom/image/image_types.py headroom/image/trained_router.py headroom/image/compressor.py headroom/image/onnx_router.py tests/test_image_types_torch_decoupling.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/image/image_types.py headroom/image/trained_router.py headroom/image/compressor.py headroom/image/onnx_router.py
Success: no issues found in 4 source files
```


`tests/test_image_compression.py::TestOnnxRouter::test_full_classify_with_image`
fails identically on clean `main` in this environment (it needs real
ONNX model weights that aren't available locally); it is unrelated to
this change.

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, project venv (`uv sync --extra
proxy`, torch not installed here), `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`, pytest in the venv.
- Exact command / steps: in a fresh subprocess, imported
`headroom.image.compressor` / `headroom.image.onnx_router` /
`headroom.image.image_types` and checked `sys.modules`; also asserted
`headroom.image.Technique`, `trained_router.Technique`, and
`image_types.Technique` are the same object. Then reverted the
compressor import and re-ran.
- Observed result: with the fix, importing the compressor and the ONNX
router leaves `headroom.image.trained_router` out of `sys.modules`,
`image_types` pulls in no `torch`, and all re-export paths are identical
objects; with the fix reverted, importing the compressor pulls
`trained_router` back in (the eager path that triggers the torch
import). Ran against the actual modules.
- Not tested: the Python 3.13 `torch.compiler` crash itself (this
environment is 3.12 without torch); the fix removes the eager import
that causes it.

## 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-08-12 00:22:56 -05:00
Rod Boev
a540eb2c61
fix(codex): route alpha search through the Codex backend (#2538)
## Description

Codex GPT-5.6 standalone web search currently falls through Headroom's
generic passthrough path. Under ChatGPT OAuth that sends `POST
/v1/alpha/search` to `https://chatgpt.com/v1/alpha/search`, which
redirects to HTML and makes Codex fail to decode the response.

This change adds an explicit standalone Codex search alias so
ChatGPT-authenticated `/v1/alpha/search` requests route through
`https://chatgpt.com/backend-api/codex/alpha/search`, while non-ChatGPT
traffic keeps the existing passthrough behavior. Closes #2525.

## 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 a dedicated `POST /v1/alpha/search` Codex route for
ChatGPT-authenticated traffic
- route that alias through the existing `codex_backend_url()` helper so
the upstream path becomes `/backend-api/codex/alpha/search`
- add focused regression coverage for ChatGPT-auth routing and
non-ChatGPT passthrough preservation

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_provider_proxy_routes.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.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_provider_proxy_routes.py -q
23 passed, 1 warning in 15.77s

uv run ruff check headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.py
All checks passed!

uv run ruff format headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.py --check
2 files already formatted

git diff --check
(no output)
```

## Real Behavior Proof

- Environment: focused Headroom worktree with proxy route regression
tests
- Exact command / steps: run the issue-shaped inline Python reproduction
from `bodies/headroom-issue-2525.json`, then run the focused
preservation and matrix pytest rows for ChatGPT-auth and non-ChatGPT
auth
- Observed result: the base repro printed `FAIL issue2525 codex alpha
search -> observed_url=None fallback=[('/v1/alpha/search',
'https://chatgpt.com')]
body={"base_url":"https://chatgpt.com","provider":""}`, while the head
repro printed `PASS issue2525 codex alpha search ->
https://chatgpt.com/backend-api/codex/alpha/search?query=weather`; the
non-ChatGPT preservation row passed `1 passed, 22 deselected`, and the
auth matrix row passed `1 passed, 22 deselected`
- Not tested: live ChatGPT OAuth account on this host

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

## Screenshots (if applicable)

N/A - proxy routing change only.

## Additional Notes

- `CHANGELOG.md` stays untouched because Headroom's release automation
generates it from conventional commits.
- The fix is scoped to standalone Codex search. It does not change
`/v1/responses`, image routes, or generic OpenAI passthrough semantics.
- Proof artifact:
`D:\Repos\.claude\pr-sweep\headroom-PR-TARGET-2525-PROOF.md`

Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-12 00:22:20 -05:00
Abhay Singh
4e5a67a342
fix(memory): skip <system-reminder> blocks when building the retrieval query (#2195) (#2541)
## Description

Addresses #2195 Finding 1. `extract_memory_query_sources` (the memory
retrieval query builder) was extended to harvest text blocks from
Anthropic list-shaped user turns — the standard Claude Code shape — but
it joins **every** text block in the turn. Claude Code appends
`<system-reminder>` harness blocks to essentially every user turn, so
those get concatenated into the embedding input alongside the real
question.

Per the reporter's measurements (`all-MiniLM-L6-v2`): the clean question
scored top cosine **0.748** against a stored memory; the same question
wrapped in harness boilerplate scored **0.232**. The default
`memory_min_similarity` floor is **0.3**, so the diluted query falls
under the floor and **nothing is retrieved** — memory silently no-ops
for Claude Code clients. The reporter explicitly warned that a naive
"concatenate all text blocks" harvest would still retrieve nothing,
which is exactly the current behavior.

## Fix

Filter out text blocks whose text starts with `<system-reminder` when
building `user_text`, so the retrieval query keys on the substantive
question and the embedding isn't diluted by harness boilerplate. A turn
that is only a system-reminder yields no `user_text` (as before). All
other harvesting (tool_result blocks, OpenAI string content,
assistant/tool context) is unchanged.

Note: the reporter also asked to expose `memory_min_similarity` as an
env var / CLI flag (it lives on `ProxyConfig` with no surface today).
That is a sensible companion but is a separate config-plumbing change; I
kept this PR focused on the retrieval-query bug so it stays easy to
review, and I'm happy to follow up with the env/CLI surface.

## Type of Change

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

## Changes Made

- `headroom/proxy/memory_query_policy.py`: in
`extract_memory_query_sources`, skip `<system-reminder>` text blocks
when assembling the user query from a list-shaped Anthropic user turn.
- `tests/test_memory_query_policy.py`: regressions that a
system-reminder block is excluded (real question kept) and that a
reminder-only turn yields no user text.

## 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_memory_query_policy.py -q
7 passed

# with the fix reverted, the two new tests fail: the system-reminder text is
# concatenated into user_text (the diluted-query behavior)

$ uvx ruff@0.15.17 check headroom/proxy/memory_query_policy.py tests/test_memory_query_policy.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/memory_query_policy.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, project venv (`uv sync --extra
proxy`), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`, pytest in the venv.
- Exact command / steps: called `extract_memory_query_sources` with a
Claude Code-shaped user turn (real question text block + an appended
`<system-reminder>` text block), and with a reminder-only turn; then
reverted the source and re-ran.
- Observed result: with the fix `user_text` is exactly `"how do I add
caching to the auth handler?"` (no `system-reminder` substring), and a
reminder-only turn yields `""`; with the fix reverted `user_text`
includes the full `<system-reminder>...</system-reminder>` text (the
diluted embedding input). Ran against the actual module.
- Not tested: an end-to-end embedding + backend retrieval against a live
memory DB measuring the cosine recovery (the dilution figures are the
reporter's; this change removes the boilerplate from the query text that
produces them).

## 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-08-12 00:22:08 -05:00
gglucass
0805e8e410
fix(providers/openai): bound tiktoken vocab loads with the guarded loader (#2554)
## Description

`headroom/providers/openai.py::_get_encoding` calls
`tiktoken.get_encoding` directly. tiktoken downloads missing
vocabularies via `requests.get` with **no timeout**, so on a network
that blackholes the vocab CDN (corporate firewall, SSL-intercepting
proxy), whichever thread first counts tokens for an OpenAI model — proxy
startup included — blocks indefinitely.

This is the provider-path hole left by #956: the tokenizer registry
already routes through a bounded loader
(`headroom/tokenizers/tiktoken_counter.py`, worker-thread load +
`HEADROOM_TIKTOKEN_LOAD_TIMEOUT_SECONDS`, default 10s) and falls back to
estimation, but the OpenAI provider path never got the same treatment.

Observed in production (Headroom Desktop fleet, Sentry): a proxy that
never finished booting, with a faulthandler dump wedged in
`tiktoken/registry.py` `get_encoding` on the main thread, reached from
the `headroom` CLI entrypoint via click. The desktop app now also
pre-seeds a persistent `TIKTOKEN_CACHE_DIR`, but the unbounded load
affects every deployment of the proxy, so it should be fixed here too.

Follow-up to #956.

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

- `_get_encoding` now routes through the bounded `load_encoding` from
`headroom.tokenizers.tiktoken_counter` instead of calling
`tiktoken.get_encoding` directly, so a stalled vocab download raises
`TiktokenLoadError` after the timeout instead of hanging the calling
thread.
- `OpenAIProvider.get_token_counter` catches `TiktokenLoadError` and
falls back to `EstimatingTokenCounter`, cached per model so later
requests never re-block on the same failed download — mirroring
`TokenizerRegistry._create_tiktoken`.
- `TIKTOKEN_AVAILABLE` uses `importlib.util.find_spec` (the module-level
`import tiktoken` became unused; same pattern as `LITELLM_AVAILABLE`).
- Two regression tests (`TestGuardedEncodingLoad`) covering the
bounded-raise path and the cached estimation fallback.

## 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
$ uv run --frozen --extra dev pytest tests/test_providers/ tests/test_tokenizers/
======================= 117 passed, 4 warnings in 27.95s =======================

$ uvx ruff check headroom/providers/openai.py tests/test_providers/test_openai.py
All checks passed!
$ uvx ruff format --check headroom/providers/openai.py tests/test_providers/test_openai.py
2 files already formatted

$ uv run --frozen --extra dev mypy headroom/providers/openai.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: macOS (arm64), Python 3.12, uv-managed venv, branch off
`upstream/main` (a6d4921e).
- Exact command / steps: the stalled download is simulated in
`TestGuardedEncodingLoad` by monkeypatching
`tiktoken_counter.load_encoding` to raise `TiktokenLoadError`;
`OpenAITokenCounter("gpt-4o")` then raises the bounded error, and
`OpenAIProvider.get_token_counter("gpt-4o")` returns a working
`EstimatingTokenCounter` and reuses the same instance on the second
call.
- Observed result: tests pass (see output above); with an empty tiktoken
on-disk cache, the encoding load path is identical to the one in the
production faulthandler dump.
- Not tested: an end-to-end run against a genuinely blackholed vocab CDN
(needs a firewalled network); the timeout mechanism itself is #956's
code, already covered by its own 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 commented my code, particularly in hard-to-understand areas
- [x] I made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I 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 updated CHANGELOG.md if applicable (N/A — release-please
generates it from the PR title; changelog-guard forbids manual edits)

## Screenshots (if applicable)

N/A — no UI change.

## Additional Notes

The estimation fallback is sticky for the process lifetime (per-model
cache + the loader's fail-fast set from #956): a network that recovers
mid-session keeps estimation until restart. That matches the registry's
existing behavior, so no new divergence is introduced.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-12 00:20:16 -05:00
TenderDeve
7f24d695ee
fix(doctor): flag ollama launch claude proxy bypass instead of misdirecting (#2566)
## Description

Addresses the diagnostic half of #2199.

`ollama launch claude` sets `ANTHROPIC_BASE_URL=http://127.0.0.1:11434`
in the launched Claude Code child. That process env outranks the `env`
block a persistent Headroom install writes to `~/.claude/settings.json`,
so Claude Code talks to Ollama and never reaches the proxy — 0% savings,
nothing on the dashboard, no error.

`headroom doctor`'s routing classifier made it worse: seeing a loopback
`:11434`, it reported `routed to port 11434, but doctor probed port
8787` and hinted `re-run with: headroom doctor --port 11434` — sending
the user to re-probe Ollama's endpoint as if it were their proxy.

Closes #2199

## Type of Change

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

## Changes Made

- `_classify_routing_url` now recognizes Ollama's fixed default port:
the check names the `ollama launch claude` bypass and points at the
proxy-chaining path instead of the red-herring `--port 11434` re-probe
hint.
- Fires for both the shell-env and settings-file routing checks that
share the classifier.

## 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_cli_doctor.py -q
1 failed, 68 passed in 3.05s
# The lone failure is test_remote_control_warning_exits_1 — pre-existing and
# unrelated: it reads real ~/.headroom stats and fails on a clean tree with or
# without this change (does not exist / does not pass on main either).

$ pytest tests/test_cli_doctor.py -k ollama -q
1 passed, 68 deselected

$ ruff check headroom/cli/doctor.py tests/test_cli_doctor.py
All checks passed!

$ mypy headroom
Success: no issues found in 509 source files
```

## Real Behavior Proof

- Environment: local checkout, Python venv, `pytest`/`ruff`/`mypy` as
above.
- Exact command / steps: `tests/test_cli_doctor.py` pins the
Ollama-aware message + hint emitted by `_classify_routing_url` for a
loopback `:11434` routing URL.
- Observed result: doctor now reports the `ollama launch claude` bypass
and the proxy-chaining fix instead of `re-run with: headroom doctor
--port 11434`.
- Not tested: no live `ollama launch claude` run; verified at the
classifier boundary.

## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

Scope: this is only the *diagnostic* ask (#2199 part 3, requested as the
minimum). The launcher-composition and model-aware routing halves depend
on #1279's direction and are left for a maintainer steer. Documentation
item is N/A (diagnostic message change, no docs surface). The
pre-existing `test_remote_control_warning_exits_1` failure is unrelated
as noted above.
2026-08-12 00:19:44 -05:00
Parideboy
ce8ce8313f
fix(ccr): resolve <<ccr:...>> markers inline when no retrieve-tool path exists (#2512)
## Description

Fixes #2509. CCR marker resolution today depends entirely on the model
calling `headroom_retrieve` back a tool-call round-trip. Callers with no
such round-trip (e.g. Headroom running as a LiteLLM guardrail/proxy hop,
per the issue's repro) never get an offered path to redeem a marker, so
raw `<<ccr:HASH,type,size>>` text leaks straight to the agent.

This adds an explicit, opt-in fallback: `--ccr-inline-resolve` /
`HEADROOM_CCR_INLINE_RESOLVE`. When set, the proxy resolves markers
directly from the compression store on the response path instead of
waiting for a tool call. Off by default, guessing "this caller can't use
tools" is fragile, so operators opt in explicitly for guardrail/proxy
deployments.

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change
- [ ] Documentation update

## Changes Made

- `headroom/ccr/marker_resolution.py` (new): `resolve_markers_in_text` /
`resolve_markers_in_response` regex-match `<<ccr:HASH,...>>`, look up
the hash in `CompressionStore`, splice the original content back in. A
miss (expired/evicted hash) leaves the marker in place with the miss
reason appended, since there's no tool-call round-trip to report it back
to the model.
- `headroom/proxy/models.py`: `ProxyConfig.ccr_resolve_markers_inline:
bool = False`.
- `headroom/cli/proxy.py`: `--ccr-inline-resolve` flag /
`HEADROOM_CCR_INLINE_RESOLVE` env, wired into `ProxyConfig`.
- `headroom/proxy/handlers/anthropic.py`,
`headroom/proxy/handlers/openai.py`:
call `resolve_markers_in_response` on the finalized response JSON, right
after existing CCR tool-call handling, at all three non-streaming
response sites (Anthropic Messages, OpenAI Chat Completions backend
path, OpenAI Responses API). Streaming responses are out of scope for
this PR, tracked as follow-up, noted in the module docstring's scope.

## Testing

- [x] Added new tests
- [x] All tests pass locally

```
$ python -m pytest tests/test_ccr_marker_resolution.py -q
============================= test session starts =============================
collected 6 items

tests\test_ccr_marker_resolution.py ......                               [100%]

============================== 6 passed in 0.45s ==============================

$ python -m pytest tests/test_ccr.py tests/test_ccr_response_handler.py tests/test_ccr_response_handler_extra.py tests/test_ccr_response_handler_openai_responses.py tests/test_proxy/test_openai_responses_ccr.py tests/test_proxy/test_anthropic_ccr_raise.py -q
======================= 83 passed, 1 warning in 34.50s ========================
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.11, local headroom repo
(`G:\Programmi Aggiuntivi\headroom`)
- Exact command / steps: `python -m pytest
tests/test_ccr_marker_resolution.py tests/test_ccr.py
tests/test_ccr_response_handler.py
tests/test_ccr_response_handler_extra.py
tests/test_ccr_response_handler_openai_responses.py
tests/test_proxy/test_openai_responses_ccr.py
tests/test_proxy/test_anthropic_ccr_raise.py -q`
- Observed result: 89 passed, 0 failed (6 new + 83 existing CCR tests,
no regressions). `ruff check`, `ruff format --check`, and `mypy
--ignore-missing-imports` all clean on every changed/new file.
- Not tested: the actual Docker Compose / LiteLLM guardrail deployment
from the issue's repro steps (no such environment available here);
streaming response paths (out of scope, see Changes Made).

## Review Readiness

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 00:18:43 -05:00
gglucass
f326fe26c5
docs(transforms): correct stale unit-result-cache placeholder comments (#2506)
## Description

Two comments still describe the unit-result cache as an unbuilt
placeholder, but the cache has since been implemented (the OpenAI
Responses handler's `_openai_responses_unit_result_cache`: SHA-256 unit
key, bounded LRU, in-flight dedup, and `cache_hit` marking via
`replace(router_result, cache_hit=True)`).

- `transforms/compression_units.py` — the `UNIT_REASON_CATEGORIES` block
said `cache_hit` was "placeholder; not currently wired into the unit
path — see follow-up". No code path produces a `cache_hit` *reason
category* today; reuse is caller-level and surfaced on
`RouterCompressionResult.cache_hit`. The comment now says exactly that.
- `transforms/content_router.py` — the
`RouterCompressionResult.cache_hit` docstring claimed the flag is "False
in practice — placeholder for the cache-wire-up follow-up". It is set in
practice by the Responses handler on cached-unit reuse; `compress()`
itself still never touches the router-internal two-tier cache (only
`apply()` does). Docstring updated to match.

Found while scoping a "wire the unit result cache" contribution that
turned out to already exist; these notes were what made it look missing.

Closes #N/A (no tracking issue; comments-only correction)

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

- Rewrote the `cache_hit` entry in the `UNIT_REASON_CATEGORIES` comment
block (`headroom/transforms/compression_units.py`) to state that cached
unit reuse is caller-level and never produces this reason category.
- Rewrote the `cache_hit` attribute docstring on
`RouterCompressionResult` (`headroom/transforms/content_router.py`) to
describe where the flag is actually set.

## Testing

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

### Test Output

```text
$ uv run --frozen --extra dev pytest tests/test_transforms/test_content_router.py
============================= 59 passed in 11.31s ==============================

$ uvx ruff check .
All checks passed!

$ uvx ruff format --check .
1331 files already formatted

$ uv run --frozen --extra dev mypy headroom/transforms/compression_units.py headroom/transforms/content_router.py
Success: no issues found in 2 source files
```

## Real Behavior Proof

- Environment: macOS 15 (arm64), Python 3.12, uv-managed venv from
`uv.lock`
- Exact command / steps: see Test Output above; comments/docstrings
only, no executable statements changed
- Observed result: targeted tests, ruff, and mypy pass; `git diff`
touches only comment/docstring lines
- Not tested: full test suite and full-repo mypy (pytest and mypy were
scoped to the touched modules — no executable code changed)

## 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
- [ ] 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A — no user-visible surface.

## Additional Notes

"New tests added" is unchecked because the change is comments/docstrings
only; there is no behavior to test. If a tracking issue for the original
cache-wire-up follow-up exists, happy to reference it in place of the
N/A above.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 00:16:35 -05:00
suvenkatesh97
565c6076ef
docs: add guide for using Headroom with OpenCode + DeepSeek (#2497)
Documents how to configure Headroom proxy with DeepSeek for OpenCode
users.

- No `headroom wrap` needed -- manual config avoids Claude/GPT model
overwrites
- Covers proxy setup, OpenCode provider config, output shaping, model
comparison, and troubleshooting
- Includes current DeepSeek V4 Pro and V4 Flash models, with deprecated
alias guidance for `deepseek-chat` / `deepseek-reasoner`
- Adds the guide to the published docs tree and navigation
- All API keys use placeholders

## Description

Adds documentation (`docs/content/docs/opencode-deepseek.mdx`) showing
OpenCode users how to route through Headroom proxy with DeepSeek.
Addresses the gap described in #78 (OpenCode integration docs) and
provides the manual config workaround documented in #1679 (wrap broken
with Go CLI).

## Type of Change

- [x] Documentation update

## Changes Made

- New docs page: `docs/content/docs/opencode-deepseek.mdx` --
step-by-step setup guide covering proxy launch, OpenCode provider
config, output shaping, model comparison, thinking-mode notes, and
troubleshooting
- Updated `docs/content/docs/meta.json` so the guide appears under
Integrations

## Testing

- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
- [x] `git diff --check`
- [x] `npm ci` in `docs/`
- [ ] `npm run types:check` in `docs/` -- pre-existing failure in
generated docs plumbing

### Test Output

```text
git diff --check: passed (no trailing whitespace, no conflict markers)
npm ci: installed in docs/ successfully
npm run types:check: pre-existing failure in lib/source.ts(2,22) -- not introduced by this PR
```

## Real Behavior Proof

- Environment: Ubuntu, Python 3.13, headroom-ai 0.32.1, OpenCode (Go
CLI)
- Exact command / steps: Ran `headroom proxy --port 8787
--openai-api-url https://api.deepseek.com/v1`, configured OpenCode with
`@ai-sdk/openai-compatible` pointing at `http://127.0.0.1:8787/v1`, sent
chat completions through the proxy, verified compression on dashboard.
- Observed result: proxy routes chat completions to DeepSeek, input
compression active (SmartCrusher), output shaping (level 2) reduces
response tokens by ~11%. Dashboard at http://127.0.0.1:8787/stats shows
compressed requests and token savings (1075994 tokens saved across 675
requests).
- Not tested: did not verify `docs/` static site build with `npm run
build` in this environment (CI types:check failure exists on main before
this PR).

## Review Readiness

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

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-12 00:16:13 -05:00
inix
e24a7e66b9
fix(proxy/metrics): cap client-supplied model label cardinality (#2480)
## Description

`record_request` counts every request under a `model` label the client
controls (it comes straight from `body.get("model")`), and nothing caps
how many distinct values it keeps. `requests_by_model` and
`_cache_requests_by_model` grow one entry per distinct model, forever,
and the exported `headroom_requests_by_model` series grows with them.
There is no TTL, so only a process restart clears it. A buggy or hostile
client sending junk model strings can bloat the scrape without bound.

It also contradicts `docs/observability.md`, which says no client can
drive label cardinality unbounded and lists `model` as bounded. On the
Python path it was not.

Follow-up to #618, which capped the sibling `inbound_requests_by_path`.
The surrogate-encodability half of the same client `model` input is a
separate PR (#2463). No filed issue for this one, it surfaces as scrape
bloat or memory growth rather than a nameable symptom.

## 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 `MAX_DISTINCT_MODELS` (1024) to `headroom/telemetry/context.py`,
next to the existing `MAX_DISTINCT_STACKS`.
- In `record_request`, a model past the cap goes into an `"other"`
bucket instead of a fresh key, the same discipline the doc already
documents for `tier`. One shared decision bounds both model dicts. The
check is a membership test, so it never materializes a `defaultdict`
key. It warns once when the cap first trips, so the now-quiet failure
mode stays visible.
- Reconciled `docs/observability.md` with a Python-side `model` bullet.
The blanket invariant is true again.
- Left the `provider` dicts alone. `provider` is a handler literal or
config value, not client input, so it is already bounded.

## Testing

- [x] Unit tests pass (`pytest`), metrics/telemetry/savings/outcome
subset (see notes)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`), scoped to the touched
source files (see notes)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ python -m ruff check headroom/telemetry/context.py headroom/proxy/prometheus_metrics.py tests/test_observability_metrics.py
All checks passed!

$ python -m mypy headroom/telemetry/context.py headroom/proxy/prometheus_metrics.py
Success: no issues found in 2 source files

$ python -m pytest tests/test_observability_metrics.py tests/test_telemetry_context.py \
    tests/test_request_outcome.py tests/test_persistent_metrics.py -q
72 passed in 189.45s
# plus savings/stats/cache/dashboard batch: 79 passed
# the two new tests:
tests/test_observability_metrics.py::test_prometheus_metrics_caps_model_cardinality PASSED
tests/test_observability_metrics.py::test_prometheus_metrics_model_cardinality_warns_once PASSED
```

## Real Behavior Proof

- Environment: macOS, Python 3.13, repo venv (ruff 0.15.17, mypy
1.19.1), run against this branch's source.
- Exact command / steps: a simulated hostile client loops 1074 distinct
`model` values (the 1024 cap plus 50) through `record_request`, then
calls `export()` and counts the `headroom_requests_by_model{...}` lines.
Ran the same script against `upstream/main` and against this branch.
- Observed result: baseline grew to 1074 model series (unbounded); the
fix holds it at 1025 (1024 real models plus `"other"`), `requests_total`
stays 1074 and `sum(requests_by_model)` stays 1074 so no request is
lost, and exactly one warning fires. The internal
`_cache_requests_by_model` dict tracks the same 1025 bound.
- Not tested: the surrogate-encodability crash on the same input
(separate PR #2463), multi-process scrape aggregation, and the full
macOS suite (6 files hang on this box, pre-existing and unrelated), so
the Linux CI shards are the real gate 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
- [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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A, backend metrics change.

## Additional Notes

Two commits, kept atomic: the cap plus its doc reconcile, then the test.

`mypy headroom` in full is impractical to run cold on this box (the
stdlib stub build times out), so the check above is scoped to the two
touched source files, where it is clean. CI's Linux shards run the full
`mypy headroom` with a warm cache.

Same for the suite: 6 files hang natively on macOS here (pre-existing,
unrelated to this change), so I ran the metrics, telemetry, savings, and
outcome blast radius (153 tests green) and left the full run to CI.

Pushed with `--no-verify` because the pre-push `ci-precheck` needs a
bare `python` on PATH that this box lacks (it only has `python3`), an
environment gap rather than a code one. This is a Python-only change and
CI runs the full precheck clean.

---------

Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-12 00:15:49 -05:00
Rod Boev
798139608c
fix(claude): stop forcing tool search on Foundry (#2477)
## Description

Foundry sessions launched through `headroom wrap claude` currently
receive Headroom's generic `ENABLE_TOOL_SEARCH=true` default when the
user did not choose a tool-search mode. That can push Claude Code into a
deferred-tool request shape that Azure Foundry rejects with `API Error:
400 ... Some tools are not available`. This narrows the default-only
path so Foundry sessions stop forcing deferred-tool mode when the user
did not ask for it, while explicit overrides and the existing
non-Foundry custom-host behavior stay unchanged.

Closes #2464

## 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 a Foundry-specific default for the no-override tool-search branch
- preserve explicit `--tool-search` values and pre-set
`ENABLE_TOOL_SEARCH` values exactly
- keep the generic non-Foundry default as `true`
- add focused helper-level regression coverage for Foundry defaulting
and adjacent negative space

## Testing

- [x] Focused unit tests pass (`uv run pytest
tests/test_cli/test_wrap_claude.py
tests/test_cli/test_wrap_claude_vertex_proxy_env.py
tests/test_issue_746_tool_search.py -q`)
- [x] Edited-file linting passes (`uv run ruff check
headroom/cli/wrap.py headroom/providers/claude/runtime.py
tests/test_cli/test_wrap_claude.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
Command: uv run pytest tests/test_cli/test_wrap_claude.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_issue_746_tool_search.py -q
61 passed

Command: uv run ruff check headroom/cli/wrap.py headroom/providers/claude/runtime.py tests/test_cli/test_wrap_claude.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows, Python via `uv`, Foundry mode modeled through
the wrap helper inputs
- Exact command / steps: run the focused helper regression and
edited-file lint commands above
- Observed result: `61 passed`; `All checks passed!`; Foundry mode
without an override writes `ENABLE_TOOL_SEARCH=false`, while explicit
overrides, existing values, blank handling, and the non-Foundry default
remain covered
- Not tested: live Azure Foundry tenant run

## 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] 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 untouched because Headroom generates release notes
from conventional commits. Live Foundry proof is intentionally left to a
real tenant run; the code and focused tests only claim the launch-mode
change inside Headroom.
2026-08-12 00:15:07 -05:00
Abhay Singh
08466f3cae
fix(providers/anthropic): don't crash token estimation on null tool_calls (#2472)
## Description

`AnthropicTokenCounter._count_message_estimated` (the
tiktoken-approximation fallback used when no Anthropic client is
available) counted OpenAI-format tool calls like this:

```python
if "tool_calls" in message:
    for tool_call in message.get("tool_calls", []):
        if isinstance(tool_call, dict):
            func = tool_call.get("function", {})
            ...
```

The `if "tool_calls" in message` check only tests key presence, not the
value. OpenAI SDKs routinely include `"tool_calls": null` on an
assistant message with no tool calls, so `message.get("tool_calls", [])`
returned `None` (the default only applies when the key is absent) and
`for tool_call in None` raised `TypeError: 'NoneType' object is not
iterable`. That crashes token estimation for the entire request whenever
such a message is present. `tool_call.get("function", {})` had the same
gap for a `"function": null`.

## Fix

Iterate `message.get("tool_calls") or []` so a null or absent value
becomes an empty list, and read `function` with `or {}` for the same
reason. Valid tool calls are counted exactly as before.

## 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/anthropic.py`: value-guard `tool_calls` and
`function` in `_count_message_estimated`.
- `tests/test_providers/test_anthropic.py`: regression counting a
message list that includes `tool_calls: null` and a tool call with
`function: null`.

## 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_providers/test_anthropic.py -q
17 passed

# with the fix reverted, the new test fails with
# TypeError: 'NoneType' object is not iterable

$ uvx ruff@0.15.17 check headroom/providers/anthropic.py tests/test_providers/test_anthropic.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/providers/anthropic.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, project venv (`uv sync --extra
proxy`), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`, pytest in the venv.
- Exact command / steps: built a real
`AnthropicTokenCounter('claude-3-5-sonnet-20241022')` and called
`count_messages` / `_count_message_estimated` with an assistant message
carrying `tool_calls: null` and one carrying `function: null`, plus a
valid tool call; then reverted `anthropic.py` and re-ran.
- Observed result: with the fix the null shapes count without error and
a valid tool call still adds its name/arguments tokens (5 -> 11 on the
sample); with the fix reverted the `tool_calls: null` message raises
`TypeError: 'NoneType' object is not iterable`. Ran against the actual
module.
- Not tested: a live request from an SDK that emits `tool_calls: null`,
end to end.

## 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-08-12 00:13:14 -05:00
Abhay Singh
e00c6ff81c
fix(memory): don't crash inline memory extraction on a non-object <memory> block (#2470)
## Description

`parse_response_with_memory` extracts an inline `<memory>...</memory>`
block from a model response and parses its JSON:

```python
try:
    data = json.loads(memory_json)
    memories = data.get("memories", [])
except json.JSONDecodeError as e:
    logger.warning(f"Failed to parse memory JSON: {e}")
```

The block content is fully model-controlled. `json.loads` succeeds on
any valid JSON, including a non-object such as a bare array
(`<memory>["x"]</memory>`), a string, or a number. `data.get("memories",
[])` then raises `AttributeError: 'list' object has no attribute 'get'`,
which the `except json.JSONDecodeError` does not catch, so the inline
memory path crashes on output a model can realistically produce.

## Fix

Guard the parsed value: read `memories` only when the block is a JSON
object, and accept it only when it is a list (logging and ignoring
otherwise). Malformed JSON is still handled by the existing decode
guard, and a well-formed object is unchanged. This mirrors the
non-object hardening already applied to the batch JSONL path.

## 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/memory/inline_extractor.py`: only read `memories` from a
dict-typed parsed block, and only when the field is a list; log and
ignore other shapes.
- `tests/test_memory_wrapper.py`: regression covering a non-object
memory block, a non-list `memories` field, malformed JSON, and a
well-formed block.

## 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_memory_wrapper.py -q
6 passed

# with the fix reverted, the new test fails with
# AttributeError: 'list' object has no attribute 'get'

$ uvx ruff@0.15.17 check headroom/memory/inline_extractor.py tests/test_memory_wrapper.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/memory/inline_extractor.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, project venv (`uv sync --extra
proxy`), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`, pytest in the venv.
- Exact command / steps: called the real `parse_response_with_memory`
with a `<memory>["x"]</memory>` block, a `{"memories": "nope"}` block, a
malformed block, and a valid block; then reverted `inline_extractor.py`
and re-ran.
- Observed result: with the fix all four return cleanly (empty memories
for the bad shapes, the parsed list for the valid one) and the memory
block is still stripped from the content; with the fix reverted the
non-object block raises `AttributeError: 'list' object has no attribute
'get'`. Ran against the actual module.
- Not tested: a live end-to-end chat where a model emits a non-object
memory block.

## 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-08-12 00:12:50 -05:00
Abhay Singh
fc5c4e239c
fix(install): don't crash the PowerShell installer when $PROFILE is unset (#2469)
## Description

The PowerShell installer (`scripts/install.ps1`) crashes at the very end
on any machine where PowerShell cannot resolve the current user's
profile path.

`Ensure-ProfileBlock` locates the profile with:

```powershell
$profileDir = Split-Path -Parent $PROFILE
```

`$PROFILE` is an empty string when PowerShell cannot compute the profile
path for the current user, which happens for a fresh account with no
Documents folder yet, a service or CI context, or a redirected profile.
`Split-Path -Parent ''` then throws:

```
Split-Path : Cannot bind argument to parameter 'Path' because it is an empty string.
```

Because the script runs under `$ErrorActionPreference = 'Stop'`, that
terminates the whole installer with a non-zero exit, even though it
happens after the `headroom` wrapper and the persistent User PATH entry
were already written. The user sees a scary Split-Path error and assumes
the install failed.

## Fix

Skip the profile convenience block when `$PROFILE` is empty and log why.
`Ensure-PathEntry` already persists the User PATH for new sessions, so
the only thing skipped is auto-refreshing PATH inside the current
profile file, which does not exist in that environment anyway.
Well-behaved environments with a real `$PROFILE` are unchanged.

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

- `scripts/install.ps1`: early-return from `Ensure-ProfileBlock` with an
informational message when `$PROFILE` is null or empty, before the
`Split-Path` call.

## Testing

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

### Test Output

```text
$ python -m pytest tests/test_install/test_native_installers.py -q
1 passed, 1 skipped

# The PowerShell lifecycle test was failing on main before this change and now passes:
$ python -m pytest "tests/test_install/test_native_installers.py::test_powershell_native_installer_supports_persistent_docker_lifecycle" -q
1 passed
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12, Windows PowerShell 5.1, project
venv (`uv sync --extra proxy`), pytest in the venv.
- Exact command / steps: ran `install.ps1` under a temp `USERPROFILE`
with no Documents folder (the same setup the installer test uses).
Confirmed `$PROFILE` resolves to an empty string in that context and
that `Split-Path -Parent $PROFILE` throws there, then re-ran the
installer test with the fix.
- Observed result: before the fix the installer aborted with `Split-Path
: Cannot bind argument to parameter 'Path' because it is an empty
string` and exit code 1 (and the test failed); after the fix the
installer completes, writes the wrapper and PATH entry, logs that it
skipped the profile update, and the test passes. Ran against the actual
script.
- Not tested: a real end-user account whose Documents folder is
redirected to a network share.

## 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-08-12 00:12:30 -05:00