Commit graph

2221 commits

Author SHA1 Message Date
Vinay Gupta
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
2026-07-15 19:57:46 +00:00
LunarECL
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 4f22cbb0
3 failed, 7723 passed, 515 skipped in 262.64s
  FAILED tests/test_cli/test_wrap_claude_base_url.py::test_wrap_marker_is_stale_when_pid_reused
  FAILED tests/test_rtk_session_savings.py::test_rtk_reader_returns_none_on_nonzero_exit
  FAILED tests/test_rtk_session_savings.py::test_lean_ctx_reader_returns_none_on_failure_and_logs
  → all three reproduce identically on unmodified origin/main (4f22cbb0), run the
    same way (same worktree + venv, sources switched): 3 failed, 7707 passed —
    this branch = baseline + the 16 new tests, nothing else changes.
    (The pre-rebase run against e8151f05 showed the same shape: one order-dependent
    flake that also reproduced on its baseline; these are env/order-dependent.)

$ uv run pytest tests/test_cli/ -q     # post-rebase
542 passed + 1 of the pre-existing failures above   # includes the 16 new test_wrap_omp.py tests

$ uv run ruff check . ; echo ruff-check-exit:$?
All checks passed!
ruff-check-exit:0

$ uv run ruff format --check .         # post-rebase
1 pre-existing violation: headroom/proxy/handlers/anthropic.py — flagged identically
on unmodified origin/main (not touched by this PR); every file this PR touches is clean

$ uv run mypy headroom               # post-rebase; output redirected to file; exit captured
Success: no issues found in 409 source files
mypy-exit:0
```

## Real Behavior Proof

- Environment: macOS 15 (arm64, M1 Pro), Python 3.12.13 (uv venv,
editable install incl. Rust `_core`), headroom @ this branch, base
extras only (no `[ml]`), Anthropic account signed into omp. Initial
proof ran on base e8151f05 with omp 16.3.6 (`@oh-my-pi/pi-coding-agent`
via bun); re-validated after the rebase onto 4f22cbb0 with omp 16.3.11 —
fresh numbers in the rebase-validation comment.

- Exact command / steps: four scenarios, run in this order —
1. Mechanism probe (why models.yml, not env): local HTTP probe server on
`127.0.0.1:18999`; ran `omp -p "say ok" --model claude-fable-5
--no-session --no-tools` once with
`ANTHROPIC_BASE_URL=http://127.0.0.1:18999`, once with
`~/.omp/agent/models.yml` containing `providers.anthropic.baseUrl:
http://127.0.0.1:18999`.
2. One-command path: `headroom wrap omp --no-rtk --port 8790 -- -p "Read
CHANGELOG.md and count how many '### Fixed' headings it contains. Answer
with just the number." --model claude-fable-5 --no-session --max-time
180`
3. Routing stats: separate proxy on :8788, wrap with `--no-proxy`, then
`GET /stats`.
4. Restore: `headroom unwrap omp`, plus an isolated
`PI_CODING_AGENT_DIR=/tmp/omp-agent-test` run with a pre-existing user
`models.yml`, then `cmp` against the original.

- Observed result: end-to-end routing through the proxy proven for every
scenario —
- Probe: env-var run → **0 probe hits**, omp answered normally
(bypassed). models.yml run → **9 hits on `/v1/messages?beta=true`** with
real Messages bodies. This is the routing mechanism the wrap uses.
- One-command run: wrap started the proxy ("Proxy ready on
http://127.0.0.1:8790"), wrote the override (`models.yml:
providers.anthropic.baseUrl=http://127.0.0.1:8790/p/headroom-wrap-omp`),
launched omp, and omp answered **"7"** (correct — real `read` tool work
through the proxy). Proxy log for the session (3 requests,
`anthropic_messages` path):
    ```
PERF model=claude-fable-5 msgs=1 tok_before=36 cache_read=0
cache_write=61939 cache_hit_pct=0
PERF model=claude-fable-5 msgs=3 tok_before=796 cache_read=0
cache_write=63308 cache_hit_pct=0
PERF model=claude-fable-5 msgs=5 tok_before=935 cache_read=63308
cache_write=215 cache_hit_pct=100
    ```
    Prompt caching survives the proxy (100% hit on the follow-up turn).
- Routing stats (:8788 session): `requests.total: 2, by_provider:
{"anthropic": 2}, by_model: {"claude-fable-5": 2}`, per-project prefix
`/p/headroom-wrap-omp` attributed.
- Unwrap: `Removed wrap-created models.yml` (file gone); isolated
pre-existing-file run: backup created, user's `my-gw` provider preserved
in the managed file, and after `unwrap omp` the restored file is
**byte-identical** (`cmp` clean).
- Compression: **not observed in this environment** — `tok_saved=0`,
`transforms=router:noop` / `too_small`. Honest reading: omp minimizes
its own tool outputs client-side (a 300-item JSON tool result reached
the proxy at only ~657 tokens) and the `[ml]` text compressor wasn't
installed; small print-mode payloads sit below crush thresholds, and
passthrough-by-default is the documented safety contract. The wrap's
value here is proven at the routing/lifecycle/cache layer; compression
numbers will match whatever the proxy does for a given content mix.

- Not tested: Windows / Linux; lean-ctx mode with omp
(`HEADROOM_CONTEXT_TOOL=lean-ctx` — `lean-ctx init --agent omp` depends
on lean-ctx recognizing the agent; failure degrades with a warning by
design); long interactive (non `-p`) sessions; `--memory` / `--learn` /
`--code-graph` flags combined with omp; OAuth-vs-API-key matrix beyond
my local account.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A — terminal evidence inline above.

## Additional Notes

- The models.yml override is regenerated from the pristine backup on
every wrap, so re-running with a different `--port` updates the endpoint
idempotently and the backup is never clobbered.
- Scope note from #1149 stands: this routes omp's **Anthropic** provider
family. omp's other providers (OpenAI-direct, Gemini, ...) resolve their
endpoints from their own registry entries; users can already point those
at Headroom with their own custom provider in `models.yml`.
- `headroom/providers/omp/` deliberately contains no install-time / MCP
pieces — this is the thin wrap + unwrap slice only.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-15 19:30:19 +00:00
Manmit Singh
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>
2026-07-15 19:09:56 +00:00
Vsevolod Rychkov
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>
2026-07-15 18:57:32 +00:00
roman-t3a
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 `b17c6d81` / `93a1f211`
(Codex/Cursor/Aider extraction).
- Routing uses the session env var only (not `config.toml` endpoint
override) so `grok login` session auth continues to work.
- Manual E2E wrap/unwrap with real Grok sessions is left for maintainer
verification.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-15 18:51:38 +00:00
Andrei Boldyrev
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>
2026-07-15 18:42:20 +00:00
JD Davis
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.
2026-07-15 18:37:20 +00:00
JD Davis
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>
2026-07-15 18:36:52 +00:00
nangsontay
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>
2026-07-15 18:19:09 +00:00
Matthew Jackson
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>
2026-07-15 18:18:52 +00:00
Chester
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>
2026-07-15 18:18:41 +00:00
nangsontay
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>
2026-07-15 18:18:34 +00:00
Ingmar Krusch
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>
2026-07-15 18:18:20 +00:00
Jervis
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>
2026-07-15 18:18:13 +00:00
Rod Boev
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>
2026-07-15 18:18:06 +00:00
Chester
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>
2026-07-15 18:17:31 +00:00
Chester
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>
2026-07-15 18:17:24 +00:00
Chester
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>
2026-07-15 18:17:17 +00:00
Zhenjia ZHOU
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).
2026-07-15 18:16:55 +00:00
Zhenjia ZHOU
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>
2026-07-15 18:15:53 +00:00
Abhay Singh
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.
2026-07-15 18:15:45 +00:00
Ayush Kumar Jha
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>
2026-07-15 18:14:58 +00:00
JD Davis
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.
2026-07-15 11:50:53 -05:00
jichaowang02-lang
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>
2026-07-15 09:26:34 -05:00
Eyal Mizrachi
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>
2026-07-15 09:25:52 -05:00
yiihao
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>
2026-07-15 09:24:44 -05:00
Tejas Chopra
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>
2026-07-15 09:23:44 -05:00
Tejas Chopra
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 `fe5176db`: `uv run --frozen --extra dev python -m
pytest tests/test_proxy_extensions.py -q`, `uvx ruff==0.15.17 check
headroom/proxy/extensions.py tests/test_proxy_extensions.py
--output-format concise`, `git diff --check`, and commit hooks all
passed.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 22:50:03 -07:00
Tejas Chopra
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)
2026-07-14 22:49:39 -07:00
JD Davis
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>
2026-07-14 20:40:28 -07:00
Rod Boev
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>
2026-07-14 20:19:46 -07:00
Rod Boev
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>
2026-07-14 20:18:51 -07:00
David Wells
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>
2026-07-14 16:07:39 -04:00
ninosat00
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>
2026-07-14 16:07:34 -04:00
Doyeon Baek
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>
2026-07-14 16:07:29 -04:00
Matthew Jackson
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>
2026-07-14 16:07:25 -04:00
Connor Campbell
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>
2026-07-14 16:07:21 -04:00
Noam Asor
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>
2026-07-14 16:07:17 -04:00
Waldo Zins
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>
2026-07-14 16:07:08 -04:00
Rocker Zhang
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>
2026-07-14 14:10:42 -04:00
Ingmar Krusch
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>
2026-07-14 14:10:39 -04:00
Eyal Mizrachi
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>
2026-07-14 14:10:34 -04:00
Carlos Duplar Mello
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]>
2026-07-14 14:10:31 -04:00
Shubham Srivastava
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>
2026-07-14 14:10:28 -04:00
Ashish Patel
7ab83c5107
fix(router): stop protecting passing build/test output as error traces (#1740)
## Description

![Error output protection false-positive
fix](https://raw.githubusercontent.com/ashishpatel26/headroom/fix/1696-error-protection-false-positive/.github/pr-images/issue-1696-error-protection-fix.svg)

`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.
2026-07-14 13:25:49 -04:00
Rod Boev
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.
2026-07-14 13:25:45 -04:00
Parideboy
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` (9fbd47ba).
- Exact command / steps: `cargo check -p headroom-core` after the
feature switch; inspected the `Cargo.lock` diff; rebuilt and ran `python
-c "import headroom; from headroom._core import detect_content_type;
print(detect_content_type('hello world'))"`; ran the ort-pin test suite
with monkeypatched `linux`/`darwin` platforms.
- Observed result: build succeeds with `ort-load-dynamic`; the lockfile
shows `ort-sys` no longer pulls the binary-download machinery
(`hmac-sha256`, `lzma-rust2`, `ureq` removed), confirming the
statically-linked prebuilt ORT is gone; import + content detection works
with `ORT_DYLIB_PATH` auto-pinned to the pip onnxruntime library; all 8
pin tests pass including the new Linux/macOS branches.
- Not tested: actual pre-AVX2 x86-64 hardware (none available — the fix
removes AVX2 code from the import path by construction, and the issue
reporters on #1278 can verify); Linux/macOS wheel runtime behavior
beyond CI's ubuntu/macOS wheel-build jobs; embedding quality/performance
under a pip-provided ORT version differing from the previously vendored
one.

## Review Readiness

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 13:25:41 -04:00
Gregory R. Warnes
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>
2026-07-14 13:25:37 -04:00
Ben Younes
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
`e97f17908d`:

```text
.venv/bin/python -m ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_openclaude.py
All checks passed!

.venv/bin/python -m mypy headroom/cli/wrap.py --ignore-missing-imports
Success: no issues found in 1 source file

.venv/bin/python -m pytest tests/test_cli -q
439 passed, 3 skipped in 10.11s
```

## Real Behavior Proof

- Environment: local Linux checkout, Python 3.13.12, pytest 9.0.3,
branch `fix/issue-1411-pr` pushed to `ousamabenyounes:fix/issue-1411`.
- Exact command / steps: ran the new OpenClaude RTK regression test with
the production injection path reverted, then restored the fix and reran
targeted, lint, type, and CLI suite checks.
- Observed result: the reverted production path failed because
`CONVENTIONS.md` was not created; the fixed path creates the OpenClaude
instruction file and includes the Headroom RTK marker block.
- Not tested: launching a real installed `openclaude` binary
interactively; tests patch process launch to assert the exact env/args
and instruction-file side effects without starting an external CLI.

## Review Readiness

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

## Checklist

- [x] Code follows the project style
- [x] Tests cover the changed behavior
- [x] Existing CLI tests pass locally
- [x] No secrets or credentials are included

## Screenshots (if applicable)

N/A.

## Additional Notes

Addressed the requested RTK setup-path review by making default `wrap
openclaude` write durable RTK instructions instead of treating RTK setup
as a binary-only no-op.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 13:25:33 -04:00
Rod Boev
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.
2026-07-14 13:25:29 -04:00