mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
2221 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f241e472b
|
fix(ccr): skip compact summaries for proactive expansion (#2242)
## Description Fixes #2186. Claude Code `/compact` continuation summaries are already session context. When Headroom tracks those summaries for CCR proactive expansion, later fresh sessions can receive stale compacted history again inside `<headroom_proactive_expansion>` blocks, increasing token usage and busting cache stability. This PR keeps CCR storage/retrieval intact but excludes probable Claude Code compact-summary payloads from the proactive-expansion tracker. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added a narrow Claude Code compact-summary detector to the CCR context tracker. - Skipped tracking compact summaries when feeding Anthropic CCR metadata into proactive expansion. - Added an original-content preview to CCR metadata so the Anthropic feed point can classify compact summaries even when compressed text loses the distinctive header. - Added regression coverage proving compact summaries are not tracked and ordinary summaries are still eligible. ## Testing - [x] Unit tests pass - [x] Linting passes - [x] Formatting check passes - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ pytest tests/test_ccr_context_tracker.py -q 37 passed $ uvx ruff==0.15.17 check headroom/ccr/context_tracker.py headroom/cache/compression_store.py headroom/proxy/handlers/anthropic.py tests/test_ccr_context_tracker.py --output-format concise All checks passed! $ uvx ruff==0.15.17 format --check headroom/ccr/context_tracker.py headroom/cache/compression_store.py headroom/proxy/handlers/anthropic.py tests/test_ccr_context_tracker.py 4 files already formatted ``` ## Real Behavior Proof - Environment: local checkout on macOS, Python test environment used by the repository. - Exact command / steps: ran the focused CCR context tracker suite after adding compact-summary detection and tracker-feed filtering. - Observed result: compact-summary payloads are not tracked for proactive expansion, ordinary summary-like tool output is still eligible, and the existing tracker behavior remains covered by the full focused suite. - Not tested: live Claude Code `/compact` session through a running proxy. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review - [x] I have added tests that prove the fix is effective - [ ] I have updated the CHANGELOG.md if applicable |
||
|
|
fcf455a7eb
|
feat(wrap): add omp target (Oh My Pi) with models.yml override and unwrap (#1811)
## Description Adds `headroom wrap omp` / `headroom unwrap omp` — a one-command wrap for [Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent) (`omp`), the pi-mono-lineage coding agent, as proposed in #1149. One honest correction to the issue: #1149 proposed reusing the `ANTHROPIC_BASE_URL` redirect from `wrap claude`. During implementation I probed that empirically and it turned out to be wrong — omp only reads `ANTHROPIC_BASE_URL` in its web-search helper; its **chat** endpoint comes from the model registry (`providers.anthropic.baseUrl` in `~/.omp/agent/models.yml`). With the env var pointed at a local probe server, omp's chat traffic still went straight to the real endpoint (0 probe hits); with a `models.yml` same-ID override, every request arrived at the probe (9/9 hits on `/v1/messages`). A same-ID override keeps omp's bundled Anthropic model catalog and stored credentials (both keyed by provider id `anthropic`), so only the endpoint moves. The wrap therefore injects a marker-fenced `providers.anthropic.baseUrl` override into `models.yml`, snapshotting the pre-wrap file **byte-for-byte** first, and `headroom unwrap omp` restores it exactly (or removes the file when the wrap created it) — the same durable-wrap + backup + unwrap contract `wrap codex` uses for `config.toml`. Closes #1149 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/providers/omp/` (new provider slice): `models_yml_path()` (honors `PI_CODING_AGENT_DIR`), `inject_models_override()` (yaml-merge preserving user providers; pristine byte-for-byte backup, never re-snapshotted while managed), `restore_models_override()` (`restored` / `removed` / `noop`; never touches an unmanaged file), `build_launch_env()` - `headroom/cli/wrap.py`: `wrap omp` (mirrors the aider/vibe `_launch_tool` shape; rtk instructions into the project's `AGENTS.md`, which omp reads natively) and `unwrap omp` (restore models.yml + scrub rtk block + stop proxy) - `headroom/telemetry/context.py`: `omp` added to `_KNOWN_WRAP_AGENTS` so the stack slug reports `wrap_omp` instead of `unknown` - `README.md` (agent matrix row + unwrap list), `llms.txt`, `CHANGELOG.md` - `tests/test_cli/test_wrap_omp.py`: 16 tests (injection fresh/merge/re-inject, restore statuses incl. unmanaged-file safety, env passthrough, CLI wiring, unwrap flows) ## Testing - [ ] Unit tests pass (`pytest`) — all new + `test_cli` tests pass; the full suite carries **3 pre-existing failures** that reproduce identically on unmodified `origin/main` (same set, same asserts — see Test Output and the rebase-validation comment) - [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 -q # post-rebase, base |
||
|
|
996c1174a8
|
feat(proxy): show real upstream provider on dashboard for OpenAI-compatible endpoints (#1594)
## Description When the proxy runs against a custom OpenAI-compatible endpoint via `--openai-api-url` (OpenRouter, Groq, Together, Azure OpenAI, …), the dashboard always showed the provider as **OpenAI**, because the OpenAI handler records every request with `provider="openai"`. This detects well-known upstreams from the `--openai-api-url` host and adds a `--provider-name` override that takes precedence (the issue's option 3). The label is resolved only where the dashboard/stats payload is built — the internal provider key stays `openai`, so pricing and request formatting are unaffected. | Upstream URL | Provider shown | |--------------|----------------| | `https://api.openai.com/v1` | OpenAI | | `https://openrouter.ai/api/v1` | OpenRouter | | `https://api.groq.com/openai/v1` | Groq | | `https://api.together.xyz/v1` | Together AI | | `https://<resource>.openai.azure.com/` | Azure OpenAI | Unknown hosts keep the `openai` label unless `--provider-name` is set. Closes #1533 ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `helpers.py`: `classify_openai_upstream()` (host → display name) + `resolve_display_provider()` (precedence: `--provider-name` > host detection > raw provider; only relabels `openai`). - `models.py`: `ProxyConfig.provider_name`. - `cli/proxy.py`: `--provider-name` flag, threaded into `ProxyConfig`. - `server.py`: relabel at the four dashboard/stats display sites (recent requests, transformations feed, `requests.by_provider`, agent-usage breakdown) via the resolver / `_remap_provider_counts`. Stored logs and metrics keys are untouched. - `docs/content/docs/proxy.mdx`: document `--provider-name`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] New tests added ### Test Output ```text $ pytest tests/test_provider_display_classification.py tests/test_dashboard_agent_usage.py -q 16 passed 13 passed $ ruff check headroom/proxy/helpers.py headroom/proxy/server.py headroom/proxy/models.py headroom/cli/proxy.py All checks passed! ``` ## Real Behavior Proof - Environment: repo branch `feat/1533-upstream-provider-classify` @ HEAD, local `.venv` (Python 3) - Exact command / steps: ran the helpers directly from the venv — `python -c "from headroom.proxy.helpers import classify_openai_upstream, resolve_display_provider; print(classify_openai_upstream('https://openrouter.ai/api/v1')); print(resolve_display_provider('openai', openai_api_url='https://openrouter.ai/api/v1')); print(resolve_display_provider('openai', openai_api_url='https://openrouter.ai/api/v1', provider_name='Groq')); print(resolve_display_provider('anthropic'))"` - Observed result: host detection relabels `openai` → `OpenRouter`, `--provider-name` overrides detection (`Groq`), and the `anthropic` label (plus the `openai` pricing key) is unchanged. Full output below: ```text classify openrouter -> OpenRouter resolve openai+openrouter url -> OpenRouter override provider-name -> Groq anthropic untouched -> anthropic ``` - Not tested: live dashboard render against a real OpenRouter key (the payload-builder logic is covered by the unit tests 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 made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
7a5d8a7ace
|
fix(mcp): reap orphaned mcp serve on client death (#2226)
## Description `headroom mcp serve` processes survive after the launching MCP client (e.g. Claude Code) exits, get reparented to init/launchd (`ppid == 1`), and never terminate — piling up one pinned Python interpreter + tree-sitter grammars per dead session (observed 3+ simultaneously). An MCP stdio server is supposed to shut down on stdin EOF, but an abrupt client `SIGKILL` leaves the MCP SDK's blocking stdin-reader thread wedged, so `await self.server.run(...)` in `run_stdio()` never returns and the process orphans. Refs #2185 (its secondary "orphaned `mcp serve` pileup", left out of #2204's `Refs`-only Perl fix), #1761 (same symptom: "orphaned `headroom mcp serve` processes accumulate … even after quitting"). ## 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/mcp_server.py`: - Added `PARENT_DEATH_POLL_INTERVAL = 5.0` module constant. - Added `HeadroomMCPServer._await_parent_death(interval)`: captures the launch ppid and resolves once it changes. Watching for a *change* (not a hard `== 1`) is portable to Linux PID subreapers, which adopt the orphan with their own pid. - Reworked `run_stdio()` to run that watchdog concurrently with `server.run()`. On parent death it `os._exit(0)`s **from inside** the `stdio_server()` context manager — the wedged stdin reader would also hang the context-manager teardown and a cooperative `server.run` cancel, so a hard exit is the only reliable reaper. The normal stdin-EOF path is unchanged: `server.run` wins the race, the watchdog is cancelled, and the context manager unwinds cleanly. `tests/test_ccr_mcp_server.py`: 3 regression tests (below). `CHANGELOG.md`: entry under Unreleased → Fixed. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_server.py -q`) - [x] Linting passes (`uv run ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py`) - [x] Type checking passes (`uv run mypy headroom/ccr/mcp_server.py`) - [x] New tests added for new functionality - [x] Manual testing performed (see Real Behavior Proof) New tests: - `test_parent_death_watchdog_fires_when_reparented` — ppid change resolves the watchdog. - `test_parent_death_watchdog_stays_quiet_with_live_parent` — a stable ppid never trips it. - `test_run_stdio_reaps_process_on_parent_death` — on reparent, `run_stdio` cleans up and hits `os._exit(0)` even though the (stubbed) `server.run` never returns. ### Test Output ```text $ uv run pytest tests/test_ccr_mcp_server.py -q collected 21 items tests/test_ccr_mcp_server.py ..................... [100%] ============================== 21 passed in 0.57s ============================== $ uv run ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py All checks passed! $ uv run mypy headroom/ccr/mcp_server.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: macOS 26.5 arm64, Python 3.14.6, headroom built from this branch via `uv sync --all-extras` (Rust extension compiled). No provider call. - Exact command / steps: launch a real `HeadroomMCPServer.run_stdio()` as a child of a throwaway parent, with stdin wired to a FIFO whose write end is held open by a separate process (so stdin **never** reaches EOF — this isolates the watchdog as the only possible reaper). Then `kill -9` the parent to reparent the server to `pid 1`, and watch. The watchdog poll interval is passed via `run_stdio(parent_death_poll_interval=…)` to A/B the exact same shipped code path: ```text ### interval=9999s (watchdog effectively OFF — reproduces the bug) ### ppid(pre-kill)=43438 -> STILL ALIVE after 8s (orphan lingers) ### interval=0.5s (watchdog ON — the fix) ### ppid(pre-kill)=43461 -> REAPED at ~2s ``` And with the default flow (`headroom mcp serve`, default 5s interval), the watchdog logs before the process exits: ```text headroom.ccr.mcp - INFO - Headroom MCP Server starting (proxy: http://127.0.0.1:8787) headroom.ccr.mcp - WARNING - parent process gone (ppid 41956 -> 1); shutting down MCP server ``` - Observed result: with the watchdog disabled the orphaned server lingers indefinitely (reproduces the reported pileup); with it enabled the orphan is reaped within one poll interval of the parent dying. - Not tested: Linux/systemd and Windows spawn paths (the change is POSIX-portable via ppid-change detection, but I only exercised macOS); the reporters' desktop-app menu-bar quit path. ## 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md ## Additional Notes - Deliberately `os._exit(0)`, not a cooperative shutdown: the failure mode is a wedged native stdin-reader thread, so both `server.run` cancellation and the `stdio_server` context-manager exit can block forever. Exiting from inside the context manager is the only path that reliably reaps the orphan; the normal EOF path never reaches it. - A Linux-only `prctl(PR_SET_PDEATHSIG)` fast-path could cut reap latency to ~0, but it is racy (must re-check `getppid()` after arming) and non-portable, so the portable poll is the primary mechanism. Happy to add prctl as a follow-up optimization if wanted. - Watchdog latency is bounded by `PARENT_DEATH_POLL_INTERVAL` (5s default); trivial to make env-configurable if a tighter bound is preferred. --- 🤖 This PR was created with [Claude Code](https://claude.com/claude-code) but checked by the author Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
cb388f6af2
|
feat(wrap): add first-class Grok CLI support (#1823)
## Description
Adds first-class Grok CLI integration so Headroom can wrap, compress,
and learn from Grok sessions the same way it does for Claude Code and
Codex.
Grok routes inference through `GROK_CLI_CHAT_PROXY_BASE_URL`; Headroom
sets that to the local proxy so chat traffic is compressed before
forwarding to xAI. MCP retrieval and session learning follow the same
patterns as Codex.
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Add `headroom/providers/grok/` provider slice (`runtime.py`,
`install.py`) with `GROK_CLI_CHAT_PROXY_BASE_URL` routing and project
attribution prefix
- Add `headroom wrap grok` and `headroom unwrap grok` CLI commands (MCP
registration, RTK/context-tool setup, proxy launch)
- Add `GrokRegistrar` for marker-delimited `[mcp_servers.headroom]`
injection in `~/.grok/config.toml`
- Add `headroom learn --agent grok` plugin parsing
`~/.grok/sessions/*/updates.jsonl` and `GrokWriter` targeting `GROK.md`
- Register Grok in install planner, install registry, MCP install list,
and agent savings tracking
- Update README agent compatibility matrix and unwrap support list
- Add unit tests for provider, wrap CLI, MCP registrar, and learn plugin
## 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 pytest tests/test_provider_grok.py tests/test_cli/test_wrap_grok.py tests/test_mcp_registry_grok.py tests/test_learn_grok_plugin.py -q
============================== 10 passed in 0.15s ==============================
$ uv run ruff check headroom/providers/grok headroom/mcp_registry/grok.py headroom/learn/plugins/grok.py headroom/cli/wrap.py headroom/learn/writer.py
All checks passed!
$ uv run mypy headroom/providers/grok headroom/mcp_registry/grok.py headroom/learn/plugins/grok.py
Success: no issues found in 5 source files
```
## Real Behavior Proof
- Environment: macOS (darwin), Python 3.13.14 via `uv`, repo at
`/Users/s/dev/headroom`, Grok CLI at `/Users/s/.grok/bin/grok`
- Exact command / steps: `cd /Users/s/dev/headroom && uv run pytest
tests/test_provider_grok.py tests/test_cli/test_wrap_grok.py
tests/test_mcp_registry_grok.py tests/test_learn_grok_plugin.py -q`; `uv
run ruff check headroom/providers/grok headroom/mcp_registry/grok.py
headroom/learn/plugins/grok.py`; `uv run python -c "from
headroom.providers.grok import build_launch_env; env, display =
build_launch_env(8787, environ={}); print(display[0])"`; `uv run python
-c "from pathlib import Path; import tempfile; from
headroom.mcp_registry.grok import GrokRegistrar; from
headroom.mcp_registry.install import build_headroom_spec;
td=tempfile.mkdtemp(); reg=GrokRegistrar(home_dir=Path(td));
print(reg.register_server(build_headroom_spec('http://127.0.0.1:8787'),
force=True).status.value)"`
- Observed result: pytest reported `10 passed`; ruff reported `All
checks passed!`; `build_launch_env` printed
`GROK_CLI_CHAT_PROXY_BASE_URL=http://127.0.0.1:8787/v1`; MCP registrar
returned `registered` and wrote the Headroom marker block to
`config.toml`
- Not tested: live `headroom wrap grok` session with authenticated Grok
API traffic through a running Headroom proxy (requires maintainer
environment with active `grok login`)
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — CLI/integration change only.
## Additional Notes
- Follows the provider-slice pattern from `
|
||
|
|
7bfb1d7f38
|
fix(cache): stable session identity and per-conversation prefix trackers under agentic clients (#2193)
## Description Running headroom as the proxy for Claude Code destroys Anthropic prompt-cache reuse (#2085: ~4.4x cache-creation inflation, 2.5–3x net cost). Tracing live Claude Code traffic through the proxy shows **two independent session-identity defects**, both of which orphan or thrash the frozen-prefix state; this PR fixes both. ### Defect 1: `<system-reminder>` turns rotate the fallback session id mid-conversation Claude Code interleaves reminder turns into the history as actual `role:"system"` messages (hook output, skills lists, file-truncation notices). `compute_session_id` hashed **every** system message, so the id rotated each time a reminder landed. Live trace (subagent reading two 80KB files; sid changes exactly when the truncation reminder appears, and the tracker restarts at turn 0): ``` REQ#2 sid=68d4ee666990 nmsg=3 [0]SYSTEM<<top-level system>> [1]user [2]SYSTEM<<skills reminder>> REQ#3 sid=6944948c9fb2 nmsg=6 ... [5]SYSTEM<<Truncated: PARTIAL view ...>> <- id rotated ``` Everything keyed on the session id is orphaned at that moment: the prefix tracker (freeze never survives past a reminder-bearing turn), beta-header stickiness, the CCR and memory-tool registries, and the compression cache. **Fix:** hash only the **leading run** of system messages (everything before the first non-system turn) — the top-level system prompt on the Anthropic path (folded in as the synthetic first message), the conventional leading system message(s) on the OpenAI path. Stable for the life of a conversation; mid-history system turns are content, not identity. ### Defect 2: conversations sharing a (now stable) id thrash one tracker With ids stable, the fallback tuple `model + system prompt` is identical across every same-type parallel subagent (and any sessions reusing one system prompt) — all of them collapse onto one `PrefixCacheTracker`, and their interleaved histories cross-contaminate the freeze state: the forwarded prefix is byte-unstable on nearly every turn and the provider cache is re-written instead of read. Reproduced against the real code paths (script below): ``` 1) fallback session ids: A=3dc639aaf4f48fa1 B=3dc639aaf4f48fa1 -> COLLIDE=True 2) single conversation, legacy : stable prefix on 4/4 later turns, trackers=1 2) interleaved (subagents), legacy : stable prefix on 0/9 later turns, trackers=1 2) interleaved, lineage resolution : stable prefix on 8/8 later turns, trackers=2 ``` **Fix:** `SessionTrackerStore.resolve_tracker` — within a session id, reuse the tracker whose previous request messages are a prefix of the incoming history (client histories are append-only, so a conversation's next request always extends its previous one); a diverging or rewritten history (client-side compaction) starts a fresh lineage. Matching uses the repo's existing canonical cross-turn equivalence (`_canonicalize_for_prefix_compare`, the same one the cache-stable delta path uses) on the **original client bytes**, so moved cache breakpoints, string<->block sugar, transport annotations, or a tail-mutating `pre_compress` hook never read as a rewrite. Byte-identical histories (templated fan-outs before they diverge) intentionally share a tracker — their provider cache line is identical too. ### Both fixes together, on live Claude Code traffic (sonnet, 2 parallel Explore agents) ``` main conversation: sid=5b7e245a... one tracker, turns 0->4, id stable across reminders agents (collide): sid=2bdffc9e... -> lineage bare (alpha) turns 0->1->2 -> lineage "~1" (beta) turns 0->1->2 ``` Before: the agents' ids rotated per reminder (every tracker stuck at turn 0), and whenever they did share an id they thrashed one tracker (`0/9` stable prefixes in the repro). ### Why not key the session id on conversation content? Draft #1912 folds the first user turn into the fallback id; this change composes with it, but identity-level keying alone can't close #2085: identical first turns (templated fan-outs) still collide, and everything keyed on the session id rotates with it when the client rewrites history. The "session" (client/workspace grouping) and the "conversation" (positional cache lineage) are different identities; only the tracker holds positional per-turn state that thrashes under collision — beta stickiness is a monotone union and the compression cache is content-addressed — so lineage resolution lives one level below the session id and leaves the id semantics (and every other consumer) untouched. ## Changes Made - `headroom/cache/prefix_tracker.py`: - `compute_session_id`: harvest only the leading system run (defect 1). - `SessionTrackerStore.resolve_tracker`: conversation-lineage resolution (defect 2). First lineage lives under the bare session id — single-conversation sessions behave byte-identically to before; degrades to `get_or_create` when messages are absent or prefix freeze is disabled. - Lineages are capped per session id (`PrefixFreezeConfig.max_lineages_per_session`, default 32). **Over-cap conversations share one overflow tracker instead of evicting an established lineage** — any eviction policy degrades every conversation once the working set exceeds the cap (under round-robin the victim is always the conversation about to arrive), while overflow sharing degrades only the over-cap tail, to exactly the pre-lineage shared behavior; `0` disables lineage splitting. Chains are stored as structural snapshots that normalize `NaN` (`json.loads` accepts bare NaN, and `NaN != NaN` would read a byte-identical resend as a rewrite). Synthetic lineage keys use a `\x00` separator, which cannot appear in an HTTP header value, so they can never collide with a client-supplied `x-headroom-session-id`. - `headroom/proxy/handlers/anthropic.py`, `openai.py`: the session id and the lineage both derive from the **same original client bytes** (a turn-dependent hook rewrite can no longer rotate one without the other); anthropic folds in its synthetic system message so explicit-header clients with different system prompts stay separate. Plus a docstring correction in `streaming.py` that falsely claimed its coarse mid-turn key "mirrors" `compute_session_id`. - `tests/test_cache/test_prefix_tracker.py`: 24 new test cases — reminder-rotation regression; interleaved isolation + per-conversation turn state; identical-first-turn share-then-split; cache_control movement (3 cases); representation churn (string<->block sugar / streaming `index` / Bedrock cachePoint); rewritten history → fresh lineage (compacted / middle-edited / truncated); legacy no-messages / freeze-disabled / empty-canonical fallbacks; NaN-in-tool-payload stability; overflow sharing, established-lineages-survive-cap, and a cap+1 round-robin no-cliff guard; TTL cleanup; session-id-not-rotated-by-lineage guard. One existing test renamed (`uses_all_system_messages` → `distinguishes_leading_system_run`) to match the new contract. - Three SimpleNamespace stub stores in existing tests gained a `resolve_tracker` field (handlers call it unconditionally — a silent `hasattr` fallback would degrade to the pre-fix behavior with no signal). One of them is the cold-start fast-pass suite (#2073), which landed while this branch was in review. - `CHANGELOG.md` entry. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Testing - [x] Unit tests pass (`pytest`) — 11 failed, 8652 passed, 528 skipped in 4:37 (the 11 are pre-existing on unmodified `main` — verified by rerunning the same node ids on a clean checkout: gh-CLI/onnx/PID-reuse/deadline flakes and order-dependent cases, none touching session/cache/proxy paths) - [x] Linting passes (`ruff check .`) — All checks passed (ruff 0.15.17, CI-pinned; `ruff format --check .` clean) - [x] Type checking passes (`mypy headroom`) — Success: no issues found in 471 source files - [x] New tests added for new functionality — 24 test cases; the rotation/isolation/no-cliff ones fail on `main` - [x] Manual testing performed — live Claude Code end-to-end, below ### Test Output ```text $ python -m pytest tests/ -q 11 failed, 8652 passed, 528 skipped, 5857 warnings in 276.68s (0:04:36) # same 11 fail on unmodified main (env/order-dependent: test_wrap_claude_base_url pid-reuse, # copilot_auth gh-cli fallback, image_compression onnx, content_router deadline, rtk/output-shaper/dedup order flakes) $ python -m pytest tests/test_cache/test_prefix_tracker.py -q 63 passed $ uvx ruff@0.15.17 check . && uvx ruff@0.15.17 format --check . All checks passed! / 1208 files already formatted $ mypy headroom Success: no issues found in 471 source files $ python repro_2085.py 1) fallback session ids: A=3dc639aaf4f48fa1 B=3dc639aaf4f48fa1 -> COLLIDE=True 2) single conversation, legacy : stable prefix on 4/4 later turns, trackers=1 2) interleaved (subagents), legacy : stable prefix on 0/9 later turns, trackers=1 2) interleaved, lineage resolution : stable prefix on 8/8 later turns, trackers=2 ``` ## Real Behavior Proof - Environment: macOS arm64, Python 3.13, `uv sync --extra dev --extra proxy`; real Claude Code CLI pointed at the proxy via `ANTHROPIC_BASE_URL=http://127.0.0.1:8790`, real Anthropic backend. - Exact command / steps: ran Claude Code sessions that launch 2–3 parallel Explore subagents (each reading multi-KB JSON files, several tool-loop turns each), with an observability wrapper printing each request's resolved session id, tracker identity, and turn counter inside the proxy. - Observed result: on `main`, subagent session ids rotate on reminder-bearing turns (trackers permanently stuck at turn 0); when conversations do share an id they share one tracker whose turn counter interleaves all of them. On this branch: ids stable for the life of each conversation; colliding subagents resolve to separate lineages (`bare`, `~1`) with clean per-conversation turn progressions (trace above). Unit-level repro shows forwarded-prefix stability going 0/9 → 8/8 for the interleaved shape. - Not tested: reporter-scale cache-economics (his 4.4x needs his long-session workload against a paid backend); happy to coordinate with @RomanAlexanderW on a before/after — the number to watch is the cache-read ratio in Claude Code transcripts recovering toward ~96%. <details> <summary>repro_2085.py</summary> ```python """Repro for #2085: concurrent conversations sharing a fallback session id (same model + system prompt — e.g. a Claude Code session and its parallel subagents) collapse onto one PrefixCacheTracker and thrash its frozen-prefix state -> byte-unstable forwarded prefixes -> the provider prompt cache is re-written on nearly every call. Uses headroom's real code paths. Run from the repo root: python ../repro_2085.py """ from headroom.cache.prefix_tracker import PrefixFreezeConfig, SessionTrackerStore MODEL = "claude-sonnet-5" # Claude Code system prompt: long, static, identical across the main session # and every parallel subagent of the same type. SYSTEM = ("You are Claude Code, Anthropic's official CLI for Claude. " * 40)[:2000] def convo(name: str, turns: int) -> list[dict]: msgs = [{"role": "system", "content": SYSTEM}] for t in range(turns): msgs.append({"role": "user", "content": f"[{name}] user turn {t}: " + ("x" * 800)}) msgs.append( {"role": "assistant", "content": f"[{name}] tool_result {t}: " + ('{"data": 1}' * 200)} ) return msgs class _Req: # request stub: no x-headroom-session-id header headers: dict = {} # --- Part 1: identity collision (real derivation) ---------------------------- store = SessionTrackerStore(PrefixFreezeConfig()) id_a = store.compute_session_id(_Req(), MODEL, convo("A", 3)) id_b = store.compute_session_id(_Req(), MODEL, convo("B", 5)) print(f"1) fallback session ids: A={id_a} B={id_b} -> COLLIDE={id_a == id_b}") # --- Part 2: interleaved conversations thrash the freeze state --------------- def run(interleave: bool, lineage_resolution: bool) -> tuple[int, int, int]: store = SessionTrackerStore(PrefixFreezeConfig()) stable_turns = 0 later_turns = 0 seq = [] for t in range(1, 6): seq.append(("A", convo("A", t))) if interleave: seq.append(("B", convo("B", t))) for _name, msgs in seq: sid = store.compute_session_id(_Req(), MODEL, msgs) if lineage_resolution: tracker = store.resolve_tracker(sid, "anthropic", messages=msgs) else: tracker = store.get_or_create(sid, "anthropic") if tracker._turn_number > 0: later_turns += 1 if tracker._forwarded_prefix_stable(msgs): stable_turns += 1 tracker.update_from_response( cache_read_tokens=5000 * len(msgs), cache_write_tokens=2000, messages=msgs, ) return stable_turns, later_turns, store.active_sessions for label, interleave, fixed in ( ("single conversation, legacy ", False, False), ("interleaved (subagents), legacy ", True, False), ("interleaved, lineage resolution ", True, True), ): stable, later, sessions = run(interleave, fixed) print(f"2) {label}: stable prefix on {stable}/{later} later turns, trackers={sessions}") ``` </details> ## 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 (CHANGELOG only — no docs describe the tracker store) - [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 - Addresses the session-identity mechanisms of #2085; intentionally does not `Closes` it — the reporter should confirm the cache-read ratio recovers on live traffic first. - Composes with draft #1912 (first-user-turn fallback id). - Known bounded tradeoffs (all strictly milder than the per-turn thrash this fixes): a fork-style branch that resends a parent's full history adopts the parent's lineage, costing the parent one cold restart at its next turn; a request that aborts before the response and is retried with different bytes starts a fresh lineage; history truncation/tail-edit starts a fresh lineage even though the shorter provider prefix may still be warm. - Hot-path cost, measured on a 199-message/2.1MB agentic history: canonical projection 0.21ms + structural snapshot 0.92ms + match loop 0.06ms with 32 candidate lineages (2.27ms absolute worst case) ≈ **1.3ms per request** — same order as the handler's existing request deepcopy (0.80ms) and below one `json.dumps` of the body (2.9ms). Chain memory is structure-only (~180-330KB per lineage; message strings are shared with state the tracker already retains). - Known semantic shift to flag: hashing only the leading system run means conversations distinguished ONLY by mid-list system messages (e.g. clients injecting a per-conversation system context late in the list) now share a fallback id. The tracker is protected by lineage resolution; the residual sharing concentrates in the CCR sticky-tool registry and the monotone beta union — the same pre-existing class as same-system-prompt conversations today. Happy to file the CCR-stickiness scoping as a follow-up. - Out of scope, observed while tracing: `SessionCcrTracker.has_done_ccr` mildly cross-contaminates conversations sharing an id (monotone, no thrash) — can file separately if useful. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
560ffae103
|
feat(deploy): Add turnkey deploy command (#1404)
## Description Adds `headroom deploy` as the turnkey, zero-config local deployment entrypoint. The command chooses the most capable deployment path it can verify on the current host, configures detected tools through the existing persistent-install machinery, starts the proxy, and preserves the existing rollback behavior if an update fails. The selection order favors performance first: NVIDIA Docker GPU passthrough when `nvidia-smi` and Docker's NVIDIA runtime are available, then plain Docker, then native scheduled recovery, then a detached Python runtime fallback. ## 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) - [x] Documentation update - [x] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made - Added the top-level `headroom deploy` command and reused the existing install manifest/apply/start/rollback path. - Added conservative runtime selection for GPU Docker, plain Docker, native schedulers, and detached Python fallback. - Added Docker runtime support for manifest-driven `--gpus all` passthrough. - Added tests for Docker selection, GPU Docker selection, detached fallback, GPU command rendering, and subprocess wrapper compliance. - Updated README and persistent-install docs to present the turnkey deployment flow and performance-first GPU behavior. - Allowed documented `opencode` targets through `headroom install apply --target`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Formatting passes (`ruff format --check`) - [x] Type checking passes in local pre-commit and CI - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text uv run --with pytest --with pytest-asyncio python -m pytest tests/test_cli/test_subprocess_utf8_encoding.py tests/test_cli/test_install_cli.py tests/test_install/test_runtime.py::test_build_runtime_command_for_docker_includes_gpu_passthrough tests/test_install/test_planner.py -q 47 passed in 1.57s uvx --from ruff==0.15.17 ruff check headroom/cli/install.py headroom/install/runtime.py tests/test_cli/test_install_cli.py tests/test_install/test_runtime.py --output-format concise All checks passed! uvx --from ruff==0.15.17 ruff format --check headroom/cli/install.py headroom/install/runtime.py tests/test_cli/test_install_cli.py tests/test_install/test_runtime.py 4 files already formatted ``` GitHub checks are green on the current head. ## Real Behavior Proof - Environment: Windows local worktree `C:\git\headroom`, Python 3.13 via `uv`; GitHub Actions Ubuntu/macOS/Windows runners for full PR CI. - Exact command / steps: Ran the focused deploy/install tests above, checked the touched Python files with the CI-pinned Ruff version, and confirmed the current PR head is mergeable with green GitHub checks. - Observed result: The deploy command, runtime selection, Docker GPU command rendering, install CLI behavior, and subprocess encoding coverage all pass locally; the branch is no longer conflicted. - Not tested: Actual RTX 4090 hardware passthrough on a physical NVIDIA workstation; the PR tests conservative detection and Docker command rendering without requiring GPU hardware in CI. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - CLI/runtime behavior only. ## Additional Notes CHANGELOG update is not included because this is an unreleased feature PR and the repository's release tooling owns release notes from conventional commits. |
||
|
|
ad6ab48cbb
|
refactor(proxy): extract tool definition serialization (#1998)
## Description Extracts canonical memory-tool definition byte serialization from `headroom.proxy.helpers` into a focused pure module. The existing helper function remains as a compatibility wrapper for sticky memory tool and CCR replay code. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made - Added `headroom.proxy.tool_definition_serialization` for deterministic compact UTF-8 tool definition serialization. - Kept `helpers.serialize_tool_definition_canonical()` as a compatibility wrapper. - Added direct unit tests for compact separators, Unicode preservation, insertion-order byte stability, and parity with the existing body canonicalizer. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Formatting passes (`ruff format --check`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_tool_definition_serialization.py tests/test_ccr_tool_always_on.py tests/test_memory_tool_session_sticky.py tests/test_proxy_byte_faithful_forwarding.py -q 85 passed, 1 warning in 2.47s uvx --from ruff==0.15.17 ruff check headroom/proxy/helpers.py headroom/proxy/tool_definition_serialization.py tests/test_tool_definition_serialization.py --output-format concise All checks passed! uvx --from ruff==0.15.17 ruff format --check headroom/proxy/helpers.py headroom/proxy/tool_definition_serialization.py tests/test_tool_definition_serialization.py 3 files already formatted ``` ## Real Behavior Proof - Environment: Windows, Python 3.13.x - Exact command / steps: Ran direct serializer tests plus CCR always-on, sticky memory tool, and proxy byte-faithful forwarding regression coverage; then checked the touched files with the CI-pinned Ruff version. - Observed result: Serializer byte contract remains directly covered while existing sticky replay and byte-faithful proxy behavior stay green. - Not tested: Full repository pytest suite locally; GitHub CI is green for the current head. ## 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 and changelog updates are not applicable for this internal refactor. The current head is mergeable and GitHub checks are green. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
96bc4cd128
|
feat(dashboard): add settings dashboard for proxy configuration (#2101)
## Description Adds a loopback-only dashboard settings panel at `/dashboard/settings` for a curated, safe subset of Headroom runtime knobs. Settings persist to `settings.json`, are applied before Click resolves `envvar=` options, and keep precedence predictable: explicit shell export > stored settings > code default. The panel also adds an Endpoints group for custom Anthropic/OpenAI upstream base URLs and optional extra forwarded headers for gateway deployments. Mutating routes are loopback-gated and same-origin guarded; secret header values are masked and admin audit records only changed key names. ## Changes Made - Added `headroom/settings_store.py` with validation, masking, atomic save, partial-update merge semantics, and env application. - Added `/settings/schema`, `/settings`, `/settings/apply`, and `/dashboard/settings` routes. - Added same-origin protection for mutating local settings routes. - Added custom Anthropic/OpenAI endpoint and extra-header plumbing through CLI, provider registry, proxy config, and handlers. - Added deployment-aware apply/restart behavior for service/docker/foreground modes. - Added docs for the settings GUI and endpoint/header configuration. - Merged current `main`, added missing retry-delay settings registry entries, fixed the UI so no-op saves do not persist every default, and removed unrelated dependency/Cargo churn from the PR diff. ## Testing ```text uvx ruff@0.15.17 check headroom/settings_store.py headroom/proxy/server.py headroom/proxy/loopback_guard.py headroom/providers/registry.py headroom/proxy/helpers.py headroom/cli/main.py headroom/cli/proxy.py tests/test_proxy/test_settings_store.py tests/test_proxy_settings_endpoints.py tests/test_provider_registry.py tests/test_cli_proxy_env.py tests/test_header_isolation.py tests/test_install/test_runtime.py tests/test_proxy/test_settings_fresh_process_precedence.py All checks passed! uvx ruff@0.15.17 format --check headroom/settings_store.py headroom/proxy/server.py headroom/proxy/loopback_guard.py headroom/providers/registry.py headroom/proxy/helpers.py headroom/cli/main.py headroom/cli/proxy.py tests/test_proxy/test_settings_store.py tests/test_proxy_settings_endpoints.py tests/test_provider_registry.py tests/test_cli_proxy_env.py tests/test_header_isolation.py tests/test_install/test_runtime.py tests/test_proxy/test_settings_fresh_process_precedence.py 14 files already formatted uv run --extra dev python -m pytest tests/test_proxy/test_settings_store.py tests/test_provider_registry.py tests/test_cli_proxy_env.py tests/test_proxy_settings_endpoints.py tests/test_header_isolation.py tests/test_install/test_runtime.py tests/test_proxy/test_settings_fresh_process_precedence.py -q 192 passed, 1 skipped, 1 warning git diff --check headroomlabs/main...HEAD # no output ``` The pushed cleanup commits also passed local pre-commit hooks. ## Review Readiness - [x] Ready for review - [x] Regression tests added - [x] Documentation updated --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
cdba2eccdd
|
feat(core): gate ONNX transforms behind a default-on ml feature (static/lexical builds) (#2165)
## Description `TextCrusher` and the BM25 relevance path can run without the ONNX-backed ML stack, but `headroom-core` previously compiled `ort`, `fastembed`, and `magika` unconditionally. This made lexical-only downstream consumers carry the ONNX Runtime dependency even when they never used embedding relevance or Magika detection. This PR makes those ML crates optional behind a new default-on `ml` Cargo feature. Default builds keep the existing ML-backed behavior. Consumers that only need lexical compression can opt out with `default-features = false`; in that mode the ML modules are compiled out and the relevance path falls back to BM25. ## 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 - `crates/headroom-core/Cargo.toml`: marks `ort`, `fastembed`, and `magika` optional; adds default-on `ml = ["dep:ort", "dep:fastembed", "dep:magika"]`. - `crates/headroom-core/src/lib.rs`: gates the shared ONNX CPU helper behind `ml`. - `crates/headroom-core/src/relevance/embedding.rs`: gates the fastembed implementation behind `ml` and provides a no-ml stub with the same scorer surface so `HybridScorer` naturally falls back to BM25. - `crates/headroom-core/src/transforms/detection.rs`: gates the Magika tier behind `ml`; no-ml builds start at the existing unidiff/plain-text fallback tiers. - `crates/headroom-core/src/transforms/mod.rs`: gates the Magika module and re-exports behind `ml`. ## Testing - [x] Default build compiles (`cargo build -p headroom-core`) - [x] Lexical-only build compiles (`cargo build -p headroom-core --no-default-features`) - [x] Default tests pass (`cargo test -p headroom-core`) - [x] Lexical-only tests pass (`cargo test -p headroom-core --no-default-features`) - [x] Dependency tree checked for no-ml build (`cargo tree -p headroom-core --no-default-features` contains no `fastembed`, `magika`, or `ort` packages) - [ ] Manual testing performed ## Real Behavior Proof - Environment: Windows 11 review worktree, Rust/Cargo workspace. - Exact command / steps: - `cargo build -p headroom-core` - `cargo build -p headroom-core --no-default-features` - `cargo test -p headroom-core` - `cargo test -p headroom-core --no-default-features` - `cargo tree -p headroom-core --no-default-features` - Observed result: both feature configurations build and test cleanly. The no-default dependency tree does not include `fastembed`, `magika`, or `ort`, while the default build still compiles the ML path. - Not tested: model-backed `RUN_FASTEMBED_TESTS=1` cases that require downloading the embedding model; those remain env-gated as before. ## 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 The no-ml build intentionally degrades embedding relevance to the existing unavailable-model behavior, so `HybridScorer` takes its BM25 fallback path. Magika detection is skipped when `ml` is disabled; detection then proceeds through unidiff and plain-text fallback tiers. --------- Co-authored-by: Matthew Jackson <mattjackson86@gmail.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
6d897e8eaa
|
fix(memory): require explicit updates for supersession (#2188)
## Description The standalone Memory MCP `memory_save` handler currently treats vector similarity as update identity. A score of `0.70` can therefore supersede a valid but distinct memory that merely shares domain vocabulary. This change makes `memory_save` append-only. Supersession remains available through explicit update paths that receive an existing memory ID. Closes #2187. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that causes existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Remove vector-similarity-based auto-supersession from the standalone MCP `memory_save` handler. - Clarify in the tool description that corrections require an explicit update path with the existing memory ID. - Add a regression test proving that a high-scoring but distinct memory is neither searched for replacement nor updated. - Preserve the existing save result summary shape for compatibility. ## Testing - [x] Focused unit tests pass - [x] Linting passes (`ruff check` on changed files) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual focused test execution performed ### Test Output ```text uv run --with pytest --with numpy pytest tests/test_memory/test_mcp_server.py -q 9 passed, 21 warnings in 0.70s uvx ruff check headroom/memory/mcp_server.py tests/test_memory/test_mcp_server.py All checks passed! uvx ruff format --check headroom/memory/mcp_server.py tests/test_memory/test_mcp_server.py 2 files already formatted ``` The warnings are pre-existing pytest configuration and `datetime.utcnow()` deprecation warnings in the test environment. ## Real Behavior Proof - Environment: Python 3.13 with the MCP module stub and an async recording backend. - Exact command / steps: run `tests/test_memory/test_mcp_server.py`; the new regression supplies a search result with similarity `0.91`, then saves a distinct fact. - Observed result: `search_memories` and `update_memory` are not called; `save_memory` is called once with the new fact and requested importance. - Not tested: live embedding backends or migration of supersession chains created by earlier versions. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project style guidelines - [x] I have performed a self-review of my code - [x] I have commented the non-obvious identity boundary - [ ] Documentation changes are limited to the MCP tool description - [x] My changes generate no new warnings - [x] I have added tests that prove the fix is effective - [ ] New and existing unit tests pass locally; the focused MCP suite passes and full CI is pending - [ ] CHANGELOG update is not included because release notes are generated from conventional commits ## Screenshots (if applicable) Not applicable. ## Additional Notes This patch intentionally does not infer replacement identity from category, entity references, or a higher vector threshold: none of those alone proves that two statements are versions of the same fact. Exposing an explicit update tool from the standalone MCP server can be considered separately without retaining the unsafe automatic behavior. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
e5b3a634df
|
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189)
## Description Running Claude Code (Anthropic) and Codex (OpenAI) against the **same** Headroom proxy instance on one port produced incorrect, unstable dashboard data. The proxy core is provider-isolated and multi-provider-safe by design; the defect was in the observability layer. The Codex `/v1/responses` **WebSocket** handler was the only path in the proxy that wrote to the request logger by hand instead of through the unified `emit_request_outcome` funnel, and it did so twice per session close: the per-turn funnel record plus an unconditional cumulative session-summary `RequestLog`. This PR removes the duplicate summary log so Codex WS emits exactly one request log per turn, matching the HTTP provider paths. ## 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 - Dropped the duplicate cumulative session-summary `RequestLog` in the Codex WS handler while preserving the per-turn `emit_request_outcome` path. - Preserved gated `request_messages` and `turn_id` on residual outcomes so dashboard telemetry keeps the useful attribution without double-counting tokens. - Ensured explicit `--anyllm-provider` wins over a leaked `HEADROOM_ANYLLM_PROVIDER` environment variable. - Registered retry delay settings that had drifted out of the settings registry. - Hardened tests against developer-shell `HEADROOM_*` / `ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused proxy/wrap test fixtures. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/pytest tests/ -q -p no:cacheprovider 8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36) $ .venv/bin/ruff check <touched files> All checks passed! ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff via project venv, branch `fix/multi-provider-runtime`. - Exact command / steps: Ran the full test suite without pytest cache provider and Ruff on all touched files; used `git stash` to confirm the stale fake-config failures pre-existed this change. - Observed result: Full suite passed with no failures; Ruff passed; Codex WS now routes end-of-session logging through `emit_request_outcome`, emitting one request log per turn with the same accounting model as Anthropic HTTP turns. - Not tested: Live simultaneous Claude + Codex dashboard run. `mypy headroom` was not run to completion; a scoped run reported one pre-existing `settings_store.py:470` coercion error outside this 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 - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - server-side observability fix; no UI markup changed. ## Additional Notes - The proxy's multi-provider routing, header/auth isolation, and per-model cache keying are already correct and unchanged here; only the WS observability write path was double-counting. - Architectural assessment: `plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`; root-cause + resolution trail: `plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`. - No live simultaneous Claude + Codex dashboard run was performed; validation is from test coverage and code review of the WS logging path. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
a352fa0168
|
fix(proxy/bedrock): wire PrefixCacheTracker updates into Bedrock backend paths (#2196)
## Description `update_from_response()` was only called from the direct-Anthropic-API branch of `handle_anthropic_messages`. Both Bedrock backend branches (streaming and non-streaming) returned before ever reaching it, so `PrefixCacheTracker` state stayed permanently empty for the life of a session on any `--backend bedrock` deployment: `extract_cache_stable_delta()` always saw no previous turn, and `--mode cache` fell back to full unmodified passthrough on every turn instead of compressing the append-only delta. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/proxy/handlers/anthropic.py`: non-streaming Bedrock branch now mirrors the direct-API branch — builds `next_original_messages`/`next_forwarded_messages` from the response, runs cache-miss attribution, and calls `prefix_tracker.update_from_response()` before returning. - `headroom/proxy/handlers/streaming.py`: `_stream_response_bedrock` gains `prefix_tracker`/`optimized_messages` parameters (previously absent entirely), accumulates raw SSE bytes only when a tracker is present, reconstructs the assistant message via the existing `_parse_sse_to_response` helper in the `finally:` block, then updates the tracker. Mirrors `_finalize_stream_response` and the OpenAI-via-backend sibling (`_stream_openai_via_backend`), which already had this wiring. - `tests/test_bedrock_prefix_tracker_wiring.py` (new): drives real `PrefixCacheTracker` instances (via `session_tracker_store`, not a fake) through both the non-streaming and streaming Bedrock paths using `TestClient`, and asserts the tracker's turn counter and last-forwarded/-original messages actually advance after a Bedrock call. A second non-streaming test drives two turns and asserts turn 2 sees a nonzero `frozen_message_count` once the cached total clears `min_cached_tokens`. Verified these tests fail against the pre-fix `anthropic.py`/`streaming.py` (turn counter stuck at 0) and pass against the fix. - `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_bedrock_prefix_tracker_wiring.py tests/test_backend_nonstreaming_cache_metrics.py tests/test_backend_streaming_cache_metrics.py tests/test_bedrock_streaming_input_tokens.py tests/test_cache/test_prefix_tracker.py tests/test_cache_prefix_overlay.py tests/test_cross_turn_cache_safety.py tests/test_proxy_anthropic_cache_stability.py -q collected 91 items tests/test_bedrock_prefix_tracker_wiring.py ... [ 3%] tests/test_backend_nonstreaming_cache_metrics.py .... [ 7%] tests/test_backend_streaming_cache_metrics.py .... [ 12%] tests/test_bedrock_streaming_input_tokens.py .. [ 14%] tests/test_cache/test_prefix_tracker.py .................................. [ 49%] tests/test_cache_prefix_overlay.py ......... [ 69%] tests/test_cross_turn_cache_safety.py ... [ 72%] tests/test_proxy_anthropic_cache_stability.py ......................... [100%] ======================== 91 passed, 1 warning in 9.15s ========================= $ uv run ruff check headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/streaming.py tests/test_bedrock_prefix_tracker_wiring.py All checks passed! $ uv run mypy headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/streaming.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: personal fork deployed as a real proxy (macOS launchd service, `headroom install apply`) with `--backend bedrock --mode cache`, fronting a live Claude Code session. - Exact command / steps: ran a two-turn streaming conversation against the running Bedrock-backed proxy, then a third append-only turn, while temporarily adding debug logging around `prefix_tracker.get_frozen_message_count()` / `get_last_original_messages()` (removed before this commit; the automated tests above are the permanent record). - Observed result: before the fix, `prev_orig_len`/`prev_fwd_len` were always 0 on every turn including turn 2+ — the tracker never advanced past its cold-start state. After the fix, turn 2 shows `prev_orig_len`/`prev_fwd_len` populated from turn 1's response, and the append-only turn 3 correctly triggers the delta-compression path (`router:noop` transform, pipeline actually runs) instead of falling to the router-never-called passthrough. In a separate live session captured while validating this fix, one turn showed `cache_write=98242` in the PERF log, and the immediately following turn showed `cache_read=98242 cache_hit_pct=94` — direct proof that the Bedrock path is now feeding real cache-read/write data back into the tracker end-to-end on live traffic, not just synthetic test fixtures. - Not tested: the live full-suite run during development surfaced one pre-existing unrelated failure in `test_provider_model_fallback.py`, confirmed independently failing on the commit prior to this fix (i.e., not introduced by this change, not fixed by it either). ## 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 - No linked issue number: found via independent investigation of a personal deployment, not filed as a `headroomlabs-ai/headroom` issue first. - This is the more consequential of two related fixes from the same investigation; the sibling PR (`fix(proxy/savings): append history point on cache-only savings too`) fixes a savings-history reporting gap that this same `--mode cache` + Bedrock deployment surfaced. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
0537cbfde4
|
feat(dashboard): persist lifetime proxy metrics (#2198)
## Description Persist bounded, aggregate-only Lifetime dashboard metrics across proxy restarts and expose them through a new `/stats-lifetime` endpoint. The change keeps session/runtime stats separate from durable lifetime stats, gates sensitive dashboard metadata for loopback or explicitly trusted dashboard clients, and updates the dashboard Lifetime view to consume the new endpoint. Closes #2137 ## 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 - [x] Code refactoring (no functional changes) ## Changes Made - Added bounded persistent lifetime metrics state and wired proxy metric events into it. - Added `/stats-lifetime` with sensitive project/persistence details gated behind dashboard metadata access checks. - Extended loopback/dashboard metadata access policy for trusted dashboard client CIDRs without widening admin/debug endpoints. - Reorganized dashboard session/lifetime presentation around runtime counters versus durable aggregates. - Added focused tests for persistent aggregation, persistence, endpoint registration, loopback gating, trusted dashboard CIDRs, and recent request ordering. - Fixed current Ruff/mypy issues in the lifetime metrics normalization code. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text uv run --extra proxy --with pytest --with pytest-asyncio pytest tests/test_persistent_metrics.py tests/test_persistent_metrics_integration.py tests/test_persistent_metrics_persistence.py tests/test_proxy_loopback_gating.py tests/test_proxy_stats_recent_requests.py -q 53 passed, 1 warning uvx ruff==0.15.17 check headroom/proxy/forwarded_headers.py headroom/proxy/loopback_guard.py headroom/proxy/persistent_metrics.py headroom/proxy/savings_tracker.py headroom/proxy/server.py tests/test_forwarded_headers.py tests/test_persistent_metrics.py tests/test_persistent_metrics_integration.py tests/test_persistent_metrics_persistence.py tests/test_proxy_loopback_gating.py tests/test_proxy_project_savings.py tests/test_proxy_stats_recent_requests.py All checks passed! uv run --extra proxy --with mypy mypy headroom/proxy/persistent_metrics.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Windows review worktree, Python 3.13.3 via uv. - Exact command / steps: Ran the focused persistent metrics, persistence, loopback gating, and recent request tests; ran CI-matching Ruff on touched files; ran mypy on the new persistent metrics module. - Observed result: `/stats-lifetime` is registered, non-loopback callers receive only non-sensitive aggregate data, loopback/trusted dashboard clients receive the full lifetime payload, admin/debug endpoints remain loopback-only, and persistent metrics normalize malformed stored state without type/lint errors. - Not tested: Full repository pytest suite, full dashboard browser screenshot pass, or live long-running proxy traffic. ## 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 ## Additional Notes No changelog entry is required for this dashboard/internal metrics iteration. The endpoint intentionally exposes only aggregate lifetime data to ordinary network callers and strips project/persistence details unless the caller passes the dashboard metadata access policy. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
551f473e04
|
fix(proxy): accept Codex websocket before upstream retries (#2203)
## Description Codex Desktop could abandon Headroom's local `/v1/responses` WebSocket handshake before Headroom's upstream retry strategy had a chance to recover. The ChatGPT-auth path waited for an upstream opening handshake with a minimum 30-second timeout before sending the local 101, while the reported Codex Desktop handshake expired after about 34 seconds. This change accepts validated ChatGPT-auth Codex WebSockets before opening the upstream connection, then keeps the existing upstream retries and HTTP fallback behind the established local session. API-key sessions retain connect-before-accept behavior so upstream `x-codex-*` headers can still be attached to their client-facing 101. The change is scoped to the pre-101 timing failure and does not address the separate large-context streaming investigation in #1944. Closes #2184 ## 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 - Accept ChatGPT-auth Codex WebSocket clients before the upstream connect and retry loop. - Preserve API-key connect-before-accept ordering and upstream `x-codex-*` handshake-header forwarding. - Keep upstream retry, relay, usage-state refresh, and WebSocket-to-HTTP fallback behavior after the local 101. - Add a deterministic regression that blocks the first upstream opening handshake and proves the local acceptance deadline is independent of it. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_openai_codex_ws_lifecycle.py -q`) - [x] Linting passes (`uv run ruff check headroom/proxy/handlers/openai.py tests/test_openai_codex_ws_lifecycle.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_openai_codex_ws_lifecycle.py -q 28 passed in 2.02s uv run ruff check headroom/proxy/handlers/openai.py tests/test_openai_codex_ws_lifecycle.py All checks passed! uv run ruff format headroom/proxy/handlers/openai.py tests/test_openai_codex_ws_lifecycle.py --check 2 files already formatted ``` ## Real Behavior Proof - Environment: Python 3.12, synced development worktree, local fake Codex client and upstream WebSocket, no live provider - Exact command / steps: Run `uv run pytest tests/test_openai_codex_ws_lifecycle.py::test_chatgpt_ws_accepts_before_stalled_upstream_connect -q`; the fake upstream blocks its first opening handshake while the client enforces a bounded local-accept deadline. - Observed result: `1 passed in 0.46s`; the ChatGPT-auth client receives its local 101 before the blocked upstream connect is released, and the handler continues into its existing upstream recovery path. - Not tested: live Codex Desktop pre-turn compaction against ChatGPT subscription infrastructure ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes `CHANGELOG.md` is unchanged because the release pipeline generates it from conventional commits. No user documentation changes are required; the handler comments and ordered-flow docstring are updated with the auth-mode-specific behavior. The broader #1944 large-context disconnect surface remains out of scope. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
2de07db281
|
fix(memory): audit passive context injection (#2212)
## Description Close the passive-memory observability loop by recording access for context rows that survive the final injection budget and tagging requests where context is actually appended. Closes #2211 ## 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 - Track only memory IDs retained after ranking, similarity filtering, entry limits, and final text truncation. - Call optional backend `record_access` with stable de-duplication and fail-open error handling. - Extend structured injection logging to stamp `memory_injected=true` when injected bytes are positive. - Thread request tags through successful Anthropic, OpenAI Chat, OpenAI Responses, Gemini, and Codex WebSocket injection sites. - Add a static contract test that all current successful handler injection logs pass tags. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run --with pytest --with pytest-asyncio --with numpy --with fastapi pytest \ tests/test_memory_handler_native_ops.py \ tests/test_memory_auto_tail.py \ tests/test_memory_handler_project_isolation.py \ tests/test_memory_injection_logging.py -q 51 passed $ uv run --with ruff ruff check <touched Python files> All checks passed! $ git diff --check (no output) ``` ## Real Behavior Proof - Environment: Python 3.13 with synthetic backend and handler fixtures - Exact command / steps: run the focused test set above - Observed result: only IDs present after the final text budget are access-recorded; access-write failures remain fail-open; positive injection logs stamp `memory_injected=true`; all six current successful injection call sites pass tags - Not tested: live provider requests, full repository suite, third-party backends without `record_access` ## Review Readiness - [x] I have performed a self-review - [ ] 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 Access accounting is intentionally best-effort: unsupported backends and write failures do not delay or fail the upstream model request. Documentation and changelog changes are not needed for this internal observability fix. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
1c9585d42e
|
fix(stats): tag streamed output token source (#2214)
## Description Preserve the existing SSE output-token fallback while making its provenance visible to request logs and downstream statistics. Closes #2213 ## 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 - Tag provider-reported streaming output tokens with `output_tokens_source=provider`. - Tag the existing `total_bytes // 40` fallback with `output_tokens_source=estimated_bytes`. - Copy incoming tags before adding provenance so caller-owned dictionaries are not mutated. - Add focused coverage for both source values and the unchanged fallback estimate. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run --with pytest --with pytest-asyncio --with fastapi --with httpx --with numpy pytest \ tests/test_proxy_streaming_request_logger.py \ tests/test_request_outcome.py \ tests/test_proxy_handler_helpers.py -q 77 passed $ uv run --with ruff ruff check headroom/proxy/handlers/streaming.py tests/test_proxy_streaming_request_logger.py All checks passed! $ uv run --with ruff ruff format --check headroom/proxy/handlers/streaming.py tests/test_proxy_streaming_request_logger.py 2 files already formatted ``` ## Real Behavior Proof - Environment: Python 3.13 with the real request logger and synthetic stream state - Exact command / steps: run the focused test set above - Observed result: parsed usage records `provider`; a 200-byte no-usage stream still records 5 output tokens and tags it `estimated_bytes` - Not tested: live provider stream, full repository suite ## Review Readiness - [x] I have performed a self-review - [ ] 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 This PR does not change the fallback formula or token totals. Documentation and changelog changes are not needed for the new internal outcome tag. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
ce52b30c8f
|
feat(memory): add explicit supersession repair (#2217)
## Description Add an explicit, reviewable way to detach one incorrect supersession edge while preserving both memories and all neighboring version history. Closes #2216 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add an atomic SQLite `detach_supersession(old_id, new_id)` primitive that requires reciprocal direct lineage. - Restore only the old memory's validity and clear only the selected edge. - Re-index both affected memories and refresh cache state through `HierarchicalMemory`. - Expose the operation through `LocalBackend`. - Add `headroom memory repair-supersession OLD_ID NEW_ID`, dry-run by default with explicit `--apply`. - Resolve unambiguous partial IDs for preview but pass full IDs to the mutation. - Add chain-locality, rejection, index/cache, dry-run, and apply-path tests. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run --with pytest --with pytest-asyncio --with numpy --with fastapi --with httpx pytest \ tests/test_memory/test_supersession_repair.py \ tests/test_memory/test_hierarchical.py::TestSQLiteMemoryStore \ tests/test_cli/test_main_help_version.py -q 22 passed $ uv run --with ruff ruff check <touched Python files> All checks passed! $ uv run --with ruff ruff format --check <touched Python files> 6 files already formatted ``` ## Real Behavior Proof - Environment: Python 3.13, temporary SQLite databases, synthetic two- and three-version chains - Exact command / steps: run the focused test set above - Observed result: detaching `v1 -> v2` restores `v1` as current, leaves `v2 -> v3` intact, re-indexes both records, refreshes cache, and keeps CLI preview read-only until `--apply` - Not tested: live proxy process, external MemoryStore/VectorIndex plugins, full repository suite ## Review Readiness - [x] I have performed a self-review - [ ] 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 - [ ] 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 This is intentionally separate from #2188: that PR prevents new false edges, while this PR repairs historical data. The CLI help requires stopping any proxy that is actively using the same database before `--apply`, because another process can retain an old in-memory index snapshot. External backend semantics and stronger cross-store rollback behavior are left visible for maintainer review before this Draft is marked ready. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
528517cff8
|
fix(diff-compressor): CJK-aware relevance scoring for hunk selection (#2220)
## Description `score_hunks` boosts diff hunks whose content overlaps the query/context (+`SCORE_CONTEXT_WORD_WEIGHT` per match); the resulting score decides which hunks survive when `max_hunks_per_file` fires. It split the context on whitespace, so a spaceless CJK query became one blob that only matched a hunk containing the whole query verbatim — relevant hunks weren't boosted and got dropped. This adds CJK character bigrams to the query match set so a CJK query boosts the hunks it overlaps. Rust-only (`diff_compressor.py` is a thin shim over Rust; hunk scoring lives only in Rust). CJK-gated: for a pure-ASCII query `cjk_bigrams` returns an empty set and the new loop is a no-op, so non-CJK scoring is byte-identical and the 20 diff parity fixtures stay green. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `crates/headroom-core/src/transforms/diff_compressor.rs`: add `is_cjk_char` + `cjk_bigrams`, and a separate loop in `score_hunks` that boosts hunks containing each CJK query bigram. The existing ASCII word loop is untouched. - Rust unit test (`cjk_bigrams` extraction) + an end-to-end test (a CJK query promotes the overlapping hunk into the kept set; the no-query baseline drops it). ## Testing - [x] Unit tests pass (`cargo test`) - [x] Linting passes (`cargo clippy` / `cargo fmt`) - [x] New tests added for new functionality - [x] Manual testing performed (see Real Behavior Proof) ### Test Output ```text $ cargo test -p headroom-core --lib diff_compressor test result: ok. 23 passed; 0 failed $ cargo clippy -p headroom-core # clean ``` ## Real Behavior Proof - Environment: macOS (Darwin), Rust via cargo, branch `feat/diff-compressor-cjk` off `main`. - Exact command / steps: `cargo test -p headroom-core --lib diff_compressor` — the `cjk_query_boosts_matching_hunk_into_kept_set` test builds a diff with 4 hunks (first / plain / cjk / last), `max_hunks_per_file = 3` (one contested middle slot between the plain hunk at change-density `0.12` and the CJK hunk at `0.06`), and compresses it once with the CJK context `数据库连接超时排查` and once with no query. - Observed result: with no query the higher-density plain hunk takes the slot (the CJK hunk `数据库连接失败重试` is dropped); with the CJK context its bigrams (`数据` / `据库` / `库连` / `连接`) match → score `0.06 + 4×0.2 = 0.86` beats the plain hunk's `0.12` → the CJK hunk survives. Both directions are asserted; before this change the spaceless CJK query matched neither hunk and the CJK hunk was always dropped. - Not tested: the Python side — `diff_compressor.py` is a thin shim that delegates `compress()` straight to Rust, so hunk scoring has no Python twin; and no new parity fixtures were recorded, since the 20 existing diff fixtures contain no CJK and therefore stay byte-identical. ## 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 scoring) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A: internal relevance-scoring fix, no user-facing surface change ## Additional Notes - Completes the relevance-scorer CJK sweep across the compressors (search, adaptive sizer, the shared BM25 tokenizer, and now diff). Rust-only — no Python parity mirror is needed because diff hunk scoring has no Python twin (the shim delegates `compress()` straight to Rust). |
||
|
|
5de12f75e3
|
docs(ccr): correct stale 5-minute TTL hints to 30 minutes (#2224)
## Description The CCR store default TTL is `DEFAULT_TTL = 1800s` (30 minutes — see `crates/headroom-core/src/ccr/mod.rs` and `config.py store_ttl_seconds=1800`), but several user-facing hints and docstrings still said "5 minutes", the old default. The opencode/openclaw retrieve tools surfaced `(default TTL: 5 minutes)` in their expiry hint — exactly the misleading message reported in #1023. (The CCR cache itself works; the row-drop store bridge that populates the retrieve store landed for #389.) This corrects the two plugin hints, the `InMemoryCcrStore` docstrings, the SQLite/backend default TTL comments, and the `smart_crusher` mirror comment. The `mod.rs` comment that references "the *old* 5-minute default" is intentionally left unchanged — it correctly describes history. ## Type of Change - [x] Documentation update ## Changes Made - `plugins/openclaw/src/tools/headroom-retrieve.ts` + `plugins/opencode/src/retrieve.ts`: retrieve-failure hint `5 minutes` → `30 minutes`. - `crates/headroom-core/src/ccr/backends/in_memory.rs`: two docstrings (`5 minutes by default`, `5-minute TTL`) → `30 minutes` / `30-minute`. - `crates/headroom-core/src/ccr/backends/mod.rs` + `sqlite.rs`: SQLite/default backend TTL comments `5-minute` → `30-minute`. - `headroom/transforms/smart_crusher.py`: mirror comment `defaults to 5 minutes` → `30 minutes`. ## Testing - [x] Linting passes (`ruff` / `cargo check`) - [x] Manual verification (see Real Behavior Proof) ### Test Output ```text $ ruff format --check headroom/transforms/smart_crusher.py # clean $ cargo check -p headroom-core # Finished, no errors ``` ## Real Behavior Proof - Environment: macOS (Darwin), branch `feat/ccr-ttl-hint-fix` off `main`. - Exact command / steps: grepped every `5 minutes` / `5-minute` TTL reference across the repo; confirmed the real default is `DEFAULT_TTL = Duration::from_secs(1800)` (`ccr/mod.rs:66`), that `InMemoryCcrStore::new()` uses `DEFAULT_TTL` (not a local 300s), and that `config.py` sets `store_ttl_seconds = 1800 # 30 minutes`. - Observed result: all stale CCR default-TTL "5 minutes" references now read "30 minutes"; the one historical reference (`mod.rs`: "the old 5-minute default") is left as-is because it is accurate. - Not tested: nothing runtime changed — these are docstring/comment/hint string edits only, so there is no behavior to exercise. ## 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 — N/A (this PR is comments/strings) - [x] I have made corresponding changes to the documentation (this *is* the doc change) - [x] My changes generate no new warnings - [ ] I have added tests — N/A (no behavior change) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A: user-facing hint/docstring correction, no functional change ## Additional Notes - Surfaced while root-causing #1023: the "cache permanently empty / TTL: 5 minutes" report is resolved on `main` (the store-bridge for #389 populates the retrieve store), but the stale "5 minutes" strings the reporter actually saw were still in the tree. This PR fixes those. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
daca1dd756
|
fix(cli/init): fail clearly on a target settings file with invalid JSON (#2227)
## Description
`headroom init` crashes with a raw traceback when a target's settings
file contains invalid JSON.
`_json_file` reads the JSON config files that init read-merge-writes
(Claude's `settings.json`, Codex's `hooks.json`, etc.):
```python
def _json_file(path: Path) -> dict[str, Any]:
if not path.exists():
return {}
content = path.read_text(encoding="utf-8").strip()
if not content:
return {}
payload = json.loads(content) # unguarded
return payload if isinstance(payload, dict) else {}
```
These are user-owned files that people hand-edit, so a stray trailing
comma or an unquoted key is entirely plausible. When that happens
`json.loads` raises `json.JSONDecodeError` and it propagates all the way
out, so `headroom init` dies with a Python traceback instead of a usable
message.
Returning `{}` on the error would be worse, not better: every caller
does `payload = _json_file(path)` then `_write_json(path, payload)`, so
an empty dict would make init overwrite the user's real settings with
just the hooks/env block — silent data loss.
## Fix
Guard the parse and convert it into an actionable `ClickException` that
names the file and the parse error, leaving the file untouched:
```python
try:
payload = json.loads(content)
except json.JSONDecodeError as e:
raise click.ClickException(
f"{path} contains invalid JSON ({e}); fix it and re-run, or move it aside."
) from e
```
`click.ClickException` is already the project's convention for
user-facing init failures (e.g. the `'claude' not found in PATH`
messages). The user now gets a clear instruction, and their file is
never clobbered.
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/cli/init.py`: wrap the `json.loads` in `_json_file` and
raise a `ClickException` on `JSONDecodeError`.
- `tests/test_cli/test_init_cli.py`: new test asserting a malformed file
raises `ClickException` (matching "invalid JSON") and is left
byte-for-byte untouched.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ uvx ruff@0.15.17 check headroom/cli/init.py tests/test_cli/test_init_cli.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/cli/init.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17` / `uvx
mypy@1.20.2`. A full `pytest` OOM-kills this box (ML stack import), so I
reproduced the behavior with a dependency-free script mirroring
`_json_file` and left the full pytest to CI.
- Exact command / steps: wrote a settings file containing `{"env": {"A":
"B",}}` (trailing comma), then called the OLD unguarded reader and the
NEW guarded reader; also re-checked a valid file round-trips.
- Observed result: OLD raises a raw `json.JSONDecodeError` (the init
traceback); NEW raises a `ClickException` containing "invalid JSON" and
leaves the file byte-for-byte unchanged; valid JSON still parses to the
same dict.
- Not tested: a full `headroom init` end-to-end run; full local `pytest`
deferred to CI (OOM).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" box is unchecked because the full suite
imports the ML stack, which I can't run here. The new test uses the
existing `_load_init_module` harness (the same one the neighbouring
`test_json_file_*` tests use), so it runs under the normal CI pytest
job; behaviour is additionally verified by the standalone proof above.
|
||
|
|
f71fef1ca6
|
fix(claude): treat non-zero claude --version exit as version-unknown … (#2233)
## Description
Treat a non-zero `claude --version` exit as an unknown Claude Code
version, even if the failing command prints a version-shaped string to
stdout or stderr.
This is a follow-up to the Remote Control gate work for #1779/#1883. The
callers rely on `None` to use the self-qualified "2.1.196+ / unknown"
warning path; accepting a version from a failed command can produce a
false exact-version warning.
## 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/claude/runtime.py`: return `None` from
`detect_claude_code_version` when the `claude --version` subprocess has
a non-zero return code.
- `tests/test_issue_1779_remote_control_gate.py`: add a regression test
where a failing process still prints `2.1.196 (Claude Code)` and must be
treated as unknown.
## 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 pytest tests/test_issue_1779_remote_control_gate.py -q
50 passed
$ uvx ruff==0.15.17 check headroom/providers/claude/runtime.py tests/test_issue_1779_remote_control_gate.py --output-format concise
All checks passed!
$ uvx ruff==0.15.17 format --check headroom/providers/claude/runtime.py tests/test_issue_1779_remote_control_gate.py
2 files already formatted
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12/3.13 test environment, local
checkout of this PR branch.
- Exact command / steps: ran the focused Remote Control gate test file,
including the new regression that stubs `claude --version` as
`returncode=1` with version-shaped stdout.
- Observed result: `detect_claude_code_version("claude")` returns `None`
for the failed command, preserving the unknown-version path; existing
parser/gate tests still pass.
- Not tested: an actual failing Claude Code binary invocation on a user
machine; the subprocess behavior is covered by the regression stub.
## 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
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
cbfa267c5f
|
fix(deps): enforce transformers security floor (#2201)
## Description Raise the production `transformers` dependency floor so the security workflow cannot resolve the CVE-2026-5241 vulnerable range reported by `pip-audit`, and refresh the small current-main test fixtures needed for the PR matrix to run green. ## 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 - Raised direct optional `transformers` declarations for `proxy`, `ml`, and `voice` extras to `>=5.5.0,<6.0`. - Refreshed `uv.lock` metadata so `uv export --extra all` resolves a patched `transformers` version for the production audit set. - Kept the current-main test fixture fixes for the ZCode setup printer and deferred compression fallback metrics. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv lock --check Resolved 238 packages in 1ms $ uv export --frozen --no-dev --no-emit-project --no-hashes --extra all --format requirements-txt | rg "^transformers==|^huggingface-hub==" huggingface-hub==1.16.1 transformers==5.13.1 $ uv export --frozen --no-dev --no-emit-project --no-hashes --extra all --format requirements-txt > requirements-prod.txt $ uvx pip-audit -r requirements-prod.txt No known vulnerabilities found $ uv run --with pytest --with pytest-asyncio --with fastapi --with httpx --with numpy pytest tests/test_cli/test_wrap_zcode.py::test_wrap_prints_proxy_urls tests/test_cold_start_fast_pass.py::test_fast_pass_failure_falls_back_to_full_deferral -q 2 passed $ uvx ruff==0.15.17 check headroom/cli/wrap.py headroom/proxy/handlers/anthropic.py --output-format concise All checks passed! $ git diff --check passed ``` ## Real Behavior Proof - Environment: Windows checkout plus the same frozen production dependency export shape used by the GitHub Actions security workflow. - Exact command / steps: raised the `transformers` floor, refreshed `uv.lock`, exported `--extra all` production requirements, ran `pip-audit`, then reproduced the focused ZCode and deferred-compression tests. - Observed result: the export resolves `transformers==5.13.1`; `pip-audit` reported no known vulnerabilities; the focused tests pass locally; CI is rerunning on the updated head. - Not tested: full GitHub Actions matrix locally; CI is running the complete suite on this PR. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] 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 - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - dependency metadata and CI fixture fix. ## Additional Notes This PR is intentionally scoped to clearing the current red mainline security gate while keeping the small fixture updates needed by the branch test matrix. |
||
|
|
6413cc75a2
|
fix(wrap): read/write instruction files as UTF-8 on Windows (#1245)
## Description Fixes #1126. On a Windows (cp1252) locale, `headroom wrap` crashes with `UnicodeDecodeError` the first time it injects guidance into a user instruction file that contains non-ASCII prose (e.g. typographic quotes `“happy places”` or an em-dash). `_inject_rtk_instructions` and `_inject_memory_agents_md` both read the existing file and append/create it with a bare `read_text()` / `open()` / `write_text()`, so the default codec (cp1252, not UTF-8) chokes on the multi-byte characters. This is the same bug class already fixed for the `learn` pipeline (#1202) and earlier for other wrap paths — here it's the instruction-file injectors. ## 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/wrap.py`: in `_inject_rtk_instructions` and `_inject_memory_agents_md`, read the existing instruction file as `encoding="utf-8", errors="replace"` and append/create with `encoding="utf-8"`. The read only feeds the marker-existence check and the append doesn't rewrite existing bytes, so replacement can't corrupt the file. - `tests/test_cli/test_wrap_encoding.py`: new regression tests. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_cli/test_wrap_encoding.py tests/test_cli/test_wrap_hintfile_agents.py -q 16 passed $ ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_encoding.py All checks passed! ``` The new tests are **red on the old code, green with the fix**: injecting into a file with a typographic quote plus a stray `0x9d` byte (undefined in cp1252 and invalid UTF-8, so a bare `open()` fails on any locale) — the append and idempotent paths fail before the fix (4 failed) and pass after (6 passed). ## Real Behavior Proof - Environment: Windows 11, Python 3.10, against the real `headroom.cli.wrap` injectors (no live agent launch; the decode failure is at file read time). - Exact command / steps: `write_bytes` an `AGENTS.md` containing `"Be in “happy places” — really.\n"` plus a stray `0x9d` byte, then call `_inject_rtk_instructions(path)` / `_inject_memory_agents_md(path)`. - Observed result: **before** the fix → `UnicodeDecodeError: 'utf-8' codec can't decode byte 0x9d` (and on a real cp1252 locale, the same on the typographic quotes alone); **after** → both return `True`, the marker is present, the pre-existing prose is preserved, and re-running is idempotent. - Not tested: a full end-to-end `headroom wrap copilot` against a live Copilot CLI (verified at the injector level, which is where the decode crash lives). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
9e376afabe
|
fix(mcp): mcp status checks ~/.claude.json, not only ~/.claude/mcp.json (#990)
## Description `headroom mcp status` only inspected `~/.claude/mcp.json`, but servers registered via `claude mcp add` (user scope) live in `~/.claude.json`. So `status` printed `✗ No config file` even when headroom was registered and `claude mcp list` reported it Connected. This detects the registration across every location Claude Code uses. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Add `find_headroom_registration()` that checks `~/.claude.json`, `~/.claude/mcp.json`, then `./.mcp.json` (first match wins). - Use it in `mcp status` for both the "Configured" check and the proxy-URL lookup. ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_cli/test_mcp_status.py -q 5 passed in 0.13s ``` ## Real Behavior Proof - Environment: Linux, Python 3.13, editable build of this branch - Exact command / steps: registered headroom in ~/.claude.json, then ran `headroom mcp status` - Observed result: prints `✓ Configured` with the ~/.claude.json path (previously `✗ No config file`) - Not tested: project-scoped ./.mcp.json discovery in a real multi-repo workflow ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
0c7087539d
|
fix: add DeepSeek V4/V3.2/R1 tokenizer mappings and context limits (#912)
The tokenizer registry routed deepseek-v4-pro, deepseek-v4-flash,
deepseek-chat, deepseek-reasoner, and other modern DeepSeek models
to the 2023-era deepseek-llm-7b-base tokenizer via prefix fallback.
This caused token counts off by 30-50%, broken context-limit detection
(V4-Pro supports 1M but got 32K), and inaccurate savings reports.
## Fix
3 files, +43/-2:
- **huggingface.py**: 16 new MODEL_TO_TOKENIZER entries with verified
HuggingFace IDs (deepseek-ai/DeepSeek-V4-Pro, V4-Flash, V3.2,
V3-0324, R1, R1-0528, Reasoner, Chat, Coder-V2, etc.)
- **openai_compatible.py**: 17 new _DEFAULT_CONTEXT_LIMITS entries
(V4-Pro/Flash -> 1M, R1/Reasoner -> 131K, V3 -> 128K, etc.)
- **openai.py**: 8 new _CONTEXT_LIMITS entries for LiteLLM-fallback.
Existing mappings untouched (backward compatible).
## Real behavior proof
- **Setup**: Windows 11, Python 3.13.14, headroom-ai 0.2.15 wheel +
source checkout at v0.24.0. No Rust extension built (headroom._core
unavailable). Touched files are at parity with v0.24.0.
- **Steps after patch**:
```
python3 -c "
from headroom.tokenizers.huggingface import get_tokenizer_name
for m in
['deepseek-v4-pro','deepseek-chat','deepseek-reasoner','deepseek-v4-flash']:
print(f'{m} -> {get_tokenizer_name(m)}')
from headroom.tokenizers.registry import get_tokenizer
for m in ['deepseek-v4-pro','deepseek-chat','deepseek-reasoner']:
print(f'{m}: {get_tokenizer(m)}')
"
```
- **Observed result**:
```
deepseek-v4-pro -> deepseek-ai/DeepSeek-V4-Pro
deepseek-v4-flash -> deepseek-ai/DeepSeek-V4-Flash
deepseek-chat -> deepseek-ai/DeepSeek-V3
deepseek-reasoner -> deepseek-ai/DeepSeek-R1
```
Previously ALL resolved to deepseek-ai/deepseek-llm-7b-base.
TokenizerRegistry routes correctly. Context limits verified
(1M / 131K / 128K). compress() import smoke-tested OK.
- **Not tested**: full proxy e2e with a live DeepSeek API key
(no available key). HuggingFace AutoTokenizer download confirmed
for V4-Pro/V3/R1 but produced GBK decode errors from hf_hub on
this zh-CN Windows locale during config fetch -- a separate
huggingface_hub issue unrelated to this change.
<!-- headroom-maintainer-template-completion:start -->
## Description
This PR prepares `fix: add DeepSeek V4/V3.2/R1 tokenizer mappings and
context limits` for review by documenting the intended change,
validation evidence, and remaining merge-readiness context.
Linked issues: None declared.
## Type of Change
- [x] Bug fix
- [ ] New feature
- [ ] Documentation
- [ ] Refactor
- [ ] Tests only
## Changes Made
- Commit: fix: add DeepSeek V4/V3.2/R1 tokenizer mappings and context
limits
- Touches `headroom/providers/openai.py`
- Touches `headroom/providers/openai_compatible.py`
- Touches `headroom/tokenizers/huggingface.py`
## Testing
- [x] GitHub checks reviewed
- [x] Metadata/template validation
- [ ] Local functional testing
### Test Output
```text
gh pr view 912 --repo chopratejas/headroom --json statusCheckRollup
- PR Governance / label: SUCCESS
- external / GitGuardian Security Checks: SUCCESS
```
## Real Behavior Proof
- Environment: GitHub PR metadata and checks for `chopratejas/headroom`
PR #912.
- Exact command / steps: Reviewed PR title, commits, changed files,
linked issues, labels, and check rollup; appended this maintainer
template completion block without replacing the author's original
description.
- Observed result: PR body now contains all required governance
sections, checked readiness fields, and a non-placeholder validation
evidence block.
- Not tested: This pass updated PR metadata only; code validation
remains represented by the linked GitHub checks and any author-provided
evidence above.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
<!-- headroom-maintainer-template-completion:end -->
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
36202f4d0b
|
fix(windows): unwedge compression on degraded ONNX runtimes (every request timing out at 30s, 0% savings) (#822)
## Summary Multiple Windows users reported (via Discord, on v0.23.0, `pip install "headroom-ai[all]"`) that the proxy delivers **zero compression** and adds **+30s latency to every request**: `Optimization failed: TimeoutError:` with `compression_first_stage ≈ 30000ms` on every optimization attempt, for the lifetime of the process. Log analysis showed the wedge starts at the **first message eligible for real compression** (earlier requests succeed because everything is skipped/excluded) — and never recovers, even though the Kompress model loaded successfully at startup. ### Root cause chain 1. `create_cpu_session_options` disabled ONNX Runtime's CPU memory arena on **all** platforms. On Windows this is catastrophic: every `Run()` falls back to per-node `VirtualAlloc`/free, slowing ModernBERT inference by 2–3 orders of magnitude (onnxruntime#11627). One reporter's perf summary showed max optimization overhead of **200,369ms** (~13 chunks × ~15s) — slow, not deadlocked. 2. The first slow inference outlives the proxy's 30s compression-stage timeout. `asyncio.wait_for` abandons the future but **cannot kill the executor thread**, which keeps holding the Kompress `BoundedSemaphore(1)`. 3. Every later compression blocks on an **unbounded** `semaphore.acquire()`, times out at exactly 30s, and leaks another thread — permanently wedging the proxy until restart. Two adjacent Windows bugs found in the same logs are fixed too: `subprocess.run(text=True)` without `encoding=` decodes child output with cp1252, so rtk's emoji output killed reader threads (`UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f`); and the OpenAI handler logged `Optimization failed: ` with an empty message because `str(asyncio.TimeoutError())` is empty. ### Fixes - **`onnx_runtime.py`** — keep the CPU arena at ORT's default on Windows; Linux/macOS keep the legacy low-RSS behavior (arena disabled) bit-for-bit. New `HEADROOM_ONNX_CPU_ARENA` env overrides either way. All ONNX sessions (Kompress, image router, memory embedders) share this helper, so one fix covers them all. - **`kompress_compressor.py`** — three layers of wedge-proofing, each fail-safing to passthrough instead of blocking: - bounded semaphore acquire (`HEADROOM_KOMPRESS_ACQUIRE_TIMEOUT_SECONDS`, default 5s) - wall-clock budget per compress/compress_batch call (`HEADROOM_KOMPRESS_TIME_BUDGET_SECONDS`, default 20s — under the 30s stage timeout, so Kompress gives up before the request is abandoned). Batch bail never emits a partially-covered text. - preload canary (`HEADROOM_KOMPRESS_CANARY_SECONDS`, default 5s, one retry to forgive cold-start warmup): machines that can never finish inference inside the stage timeout get ML compression disabled up front with one actionable warning, instead of a guaranteed 30s timeout per request. - Setting any knob `<= 0` disables that guard (restores legacy behavior). First give-up logs at WARNING with remediation hints; repeats drop to DEBUG. - **`proxy/helpers.py`, `interceptors/astgrep.py`** — `encoding="utf-8", errors="replace"` on rtk/lean-ctx/ast-grep subprocess calls. - **`handlers/openai.py`** — failure log now includes request id + exception type, matching the Anthropic handler. ### Non-Windows perf - Session options on Linux/macOS are unchanged (pinned by tests). - The only new hot-path cost is one `time.monotonic()` + a bounded acquire per chunk: micro-benchmarked at sub-microsecond (bounded acquire measured marginally *faster* than the old context-manager acquire), vs 50–500ms of inference per chunk. - Real-model smoke run on macOS: identical compression output (ratio 0.262 on a 1020-word sample), canary passes, budget/acquire give-up paths verified against the real ONNX stack by forcing tiny env values. Related (same symptom, different root cause — **not** addressed here): #810 tracks the blocked-tiktoken-download hang, which produces the same per-request 30s `TimeoutError` signature. The bounded-acquire/budget changes in this PR limit the blast radius of Kompress-side slowness only. ## Validation - `.venv/bin/ruff check headroom/ tests/...` — clean - `.venv/bin/ruff format --check` — clean (355 files) - `.venv/bin/mypy` on all five changed source files — no issues - `python -m pytest tests/test_onnx_runtime.py tests/test_kompress_failsafe.py tests/test_subprocess_encoding.py` — 25 passed (new coverage: arena platform matrix + env overrides, stuck-semaphore passthrough for compress and batch, budget bail incl. mid-batch no-data-loss, canary trip/pass/retry/disable/error-safety, UTF-8 subprocess kwargs) - `python -m pytest tests/test_transforms_content_router.py tests/test_proxy_handler_helpers.py tests/test_codex_ws_compression_scheduler.py tests/test_proxy_warmup.py tests/test_proxy_pipeline_lifecycle.py` — 52 passed (existing suites for touched areas) <!-- headroom-maintainer-template-completion:start --> ## Description This PR prepares `fix(windows): unwedge compression on degraded ONNX runtimes (every request timing out at 30s, 0% savings)` for review by documenting the intended change, validation evidence, and remaining merge-readiness context. Linked issues: None declared. ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Documentation - [ ] Refactor - [ ] Tests only ## Changes Made - Commit: fix(windows): unwedge compression on degraded ONNX runtimes - Commit: fix(kompress): run preload canary off the startup path - Touches `headroom/onnx_runtime.py` - Touches `headroom/proxy/handlers/openai.py` - Touches `headroom/proxy/helpers.py` - Touches `headroom/proxy/interceptors/astgrep.py` - Touches `headroom/transforms/kompress_compressor.py` - Touches `tests/test_kompress_failsafe.py` ## Testing - [x] GitHub checks reviewed - [x] Metadata/template validation - [ ] Local functional testing ### Test Output ```text gh pr view 822 --repo chopratejas/headroom --json statusCheckRollup - CI / changes: SUCCESS - CodeQL / Analyze (actions): SUCCESS - Evaluation Suite / smoke-test: SUCCESS - Init E2E / docker-init-e2e: SUCCESS - PR Governance / label: SUCCESS - Wrap E2E / docker-wrap-e2e: SUCCESS - CodeQL / Analyze (c-cpp): SUCCESS - CodeQL / Analyze (javascript-typescript): SUCCESS - CodeQL / Analyze (python): SUCCESS - CodeQL / Analyze (rust): SUCCESS - Evaluation Suite / weekly-suite: SKIPPED - CI / commitlint: SUCCESS ``` ## Real Behavior Proof - Environment: GitHub PR metadata and checks for `chopratejas/headroom` PR #822. - Exact command / steps: Reviewed PR title, commits, changed files, linked issues, labels, and check rollup; appended this maintainer template completion block without replacing the author's original description. - Observed result: PR body now contains all required governance sections, checked readiness fields, and a non-placeholder validation evidence block. - Not tested: This pass updated PR metadata only; code validation remains represented by the linked GitHub checks and any author-provided evidence above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review <!-- headroom-maintainer-template-completion:end --> --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
cb6c828457
|
fix(proxy): one bad extension no longer aborts proxy startup (#2215)
## What
`install_all()` (the `headroom.proxy_extension` loader) previously let
any exception from an extension's `install()` **propagate and abort
proxy startup** — one broken or version-incompatible third-party
extension took the whole proxy down, and every other extension with it.
This makes extension loading resilient:
- catch a failing `install()`, log it (with traceback), record it as
**skipped**
- continue installing the rest — a failure disables that one extension,
not the proxy
- print a `SKIPPED` line to the console (the startup banner lists
*enabled* extensions before install runs, so a skip would otherwise be
logging-config dependent)
## Why
Found while testing several proxy extensions together in a clean venv: a
plugin built against a newer core API raised `ModuleNotFoundError` from
`install()` and crashed the proxy at startup. An extension that fails
its own environment/auth check should disable itself — it should not
take the whole proxy down.
## Real behavior proof
Before — one extension failing in `install()`:
```
... proxy did NOT come up (/livez never answered)
```
After — same setup, one extension deliberately broken:
```
[headroom] proxy extensions SKIPPED: myorg_ext (install failed — running without them; see logs)
/livez: 200 healthy # proxy up; the other extensions installed
```
Loader unit check (fake failing extension):
```
returned installed: ['good_ext'] # bad one excluded
bad_ext skipped (not in installed): True
good_ext survived: True
warning logged for bad_ext: True
```
## Tests
- `mypy headroom/proxy/extensions.py` → `Success: no issues found`
- `ruff check headroom/proxy/extensions.py` → `All checks passed!`
- Verified in-process (catch/skip/continue + logging) and end-to-end
against a running proxy (`/livez` 200 with a deliberately failing
extension).
## Maintainer Follow-up
- Added `tests/test_proxy_extensions.py` covering skip-and-continue
behavior for a failed extension and the missing-extension warning path.
- Removed an informal implementation comment from
`headroom/proxy/extensions.py`.
- Validation on `
|
||
|
|
79d8056fd7
|
fix(mcp): regenerate stale server.json (0.27.0 -> 0.32.0) (#2218)
## Description The committed `server.json` pinned version `0.27.0` while `pyproject.toml` is at `0.32.0`. `tests/test_mcp_registry/test_server_json.py::test_root_server_json_matches_builder` asserts the committed artifact equals `render_server_json()`, so it fails on `main`. This regenerates `server.json` from the current metadata. Found while getting the security PR (#2207) CI green. The two other pre-existing failures it was grouped with were **already fixed on `main`** by recent commits — `test_cold_start_fast_pass` (`record_compression_failed` added to the metrics double) and `test_cli/test_wrap_zcode` (watcher mock now passes the port) — so this PR only needs the `server.json` regen. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Regenerated `server.json` from `render_server_json()` so the committed artifact matches the current package version (`0.32.0`). ## Testing - [x] Unit tests pass (`pytest`) — the previously-failing tests - [x] Linting passes (`ruff check`) ### Test Output ```text $ pytest tests/test_mcp_registry/test_server_json.py::test_root_server_json_matches_builder \ tests/test_cold_start_fast_pass.py::test_fast_pass_failure_falls_back_to_full_deferral \ tests/test_cli/test_wrap_zcode.py::test_wrap_prints_proxy_urls -q 3 passed ``` ## Real Behavior Proof - Environment: branch off current `main` (`ea3d5a86`), Python 3.12, project `.venv`. - Steps: `python -c "from headroom.mcp_registry import render_server_json; open('server.json','w').write(render_server_json())"`, then ran the MCP registry test. - Observed: `server.json` `version` → `0.32.0`; `test_root_server_json_matches_builder` passes. - Not tested: full suite (single generated-artifact change). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] Documentation changes (N/A) - [x] My changes generate no new warnings - [ ] Tests added (N/A — regenerates an artifact an existing test already guards) - [x] New and existing unit tests pass locally - [ ] CHANGELOG (N/A) ## Additional Notes `server.json` is a generated artifact (`headroom/mcp_registry/server_json.py`) — regenerate with `render_server_json()` after any version bump. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
ea3d5a86b7
|
fix(deps): clear Dependabot lockfile alerts (#2175)
## Description Clears the current dependency/security-audit blockers that are making unrelated PRs red: - `transformers 5.3.0` / `CVE-2026-5241`, fixed by requiring `transformers>=5.5.0` in the locked optional dependency set. - `sqlitedict <=2.1.0` via the optional `benchmark` extra's `lm-eval[api]` dependency. There is no patched `sqlitedict` release, so this PR removes the published/locked `benchmark` extra instead of shipping a known-vulnerable transitive dependency. - `esbuild >=0.27.3,<0.28.1` in the OpenCode plugin lockfile, fixed by forcing `esbuild@0.28.1` through the OpenCode npm override and regenerated lockfile. The benchmark code still invokes `python -m lm_eval`; researchers who need that harness should install `lm-eval[api]` in their benchmark environment until its transitive vulnerability has a patched release. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `pyproject.toml`: remove the `benchmark` optional extra, document external `lm-eval[api]` installation guidance, and require `transformers>=5.5.0`. - `uv.lock`: regenerate without the `benchmark` extra, removing `lm-eval` and `sqlitedict` lock entries and locking the patched transformers floor. - `plugins/opencode/package.json`: add an `overrides` entry for `esbuild@0.28.1`. - `plugins/opencode/package-lock.json`: regenerate the OpenCode lockfile with `esbuild@0.28.1`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text uv lock --check rg -n -F 'sqlitedict' uv.lock # no matches rg -n -F 'name = "lm-eval"' uv.lock # no matches rg -n -F "extra == 'benchmark'" uv.lock # no matches rg -n -F '0.27.7' plugins/opencode/package-lock.json plugins/opencode/package.json # no matches npm ls esbuild --package-lock-only npm audit --package-lock-only # found 0 vulnerabilities git diff --check ``` Previous GitHub checks were green. After merging current `main`, fresh GitHub checks are running again; local targeted validation still passes. ## Real Behavior Proof - Environment: Windows 11, Python 3.13.3, uv, npm in `plugins/opencode`, Dependabot/pip-audit alert metadata from the failing PR jobs. - Exact command / steps: inspected the regenerated Python and npm lockfiles with `rg`, checked the uv lock with `uv lock --check`, checked OpenCode's dependency tree with `npm ls esbuild --package-lock-only`, and ran `npm audit --package-lock-only`. - Observed result: `uv.lock` no longer contains `sqlitedict`, `lm-eval`, or a `benchmark` extra marker; `transformers` resolves at the patched `>=5.5.0` floor; OpenCode's lock resolves `esbuild@0.28.1`; `npm audit --package-lock-only` reports 0 vulnerabilities; GitHub `Dependency audit (pip-audit)` passes. - Not tested: running the external `lm-eval` harness after installing it separately. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [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 - dependency and lockfile security fix. ## Additional Notes The `benchmark` extra can be restored once the upstream `lm-eval[api]` dependency chain stops pulling a vulnerable `sqlitedict` release. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
dbbef4bd41
|
fix(code-compressor): recover valid Python rewrites after local syntax rejection (#2202)
## Description A compile-invalid Python definition rewrite currently makes `CodeAwareCompressor` discard every otherwise valid rewrite in the file and return the original source at 0 percent reduction. The existing whole-file safety guard stays in place, while a Python-only recovery replay now preserves the rejected definition and keeps independent valid compression. The recovery reuses the current Python validation authority in `ast.parse()` plus `compile()`, runs only after the first assembled module already fails `_verify_syntax()`, and stays out of non-Python paths. Closes #1233 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added a Python-only recovery replay after the first assembled module fails syntax validation. - Preserved only the invalid function or class rewrite while allowing independent valid definitions to remain compressed. - Kept the existing whole-file syntax guard and original-source fallback as the terminal safety check. - Added focused invalid-node, valid-modern-syntax, and fail-safe coverage. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v`) - [x] Linting passes (`uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v 5 passed in 0.34s uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py All checks passed! uv run ruff format headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py --check 2 files already formatted ``` ## Real Behavior Proof - Environment: Windows, synced `uv` environment with `dev` and `code` extras installed - Exact command / steps: run the focused invalid-node regression through public `compress(..., language="python")` - Observed result: `1 passed in 0.19s`; the invalid candidate stays original, the neighboring valid candidate remains compressed, and `headroom-PR-TARGET-1233-PROOF.md` records the base `ratio=1.0` whole-file rollback against the fixed head behavior. - Not tested: the stale future-import mismatch discussed in the old issue comment, already covered on current main ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - The stale future-import comment on #1233 is not the live slice here; current main already validates Python with `compile()` and already covers that ordering case. - This fix keeps the existing whole-file fail-safe and does not broaden into cross-language recovery or new syntax models. - `CHANGELOG.md` remains unchanged because Headroom generates release notes from conventional commits. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
8522fcbc40
|
fix(code): quarantine Perl parser from code-aware compression (#2204)
## Description `headroom proxy` can wedge when code-aware compression enters the tree-sitter Perl external scanner and the native scan keeps the GIL indefinitely. Current main still has two routes into that scanner: explicit `perl` or `pl` hints flow through `CodeAwareCompressor`, and `detect_language()` can nominate Perl on non-Perl code because its prefilter matches generic sigils such as decorators, JSDoc tags, and shell variables before phase 2 parses every surviving candidate grammar. This change quarantines Perl at the code-aware compression funnel without widening scope. Perl remains recognized at the input boundary, but live proxy compression no longer requests a Perl parser. Non-Perl code keeps its existing code-aware behavior. Real Perl falls back through the existing safe Kompress or passthrough contract instead of entering tree-sitter. The diff stays inside `headroom/transforms/code_compressor.py` plus focused parser-safety regressions. Refs #2185 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added a Perl quarantine list in `headroom/transforms/code_compressor.py` and used it to stop Perl candidate parsing during language detection. - Added a hard `_get_parser()` guard so no live code-aware path can construct a Perl parser. - Routed resolved explicit Perl hints through the existing safe fallback or passthrough contract before AST compression. - Added focused parser-safety regressions for non-Perl candidate bleed, explicit `perl` and `pl`, inferred real Perl, mixed fenced Perl content, fallback-disabled passthrough, and non-Perl negative space. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_perl_scanner_safety.py -q`) - [x] Linting passes (`uv run ruff check headroom/transforms/code_compressor.py tests/test_perl_scanner_safety.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_perl_scanner_safety.py -q 8 passed in 1.93s uv run ruff check headroom/transforms/code_compressor.py tests/test_perl_scanner_safety.py All checks passed! uv run ruff format headroom/transforms/code_compressor.py tests/test_perl_scanner_safety.py --check 2 files already formatted ``` ## Real Behavior Proof - Environment: Windows, Python from the synced `uv` environment, `dev` and `code` extras installed, no provider call - Exact command / steps: Run `uv run pytest tests/test_perl_scanner_safety.py -q`. - Observed result: `8 passed in 1.93s`; the suite proves explicit `perl` and `pl`, inferred real Perl, mixed fenced Perl, and non-Perl negative-space routes all avoid Perl parser entry. - Not tested: the reporter's macOS payload and long-running concurrent workload ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Use `Refs #2185`, not `Closes #2185`. The wedge surface is this PR's scope, but #2185 also carries a separate orphaned `headroom mcp serve` report that this slice does not address. - This is a reachability fix. It does not repair the upstream Perl scanner and it does not harden other grammars against the same class of native wedge. - `CHANGELOG.md` remains unchanged because Headroom generates release notes from conventional commits. - Merged PR https://github.com/headroomlabs-ai/headroom/pull/2114 addresses a different cooperative compression stall and stays separate from this native parser-entry slice. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
2a954b69b4
|
feat(wrap): add ZCode desktop app support (#1845)
## Description Add `headroom wrap zcode` and `headroom unwrap zcode` commands for the ZCode desktop app (zcode.z.ai). ZCode is a desktop Electron IDE built by Z.AI, optimized for GLM-5.2 models. It has no CLI binary, so this follows the Pattern-B (proxy-only, print instructions) approach — same as Cursor, Cline, and Continue. **Upstream auto-detection:** `headroom wrap zcode` now reads `~/.zcode/v2/config.json` to detect the enabled provider and automatically configures the proxy upstream — no manual flags needed. Closes #1844 ## 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 - New module: `headroom/providers/zcode/__init__.py` and `runtime.py` (ZCodeProxyTargets, ZCodeUpstream, build_proxy_targets, detect_upstream, upstream_to_proxy_urls, render_setup_lines) - New CLI command: `headroom wrap zcode` in `headroom/cli/wrap.py:4815` — starts proxy, injects RTK into AGENTS.md, prints Base URL setup instructions - New CLI command: `headroom unwrap zcode` in `headroom/cli/wrap.py:5960` — removes RTK markers, stops proxy - New helper: `zcode_config_dir()` in `headroom/install/paths.py` - Updated `_run_proxy_only_watcher` to accept `anthropic_api_url`/`openai_api_url` params - Updated README.md: ZCode row in compatibility matrix, unwrap list, wrap command list - Updated CHANGELOG.md: entry under [Unreleased] > Added ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) — pre-existing numpy type stubs issue prevents full mypy run - [x] New tests added for new functionality (24 tests in `tests/test_cli/test_wrap_zcode.py`) - [x] Manual testing performed ### Test Output ```text tests/test_cli/test_wrap_zcode.py ........................ [100%] 24 passed, 1 warning in 0.22s ``` ## Real Behavior Proof - Environment: macOS 15.5, Python 3.12.13, headroom installed via `pip install -e .[dev]` - Exact command / steps: `headroom wrap zcode --port 9000` then `headroom unwrap zcode --port 9000` - Observed result: Wrap detects provider from `~/.zcode/v2/config.json` (e.g. "Z.ai Coding"), starts proxy with correct upstream on port 9000, injects RTK into AGENTS.md, prints detected provider + upstream + Base URL setup instructions. Unwrap removes RTK markers, deletes empty AGENTS.md, stops proxy. - Not tested: Actual ZCode app integration (ZCode is a desktop Electron app; Base URL configuration is manual in Settings > Model Settings) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas — N/A: code follows existing patterns - [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 — CLI-only changes ## Additional Notes - **Pattern-B approach:** ZCode is a desktop Electron app with no CLI binary. Same pattern as Cursor, Cline, and Continue — proxy-only, print instructions. - **Upstream auto-detection:** Reads `~/.zcode/v2/config.json`, finds the enabled provider, and passes its `baseURL` to the proxy. Falls back to Z.ai Anthropic endpoint if no config found. - **httpProxy investigation:** ZCode has an `httpProxy` setting in `~/.zcode/v2/setting.json`, but it is an Electron-level forward proxy (CONNECT tunneling), incompatible with headroom reverse proxy. The Base URL approach in Model Settings is the correct integration point. - **No dependencies added:** This PR adds zero new dependencies. --------- Co-authored-by: Epicism <epicism@Epiphanie.local> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
5709291914
|
chore(release): harden local artifact smokes (#1824)
## Description
Harden the release artifact workflow so maintainers can reproduce npm
and Python release checks locally and so OpenClaw packaging does not
depend on a not-yet-published SDK version. This follow-up also keeps the
OpenClaw loader export contract explicit and updates the PR after the
branch was merged with current `headroomlabs/main`.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [x] Documentation update
## Changes Made
- Added reusable local release smoke scripts for npm assets, Python
wheel/sdist artifacts, and the combined release gate.
- Switched the release workflow npm asset build to the reusable npm
builder.
- Made OpenClaw release asset building install against the just-built
local SDK tarball before rewriting packed metadata to the release
dependency range.
- Kept the OpenClaw source dependency registry-installable at
`headroom-ai@^0.22.3` while the packed artifact still ships
`^<release-version>`.
- Added `registerHeadroomPlugin` as a named export while preserving the
default `{ register }` OpenClaw loader contract.
- Added Windows development bootstrap docs/script and regression tests
for version sync, npm asset ordering, OpenClaw source installability,
and Python wheel smoke import isolation.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
uv run --with pytest python -m pytest tests/test_release_workflows.py scripts/tests/test_version_sync.py -q
44 passed, 1 warning
node --check scripts/build_npm_release_assets.mjs
node --check scripts/verify_npm_release_assets.mjs
python -m py_compile scripts/build_python_release_smoke.py scripts/release_smoke_all.py scripts/version-sync.py
python scripts/verify-versions.py
All versions aligned at 0.31.0
npm ci (plugins/openclaw)
added 88 packages, audited 89 packages, found 0 vulnerabilities
python scripts/release_smoke_all.py --out release-assets-local/all-pr1824-postmerge-fixed-20260710-104739
Verified npm release assets for 0.31.0
wheel metadata OK: headroom_ai-0.31.0-cp310-abi3-win_amd64.whl contains headroom/_core.pyd
sdist License-File metadata OK: ['LICENSE', 'NOTICE']
smoke-import OK: version=0.31.0 hello=headroom-core
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.14.3, Node v24.14.0, npm 11.9.0, uv
0.11.15, Rust/Cargo already installed in the local development
environment.
- Exact command / steps: Ran `python scripts/release_smoke_all.py --out
release-assets-local/all-pr1824-postmerge-fixed-20260710-104739` after
syncing this branch with current `headroomlabs/main`.
- Observed result: The command built `headroom-ai-0.31.0.tgz`,
`headroom-openclaw-0.31.0.tgz`,
`headroom_ai-0.31.0-cp310-abi3-win_amd64.whl`, and
`headroom_ai-0.31.0.tar.gz`; npm verifier passed; the wheel installed
into a fresh venv and imported `headroom._core`.
- Not tested: Full GitHub Actions release matrix and publish jobs with
real PyPI/npm/GitHub Packages credentials.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A.
## Additional Notes
The post-merge full smoke initially failed because the OpenClaw source
package depended on `headroom-ai@^0.31.0`, which is not available on
public npm yet. The builder now installs OpenClaw against the just-built
local SDK tarball before build/pack, and then rewrites packed metadata
to `^0.31.0`.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
36577d9547
|
fix(search_compressor): don't let a date in a path hijack the line-number parse (#2084)
## Description
`SearchCompressor::parse_match_line` splits a grep/ripgrep line into
`(file, line_number, content)` by finding the **leftmost**
`<sep><digits><sep>` triplet, where `<sep>` is `:` or `-`. A path
segment that itself contains such a triplet hijacks the parse — and that
shape is everyday, not exotic:
| real ripgrep line | parsed as |
|---|---|
| `logs/2026-05-03/app.log:12:ERROR boom` | `("logs/2026", 5,
"03/app.log:12:ERROR boom")` |
| `advisories/CVE-2021-44228.md:8:Log4Shell` | `("advisories/CVE", 2021,
"44228.md:8:Log4Shell")` |
| `src/v1-2-beta/mod.rs:3:fn x()` | `("src/v1", 2, "beta/mod.rs:3:fn
x()")` |
| `migrations/20240101-002-add_users.sql-9-…` | `("migrations/20240101",
2, "add_users.sql-9-…")` |
**This is silent corruption, not a drop.** The parse *succeeds*, so the
line is never counted in `stats.lines_unparsed` and never falls back to
passthrough. The bogus path becomes the **grouping key** in
`parse_search_results`, so unrelated files collapse into one bucket, and
the bogus path + line number + mangled body are what get scored, capped,
and rendered into the compressed output handed to the model. **The LLM
is shown a file and a line that do not exist.**
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
One file, one function:
`crates/headroom-core/src/transforms/search_compressor.rs`.
`parse_match_line` becomes a 3-tier scan:
- **Colon tier** — leftmost `:\d+:` whose path part contains no
whitespace. `:` is grep's *match* separator and a path practically never
contains one (the Windows drive colon is already skipped by the existing
`scan_start` logic), so leftmost is right. The whitespace bound stops a
`foo.rs:12:` reference *inside the body* of a `-` context line from
hijacking the parse.
- **Dash tier** — **last** `-\d+-` whose path part contains no
whitespace. `-` is grep's *context* separator, and unlike `:` it
genuinely appears inside real paths (`2026-05-03`, `CVE-2021-44228`,
`20240101-002-…`), so the marker is the *last* triplet in the path
token, not the first.
- **Permissive tier** — the original leftmost-any rule, byte-for-byte
unchanged. Only reached when neither typed tier matched (e.g. a path
containing a space), so those lines behave exactly as before.
- Also tightened in the typed tiers: the closing separator must equal
the opening one — grep emits `file:12:body` or `file-12-body`, never a
mix.
- Added 4 tests: 2 reproducing the bug, 2 regression guards against the
naive fixes.
**Safety argument (verified by execution):** with `parse_match_line`
temporarily forced to the Permissive tier alone, all 18 pre-existing
`search_compressor` tests still pass — i.e. the fallback is a faithful
reproduction of today's rule, so the change can only *add* correct
parses on lines a typed tier claims, never remove one.
This is the next bug in a family the module already tracks: the doc has
a "Bug fixes vs Python" section and three `fixed_in_3e2_*` tests
hardening this same parser against Windows drive colons and dashes in
filenames. `pre-commit-config.yaml-42-…` (dash before a *non*-digit) is
covered; `2026-05-03` (dash before a digit run followed by another dash)
was not.
## Testing
- [x] Unit tests pass
- [x] Linting passes (`cargo clippy -p headroom-core --all-targets` → 0
warnings)
- [x] Formatting passes (`cargo fmt --all -- --check`)
- [x] New tests added (4: 2 reproducing the bug, 2 regression guards
against naive fixes)
- [x] Manual testing performed (see Real Behavior Proof)
### Test Output
**Before the fix** (new tests run against the unmodified scan rule):
```text
$ cargo test -p headroom-core --lib search_compressor
---- transforms::search_compressor::tests::date_stamped_path_is_not_misread_as_line_number_marker stdout ----
assertion `left == right` failed
left: Some(("logs/2026", 5, "03/app.log:12:ERROR boom"))
right: Some(("logs/2026-05-03/app.log", 12, "ERROR boom"))
---- transforms::search_compressor::tests::date_stamped_paths_are_not_collapsed_into_one_bogus_file stdout ----
assertion `left == right` failed
left: ["logs/2026"]
right: ["logs/2026-05-03/app.log", "logs/2026-05-04/app.log"]
test result: FAILED. 18 passed; 2 failed; 0 ignored
```
**After the fix:**
```text
$ cargo test -p headroom-core --lib search_compressor
test result: ok. 20 passed; 0 failed; 0 ignored; 835 filtered out
$ cargo test -p headroom-core --lib # whole crate — no regressions
test result: ok. 854 passed; 0 failed; 1 ignored
$ cargo test -p headroom-parity
test result: ok. 4 passed; 0 failed
$ cargo fmt --all -- --check -> OK
$ cargo clippy -p headroom-core --all-targets -> 0 warnings, 0 errors
```
Regression guards added for the two ways a naive fix breaks:
- `digit_terminated_path_still_parses_ripgrep_context_line` —
`logs/app.log.1-42-rotated line` (path ends in a digit, so the context
separator is digit-preceded).
- `body_line_reference_does_not_hijack_a_context_line` —
`src/main.py-44-see foo.rs:12:bar` (body quotes a `file:line:`
reference).
## Real Behavior Proof
Per CONTRIBUTING — unit tests alone don't prove user-visible behavior,
so this was reproduced against the **released build** (`headroom-ai`
0.26.0 from PyPI, the compiled `_core.abi3.so`), driving the **public
`SearchCompressor.compress()` API** on **real `rg` output over real
files on disk** — not fixtures or mocks.
- Environment: macOS (Darwin 25.5.0, arm64), Python 3.13, released
`headroom-ai` 0.26.0 (`site-packages/headroom/_core.abi3.so`); patched
build = this branch compiled with `cargo build --release -p
headroom-py`, rustc 1.96.0.
- Exact command / steps: created 20 real log files at
`logs/2026-05-01/app.log` … `logs/2026-05-20/app.log` (12 real `ERROR`
lines each); ran `rg -n ERROR logs > rg_big.txt` (240 real match lines);
then called
`SearchCompressor(SearchCompressorConfig()).compress(open("rg_big.txt").read())`
on the shipped 0.26.0 build and on the patched build, comparing
`files_affected`, the rendered output, and whether each referenced path
exists on disk.
- Observed result: on shipped 0.26.0, the 20 distinct real files
collapse into **1 bogus bucket** `logs/2026` (a path that does **not**
exist on disk), per-line paths are mangled to
`logs/2026:5:01/app.log:10:`, 19 of 20 files effectively vanish from the
output, and `lines_unparsed: 0` means **nothing signals the
corruption**. On the patched build, same input and same API:
`files_affected: 20` (matches reality), every path in the compressed
output exists on disk (`all_exist=True`), and per-file match counts and
line numbers are correct.
- Not tested: the end-to-end proxy path (`headroom-proxy` against a live
LLM provider) — I exercised the `SearchCompressor` public API directly,
which is the surface `SearchOffload` and the MCP `headroom_compress`
tool wrap. I also did not test Windows path behavior on an actual
Windows host (the existing `scan_start` drive-letter logic is untouched,
and its tests still pass).
**Observed on the SHIPPED 0.26.0 build (the bug, in the released
product):**
```text
SHIPPED headroom 0.26.0 | real `rg -n ERROR logs` output, 240 lines
lines_unparsed : 0 <-- corruption is SILENT: nothing reported as unparsed
original_match_count: 240
files_affected : 1 <-- 20 distinct real files collapsed into ONE bucket
=== compressed output actually handed to the model ===
logs/2026:5:01/app.log:10:ERROR failure 1 connection refused upstream timeout on 2026-05-01 ...
logs/2026:5:20/app.log:21:ERROR failure 12 connection refused upstream timeout on 2026-05-20 ...
logs/2026:5:01/app.log:11:ERROR failure 2 connection refused upstream timeout on 2026-05-01 ...
[... and 235 more matches in logs/2026]
[240 matches compressed to 5. Retrieve more: hash=39c894009014d42b856ddd8a]
=== do the file paths in that output exist on disk? ===
logs/2026 exists_on_disk=False
```
**Observed on the PATCHED build (same input, same API, only the patch
differs):**
```text
PATCHED headroom-core | same real `rg` output, 240 lines
lines_unparsed : 0
original_match_count: 240
files_affected : 20 <-- was 1 (bogus) on the shipped build
=== compressed output handed to the model ===
logs/2026-05-01/app.log:10:ERROR failure 1 connection refused upstream timeout on 2026-05-01 ...
logs/2026-05-01/app.log:11:ERROR failure 2 connection refused upstream timeout on 2026-05-01 ...
[... and 7 more matches in logs/2026-05-01/app.log]
logs/2026-05-02/app.log:10:ERROR failure 1 connection refused upstream timeout on 2026-05-02 ...
=== do the file paths in that output exist on disk? ===
logs/2026-05-01/app.log exists_on_disk=True
logs/2026-05-02/app.log exists_on_disk=True
...all distinct paths referenced, all_exist=True
```
## 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] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**Known residual ambiguity (stating it rather than hiding it).** grep
output is inherently ambiguous — `logs/2026-05-03/x:12:y` *could*
legitimately be a file literally named `logs/2026` with context line 5.
The tiers pick the overwhelmingly more likely reading. Two contrived
cases still parse the old way, both preserved deliberately:
1. a path containing a whitespace character;
2. a `-`-context line whose body is a whitespace-free token containing
its own `-N-` triplet.
If you'd prefer a different disambiguation policy (e.g. only trusting
`:` and treating all `-` context lines as unparseable, or gating on
filesystem existence), I'm happy to rework — the tiering is deliberately
isolated to one function so the policy is easy to swap.
N/A checklist items: no documentation or CHANGELOG change (internal
parser fix, no public API or behavior contract change); no screenshots
(no UI surface).
---------
Signed-off-by: dosthcpp <drakedog19@gmail.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
6bdc8c44a3
|
docs(metrics): ship an importable Grafana dashboard (#2168)
## Description
<!-- Briefly explain the change and why it is needed. -->
The metrics docs describe the `headroom_*` Prometheus metric family and
suggest example Grafana panels, but ship no importable dashboard — users
have to build one by hand. This adds a ready-to-import Grafana dashboard
built **only** on documented metric names (`headroom_requests_total`,
`headroom_tokens_saved_total`, `headroom_tokens_input_total`, and the
`headroom_overhead_ms_*` millisecond summary), and links it from the
**Grafana Dashboard** section of `docs/content/docs/metrics.mdx`.
This is a docs/examples-only addition — no source code changes.
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `examples/grafana/headroom-dashboard.json` — a ready-to-import
Grafana dashboard (7 panels, uid `headroom-compression`) built entirely
on Headroom's documented `/metrics` names. Panels cover tokens saved,
input tokens, request rate, average processing overhead
(`headroom_overhead_ms_sum` / `headroom_overhead_ms_count` with
min/max), tokens-saved/sec, and request rate by pool. It uses **no
histograms** (the proxy emits none). The `pool`/`source` template
variables use regex matchers (`=~`) so they are optional and match
series without those labels.
- Updated `docs/content/docs/metrics.mdx` — linked the new dashboard
from the **Grafana Dashboard** section with import instructions, keeping
the existing ad-hoc PromQL query table alongside it.
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
Docs/examples-only change, manually verified: the dashboard JSON is
well-formed and every PromQL query references only the documented
`headroom_*` metric names from `docs/content/docs/metrics.mdx`.
### Test Output
```text
$ python3 -c "import json; d=json.load(open('examples/grafana/headroom-dashboard.json')); print('valid JSON,', len(d['panels']), 'panels, uid', d['uid'])"
valid JSON, 7 panels, uid headroom-compression
```
PromQL queries used by the panels (all against documented `headroom_*`
metrics):
```text
sum(headroom_tokens_saved_total{pool=~"$pool", hook=~"$hook"})
sum(headroom_tokens_input_total{pool=~"$pool", hook=~"$hook"})
sum(rate(headroom_requests_total{pool=~"$pool", hook=~"$hook"}[$__rate_interval]))
sum(rate(headroom_overhead_ms_sum{pool=~"$pool", hook=~"$hook"}[$__rate_interval])) / clamp_min(sum(rate(headroom_overhead_ms_count{pool=~"$pool", hook=~"$hook"}[$__rate_interval])), 1)
sum(rate(headroom_tokens_saved_total{pool=~"$pool", hook=~"$hook"}[$__rate_interval])) by (pool)
max(headroom_overhead_ms_max{pool=~"$pool", hook=~"$hook"})
min(headroom_overhead_ms_min{pool=~"$pool", hook=~"$hook"})
sum(rate(headroom_requests_total{pool=~"$pool", hook=~"$hook"}[$__rate_interval])) by (pool)
```
## Real Behavior Proof
- Environment: local checkout of the PR branch; Python 3 for JSON
validation.
- Exact command / steps: ran the JSON-validation command above (see Test
Output) — parses cleanly, reports 7 panels and uid
`headroom-compression`; then read every panel target and confirmed each
PromQL query references only metric names documented in
`docs/content/docs/metrics.mdx` (`headroom_requests_total`,
`headroom_tokens_saved_total`, `headroom_tokens_input_total`,
`headroom_overhead_ms_{sum,count,min,max}`). No histogram metrics are
referenced.
- Observed result: JSON is valid and importable via Grafana's
**Dashboards → New → Import → Upload**; no datasource UID is hard-coded,
so the importer prompts for a Prometheus datasource. Queries match the
documented metric family.
- Not tested: a full live Grafana import against a running proxy
scraping real `/metrics` was not performed in CI. Verification was
limited to JSON validity and query/metric-name correctness against the
documented metrics.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
Additive docs/examples only — no source code, tests, or runtime behavior
changed.
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — dashboard is imported from JSON; see the PromQL and panel list
above.
## Additional Notes
<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->
Test-related checklist items are N/A: this is an additive docs/examples
change with no application code, so `pytest`/`mypy`/`ruff` and new unit
tests do not apply. The dashboard JSON was validated and its queries
checked against the documented metric names instead.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
021a762bf8
|
feat(compress): expose frozen_message_count in library-mode compress() (#2178)
## Description `read_lifecycle.apply()` already supports a frozen message prefix (`frozen_message_count`) — stale-Read replacements inside the prefix are skipped so compression never rewrites messages the provider's prompt cache has anchored. But only the proxy handlers can pass it: `ContentRouter` reads it from transform kwargs, `CompressConfig` has no such field, and the public `compress()` never forwards it. Library-mode callers that manage their own conversation loop (SDK integrations, offline evaluation, sidecar scoring) therefore can't stop transforms from rewriting already-sent history. On cached Anthropic traffic that's expensive: every byte after the first rewritten one stops billing as a 0.1× cache read and re-bills as a cache write (1.25× at the 5-minute TTL, 2× at the 1-hour TTL) — measured on live coding-agent traffic, retroactive stale-Read rewrites were the dominant cache-bust source once tool injection went session-sticky (PR-B7). Relates to #809 (cache-bust economics discussion); does not close it. ## 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 - `CompressConfig.frozen_message_count: int = 0` — documented field; default `0` preserves existing behavior exactly. - `compress()` forwards it through `pipeline.apply()` to the transforms, matching what the proxy handlers already do. - `compress()` docstring: added to the kwargs shorthand list. - CHANGELOG entry under Unreleased → Features. - Four tests in `tests/test_compress_api.py` (`TestFrozenMessageCount`). ## 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_compress_api.py tests/test_transforms/test_read_lifecycle.py \ tests/test_compression_safety_rails.py tests/test_compress_failure.py -q 59 passed, 1 warning in 3.05s $ uv run ruff check headroom/compress.py tests/test_compress_api.py All checks passed! $ uv run mypy headroom Success: no issues found in 471 source files ``` ## Real Behavior Proof - Environment: Linux, Python 3.12.3, this branch installed via `uv sync --extra dev` - Exact command / steps: build an Anthropic-format conversation with a stale Read (file read at message 2, edited at message 3), then: ```python r0 = compress(msgs, model="claude-sonnet-4-5-20250929") r5 = compress(msgs, model="claude-sonnet-4-5-20250929", frozen_message_count=5) ``` - Observed result: without frozen prefix the stale Read is rewritten; with frozen_message_count=5 the Read remains byte-identical. ```text without frozen prefix: stale Read rewritten: True transforms: ['read_lifecycle:stale:/app/config.py'] with frozen_message_count=5: Read byte-identical: True transforms: [] ``` - Not tested: proxy-mode code paths (untouched — they already pass `frozen_message_count` their own way); Rust crates (untouched). ## 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 — library API change, no UI. ## Additional Notes Default `0` makes this a strict superset of current behavior — no caller sees any change without opting in. The motivation data comes from a proxy-side measurement tool that prices compression's cache effects on live Anthropic agent traffic (per-request cache-adjusted dollars); happy to share methodology in #809 if useful. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
5dbe3314a1
|
fix(auth): support GitHub Enterprise Copilot OAuth domain (#2192)
## Description Adds GitHub Enterprise OAuth domain support for Copilot auth. When `GITHUB_COPILOT_ENTERPRISE_URL` is set, the default OAuth domain resolves to that enterprise host; explicit `--domain` values still take precedence. Closes #1152 ## 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 - `headroom/copilot_auth.py`: derive the default OAuth domain from `GITHUB_COPILOT_ENTERPRISE_URL` when present, falling back to `github.com` for unset or blank values. - `headroom/cli/copilot_auth.py`: keep explicit CLI domain overrides authoritative even when the enterprise env var is set. - `README.md`: document the enterprise OAuth environment setting and precedence. - Added regression tests for enterprise URL handling, blank/unset fallback, and explicit CLI override precedence. ## 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_copilot_auth.py tests/test_cli/test_copilot_auth.py -q 69 passed in 1.05s uvx ruff@0.15.17 check headroom/cli/copilot_auth.py headroom/copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_copilot_auth.py All checks passed! uvx ruff@0.15.17 format --check headroom/cli/copilot_auth.py headroom/copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_copilot_auth.py 4 files already formatted ``` ## Real Behavior Proof - Environment: Windows 11 development checkout, Python 3.13.3, with focused Copilot auth tests using monkeypatched enterprise env vars. - Exact command / steps: ran the Copilot auth unit/CLI tests plus ruff check and format-check against the touched auth files and tests. - Observed result: `GITHUB_COPILOT_ENTERPRISE_URL=https://ghe.example.com` resolves `default_oauth_domain()` to `ghe.example.com`; unset/blank enterprise env vars fall back to `github.com`; and `headroom copilot-auth login --domain github.com` still honors the explicit override when enterprise env vars are set. - Not tested: live OAuth against a real GitHub Enterprise Server instance. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - CLI/auth behavior and README update. ## Additional Notes `GITHUB_COPILOT_ENTERPRISE_URL` takes precedence over `GITHUB_COPILOT_ENTERPRISE_DOMAIN`; explicit `--domain` remains authoritative for the login command. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
a61f534426
|
fix(ccr): store pre-protection original, not tag placeholder, in CCR (#1208)
## Description
When `ContentRouter` protects custom tags (e.g. `<system-reminder>`)
into `{{HEADROOM_TAG_N}}` placeholders before invoking Kompress, CCR can
persist the protected **placeholder intermediate** as the entry's
`original_content` instead of the pre-protection source text. A later
**full retrieve** (or proactive expansion / model-initiated retrieve) of
such an entry then returns `{{HEADROOM_TAG_0}}` and the real protected
block is lost from the retrieval path. The immediate upstream request is
unaffected — `restore_tags` correctly restores the compressed output
before it goes upstream; the confirmed corruption is in CCR storage and
only surfaces on later retrieval/expansion.
This threads the pre-protection `content` through as `ccr_original` so
CCR stores the real source text while the model still sees the
placeholdered text.
Closes #1209
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `headroom/transforms/content_router.py`: `_try_ml_compressor` passes
`ccr_original=content` to `compressor.compress(...)` **only when tags
were actually protected** (untagged callers keep the historic call shape
— backward compatible).
- `headroom/transforms/kompress_compressor.py`: `compress()` gains a
`ccr_original` kwarg; `compress_batch()` gains a per-item
`ccr_originals` list (validated against `len(contents)`).
- All four CCR store sites store `ccr_original` when present, else
`content`: inline `compress()`, single-content
`compress()`→`compress_batch` delegation, `compress_batch` sequential
fallback, and `compress_batch` batched/GPU path. The stored original's
token count is recomputed from the stored text.
- `tests/test_ccr_tag_placeholder_regression.py` (new, 5 tests): router
boundary forwarding, untagged backward-compat, `ccr_originals` length
validation, and two store-site tests driving the real `compress()` /
batched `compress_batch()` all the way to `_store_in_ccr` (a tiny fake
model stands in for the 274MB ModernBERT).
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [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
$ .venv/bin/python -m pytest tests/test_ccr_tag_placeholder_regression.py -q
============================= test session starts ==============================
platform darwin -- Python 3.12.12, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/.../headroom.worktrees/ccr-tag-placeholder
configfile: pyproject.toml
plugins: anyio-4.14.0
collected 5 items
tests/test_ccr_tag_placeholder_regression.py ..... [100%]
========================= 5 passed, 1 warning in 0.15s =========================
```
Fail-before / pass-after was confirmed against a freshly built Rust
`_core`: with the fix reverted the new tests fail (router forwards no
`ccr_original` → `None`/placeholder reaches the store; `compress_batch`
rejects the unknown `ccr_originals` kwarg with `TypeError`); with the
fix applied all 5 pass. The surrounding kompress/ccr/router suites stay
green (8 unrelated failures are pre-existing — identical with the patch
stashed — from missing optional test deps such as `pytest-asyncio`, not
caused by this change).
## Real Behavior Proof
- Environment: macOS (darwin), Python 3.12.12, locally built Rust
`_core` via `maturin develop`, pytest 9.1.1.
- Exact command / steps: `maturin develop` to build `_core`, then
`python -m pytest tests/test_ccr_tag_placeholder_regression.py -q`.
- Observed result: 5 passed with the fix applied; the same suite fails
before the fix (placeholder/`None` reaches `_store_in_ccr`;
`compress_batch` rejects `ccr_originals`).
- Not tested: end-to-end live proxy full-retrieve against a 274MB
ModernBERT model (tests use a fake model to keep them deterministic and
offline); `ruff`/`mypy` not run locally.
> Note: this fixes new CCR writes. Pre-existing entries written before
the fix keep their placeholder `original_content` until they expire.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
Docs/CHANGELOG unchanged: this is an internal CCR correctness fix with
no public API or user-facing behavior change beyond correct
full-retrieve content. `ruff`/`mypy` were not run in the local build
environment.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
a069979466
|
fix(content_router): pin FREEZE_BLOCK_DECISION verdict to stop cache-write churn (#1620)
## Description The per-block freeze decision was inert. On the cached-block re-check, a later tighter `min_ratio` (context pressure rises within a session) could downgrade an earlier "compress" verdict to skip, restore the original block, and bust the prefix cache — a self-inflicted cache-write churn that costs the very tokens compression saved. This pins the decision instead. A frozen "compress" verdict re-accepts (`accept_threshold = 1.0`) rather than re-running the per-turn `min_ratio` gate, so a block that was accepted stays accepted. First-sighting still uses the live `min_ratio` gate (`accept_threshold = min_ratio`): the freeze only pins past accepts, it never loosens the first decision (that would be a silent ratio bet). Gated behind `HEADROOM_FREEZE_BLOCK_DECISION`, default off, byte-identical to today when unset. Composes with the #1307 reversibility guard on the compress path: a frozen accept still defers to the lossy-unrecoverable skip. Closes #1619 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `content_router.py`: on the cached-block hit path and the first-sighting path, compute `accept_threshold` (1.0 when a "compress" verdict is frozen for the block, else the live `min_ratio`) and gate accept on it; record the pin when the legacy re-check would have downgraded. - Frozen verdicts are stored per content-block key and only ever hold "compress" (a "skip" never warms the result cache). - No change when `HEADROOM_FREEZE_BLOCK_DECISION` is unset. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom` — no new errors) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_transforms_content_router.py -q 38 passed in 0.23s # 11 freeze/pin/churn cases + 27 existing; run against a real 0.28.0 _core. # - test_freeze_off_is_byte_identical_flapping_baseline: freeze-off == baseline byte-for-byte # - test_freeze_on_pins_compress_verdict_across_turns: pin fires; downgrade prevented $ ruff format --check . && ruff check . -> clean ``` ## Real Behavior Proof - Environment: isolated git worktree on latest `main`, real `_core.abi3.so` built for this tree via maturin (not a stale symlink), scratch venv. - Exact command: `pytest tests/test_transforms_content_router.py -q` - Observed: freeze-off path is byte-identical to the flapping baseline; with freeze on, the verdict is pinned across turns (pin-count assertion passes) so the block is not downgraded/restored and no cache-write churn occurs. - Not tested: end-to-end proxy A/B token-savings delta (follow-up; feature ships default-off). ## Screenshots (if applicable) N/A ## Review Readiness Ready for review. Default-off, composes with #1307, self-contained to the block-decision path. ## Checklist - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code in hard-to-understand areas - [x] Documentation intentionally deferred until the default-off approach is confirmed - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes - [x] CHANGELOG intentionally deferred until the default-off approach is confirmed ## Additional Notes Neighbour of #625 (prefix-stability). Docs/CHANGELOG intentionally deferred until the approach is confirmed. The end-to-end A/B is a follow-up; the churn-prevention is proven at the router unit level here. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
896454e978
|
feat(install): add apply flag parity, --env passthrough, and EIO retry (#2152)
## Description Three related gaps in `headroom install apply` and its supervisor lifecycle, found operating a real persistent deployment on this fork: 1. `install apply` only exposed a fixed subset of `headroom proxy`'s flags (`--backend`, `--region`, `--mode`, `--port`, `--memory`, `--telemetry`, `--no-http2`). Deployments that need code-aware compression, tool-result interception, per-tool lossy-compression protection, or a named AWS profile for Bedrock had no native way to configure them through `install apply` — the generated `manifest.json` would have to be hand-edited after the fact, which silently reverts on the next `install apply` and isn't tracked anywhere. 2. Supervised runners (macOS launchd, Linux systemd/cron, Windows services/tasks) all start their runner scripts with a bare environment and do not inherit the interactive shell's exports. In particular, a custom `HEADROOM_WORKSPACE_DIR` never reached the supervised process, so `headroom install agent run` looked for its manifest in the wrong location and failed outright with "No deployment profile named 'default' is installed" even though `install apply` itself had succeeded moments earlier. 3. `install_supervisor`'s macOS branch does an unconditional `launchctl bootout` followed by a bare `bootstrap` with no retry, unlike `start_supervisor` (already fixed by #1290), which rides out the ~15s EIO (error 5) window launchd exhibits for several seconds after a bootout. This left `install apply`'s own reinstall path exposed to the same race #1290 fixed elsewhere — requiring the exact manual recovery (bootout + remove the plist + reapply) #1290 was meant to eliminate. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/cli/install.py`: `install apply` gains `--code-aware/--no-code-aware`, `--intercept-tool-results`, `--protect-tool-results <tool1,tool2>`, and `--bedrock-profile <profile>`, mirroring the equivalent flags already on `headroom proxy` (same names, same help text style). Also gains `--env KEY=VALUE` (repeatable). - `headroom/install/planner.py`: `build_manifest()` threads all five new parameters into `proxy_args`/`base_env`, following the exact pattern already used for `--region`/`--no-http2`. `--env` entries are merged into `base_env` last, so they can override auto-derived defaults. - `headroom/install/supervisors.py`: - `_render_unix_runner`/`_render_windows_runner` emit `export`/`$env:` lines for `base_env` before the `exec`, so `run-headroom.sh`/`ensure-headroom.sh` (and Windows equivalents) carry the environment forward to both the outer `install agent run` process and the proxy subprocess it spawns. The Docker runtime path already threaded `base_env` into `docker run --env`; this closes the same gap for the process-based runtime. - New `_bootstrap_with_retry()` helper extracted from `start_supervisor`'s existing retry loop (from #1290), now shared by both `start_supervisor` and `install_supervisor`. - `tests/test_install/test_planner.py`: new tests for all five flags (default-omitted and persisted cases), following the existing `--no-http2` test pattern. - `tests/test_install/test_supervisors.py`: new tests for `--env` propagation into rendered runner scripts, and for `install_supervisor`'s retry-until-success and raise-after-exhausted-retries paths (mirroring the existing `start_supervisor` coverage). Also fixes a pre-existing test's mock that returned `None` from a `subprocess.run` stub — this only worked before because the old bare `bootstrap` call site never inspected the return value; the new `_bootstrap_with_retry()` call does. - `CHANGELOG.md`: added `### Features` and `### Fixed` entries 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_install/ tests/test_cli/test_wrap_persistent.py tests/test_cli/test_init_cli.py -q ============================= test session starts ============================== platform darwin -- Python 3.13.13, pytest-9.0.3, pluggy-1.6.0 collected 215 items tests/test_install/test_health.py ... [ 1%] tests/test_install/test_native_installers.py ss [ 2%] tests/test_install/test_paths.py ... [ 3%] tests/test_install/test_planner.py .................. [ 12%] tests/test_install/test_providers.py ................................... [ 28%] ...... [ 31%] tests/test_install/test_runtime.py .................... [ 40%] tests/test_install/test_state.py ..... [ 42%] tests/test_install/test_supervisors.py ......................... [ 54%] tests/test_cli/test_wrap_persistent.py ............................ [ 67%] tests/test_cli/test_init_cli.py ........................................ [ 86%] .............................. [100%] ======================== 213 passed, 2 skipped in 0.57s ======================== $ uv run ruff check headroom/cli/install.py headroom/install/planner.py headroom/install/supervisors.py tests/test_install/ All checks passed! $ uv run mypy headroom/cli/install.py headroom/install/planner.py headroom/install/supervisors.py Success: no issues found in 3 source files ``` ## Real Behavior Proof - Environment: personal fork deployed as a real proxy (macOS launchd service via `headroom install apply`), profile `default`, backend `bedrock` with a named AWS SSO profile. - Exact command / steps: (flags 1 & 2) ran `headroom install apply --backend bedrock --mode token --code-aware --protect-tool-results Bash --bedrock-profile sso-bedrock --env HEADROOM_WORKSPACE_DIR=/Users/<redacted>/.headroom-workspace --env AWS_PROFILE=sso-bedrock --env AWS_REGION=eu-west-1`, then inspected the generated `manifest.json`, the rendered `run-headroom.sh`, and the running launchd job. - Observed result: before this PR, none of `--code-aware`, `--protect-tool-results`, `--bedrock-profile`, or `--env` were accepted flags on `install apply` at all (`Error: No such option`). Reproduced the `--env` gap specifically by running the exact command a launchd job invokes with a stripped environment (no `HEADROOM_WORKSPACE_DIR`, no `AWS_PROFILE`) — it failed to find the manifest; with the interactive shell's env forwarded manually, it started fine. The generated plist had no `EnvironmentVariables` key and `run-headroom.sh` was a bare `exec`, confirming this wasn't a config mistake but a real gap between `install apply`'s flag surface and what a supervisor actually runs with. After this PR, `install apply` with all the flags above produces a launchd job that starts clean, reports healthy, and successfully proxies a real request to Bedrock (200, not just a green health check) using the named AWS profile with no `AWS_PROFILE` env var needed elsewhere. - Exact command / steps: (EIO retry, flag 3) triggered the same EIO race #1290 documents by running `headroom install apply` twice in quick succession against the same profile (the second run's `install_supervisor` bootout+bootstrap lands inside the first run's launchd settle window). - Observed result: before this PR, the second `install apply` occasionally failed outright with `CalledProcessError` from the bare `subprocess.run(..., check=True)` bootstrap call, requiring the manual bootout+`rm` plist+reapply recovery. After this PR (with `_bootstrap_with_retry` in place), the same back-to-back sequence completes successfully every time observed, riding out the EIO window instead of failing. - Not tested: Linux systemd/cron and Windows service/task supervisor paths for the `--env` propagation — verified via the new unit tests (which cover the runner-script rendering directly) but not against a live Linux or Windows machine, since this deployment is macOS-only. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — CLI/install 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 `install apply`'s flag surface in detail (it's discoverable via `--help`), so there is no existing section to update for the new flags. - Re-derivation note: this PR's `install_supervisor` EIO-retry fix and its `_bootstrap_with_retry` extraction are written directly against current `upstream/main`'s post-#1290 shape of `start_supervisor` (inline retry loop with `_MACOS_BOOTSTRAP_RETRIES`/`_MACOS_BOOTSTRAP_RETRY_DELAY`), not cherry-picked from an older fork commit that predated #1290 — the diff here is intentionally different from what a naive cherry-pick would have produced. - No linked issue number: found via operating a real persistent deployment on a personal fork, not filed as a `headroomlabs-ai/headroom` issue first. Checked `gh pr list --search` for "install apply flags/env" and "bootstrap EIO"/"install_supervisor bootstrap retry" — no open or merged coverage found beyond #1290 (which fixes `start_supervisor` only, a different call site from the one this PR fixes). Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
14011b42dd
|
fix(wrap): drop -p short flag from wrap claude so claude's own -p/--print passes through (#2048)
## Description `headroom wrap claude` declares `--port/-p`, and click parses wrapper options anywhere in the argv before unknown options fall through to `CLAUDE_ARGS`. So a user running claude's headless print mode through the wrapper — `headroom wrap claude -p "some prompt"` — fails with `Invalid value for '--port' / '-p': 'some prompt' is not a valid integer range`, and claude's own `-p`/`--print` can never reach claude. This bites hardest when `claude` is shell-aliased to `headroom wrap claude ...`: every `claude -p` invocation breaks. This PR drops the `-p` short alias from `wrap claude`'s `--port` option (long form stays; other subcommands' `-p` are untouched), so `-p` now falls through to `CLAUDE_ARGS` like any other claude flag. 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/cli/wrap.py`: removed `"-p"` from the `wrap claude` command's `--port` option; added a comment stating why the short alias must not exist there. ## Testing - [x] Unit tests pass (`pytest`) — targeted CLI suites, see output - [x] Linting passes (`ruff check .`) — on the touched file - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_cli/test_wrap_codex.py tests/test_cli/test_wrap_claude_base_url.py tests/test_cli/test_unwrap_claude.py -q 125 passed in 6.68s $ ruff check headroom/cli/wrap.py All checks passed! Full tests/test_cli run: 549 passed, 2 failed — test_wrap_copilot_auto_detects_running_proxy_backend fails identically on a clean upstream/main checkout (pre-existing, environment-sensitive), and test_wrap_codex_prepare_only_registers_serena_when_uvx_exists passes in isolation on this branch (full-suite ordering interaction, not this change). ``` ## Real Behavior Proof - Environment: Fedora 44, Python 3.14 editable install, `claude` aliased to `systemd-run --user --scope ... headroom wrap claude --no-context-tool` via terminal shell integration - Exact command / steps: `claude --model sonnet -p "Say only: ALIAS-P-FIXED"` in a fresh interactive shell (alias → wrapper → proxy → claude) - Observed result: before the fix — `Error: Invalid value for '--port' / '-p': ... is not a valid integer range` (exit 2, claude never spawns). After — headroom banner, proxy attach, claude prints `ALIAS-P-FIXED`, exit 0; `Extra args: --model sonnet -p Say only: ALIAS-P-FIXED` shows the passthrough. - Not tested: Windows; other wrapped tools' `-p` flags (left untouched by design); mypy (not 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] 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 - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — CLI flag parsing. ## Additional Notes Docs/CHANGELOG: no user-facing docs mention `-p` as a `wrap claude` port alias, so no doc change; happy to add a CHANGELOG entry if maintainers want one. No new test added because the passthrough behavior is covered by the manual end-to-end proof above; can add a click-runner test asserting `-p` lands in `CLAUDE_ARGS` if preferred. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
5d17e9addc
|
fix: check feature configuration before reusing persistent deployments (#1330)
## Description
A persistent proxy started for one use case (e.g. `--backend anthropic`)
would be silently reused for another (e.g. `--subscription
--provider-type openai`) causing 401 auth failures because
`_ensure_proxy()` only checked health + version, skipping the feature
configuration check (memory, openai_api_url, learn, code_graph).
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 feature configuration check in the persistent deployment path of
`_ensure_proxy()` in `headroom/cli/wrap.py:1726-1752`. When features
mismatch, the code now falls through to the non-persistent path which
handles proxy restart with upgraded config.
- Added three new tests in `tests/test_cli/test_wrap_persistent.py`:
-
`test_ensure_proxy_restarts_persistent_deployment_for_feature_mismatch`
— verifies proxy restart when openai_api_url differs
- `test_ensure_proxy_restarts_persistent_deployment_for_memory_mismatch`
— verifies proxy restart when memory is requested but not enabled
- `test_ensure_proxy_reuses_persistent_deployment_when_features_match` —
verifies proxy reuse when all features match
- Updated `CHANGELOG.md` with fix description
## 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
$ ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_persistent.py
All checks passed!
$ ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_persistent.py
2 files already formatted
$ python -c "
from tests.test_cli.test_wrap_persistent import *
import pytest
test_ensure_proxy_restarts_persistent_deployment_for_feature_mismatch(pytest.MonkeyPatch())
print('Test 1 passed: feature mismatch restarts proxy')
test_ensure_proxy_restarts_persistent_deployment_for_memory_mismatch(pytest.MonkeyPatch())
print('Test 2 passed: memory mismatch restarts proxy')
test_ensure_proxy_reuses_persistent_deployment_when_features_match(pytest.MonkeyPatch())
print('Test 3 passed: matching features reuse proxy')
print('All tests passed!')
"
Test 1 passed: feature mismatch restarts proxy
Test 2 passed: memory mismatch restarts proxy
Test 3 passed: matching features reuse proxy
All tests passed!
$ .venv/bin/mypy headroom
headroom/proxy/server.py:1230: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1301: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1305: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
Success: no issues found in 397 source files
```
## Real Behavior Proof
- Environment: Linux (WSL2) Ubuntu 24.04, Python 3.12, persistent
deployment via `headroom install agent run --profile default` with
`--backend anthropic`
- Exact command / steps:
1. Started persistent deployment: `headroom install agent run --profile
default`
2. Ran copilot wrapper: `headroom wrap copilot --subscription
--provider-type openai --wire-api responses --memory -- --model
gpt-5.4-mini`
- Observed result:
- Before fix: Proxy forwarded to `api.openai.com` instead of
`api.githubcopilot.com`, causing 401 auth errors
- After fix: Proxy correctly forwards to `api.githubcopilot.com` and
copilot works as expected
- Not tested: macOS, Windows, enterprise/data-residency accounts, other
wrapped tools (claude, codex, aider)
## 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
## Additional Notes
The fix is minimal and targeted. It only adds a feature configuration
check in the persistent deployment path without changing any other
behavior. The non-persistent proxy path already had this check; this PR
brings the same logic to persistent deployments.
Co-authored-by: carlosduplar <[email protected]>
|
||
|
|
f9f3162d38
|
docs(proxy): document HEADROOM_SAVINGS_PROFILE and correct --mode default (#2031) (#2040)
## Description
`HEADROOM_SAVINGS_PROFILE` is an implemented env var
(`headroom/agent_savings.py`) that selects a named profile bundling
Headroom's whole compression posture (proxy mode, keep-ratio, which
messages are compressed, `force_kompress`, etc.) at proxy startup. It
was entirely undocumented — `grep` over `docs/` found zero mentions.
Related, the proxy docs were **misleading about the default optimization
mode**: `docs/content/docs/proxy.mdx` stated `--mode` defaults to
`token`, but the code default is `cache`:
```python
# headroom/cli/proxy.py — the Click option has no default
@click.option("--mode", default=None, ...)
# ... mode resolution (default is CACHE):
effective_mode = normalize_proxy_mode(mode or os.environ.get("HEADROOM_MODE") or PROXY_MODE_CACHE)
```
A bare `headroom proxy` (no `--mode`, no `HEADROOM_MODE`) runs in
**cache** mode, and the default `coding` savings profile also sets
`proxy_mode="cache"` — which is exactly what the issue reporter found
confusing.
This documents `HEADROOM_SAVINGS_PROFILE` and corrects the `--mode`
default rows so the doc is accurate and internally consistent.
Closes #2031
## 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
`docs/content/docs/proxy.mdx` only:
- Corrected the `--mode` default in the Core-options table and the
Context-management table (`token` → `cache`), each pointing to the new
Savings profiles section for the reason.
- Added a `### Savings profiles` section documenting: the
`HEADROOM_SAVINGS_PROFILE` env var; a table of the four built-in
profiles (`coding` default, `balanced` fallback, `agent-90`, `general`)
with target savings, mode, and `force_kompress`; the unset→`coding`
default; the unknown-value→`balanced` warning-and-fallback (proxy never
fails to start); and the mode precedence (explicit `--mode` >
`HEADROOM_MODE` seeded by a profile > `cache` default), with an example.
No code change. Every documented value is pinned to
`headroom/agent_savings.py` (profile definitions) and
`headroom/cli/proxy.py` (default-mode resolution).
## Testing
- [x] Unit tests not run; docs-only source verification performed
- [x] Linting not run; docs-only MDX/source verification performed
- [x] Type checking not applicable; no Python code changed
- [x] New tests not applicable; documentation-only correction
- [x] Manual testing performed
### Test Output
Docs-only change; verification is cross-checking every documented value
against the source of truth:
```text
$ grep -n "DEFAULT_PROFILE = \|FALLBACK_PROFILE = " headroom/agent_savings.py
14:FALLBACK_PROFILE = "balanced"
18:DEFAULT_PROFILE = "coding"
# profile modes / knobs (agent_savings.py):
# coding → proxy_mode="cache", force_kompress=False, target_ratio=None (emergent)
# balanced → proxy_mode="token", force_kompress=False, target_ratio=0.30
# agent-90 → proxy_mode="token", force_kompress=True, target_ratio=0.10
# general → proxy_mode="token", force_kompress=False, target_ratio=None (emergent)
$ grep -n "effective_mode\|PROXY_MODE_CACHE" headroom/cli/proxy.py
# effective_mode = normalize_proxy_mode(mode or os.environ.get("HEADROOM_MODE") or PROXY_MODE_CACHE)
# → confirms the real default optimization mode is cache, not token
```
MDX sanity: code fences balance (even count) and the `### Savings
profiles` heading slugifies to `#savings-profiles`, matching the two
in-page anchor links added to the mode rows.
## Real Behavior Proof
- **Environment:** Windows 11; docs source inspected against the working
tree at the current `main` base.
- **Exact command / steps:** Each documented fact is grounded in code —
profile names, modes, `force_kompress`, and target ratios come from
`headroom/agent_savings.py:_PROFILES`; the default profile (`coding`)
from the `os.environ.get("HEADROOM_SAVINGS_PROFILE") or "coding"` reads
in `headroom/cli/proxy.py` and `headroom/proxy/server.py`; the `cache`
default mode from `headroom/cli/proxy.py`'s `mode or HEADROOM_MODE or
PROXY_MODE_CACHE`; the unknown-value fallback from
`get_agent_savings_profile` (`agent_savings.py`).
- **Observed result:** The new section's table and prose match those
sources exactly, and the previously-wrong `--mode` default rows now
state `cache`.
- **Not tested:** A live render of the Fumadocs/Next.js docs site (no
local docs build run here) — the change is MDX-syntax-valid (balanced
fences, well-formed table, standard heading-anchor slug).
## 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] Code comments not applicable; documentation-only change
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] Tests not applicable; docs-only facts verified against source
- [x] New and existing unit tests pass locally with my changes
- [x] CHANGELOG not applicable; documentation-only correction
## Screenshots (if applicable)
N/A (docs prose/table addition; a rendered screenshot can be added if
the docs site is built for preview).
## Additional Notes
- Test/tests-added checklist items are N/A — this is a
documentation-only change.
- Out of scope (intentionally): the `--mode` Click **help text** in
`headroom/cli/proxy.py` also says "default: token" and is likewise
inaccurate, but correcting Python help text is a code change beyond this
docs issue — noted as a possible follow-up.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
7ab83c5107
|
fix(router): stop protecting passing build/test output as error traces (#1740)
## Description  `content_has_strong_error_indicators()` (`headroom/transforms/error_detection.py`) protects any message/content-block from compression when it contains 2+ distinct indicator keywords (`error`, `fail`, `exception`, `traceback`, `fatal`, `panic`, `crash`). That heuristic false-positives on **passing** build/test tool output: `tsc`'s `"Found 0 errors"` plus a passing test run's `"0 failures"` trips both `error` and `fail` — 2 distinct hits — despite nothing failing. In a long JS/TS coding session this fired on nearly every request (confirmed against the `stats.json` attached to #1696), permanently protecting legitimate tool output from ever being compressed and explaining the reported 0.3% savings vs. the advertised 60-95%. Closes #1696 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/transforms/error_detection.py`: strip common zero-result phrases before the 2-keyword scan in `content_has_strong_error_indicators()` — both `"N word"`/`"word N"` forms (`"0 errors"`, `"no failures"`) and `"label:value"`/`"label=value"` forms (`"Failures: 0"`, `"errors=0"`), covering `error(s)` and `fail`/`failed`/`failing`/`failure(s)` (the scan matches `fail` by substring, so all inflections needed covering). - `tests/test_error_detection.py` (new file — no prior coverage existed): 7 tests covering real error/traceback detection, single-keyword safety, `tsc`/`eslint` passing summaries, the `"0 failed"` regression a reviewer caught, label:value formats, and that a genuine second indicator elsewhere in the same blob still triggers protection. - `.github/pr-images/issue-1696-error-protection-fix.svg`: diagram explaining the mechanism (embedded above). ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) — not run locally, CI `lint` check is green - [ ] Type checking passes (`mypy headroom`) — not run locally, CI is green - [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_error_detection.py tests/test_transforms/test_content_router.py tests/test_transforms_content_router.py -q tests\test_error_detection.py ....... [ 7%] tests\test_transforms\test_content_router.py ........................... [ 34%] ........................... [ 62%] tests\test_transforms_content_router.py ................................ [ 94%] ..... [100%] ============================= 98 passed in 1.21s ============================== ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.14.5, local venv, `headroom._core` built via `maturin develop --release` (not prebuilt in a fresh checkout) - Exact command / steps: ran the reporter's exact scenario patterns (`"Found 0 errors\nTests: 0 failures, 42 passed"`, eslint's `"0 problems (0 errors, 0 warnings)"`) through `content_has_strong_error_indicators()` directly, before and after the fix - Observed result: before → `True` (wrongly protected); after → `False` (correctly compressible). Real failure text (`Traceback... fatal error`) still returns `True` after the fix. - Not tested: have not reproduced the full KiloCode/proxy session end-to-end locally (no access to the reporter's actual traffic) — root cause was confirmed via the `stats.json` they attached to the issue, which shows `router:protected:error_output` firing on nearly every request in their 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 heuristic, no user-facing docs reference it) - [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) ## Screenshots (if applicable) See the diagram embedded in Description above. ## Additional Notes **Investigation trail**: ruled out `protect_recent_reads_fraction` (proxy already overrides its `0.0` dataclass default to `0.3` in token mode, the default) before confirming the error-protection false-positive via the reporter's `stats.json`. **Response to review comments** (@AbelVM, @sparkbugz): prose mentions like `"Fix the errors in the code."` or `"console.error(...)"` contain only 1 distinct indicator keyword and were already safe under the pre-existing 2-keyword threshold — not something this PR changes. The broader concern about other CI summary formats is addressed above (label:value forms). A case like genuine prose that happens to mention *two* distinct keywords together (e.g. "there are errors and it failed") is a known limitation of a keyword-substring heuristic in general, predates this PR, and is out of scope here — downstream compressors (LogCompressor) still preserve real error lines even when a block isn't gate-protected, so the failure mode there is "slightly stricter than ideal," not data loss. **Response to @JerrettDavis's CHANGES_REQUESTED**: fixed in the follow-up commit — `"0 failed"` is now stripped (previously only `failing`/`failure(s)` were), with a regression test for the exact reproduction given. |
||
|
|
4ea96a417c
|
feat(mcp): add streamable HTTP MCP transport (#1773)
## Description `headroom mcp serve` only exposed stdio, which blocked MCP clients that require a Streamable HTTP endpoint. This PR adds an explicit HTTP transport mode around the existing Headroom MCP server while keeping stdio as the default and keeping tool registration single-sourced. Closes #1346. ## 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add `headroom mcp serve --transport http` with host, port, and path options. - Serve `headroom_compress`, `headroom_retrieve`, and `headroom_stats` through the same MCP server instance used by stdio. - Keep `headroom mcp serve` defaulting to stdio for current Claude Code and local MCP host configs. - Update MCP docs for stdio and HTTP setup without implying the proxy automatically owns `/mcp`. - Keep the scope clean, rebased, and covered by focused tests. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q`) - [x] Linting passes (`uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py`) - [x] Type checking passes (`uv run mypy headroom --ignore-missing-imports`) - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q 20 passed in 0.53s uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py All checks passed! uv run ruff format headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py --check 5 files already formatted uv run mypy headroom --ignore-missing-imports Success: no issues found in 407 source files ``` ## Real Behavior Proof - Environment: Local Python environment with Headroom dev dependencies and MCP extra installed. - Exact command / steps: Start `headroom mcp serve --transport http --host 127.0.0.1 --port <test-port> --path /mcp`, then perform an MCP SDK Streamable HTTP initialize/list-tools exchange. - Observed result: The HTTP transport initializes and lists the existing Headroom MCP tools; `headroom mcp serve` without `--transport` still selects stdio, and mixed-case `--transport HTTP` routes to the HTTP transport. - Not tested: live validation against external MCP hosts ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes `CHANGELOG.md` is not edited because this repository generates changelog entries from conventional commits. Full-suite validation is left to CI. |
||
|
|
c46cd8f950
|
fix(core): load ONNX Runtime dynamically so headroom._core imports on non-AVX2 x86-64 (#1715)
## Description
`import headroom._core` dies with SIGILL (`Illegal instruction`) on
x86-64 CPUs without AVX2 (Pentium N4200, Celeron N4500, AMD FX 8350 —
all reported on the issue). The repo sets no `RUSTFLAGS`/`target-cpu`
anywhere, so first-party Rust code is baseline x86-64; the AVX2 code
comes from Microsoft's prebuilt ONNX Runtime, statically linked into the
extension by fastembed's `ort-download-binaries-rustls-tls` feature on
non-Windows targets. Because it is statically linked, its code is mapped
and initialized when the extension module loads — **before** the runtime
AVX2 guard from #1162 can run, which is why that fix helped Magika init
but not the import-time crash.
Fix, mirroring what Windows already does for its own reasons (DirectML
link libs): build with `ort-load-dynamic` on every platform, so ONNX
Runtime is only `dlopen`'d at first use, where the #1162 AVX2 guard
falls back to the non-ONNX detection tiers on unsupported CPUs. Since
both target blocks became identical, they are collapsed into one
platform-independent `fastembed` dependency.
To keep Magika/fastembed working out of the box on Linux/macOS, the
existing `ORT_DYLIB_PATH` auto-pin (`headroom/_ort.py`, previously
Windows-only) now resolves the pip `onnxruntime` package's shared
library on all platforms (`onnxruntime.dll` / `libonnxruntime.so*` /
`libonnxruntime*.dylib`). The pip `onnxruntime` CPU wheels use runtime
CPU dispatch, so they also work on pre-AVX2 machines — non-AVX2 users
get working ML detection instead of a crash. Without the `onnxruntime`
package, ML detection degrades gracefully to the non-ONNX tiers exactly
as it already does on Windows.
Fixes #1278
## 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
- `crates/headroom-core/Cargo.toml`: replaced the per-target `fastembed`
blocks (`ort-download-binaries-rustls-tls` on non-Windows,
`ort-load-dynamic` on Windows) with a single platform-independent
dependency on `ort-load-dynamic`, with a comment documenting both the
DirectML and the AVX2/#1278 rationale.
- `Cargo.lock`: regenerated — `ort-sys` drops its static-download
dependencies (`hmac-sha256`, `lzma-rust2`, `ureq`); no version bumps.
- `headroom/_ort.py`: `ORT_DYLIB_PATH` auto-pin extended from
Windows-only to all platforms via a small `_find_dylib` helper that
resolves the platform's shared-library name inside the pip `onnxruntime`
package.
- `tests/test_transforms/test_ort_dylib.py`: replaced the obsolete
`test_noop_on_non_windows` with Linux (versioned `.so`) and macOS
(`.dylib`) pin tests; module docstring updated.
- `docs/content/docs/configuration.mdx`: `ORT_DYLIB_PATH` row updated
from Windows-only wording to the cross-platform behavior.
## 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
$ cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings && cargo test -p headroom-core --lib
clean
test result: 844 passed; 0 failed; 1 ignored
$ python -m pytest tests/test_transforms/test_ort_dylib.py -q
8 passed
$ ruff check headroom/_ort.py tests/test_transforms/test_ort_dylib.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11 (AVX2-capable — the SIGILL itself is not
reproducible on this machine), Python 3.13, Rust 1.95.0, local checkout
branched from `upstream/main` (
|
||
|
|
1590913bb7
|
fix(build): support Intel macOS (x86_64-apple-darwin) via ort-load-dynamic (fixes #941) (#1797)
## Problem `headroom-ai` fails to build from source on Intel macOS (`x86_64-apple-darwin`), both with and without the `[all]` extra: ``` error: ort-sys@2.0.0-rc.12: ort does not provide prebuilt binaries for the target `x86_64-apple-darwin` with feature set (no features). ``` Reported in #941. `ort-sys`'s `download-binaries` strategy (used by the `ort-download-binaries-rustls-tls` fastembed feature that `headroom-core` depends on for all non-Windows targets) only ships prebuilt ONNX Runtime binaries for Windows, Linux (x86_64/aarch64), and macOS **Apple Silicon**. There's currently no way to install this package from source on an Intel Mac at all — with or without `[all]`, since the ONNX dependency lives in `headroom-core` itself, not behind a pip extra. ## Root cause `crates/headroom-core/Cargo.toml` already has a working fallback for this *exact* class of problem — for Windows, it swaps `fastembed`'s `ort-download-binaries-rustls-tls` feature for `ort-load-dynamic`, which dlopen's a system-provided ONNX Runtime at runtime instead of requiring a bundled prebuilt binary for the exact target triple. Intel macOS just never got the same treatment. ## Fix Extends the existing `ort-load-dynamic` branch to also cover `target_os = "macos", target_arch = "x86_64"`. ## Documentation Also adds an Intel-macOS subsection next to the existing "Corporate / SSL-inspection environments" section, since the `ORT_STRATEGY=system` + `ORT_LIB_LOCATION` mechanism documented there for a different reason is *also* a fully working, no-source-patch workaround available today: ```bash brew install onnxruntime ORT_STRATEGY=system \ ORT_LIB_LOCATION="$(brew --prefix onnxruntime)/lib" \ ORT_PREFER_DYNAMIC_LINK=1 \ pip install "headroom-ai[all]" export ORT_DYLIB_PATH="$(brew --prefix onnxruntime)/lib/libonnxruntime.dylib" ``` Two things cost real debugging time and seemed worth documenting either way: `ORT_LIB_LOCATION` must point at the `lib/` subdirectory specifically (the Homebrew keg has no single-file library at the prefix root — pointing at the bare prefix gets a *different*, more confusing error: "could not link to the ONNX Runtime build"), and `ORT_PREFER_DYNAMIC_LINK=1` is required — without it, `ORT_STRATEGY=system` still attempts static linking, which the Homebrew keg doesn't provide. ## Testing - `cargo check -p headroom-core` and a full `maturin build --release` succeed on Intel macOS (macOS 26.5.1) with this patch and `ORT_DYLIB_PATH` pointed at a Homebrew onnxruntime 1.27.0. - Verified beyond just compiling: loaded the built wheel's `_core.abi3.so` directly and called `detect_content_type` (the magika/ONNX-backed classifier, which shares the ONNX Runtime instance per the comment in `headroom-core/Cargo.toml`). Ran successfully, no dyld/link errors. - Independently verified the doc-only workaround builds a working wheel through the **unmodified** sdist via `pip wheel` — no Cargo.toml changes needed for that path at all. - Not tested on Apple Silicon or Linux; the `cfg()` predicate is scoped to `(target_os = "macos", target_arch = "x86_64")` so it shouldn't affect either. Happy to split this into two PRs (code fix / doc fix) if that's easier to review. ## Note I noticed a branch, `fix-wheel-matrix-vendored-openssl-and-drop-intel-mac`, that appears to drop Intel macOS from the release wheel matrix rather than fix source builds for it. It looked stale relative to `main` (interleaved with much older history) when I checked, so I wasn't sure whether it reflects current intent — if the project has already decided to drop Intel macOS support rather than fix it, feel free to close this instead, no worries either way. Co-authored-by: JD Davis <mxjerrett@gmail.com> |
||
|
|
541500811f
|
feat(cli): add wrap openclaude for OpenClaude CLI (#1416)
## Description
Adds `headroom wrap openclaude`, a Click subcommand that launches the
prose-format OpenClaude CLI through the local Headroom proxy using the
same OpenAI/Anthropic base URL environment shape as `wrap aider`.
Fixes #1411.
## Type of Change
- [x] Bug fix
- [x] New feature
- [ ] Breaking change
- [ ] Documentation update
- [x] Tests
## Changes Made
- Added the `wrap openclaude` command path for OpenClaude CLI launch env
routing.
- Kept `--no-context-tool` / `--no-rtk` support for proxy-only launch
behavior.
- Fixed the default RTK setup path requested in review: when RTK is
selected and installed, `wrap openclaude` now injects the RTK
instruction marker block into `CONVENTIONS.md` at the project root
instead of only downloading the binary.
- Added a regression test for the default RTK path so the PR fails if
OpenClaude stops receiving RTK instructions.
## 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
RED, with the production RTK injection path temporarily reverted while
keeping the new regression test:
```text
.venv/bin/python -m pytest tests/test_cli/test_wrap_openclaude.py::test_wrap_openclaude_default_rtk_injects_instructions -q
FAILED tests/test_cli/test_wrap_openclaude.py::test_wrap_openclaude_default_rtk_injects_instructions
E AssertionError: assert False
E + where False = exists()
E + where exists = PosixPath('/tmp/pytest-of-ousama/pytest-1/test_wrap_openclaude_default_r0/CONVENTIONS.md').exists
```
GREEN, after restoring the fix:
```text
.venv/bin/python -m pytest tests/test_cli/test_wrap_openclaude.py -q
3 passed in 0.46s
```
Additional validation on the pushed commit
`
|
||
|
|
e9e9cd55b7
|
feat(mcp): publish canonical server.json (#1510)
## Description Headroom can launch its MCP server, but did not publish a canonical `server.json` that registries and MCP hosts can consume directly. This PR adds a shared descriptor builder, commits a root `server.json`, parity-tests that artifact against the builder and existing runtime spec, and updates docs so registry authors do not need to reconstruct `headroom mcp serve` from prose. Closes #929. ## 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added a shared `server_json.py` descriptor builder for Headroom MCP publication metadata. - Published a canonical root `server.json` and parity-tested it against the builder. - Encoded the publishable uvx contract as `headroom-ai[mcp]` plus `headroom mcp serve`. - Updated README and MCP docs to point registry authors at the canonical descriptor. - Added the README ownership marker used by MCP Registry verification. - Kept existing registrars and `headroom mcp install` behavior unchanged. ## Testing - [x] Unit tests pass - [x] Linting passes - [x] Type checking passes - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text Focused registry/server-json tests and reviewer approval were completed on this PR before the governance body cleanup. The current body update is documentation-only metadata for PR governance. ``` ## Real Behavior Proof - Environment: Headroom development checkout with MCP test dependencies. - Exact command / steps: Inspected the generated `server.json` contract and parity coverage against the descriptor builder and runtime MCP spec. - Observed result: The committed descriptor matches the builder/runtime contract and advertises the intended `headroom-ai[mcp]` / `headroom mcp serve` launch path. - Not tested: live publication to third-party registries ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes This body was normalized by a maintainer after approval so the governance parser reflects the already-reviewed PR state. |