chore: sync canonical upstream - #1
Merged
Merged
Conversation
…law (headroomlabs-ai#1969) (headroomlabs-ai#2120) ## Description `headroom wrap openclaw` installed a non-existent npm spec — the `--plugin-spec` default was `headroom-ai/openclaw`, which npm reads as a GitHub shorthand and fails; the published package is `headroom-openclaw` (see `plugins/openclaw/package.json`). Fix introduces a single `OPENCLAW_NPM_PACKAGE = "headroom-openclaw"` constant (kept in sync with `package.json` and the release env), uses it as the default, and defers writing the `plugins.entries.headroom` config until after a successful install so a hard failure leaves no stale entry. Closes headroomlabs-ai#1969 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/providers/openclaw/wrap.py` + `__init__.py`: canonical `OPENCLAW_NPM_PACKAGE` constant. - `headroom/cli/wrap.py`: use it as `--plugin-spec` default; write config only after successful install. - `tests/test_cli/test_wrap_openclaw.py`: expect `headroom-openclaw`; install-before-config ordering; failed-install-writes-no-config test. ## Testing - [x] Unit tests pass (`pytest tests/test_cli/test_wrap_openclaw.py`) — 29 passed - [x] Linting passes (`ruff check`) ### Test Output ```text 29 passed ruff: All checks passed! ``` ## Real Behavior Proof - Before: `wrap openclaw` → npm "unsupported spec" error; a failed install left a stale config entry. - After: installs `headroom-openclaw`; no config written on failure.
…ched prompts (headroomlabs-ai#2110) (headroomlabs-ai#2119) ## Description `DynamicContentDetector` / `RegexDetector` in `headroom/cache/dynamic_detector.py` (used by the `cache_aligner` transform) misclassified ordinary English words and code identifiers (e.g. `in_pr`) as "dynamic content," extracting them from the system prompt and re-appending a `[Dynamic Context]` tail that grows unboundedly and corrupts the cached prompt over a session. Fix tightens detection to require genuinely-dynamic shapes (timestamps, UUIDs, hashes, numbers-with-units, ISO dates) rather than bare tokens — no hardcoded wordlist — and bounds the tail. `cache_aligner` is off by default, so blast radius is limited, but the detector logic is now correct. Closes headroomlabs-ai#2110 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/cache/dynamic_detector.py`: raise the evidence bar so ordinary words/identifiers aren't extracted; bound the dynamic tail. - `tests/test_cache/test_dynamic_detector.py`: assert false positives (ordinary words/identifiers) are NOT extracted while real dynamic values still are. ## Testing - [x] Unit tests pass (`pytest tests/test_cache/test_dynamic_detector.py`) - [x] Linting passes (`ruff check`) ### Test Output ```text 55 passed, 2 skipped ruff: All checks passed! ``` ## Real Behavior Proof - Before: identifiers like `in_pr` extracted into a growing `[Dynamic Context]` tail, corrupting cached prompts. - After: ordinary tokens stay in place; only genuinely-dynamic values are detected.
…nt) (headroomlabs-ai#2153) ## What One-line type fix: `_EMBEDDER_CACHE` is keyed by a 3-tuple `(backend, model, ollama_base_url)` but was still annotated `dict[tuple[str, str], Embedder]`. ## Why `mypy headroom --ignore-missing-imports` (the CI `lint` job) fails on `main` at `factory.py:187`/`:219` because of this mismatch. Since the lint job runs whole-package mypy, **every open PR is currently failing lint on this bug** — none of them introduced it. This unblocks the lint gate repo-wide. ## Proof `mypy headroom --ignore-missing-imports` → `Success: no issues found in 469 source files`. `ruff check .` + `ruff format --check .` clean. ## Scope Type annotation only; no runtime behavior change. The 3-tuple key itself is pre-existing (the Ollama base_url was already part of the key to avoid cross-server embedder cache collisions).
…2097) ## Description Pip-audit found 6 vulnerabilities in 2 packages in the lockfile: | Package | From | To | Vulns Fixed | |---------|------|----|-------------| | click | 8.3.1 | 8.4.2 | PYSEC-2026-2132 | | pillow | 12.2.0 | 12.3.0 | PYSEC-2026-2253~2257 | Closes #N/A (no issue filed — security workflow failure) ## Type of Change - [x] Bug fix (non-breaking) - [ ] New feature (non-breaking) - [ ] Breaking change - [ ] Documentation update ## Changes Made - `uv.lock`: Upgraded click from 8.3.1 to 8.4.2, pillow from 12.2.0 to 12.3.0 ## Testing - [x] `uv lock --upgrade-package` resolved cleanly - [x] `ruff check headroom/` passes - [x] CLI import verified (click 8.4.2 loads correctly) ``` $ uv run python -c "import click; print(click.__version__)" click: 8.4.2 $ uv tree --depth=1 | grep -E "click|pillow" click v8.4.2 pillow v12.3.0 (extra: all) pillow v12.3.0 (extra: image) ``` ## Real Behavior Proof - Environment: headroom main (upstream/main c365c7f), uv-managed Python 3.12 - Exact command / steps: `uv lock --upgrade-package "pillow>=12.3.0" --upgrade-package "click>=8.3.3"` on upstream/main; then `git diff --stat`, `uv run python -c "import click; print(click.__version__)"` - Observed result: uv.lock only changed; click 8.3.1 → 8.4.2, pillow 12.2.0 → 12.3.0; ruff passes; no test regressions - Not tested: E2E proxy (dependency-only change — no code changes) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Co-authored-by: lennney <lennney@users.noreply.github.com>
…labs-ai#1815) ## Description Compress Hermes Studio scoped coding-agent passthrough requests in the generic OpenAI passthrough handler. Hermes can route scoped Claude Code and Codex traffic through Headroom while preserving its own proxy paths; this PR keeps Hermes responsible for scoped proxy authentication/provider adaptation while still applying Headroom compression to supported chat payloads before forwarding. The compression remains narrow-scoped: - Only chat messages with `user` or `assistant` roles are compressed. - Tool, function, reasoning, and system items are preserved byte-stable. - Non-dict items in the Responses `input` array are preserved and spliced back. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Detect `/api/codex-proxy/.../v1/responses` paths and compress supported Responses `input` chat items before forwarding. - Detect `/api/claude-code-proxy/.../v1/messages` paths and compress supported Anthropic `messages` payloads before forwarding. - Preserve bypass, malformed payload, missing-model, tool/function, reasoning/system, and non-dict passthrough behavior. - Add regression coverage in `tests/test_hermes_passthrough_compression.py`. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_hermes_passthrough_compression.py -v test_codex_proxy_preserves_tool_and_function_items PASSED test_codex_proxy_preserves_nondict_items PASSED test_codex_proxy_bypass_header_skips_compression PASSED test_codex_proxy_malformed_input_preserved PASSED test_codex_proxy_compression_applies_to_chat_messages PASSED test_claude_proxy_preserves_tool_use_items PASSED test_claude_proxy_bypass_header_skips_compression PASSED test_claude_proxy_no_model_forwarded_unchanged PASSED test_claude_proxy_compression_applies_to_chat_messages PASSED test_non_hermes_routes_not_affected PASSED ``` ## Real Behavior Proof - Environment: Author-reported local test environment for `headroom/proxy/handlers/openai.py` and `tests/test_hermes_passthrough_compression.py`. - Exact command / steps: `python -m pytest tests/test_hermes_passthrough_compression.py -v`. - Observed result: The 10 Hermes passthrough regression tests passed, covering Codex and Claude scoped proxy routes plus preservation/bypass cases. - Not tested: End-to-end Hermes Studio traffic against a live upstream service is not covered by this PR body evidence. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes Generated with Claude Code. The unchecked checklist items are not required for this narrow proxy-handler test change. --------- Co-authored-by: x1051445024 <你的GitHub注册邮箱> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
…eadroomlabs-ai#2037) (headroomlabs-ai#2127) ## Description `_resolve_openai_upstream_base` ran the `X-Headroom-Base-Url` value through `_normalize_origin`, which strips the path. A custom OpenAI-compatible upstream served from a sub-path, such as `https://host/api/v1`, was routed to the bare origin and returned `proxy_error` (headroomlabs-ai#2037). This re-attaches the path after origin normalization. This is a clean extraction of the path fix from headroomlabs-ai#2047, which bundled it with an unrelated `supports_websockets = true` to `false` default change across init/wrap/codex. headroomlabs-ai#2047 can be closed in favor of this narrower fix. Closes headroomlabs-ai#2037 ## 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`: re-attach the request header path component in `_resolve_openai_upstream_base` after origin normalization. - `tests/test_proxy/test_openai_upstream_header.py`: assert sub-paths are preserved and trailing slashes are normalized. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_proxy/test_openai_upstream_header.py 5 passed $ ruff check headroom/proxy/handlers/openai.py tests/test_proxy/test_openai_upstream_header.py All checks passed! ``` ## Real Behavior Proof - Environment: local proxy header resolution path, custom OpenAI-compatible upstream configured via `X-Headroom-Base-Url`. - Exact command / steps: resolve `X-Headroom-Base-Url: https://gateway.example/api/v1` through `_resolve_openai_upstream_base` / `_resolve_openai_upstream`. - Observed result: before the fix, the upstream resolved to `https://gateway.example` and lost `/api/v1`, causing the proxy to route to the wrong endpoint. After the fix, it resolves to `https://gateway.example/api/v1`; a trailing slash is normalized away. - Not tested: end-to-end request against a live third-party OpenAI-compatible gateway. The regression is covered at the proxy routing helper layer where the path was dropped. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes - Documentation is not updated because this fixes the existing header behavior rather than changing a documented user-facing contract. - Changelog is not updated in this PR; the change is scoped to the regression and test. - `mypy headroom` was not run in the author's workflow.
…rse (headroomlabs-ai#2129) ## Description The Bedrock Converse API hard-rejects any request containing a tool name over 64 characters (`toolConfig.tools.N.member.toolSpec.name`). Claude Code includes every globally-added claude.ai MCP connector tool in every request it sends, even connectors the user hasn't enabled locally. One org-wide connector with a 65-char tool name is enough to fail every single request routed through this backend's Bedrock path, with no way to remove or disable the connector client-side. Direct Bedrock mode (`CLAUDE_CODE_USE_BEDROCK=1`, bypassing this proxy) is unaffected: it hits Bedrock's native Anthropic-compatible endpoint, which has no such length limit. Only the Converse API, which this LiteLLM-backed `bedrock` provider path uses, enforces it. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/backends/litellm.py`: `send_message` and `stream_message` both filter tools with names over 64 characters out of the payload before converting/forwarding, but only for `self.provider == "bedrock"`. Other providers are untouched. - `tests/test_backend_bugs.py`: new `TestBedrockOversizedToolNameFiltering` covering both `send_message` and `stream_message` — an oversized (65-char) name is dropped on `bedrock`, a name at exactly the 64-char boundary is kept, and non-`bedrock` providers forward oversized names unfiltered (the limit is a Bedrock Converse constraint, not a general one). - `CHANGELOG.md`: added a `### Fixed` entry under `Unreleased`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv run pytest tests/test_backend_bugs.py tests/test_backend_anyllm.py -q ============================= test session starts ============================== platform darwin -- Python 3.13.13, pytest-9.0.3, pluggy-1.6.0 collected 57 items tests/test_backend_bugs.py .......................................... [ 73%] tests/test_backend_anyllm.py ............... [100%] ============================== 57 passed in 1.42s ============================== $ uv run ruff check headroom/backends/litellm.py tests/test_backend_bugs.py All checks passed! $ uv run mypy headroom/backends/litellm.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - **Environment:** personal fork deployed as a real proxy (macOS launchd service, `headroom install apply`) with `--backend bedrock --mode token --code-aware --bedrock-profile sso-bedrock`, fronting a live Claude Code session with a globally-added-but-not-locally-enabled claude.ai MCP connector (`TopCounsel`) whose tool name is 65 characters. - **Exact command / steps:** run any Claude Code request through this deployment while the org-wide `TopCounsel` connector is present (it is included in the tool list on every request regardless of local enablement). - **Observed result:** before the fix, every request failed with a LiteLLM `BedrockException`: `1 validation error detected: Value 'mcp__claude_ai_TopCounsel_by_The_L_Suite__complete_authentication' at 'toolConfig.tools.N.member.toolSpec.name' failed to satisfy constraint: Member must have length less than or equal to 64`. After applying the fix (filtering the oversized tool out before the LiteLLM call), the same session proceeds normally with no validation error, confirmed live against this deployment. - **Not tested:** truncating the name instead of dropping it was tried and discarded during investigation — the model echoes the truncated name back in `tool_use` blocks, and Claude Code matches tool calls by the original full name, so truncation breaks routing on the return path. This PR drops the tool entirely rather than truncating, which is why it is not present as an alternative in the diff. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — backend logic change, no UI surface. ## Additional Notes - "I have made corresponding changes to the documentation" is unchecked: no file in `docs/`, `README.md`, or `CONTRIBUTING.md` documents backend-specific tool-list filtering behavior, so there is no existing section to update. - No linked issue number: this was found via independent investigation of a personal deployment (a live Bedrock validation failure), not filed as a `headroomlabs-ai/headroom` issue first. Checked `gh pr list`/`gh issue list` for existing coverage of "Bedrock Converse 64-char tool name" and found none open or merged. - A native Bedrock Anthropic-compatible endpoint backend (avoiding Converse's tool-name limit entirely) would be the more complete long-term fix, but is out of scope for this PR. --------- Co-authored-by: Ingmar Krusch <ingmar.krusch@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
…i#2132) Fixes headroomlabs-ai#2131. ## Description `plugin_config_dir` / `plugin_workspace_dir` rejected `/` and `\` in the plugin name but accepted `.` and `..`. Since the returned path is `<root> / "plugins" / name`, `plugin_config_dir("..")` resolved to the whole config root and `plugin_workspace_dir("..")` to the whole workspace root: savings ledger, memory DB, license cache, logs, and every other plugin's state. That defeated the sandbox the helper was written to enforce. Both callers are folded onto a shared `_validate_plugin_name` that rejects the empty string, both path separators, `.`, `..`, and NUL. Closes headroomlabs-ai#2131 ## 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/paths.py`: added `_validate_plugin_name` and shared it across `plugin_config_dir` and `plugin_workspace_dir`. - `tests/test_paths.py`: expanded invalid-name coverage for `.`, `..`, and NUL and added a sandbox-escape regression test. - `CHANGELOG.md`: noted the path traversal fix. ## 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 tests/test_paths.py -q ........................................................................... [100%] 79 passed in 0.14s $ uv run ruff check headroom/paths.py tests/test_paths.py All checks passed! $ uv run ruff format --check headroom/paths.py tests/test_paths.py 2 files already formatted ``` ## Real Behavior Proof - Environment: macOS 25.4 (Darwin arm64), Python 3.12.13, `uv 0.11.28`, branch `fix/plugin-path-traversal`. - Exact command / steps: set `HEADROOM_CONFIG_DIR=/tmp/hc` and `HEADROOM_WORKSPACE_DIR=/tmp/hw`, then call `plugin_config_dir("..")`, `plugin_config_dir(".")`, and `plugin_config_dir("legit-plugin")`. - Observed result: before the patch, `plugin_config_dir("..")` resolved to `/private/tmp/hc`, escaping the plugin sandbox. After the patch, `plugin_config_dir("..")` and `plugin_config_dir(".")` raise `ValueError`; a normal plugin name resolves under `/private/tmp/hc/plugins/legit-plugin`. - Not tested: Windows behavior and plugin-registry integration. The added check is a pure string validation at the path-helper layer. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes - Documentation is not updated because this is a helper-level sandbox fix rather than a user-facing behavior change; the changelog entry captures it. - `mypy headroom` was not run in the author's workflow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
…eadroomlabs-ai#2134) ## Description `compress(messages, config=my_cfg, protect_recent=0, target_ratio=0.2)` used to write those kwarg values onto the caller's `my_cfg` object — so a shared per-agent `CompressConfig` was silently rewritten every time a call passed a single override. The next call that did NOT override that field then saw the previous request's value instead of the original default. Copy the config once at entry with `dataclasses.replace` before applying kwarg overrides (and before the savings-profile pass, which also mutates in place). Existing behavior for callers that pass **only** kwargs, or **only** a config, is unchanged. Issue headroomlabs-ai#2133 has the root-cause walkthrough. Closes headroomlabs-ai#2133 ## 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/compress.py`: copy the incoming `CompressConfig` once at entry with `dataclasses.replace` before applying kwarg overrides, so the caller's object is no longer mutated. The savings-profile branch already did a defensive `replace(cfg)`; that copy is now hoisted up front so both the kwarg and profile paths share the same guarantee. - `tests/test_compress_api.py`: added `test_kwargs_do_not_mutate_caller_config`, which fails on unpatched `main` and passes on this branch, covering the previously broken kwarg leg. - `CHANGELOG.md`: noted the fix. ## 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 tests/test_compress_api.py -q ................. [100%] 17 passed in 2.88s $ uv run ruff check headroom/compress.py tests/test_compress_api.py All checks passed! $ uv run ruff format --check headroom/compress.py tests/test_compress_api.py 2 files already formatted ``` ## Real Behavior Proof - Environment: macOS 25.4 (Darwin arm64), Python 3.12.13, `uv 0.11.28`, branch `fix/compress-mutates-caller-config`, model `claude-sonnet-4-5-20250929` used for token counting. - Exact command / steps: build `c = CompressConfig(protect_recent=4, target_ratio=0.8)`, call `compress(msgs, model="claude-sonnet-4-5-20250929", config=c, protect_recent=0, target_ratio=0.2)` on a 3000-char user message, then read `c.protect_recent` and `c.target_ratio` back (full snippet run via `uv run python <<'PY' ... PY` — see the code block below). - Observed result: before the patch, `c.protect_recent` became `0` and `c.target_ratio` became `0.2` (caller's config silently rewritten). After the patch, `c.protect_recent` stays `4` and `c.target_ratio` stays `0.8`; caller's config unchanged. `uv run pytest tests/test_compress_api.py` reports 17 passed including the new `test_kwargs_do_not_mutate_caller_config` case. - Not tested: end-to-end proxy path with `savings_profile` set (the pre-fix code already did a defensive `replace(cfg)` on that branch, so the profile leg was safe; this change hoists that copy up front and the added unit test covers the kwarg leg that was broken — I did not spin up the proxy to reconfirm the profile branch end-to-end). No concurrent-caller / threading regression test was added — the fix removes the mutation entirely which sidesteps the race, but there is no explicit multi-thread reproducer. ### Reproducer **Before the patch (unpatched `main`)** ```text before: protect_recent=4, target_ratio=0.8 after : protect_recent=0, target_ratio=0.2 # <-- caller's cfg silently rewritten caller's config MUTATED ``` **After the patch (this branch)** ```text $ uv run python <<'PY' from headroom.compress import compress, CompressConfig c = CompressConfig(protect_recent=4, target_ratio=0.8) print(f"before: protect_recent={c.protect_recent}, target_ratio={c.target_ratio}") msgs = [{"role":"user","content":"x"*3000}] compress(msgs, model="claude-sonnet-4-5-20250929", config=c, protect_recent=0, target_ratio=0.2) print(f"after : protect_recent={c.protect_recent}, target_ratio={c.target_ratio}") print("caller's config", "unchanged" if (c.protect_recent, c.target_ratio) == (4, 0.8) else "MUTATED") PY before: protect_recent=4, target_ratio=0.8 after : protect_recent=4, target_ratio=0.8 caller's config unchanged ``` ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation <!-- N/A: no user-facing doc covers the CompressConfig / kwargs contract; see Additional Notes --> - [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 - **Documentation checklist item** — left unchecked as N/A. The behavior being fixed is internal to `headroom.compress.compress()`; the mutation contract of `CompressConfig` + kwargs is not covered in any user-facing doc (`wiki/compression.md`, `wiki/text-compression.md`, `wiki/image-compression.md`, and `docs/content/docs/shared-context.mdx` document a different / higher-level API surface). The `CHANGELOG.md` entry is the appropriate place for this fix. - **`mypy headroom` checklist item** — left unchecked because I did not run it in this workflow; the change is a two-line refactor within a well-typed function and no signatures moved. - The prior body's `## Summary`, `## Test plan`, and `## Real behavior proof` sections were reorganized into the six template-required headings so the PR-governance check passes. All technical content (root-cause, before/after reproducer, and test output) is preserved above; no code changes were made in this update. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
…apacity (headroomlabs-ai#2136) Fixes headroomlabs-ai#2135. ## Summary `SharedContext.put` ran `_evict_if_needed` before writing, and the eviction loop only checked `len(self._entries) >= self._max_entries`. When a caller updated a key that was already cached at capacity, the put would not have grown the map — but the loop still dropped the oldest unrelated entry. Same defect class as fixed for `SemanticCache` in headroomlabs-ai#2094: the eviction path must know the incoming key so an update is not treated as an insert. This mirrors that fix over to `SharedContext`. Threads the incoming key through `_evict_if_needed` and skips capacity eviction when it names an entry that already exists. Expired-entry cleanup still runs unconditionally. Issue headroomlabs-ai#2135 has the reproduction and impact writeup. ## Test plan - [x] `uv run pytest tests/test_shared_context.py` — 16 passed (added `test_updating_existing_key_at_capacity_does_not_evict`). - [x] `uv run ruff check headroom/shared_context.py tests/test_shared_context.py` — clean. - [x] `uv run ruff format --check headroom/shared_context.py tests/test_shared_context.py` — already formatted. ## Real behavior proof **Setup:** macOS 25.4 (Darwin arm64), Python 3.12.13, `uv 0.11.28`, this branch (`fix/shared-context-evict-on-update`). **Before the patch (unpatched `main`)** \`\`\` before update: ['a', 'b', 'c'] after update: ['b', 'c'] # <-- 'a' evicted, even though 'c' was an update \`\`\` **After the patch (this branch)** \`\`\` \$ uv run python <<'PY' from headroom.shared_context import SharedContext ctx = SharedContext(ttl=3600, max_entries=3) ctx.put(\"a\", \"x\"*400) ctx.put(\"b\", \"x\"*400) ctx.put(\"c\", \"x\"*400) print(\"before update:\", sorted(ctx.keys())) ctx.put(\"c\", \"y\"*400) # update existing at capacity print(\"after update: \", sorted(ctx.keys())) print(\"c value:\", ctx.get(\"c\", full=True)[:12] + \"...\") PY before update: ['a', 'b', 'c'] after update: ['a', 'b', 'c'] c value: yyyyyyyyyyyy... \`\`\` **Test output** \`\`\` \$ uv run pytest tests/test_shared_context.py -q ................ [100%] 16 passed in 2.17s \`\`\` **What I did NOT test** - Multi-thread test — the fix is inside the existing `self._lock`, so serialization semantics are unchanged; I did not add a concurrent-put stress test. - Interaction with TTL expiry AND capacity in one call — the existing `test_evicts_oldest_at_capacity` and `test_expired_entry_returns_none` still pass, but I did not add a combined case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
…oomlabs-ai#2027) ## Description Scopes the `[1m]` context-window tier suffix sanitizer to Anthropic `/v1/messages` requests only (addresses PR headroomlabs-ai#2027 review feedback). The original patch applied the rewrite to every buffered compressible endpoint, which would have silently mutated OpenAI Chat Completions and OpenAI Responses request model IDs. The `[1m]` marker is an Anthropic/Claude Code compatibility signal emitted by the Headroom CLI; the existing Python parity behavior (`sanitize_anthropic_model_id()`) is Anthropic-specific and must not leak onto OpenAI shapes. Refactors the helper into `compression::sanitize_anthropic_model_id_in_body`, drops the dead `sanitize_model_id` helper in `sse/anthropic.rs`, and adds 8 unit tests + 5 wiremock-backed integration tests that pin the scope. All 420 `headroom-proxy` tests pass; `cargo fmt` and `cargo clippy -D warnings` clean. ## 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 - Move `sanitize_request_model_id` out of `proxy.rs` and into `compression::sanitize_anthropic_model_id_in_body` (Anthropic-specific name; private `trim_anthropic_model_id_suffix` helper for unit-testable pure behavior). - Gate the call site on `CompressibleEndpoint::AnthropicMessages` **after** classification. The OpenAI Chat Completions and OpenAI Responses arms get an explicit no-op match so the sanitizer cannot re-apply to those paths. - Drop the dead `sanitize_model_id` helper in `sse/anthropic.rs` (it was `#[allow(dead_code)]` with no callers). - 8 new unit tests in `compression/mod.rs`: trailing `[1m]` stripped, Claude-style suffix stripped, no-suffix passthrough (byte-equal), non-string model, missing `model` field, non-JSON body, `[1m]` mid-string, and the pure trim helper. - 5 new integration tests in `tests/integration_anthropic_model_sanitize.rs` that boot a real Rust proxy in front of a wiremock upstream. ## Testing - [x] Unit tests pass (`cargo test -p headroom-proxy` → 420 passed, 35 suites) - [x] Linting passes (`cargo clippy -p headroom-proxy --tests --all-features -- -D warnings` clean) - [x] Type checking passes (`cargo check -p headroom-proxy --tests --all-features` clean) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ cargo test -p headroom-proxy --test integration_anthropic_model_sanitize Compiling headroom-proxy v0.x.x Finished `test` profile [unoptimized + debuginfo] target(s) Running tests/integration_anthropic_model_sanitize.rs test anthropic_messages_strips_1m_suffix_glm ... ok test anthropic_messages_strips_1m_suffix_claude ... ok test anthropic_messages_passthrough_when_no_suffix ... ok test openai_chat_completions_passthrough_with_1m_model ... ok test openai_responses_passthrough_with_1m_model ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text $ cargo test -p headroom-proxy test result: ok. 420 passed; 0 failed; 0 ignored; 0 measured; 235 filtered out finished in 10.93s ``` ```text $ cargo clippy -p headroom-proxy --tests --all-features -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) ``` ## Real Behavior Proof - **Environment:** macOS 14.x; `rustc` pinned via `rust-toolchain.toml`; `cargo` 1.x. No network access required (wiremock upstream). - **Exact command / steps:** 1. `cargo test -p headroom-proxy --test integration_anthropic_model_sanitize` — confirms `/v1/messages` strips `glm-5.2[1m]` and `claude-3-7-sonnet[1m]`; confirms `/v1/chat/completions` and `/v1/responses` leave the body byte-equal (SHA-256 asserted). 2. `cargo test -p headroom-proxy` — full suite green (420 passed). 3. `cargo clippy -p headroom-proxy --tests --all-features -- -D warnings` — clean. 4. `cargo fmt -p headroom-proxy --check` — clean. 5. Source inspection of `crates/headroom-proxy/src/proxy.rs` after the change: the call site is now in a `match endpoint` arm that explicitly returns `buffered` for the OpenAI variants, so the sanitizer cannot re-apply to those paths. - **Observed result:** all 5 new integration tests pass, all 420 crate tests pass, clippy and fmt clean. The OpenAI tests assert SHA-256 byte equality on a body whose `model` field ends in `[1m]`; if the sanitizer were to re-leak onto OpenAI shapes these would fail loudly with a length delta. - **Not tested:** a live Anthropic API call (would require real credentials and is not required to prove the byte-level scope fix). The Python proxy's `sanitize_anthropic_model_id()` is the documented parity reference (Python PR headroomlabs-ai#1840, issue headroomlabs-ai#1812). ## 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 (N/A — no user-facing docs change; the Python proxy's `sanitize_anthropic_model_id` is the parity reference cited in code comments) - [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 (project uses git log + PR titles; this PR's title follows the conventional commit shape) ## Screenshots (if applicable) N/A — backend behavior, no UI change. ## Additional Notes - The OpenAI integration tests rely on a JWT-style `Authorization: Bearer` header to classify the request as `AuthMode::OAuth` and short-circuit the PR-E4 `prompt_cache_key` injector. This is the same control variable the existing `integration_chat_completions.rs` tests use to isolate dispatcher byte-fidelity from the E4 hook. Comments in each test explain the relationship. - The dead helper in `sse/anthropic.rs` is removed, so the diff is net negative on LoC for the SSE module. - The Python parity reference is `sanitize_anthropic_model_id()` (Python PR headroomlabs-ai#1840, issue headroomlabs-ai#1812); the function name and the call-site scope are the explicit parity contract. - Branch was rebased onto `upstream/main` (91 commits behind) before force-push to the fork; conflict-free rebase. The original PR commit and the fix are the only two commits on the PR. --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> Co-authored-by: Abhishek Mittal <abhishek.mittal@users.noreply.github.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
PR governanceThis PR does not yet satisfy the required template fields:
Please update the PR body, or move the PR back to draft while it is still in progress. |
br4vesirrobin
force-pushed
the
chore/sync-upstream-readme-autobahn
branch
from
July 14, 2026 01:13
170400f to
258441d
Compare
Raise the production transformers floor to a version fixed for CVE-2026-5241 and refresh uv.lock so pip-audit passes.
…-readme-autobahn # Conflicts: # pyproject.toml # uv.lock
br4vesirrobin
pushed a commit
that referenced
this pull request
Jul 29, 2026
…eadroomlabs-ai#2123) ## Description `verbosity._ordered_events` and `_parse_session` disagree about empty assistant turns, which desyncs the response list and produces spurious fast-skips. `_parse_session` only creates a `_Response` when an assistant message actually said something: ```python if words > 0 or out_tok > 0: responses.append(_Response(...)) ``` But `_ordered_events` consumes one `responses[ri]` for **every** assistant line, with no matching filter: ```python if ltype == "assistant" and ri < len(responses): out.append((responses[ri].ts, "assistant", responses[ri])) ri += 1 ``` So an assistant turn with no text and no output tokens — for example a pure `tool_use` turn where `usage` is absent — creates no `_Response` at parse time, yet still consumes a slot in `_ordered_events`. That slot actually belongs to a *later* real response, so the two lists drift by one. A human reply that follows the real answer is then paired with the next answer's (future) timestamp, `ts - last_resp.ts` goes negative, and since a negative gap is always below the read-fraction threshold, a spurious `fast_skip` is recorded. That inflates `fast_skip_rate`, which feeds `pressure`, which lowers the recommended verbosity level. The user side of `_ordered_events` already replicates its parse-site filter (`_human_text(...) is None -> continue`); only the assistant side was missing the equivalent guard. That asymmetry is the bug. ## Fix In `_ordered_events`, compute `words`/`out_tok` for the assistant line the same way `_parse_session` does and only consume a response when `words > 0 or out_tok > 0`, keeping the two functions in lockstep. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/learn/verbosity.py`: `_ordered_events` applies the `words > 0 or out_tok > 0` guard on the assistant branch before consuming a response, with a comment explaining the desync. - `tests/test_verbosity_learn.py`: add `_empty_assistant` helper and `test_empty_assistant_message_does_not_desync_fast_skip` (an empty assistant turn before a real answer + a slow reply must not record a fast skip). - `CHANGELOG.md`: Bug Fixes entry. ## Testing - [x] Unit tests pass (`uv run --extra dev pytest tests/test_verbosity_learn.py::TestSignalExtraction::test_empty_assistant_message_does_not_desync_fast_skip -q`) - [x] Linting passes (`uvx ruff@0.15.17 check headroom/learn/verbosity.py tests/test_verbosity_learn.py headroom/memory/factory.py`) - [x] Type checking passes (`uvx mypy==1.20.2 headroom/memory/factory.py`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uvx ruff@0.15.17 check headroom/learn/verbosity.py tests/test_verbosity_learn.py All checks passed! $ python -m py_compile headroom/learn/verbosity.py tests/test_verbosity_learn.py OK ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17`. Importing `headroom` pulls in the torch/transformers stack and a full `pytest` gets OOM-killed on this box, so I verified the alignment with a dependency-free script that models the parse-site filter, the old vs new `_ordered_events` consume, and the resulting human-to-response pairing, and left the full pytest to CI. - Exact command / steps: built an event stream `[empty assistant, real answer #1, fast human reply, real answer headroomlabs-ai#2, reply]`, computed the response list from the parse filter, then walked the old (unfiltered) and new (filtered) consume to find the gap between the first human and the response paired before it. - Observed result: old consume pairs the reply with answer headroomlabs-ai#2 (a future timestamp) -> gap `-8` (spurious fast_skip); new consume keeps alignment and pairs it with answer #1 -> gap `+1`. The new test builds a session with an empty assistant turn and a genuinely slow reply and asserts `fast_skips == 0`. - Not tested: a real Claude Code transcript end to end; full local `pytest` deferred to CI (OOM, per above). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes Merged current `main` to pick up the repository-wide mypy cache-key annotation fix, then verified the focused regression locally. the change adds the existing parse-site filter to one branch of a pure file-parsing function, verified by the standalone alignment proof and the new regression test for CI. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
br4vesirrobin
pushed a commit
that referenced
this pull request
Jul 29, 2026
) ## Description `main` CI is red on three independent test failures. All three are **test-side** bugs (stale cache, semantic merge conflict, stale mock) — no product code regressed. Each test passed in isolation but failed on `main`, and each also blocks the `chore: release main` PR (headroomlabs-ai#1923). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - **`test_l2_appends_transform_label`** — `tool_desc_max_chars()` memoises into a module global. An earlier test in shard 1 reads it with the env unset, pinning the cache to `0`, so this test's `setenv("HEADROOM_TOOL_DESC_MAX_CHARS=20")` was swallowed (`assert 0 == 20`). Reset the cache before reading and after, mirroring the sibling `test_l2_skips_label_when_disabled`. - **`test_dashboard_uses_cached_stats_and_lazy_history_feed_polling`** — semantic merge conflict: headroomlabs-ai#2198 (persist lifetime metrics) intentionally retired the session-card `Filtered (lifetime)` row and moved CLI-filtering lifetime into the history tab as `Lifetime Saved`, while the assertion from headroomlabs-ai#1433 still checked the old string. Assert the current `Lifetime Saved` label. - **`test_smart_crusher_log_fallback_runs_for_valid_json`** — stale mock: headroomlabs-ai#1857 made token counting whitespace-aware, so the router now rates the JSON above the naive `len(content.split())==8` the no-op kompress mock reported, making it look like a saving and short-circuiting before the Log fallback. Mock now reports `_estimate_tokens(content)` to match the router. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) ### Test Output ```text $ pytest tests/test_anthropic_compaction_transforms.py \ tests/test_proxy_dashboard_stats_cache.py \ tests/test_transforms_content_router.py -q 78 passed, 1 skipped in 12.14s $ ruff check <the three files> All checks passed! $ ruff format --check <the three files> 3 files already formatted ``` ## Real Behavior Proof - Environment: local `.venv`, Python 3.12.6, pytest 9.0.2 (same three tests that fail on the `main` CI shards 1/3/4). - Exact command / steps: ran the three previously-failing tests by node id — all pass. Reproduced the shard-isolation failure for #1 by calling `tool_desc_max_chars()` with the env unset (cache → 0) before the test, confirmed the reset makes it pass. - Observed result: 3/3 target tests pass; 78 passed / 1 skipped across the three full files. - Not tested: full suite (unchanged product code); CI shards will re-run on this PR. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes `mypy headroom` (the CI-enforced scope) is unaffected — these edits touch only `tests/`, which CI does not type-check. Once this lands on `main`, the `chore: release main` PR (headroomlabs-ai#1923) drops to just the `test_root_server_json_matches_builder` failure, which is the release version-bump `server.json` regen (not a code bug).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
headroomlabs-ai/headroommaininto the maintained forkValidation
Autobahn dependency
This unblocks the AiTool workspace fresh-clone
./setup.shsmoke test; merge only when every hosted check is green.