Commit graph

2260 commits

Author SHA1 Message Date
Tanmay Garg
4e2bbfee3f
fix(opencode): Use opencode.jsonc when present (#1590)
## Description

Fix OpenCode proxy injection so it respects user configurations that use
the `.jsonc` extension, preventing Headroom from creating a duplicate
`.json` file that overrides it.

Closes #1588

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

- Updated `opencode_config_path` in `paths.py` to check for `.jsonc`
- Updated backup creation in `config.py` to preserve the original
extension

## Testing

<!-- Check what you actually ran, then paste the real command output
below. -->

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

### Test Output

```text
N/A
```

## Real Behavior Proof

- Environment: local headroom dev
- Exact command / steps: creating a dummy
`.config/opencode/opencode.jsonc` and running `headroom wrap opencode`.
- Observed result: Headroom successfully injects into `.jsonc` and
creates a backup named `opencode.jsonc.headroom-backup`.
- Not tested: N/A

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

## Additional Notes
2026-07-16 13:51:21 -07:00
guyoron1
f42ce4a239
fix: harden fd lifecycle and SystemError handling in runtime and proxy kill (#1556)
## Description

Make `pid_alive()` safe on Windows even when `psutil` is not installed,
and harden `_kill_proxy_by_pid` exception handling for stale PIDs.

### Problem

`headroom._subprocess.pid_alive()` falls back to `os.kill(pid, 0)` when
`psutil` cannot be imported. On Windows, CPython routes `os.kill(pid,
0)`
through `TerminateProcess` — a destructive call that **kills the target
process**. Since `psutil` is not a declared runtime dependency in
`pyproject.toml`, a normal lightweight install can hit that fallback,
meaning `runtime_status()` can silently terminate a live proxy.

### Fix

- **`headroom/_subprocess.py`**: On `win32`, bypass `os.kill` entirely
and probe via `kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)`.
  If `ctypes` also fails, return `True` conservatively (assume alive)
  to prevent false-negative liveness from causing callers to kill a
  running process.
- **`headroom/cli/wrap.py`**: Widen `_kill_proxy_by_pid` exception
  handlers on both SIGTERM and SIGKILL paths to catch `OSError` and
  `SystemError` (Windows `WinError 87`), preventing crashes from
  stale/invalid PIDs.

## Type of Change

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

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom/_subprocess.py`)

### New Tests

- `test_pid_alive_win32_no_psutil_never_calls_os_kill` — simulates
  `win32` + broken `psutil`, asserts `os.kill` is never called and
  the `kernel32.OpenProcess` path is used instead
- `test_pid_alive_win32_no_psutil_no_ctypes_returns_conservative` —
  simulates `win32` + broken `psutil` + broken `ctypes`, asserts
  `os.kill` is never called and `True` is returned conservatively
2026-07-16 13:51:03 -07:00
Gautam Sharma
5279c33b19
fix(memory): preserve semantically similar memories (#2303)
## Description

Prevent memory_save from automatically deleting semantically similar but
distinct memories. The previous fire-and-forget deduplication path
deleted existing memories at cosine similarity scores of 0.92 or higher
after the save had already returned success. Similarity remains
available as a consolidation hint, while supersession now requires an
explicit memory_update or memory_delete operation.

  ## Type of Change

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

  ## Changes Made

  - Removed the automatic background deletion scheduled by memory_save.
- Removed the automatic-dedup threshold and background coroutine that
were no longer needed.
  - Preserved the existing similarity search and consolidation hint.
  - Kept explicit memory_update and memory_delete behavior unchanged.
- Added a regression test proving that distinct memories survive even at
0.99 simulated similarity.
  - Added an Unreleased changelog entry.

  ## Testing

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

  ### Test Output

$ uv run --extra dev --frozen pytest
tests/test_memory_handler_native_ops.py
  33 passed

  $ uv run --extra dev --frozen ruff check .
  All checks passed!

$ uv run --extra dev --frozen ruff format --check
headroom/proxy/memory_handler.py tests/test_memory_handler_native_ops.py
  2 files already formatted

  $ uv run --extra dev --frozen mypy headroom --ignore-missing-imports
  Success: no issues found in 504 source files

  $ uv run --extra dev --frozen pytest
  9361 passed, 565 skipped, 4 failed

The four full-suite failures are unrelated to this diff: the Anthropic
compaction test passed in isolation; the Codex recovery test exceeded
the macOS AF_UNIX path limit; the dashboard test expects text absent
from the existing implementation; and the content-router test expects a
  fallback absent from the existing strategy chain.

The repository-wide format check also flags pre-existing formatting in
the untouched headroom/proxy/handlers/anthropic.py.

  ## Real Behavior Proof

- Environment: macOS on Apple Silicon, CPython 3.12.13, real
LocalBackend, temporary SQLite database, and the local
sentence-transformers
    embedding backend; no external provider or model API.

- Exact command / steps: Ran uv run --extra dev --frozen python with a
temporary database, saved User's primary backend framework at work is
FastAPI., queried its similarity to User's primary backend framework at
home is FastAPI., saved the second fact through
    MemoryHandler._execute_save, and listed the user's memories.

- Observed result: The real embedding similarity was 0.9387, above the
former 0.92 deletion threshold. The second save returned saved,
included the consolidation hint, retained the original memory, and left
both distinct facts in the database (memory_count: 2).

- Not tested: Live OpenAI or Anthropic provider calls, a deployed proxy
or MCP client session, and Qdrant or Neo4j memory backends. These
paths share the handler policy changed here; backend-specific explicit
update and delete behavior is unchanged.

  ## Review Readiness

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

  ## Checklist

  - [x] My code follows the project's style guidelines
  - [x] I have performed a self-review of my code
- [ ] 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 documentation and code-comment checklist items are not applicable
because this change removes unsafe behavior without introducing a new
public interface or complex implementation. The full-suite checkbox
remains unchecked because four unrelated tests failed locally, as
  documented above.
2026-07-16 11:32:38 -07:00
Rod Boev
26b43f64d6
fix(proxy): keep anthropic ccr compression active across deferred injection (#2291) (#2297)
## Description

Large native Claude Code requests on the Anthropic path can still
forward with zero request compression after CCR tool injection is
deferred on a frozen prefix. The stale skip branch treats deferred
injection as a reason to bypass request compression entirely, even
though the later sticky CCR path already knows when new markers actually
require the tool. This removes that stale bypass so compression still
runs while the reversible CCR path stays intact. Closes #2291.

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

- Remove the stale `should_skip_ccr_request_compression` branch from
`headroom/proxy/handlers/anthropic.py`, so deferred CCR tool injection
no longer bypasses request compression in token, non-cache, or cache
mode.
- Keep the existing sticky CCR injection path as the only place that
decides whether historical markers need the retrieval tool reintroduced.
- Update `tests/test_proxy/test_anthropic_ccr_deferred_injection.py` to
cover the two broken zero-compression cases and preserve the
already-reversible frozen-prefix path.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py::test_cache_mode_compresses_delta_but_replays_cached_prefix_when_markers_are_historical
tests/test_proxy/test_anthropic_ccr_deferred_injection.py::test_non_token_non_cache_mode_keeps_compression_and_injects_tool_for_new_markers
tests/test_proxy/test_anthropic_ccr_deferred_injection.py::test_existing_retrieve_tool_keeps_reversible_ccr_path_when_prefix_is_frozen
tests/test_openai_tool_search_deferral.py
tests/test_openai_responses_compression_units.py -q`)
- [x] Linting passes (`uv run ruff check .`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
======================= 53 passed, 1 warning in 12.71s ========================
All checks passed!
1310 files already formatted
```

## Real Behavior Proof

- Environment: synced branch and base worktrees on a local Windows proxy
test host
- Exact command / steps: run the updated Anthropic deferred-injection
regressions directly against the base package tree and the branch
package tree, then run the focused branch pytest suite above
- Observed result: the base package tree fails the two updated
zero-compression regressions (`FAIL
test_cache_mode_compresses_delta_but_replays_cached_prefix_when_markers_are_historical`,
`FAIL
test_non_token_non_cache_mode_keeps_compression_and_injects_tool_for_new_markers`)
while preserving the already-reversible path; the branch package tree
prints three `PASS` lines for the same trio and keeps the neighboring
OpenAI suites green in the 53-test focused run
- Not tested: a live upstream Claude Code request with the reporter's
exact provider/model 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
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

`CHANGELOG.md` is not applicable here because Headroom's release
pipeline derives it from conventional commits.

Scope is limited to the Anthropic CCR request-compression seam that
current #2291 evidence exercises. OpenAI tool-search deferral is
untouched because the current live issue is a native Claude Code path
and the concrete stale skip branch on `origin/main` is in
`anthropic.py`.

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-16 09:32:27 -07:00
Tejas Chopra
1d79e70f95
fix(tests): repair three main-branch test failures (#2306)
## Description

`main` CI is red on three independent test failures. All three are
**test-side** bugs (stale cache, semantic merge conflict, stale mock) —
no product code regressed. Each test passed in isolation but failed on
`main`, and each also blocks the `chore: release main` PR (#1923).

Closes #

## Type of Change

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

## Changes Made

- **`test_l2_appends_transform_label`** — `tool_desc_max_chars()`
memoises into a module global. An earlier test in shard 1 reads it with
the env unset, pinning the cache to `0`, so this test's
`setenv("HEADROOM_TOOL_DESC_MAX_CHARS=20")` was swallowed (`assert 0 ==
20`). Reset the cache before reading and after, mirroring the sibling
`test_l2_skips_label_when_disabled`.
- **`test_dashboard_uses_cached_stats_and_lazy_history_feed_polling`** —
semantic merge conflict: #2198 (persist lifetime metrics) intentionally
retired the session-card `Filtered (lifetime)` row and moved
CLI-filtering lifetime into the history tab as `Lifetime Saved`, while
the assertion from #1433 still checked the old string. Assert the
current `Lifetime Saved` label.
- **`test_smart_crusher_log_fallback_runs_for_valid_json`** — stale
mock: #1857 made token counting whitespace-aware, so the router now
rates the JSON above the naive `len(content.split())==8` the no-op
kompress mock reported, making it look like a saving and
short-circuiting before the Log fallback. Mock now reports
`_estimate_tokens(content)` to match the router.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)

### Test Output

```text
$ pytest tests/test_anthropic_compaction_transforms.py \
         tests/test_proxy_dashboard_stats_cache.py \
         tests/test_transforms_content_router.py -q
78 passed, 1 skipped in 12.14s

$ ruff check <the three files>
All checks passed!
$ ruff format --check <the three files>
3 files already formatted
```

## Real Behavior Proof

- Environment: local `.venv`, Python 3.12.6, pytest 9.0.2 (same three
tests that fail on the `main` CI shards 1/3/4).
- Exact command / steps: ran the three previously-failing tests by node
id — all pass. Reproduced the shard-isolation failure for #1 by calling
`tool_desc_max_chars()` with the env unset (cache → 0) before the test,
confirmed the reset makes it pass.
- Observed result: 3/3 target tests pass; 78 passed / 1 skipped across
the three full files.
- Not tested: full suite (unchanged product code); CI shards will re-run
on this PR.

## Review Readiness

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

## Additional Notes

`mypy headroom` (the CI-enforced scope) is unaffected — these edits
touch only `tests/`, which CI does not type-check. Once this lands on
`main`, the `chore: release main` PR (#1923) drops to just the
`test_root_server_json_matches_builder` failure, which is the release
version-bump `server.json` regen (not a code bug).
2026-07-16 09:21:41 -07:00
Tejas Chopra
718c8dc559
fix(proxy): repair main lint (ruff-format drift + mypy host_header) (#2268)
## Description

`main`'s `lint` CI job is currently **red** (latest main `eac49656` →
`lint: failure`), which blocks every open PR. Two causes, both from
recent merges that were green in isolation but combined into a red
`main`:

- **ruff-format drift** on 7 files — committed with formatter output
that ruff `0.15.17` (the CI-pinned version) rewrites.
- **mypy error** in `server.py`:
`_request_has_same_origin_or_no_provenance(request, host_header)` —
`host_header` is `request.headers.get("host")` (`str | None`) but the
function requires `str`.

These passed per-PR because each PR's checks ran against an older base;
the serialized `main` state is what went red — a logical-merge /
tool-version gap that per-PR CI doesn't catch without a strict merge
queue.

## Type of Change

- [x] Bug fix (CI/lint repair)

## Changes Made

- `ruff format` (0.15.17) the 7 drifted files — formatting only, no
logic changes: `cli/proxy.py`, `proxy/forwarded_headers.py`,
`proxy/savings_tracker.py`, `proxy/server.py`, `tests/conftest.py`,
`tests/test_persistent_metrics_persistence.py`,
`tests/test_proxy_loopback_gating.py`.
- Add `assert host_header is not None` after the
`is_ip_literal_host_header()` guard (which already rejects a missing
Host), narrowing the type for the same-origin check.

## Testing

- [x] `ruff check .` — clean
- [x] `ruff format --check .` — clean (tracked)
- [x] `mypy headroom --ignore-missing-imports` — clean

### Test Output

```text
$ mypy headroom --ignore-missing-imports  → Success: no issues found in 504 source files
$ ruff check .                            → All checks passed (tracked)
$ ruff format --check .                   → clean (tracked)
```

## Real Behavior Proof

- Environment: branch off current `main` (`eac49656`), ruff 0.15.17 +
mypy 1.20.2 (CI-pinned).
- Confirmed `lint: failure` on main's latest CI run; after this change
all three lint steps pass locally.
- Not tested: full pytest suite — formatting + a type-narrowing `assert`
only, no behavior 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 (this *is* the
style fix)
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [ ] Tests added (N/A — no behavior change)
- [x] New and existing unit tests pass locally
- [ ] CHANGELOG (N/A)

## Additional Notes

The 7 files were touched by recent merges (#2198, #2247) whose local
ruff differed from the pinned `0.15.17`. Merging this unblocks the
`lint` gate for all open PRs (including #2207).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 23:18:50 -07:00
kaz
eac49656a1
feat(wrap): add headroom wrap kimi for Kimi CLI (#1426)
## Description

Adds `headroom wrap kimi`, routing Kimi CLI through the Headroom proxy.

Kimi CLI speaks an OpenAI-compatible `/chat/completions` API (its
`kosong` backend wraps `AsyncOpenAI`) and lets the base URL be
overridden via `KIMI_BASE_URL`. This wrapper points it at the local
proxy. Kimi's own OAuth bearer is forwarded upstream unchanged, so —
unlike the Copilot subscription path — no extra login or token exchange
is needed.

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- `headroom/providers/kimi/`: new slice; `build_launch_env` sets
`KIMI_BASE_URL` with the per-project base-URL prefix, mirroring the
aider/vibe slices.
- `headroom/cli/wrap.py`: `kimi` subcommand; falls back to the
`kimi-cli` binary when `kimi` is not on `PATH`; `--kimi-api-url`
overrides the upstream coding endpoint (default
`https://api.kimi.com/coding/v1`).
- `tests/test_cli/test_wrap_kimi.py`: 8 tests for the wrap command.
- `README.md`: Kimi CLI row in the agent-compatibility matrix.

## 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_kimi.py -q
........                                                                 [100%]
8 passed in 0.36s

$ ruff check headroom/providers/kimi headroom/cli/wrap.py tests/test_cli/test_wrap_kimi.py
All checks passed!

$ ruff format --check headroom/providers/kimi headroom/cli/wrap.py tests/test_cli/test_wrap_kimi.py
4 files already formatted
```

## Real Behavior Proof

- Environment: macOS; Kimi CLI (`kimi` / `kimi-cli`); `headroom proxy`
started with `--openai-api-url https://api.kimi.com/coding/v1`.
- Exact command / steps: start `headroom proxy --port 8787
--openai-api-url https://api.kimi.com/coding/v1`, then `curl -s
http://localhost:8787/v1/chat/completions` with the Kimi OAuth bearer
and a one-line `kimi-for-coding` chat request (`"Reply with exactly:
PONG"`).
- Observed result: `HTTP 200`; `choices[0].message.content == "PONG"`
from `kimi-for-coding`; the OAuth bearer was forwarded and accepted
upstream; the per-project path `/p/<name>/v1/chat/completions` also
returned `HTTP 200`.
- Not tested: Windows/Linux PATH discovery; the `--learn` / `--memory`
live paths beyond flag wiring.

## Review Readiness

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

## Checklist

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

## Additional Notes

- `ruff check` and `ruff format --check` pass locally; `mypy` was run on
the new `headroom/providers/kimi` slice only (clean), so the full-tree
`mypy headroom` box is left unchecked and is left to CI.
- The slice deliberately reuses `codex.proxy_base_url` and
`with_project_prefix`, identical to the aider/vibe wrappers, so
per-project savings attribution works without Kimi sending custom
headers.
- Kimi's separate search/fetch services are out of scope for
`KIMI_BASE_URL` and continue to hit Kimi directly; only the LLM
`/chat/completions` traffic is compressed.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 21:40:58 +00:00
Ashish
46d4378cf7
feat(evals): weekly HotpotQA answer-recall report on the prose path (#1188)
## Description

Follow-up to **#1187** (the offline fidelity gate). That gate is
hermetic and **structured-only** (JSON tool outputs via Rust
compressors) so it can block every PR with zero setup. This PR adds the
genuinely-uncovered piece: **prose answer-recall on a real dataset
(HotpotQA)** in the **model-allowed weekly job**, where compression
routes through Kompress (ModernBERT).

> **Stacked on #1187.** Until that merges, this PR's diff shows its
commit too; it reduces to just `c71cc0cb` once #1187 lands. Please
review/merge #1187 first.

Closes #

## Type of Change

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

## Changes Made

- **`CompressionOnlyRunner.evaluate_dataset_recall(suite)`**: for each
QA case, compress the supporting `context` via the production routing
path (`ContentRouter`) and check the `ground_truth` answer survives
(`compute_information_recall`). Counts only **probeable** cases — answer
literally present in the context and non-trivial (skips `yes/no`,
too-short) — so the aggregate is meaningful rather than inflated by
un-measurable cases.
- **`.github/workflows/eval.yml`**: a non-blocking step in the existing
`weekly-suite` job (schedule/manual only) drives it with
`load_hotpotqa(n=50)`. Defensive: a dataset download or model failure
emits `:⚠️:` and `|| true`, never failing the job.
- **Hermetic unit test** (`tests/test_dataset_recall_runner.py`):
exercises the method with synthetic JSON-array contexts (SmartCrusher /
Rust — no model, no network), so it runs in the standard `[dev]` shard.

### Scope notes

- **Prose path only.** BFCL / tool-schema integrity is already covered
by the existing `evaluate_tool_schema_compaction` eval (which runs in
the PR smoke-test), so this targets the previously-uncovered prose
recall path. NQ is an easy further extension using the same method +
`load_natural_questions`.
- **Why weekly, not per-PR.** Real datasets need a network download +
the ModernBERT model. The `weekly-suite` job already installs `[all]`
and genuinely runs every Monday (verified: 5 consecutive successful
scheduled runs), so it's the correct home — keeping PR CI fast and
hermetic.

## 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
$ HF_HUB_OFFLINE=1 python -m pytest tests/test_dataset_recall_runner.py -q
..                                                                       [100%]
2 passed in 0.20s
```

## Real Behavior Proof

- Environment: local checkout of `feat/weekly-dataset-recall`, `pip
install -e ".[dev]"`, `HF_HUB_OFFLINE=1` (proves the unit tests need no
model/network)
- Exact command / steps: `HF_HUB_OFFLINE=1 python -m pytest
tests/test_dataset_recall_runner.py -q` -> `6 passed in 0.36s`; coverage
JSON confirms the runner's per-case exception handler and both
`warm_kompress_model` outcomes are exercised
- Observed result: with a synthetic suite of 3 cases (one probeable
answer in an error row, one trivial `yes`, one absent answer),
`evaluate_dataset_recall` counts only the 1 probeable case (`passed=1`,
`accuracy_rate=1.0`, `benchmark="dataset_recall:synthetic"`); a
monkeypatched compressor crash records the error and counts the case
failed instead of aborting; the new weekly-suite YAML step parses via
`yaml.safe_load` and sits under the `schedule || workflow_dispatch`
guard
- Not tested: the live HotpotQA download + ModernBERT compression --
exercised only by the weekly job (or `workflow_dispatch`), by design

## Review Readiness

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

## Checklist

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

## Additional Notes

- CHANGELOG/version intentionally untouched: repo uses
**release-please**.
- The weekly job can be triggered on demand via **workflow_dispatch** to
see the HotpotQA recall numbers without waiting for Monday.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-15 21:40:55 +00:00
Semianchuk Vitalii
63f74aa3e6
fix: replace computer_call_output with apply_patch_call_output in output_shaper (#2250)
The _RESPONSES_TOOL_OUTPUT_TYPES frozenset in output_shaper.py had
computer_call_output instead of apply_patch_call_output, making it
inconsistent with the canonical definitions in handlers/openai.py and
output_turn_policy.py. This caused apply_patch_call_output items to be
misclassified, preventing effort routing optimization for those turns.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 21:33:25 +00:00
AxelRay
81d40a6437
fix(proxy): record Prometheus metrics for POST /v1/compress (#2247)
## Description

`POST /v1/compress` compressed messages correctly but never recorded
Prometheus business metrics. Standalone compress microservice
deployments (including LiteLLM `guardrail: headroom`) left
`headroom_requests_total`, token counters, latency, and
by_model/by_provider families at zero.

This wires the existing request-outcome funnel into `handle_compress` so
success and timeout paths update the same counters as reverse-proxy
handlers, and hard failures call `record_failed`.

Closes #2244

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

- On successful compression, record a `RequestOutcome` with
`provider="compress"`, model, token before/after/saved, latency,
transforms, tags, and client.
- On compression timeout (fail-open), record zero-savings outcome plus
`record_compression_failed("timeout")`.
- On hard compression errors (503), call
`metrics.record_failed(provider="compress")`.
- Leave bypass header and empty-message early returns unrecorded (no
real compression work).
- Response schemas and status codes unchanged.
- Add regression tests for success, timeout, and hard-failure metric
recording.

## 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 pytest tests/test_proxy_compress_endpoint.py -q
# 16 passed

uv run ruff check headroom/proxy/handlers/openai.py tests/test_proxy_compress_endpoint.py
# passed
```

## Real Behavior Proof

- Environment: local checkout of this branch; FastAPI TestClient
fixtures for `/v1/compress` (loopback client)
- Exact command / steps:
  - `uv run pytest tests/test_proxy_compress_endpoint.py -q`
- `uv run ruff check headroom/proxy/handlers/openai.py
tests/test_proxy_compress_endpoint.py`
- Observed result:
- Success path awaits `_record_request_outcome` with `tokens_saved =
max(0, before-after)` and `provider="compress"`
- Timeout path records zero-savings outcome and
`record_compression_failed("timeout")`
- Hard failure path awaits `record_failed(provider="compress")` and
still returns 503
- Not tested: live multi-process scrape of `GET /metrics` while a real
headroom process handles LiteLLM guardrail POSTs

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

- Focused compress endpoint suite only; full-repo mypy was not run.
- No public API or response-schema changes.
2026-07-15 21:27:59 +00:00
Dávid Balatoni
5424e99a65
Clarify uv tool install path on macOS (#1196)
## Description

Clarifies the recommended install path for the Headroom CLI on macOS
Apple Silicon and Linux. The docs now prefer `uv tool install --python
3.13 "headroom-ai[all]"` for host-level CLI use, keep `pip install`
scoped to Python project environments, and call out absolute executable
paths for MCP clients that do not inherit interactive shell `PATH`.

## 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 `uv tool install --python 3.13` guidance to the README, docs
install page, quickstarts, and wiki install pages.
- Documented `uv tool update-shell` for shells that cannot find the
installed `headroom` command.
- Clarified absolute MCP server command paths for clients that do not
inherit the interactive shell `PATH`.
- Pointed Intel macOS users at the Docker-native install path until
native wheel support lands.

## Testing

Describe the tests you ran to verify your changes:

- [ ] Unit tests pass (`pytest`) - not run; docs-only change.
- [ ] Linting passes (`ruff check .`) - not run; docs-only change.
- [ ] Type checking passes (`mypy headroom`) - not run; docs-only
change.
- [ ] New tests added for new functionality - not applicable.
- [x] Manual testing performed
- [x] `git diff --check upstream/main...HEAD`

## Real Behavior Proof

```bash
$ git diff --check upstream/main...HEAD
# exits 0; no whitespace errors
```

`npm --prefix docs run types:check` was also attempted. It regenerated
MDX and route types successfully, then failed in existing docs app code
because `@/lib/...` imports cannot resolve from files such as
`app/(home)/layout.tsx`, `app/api/search/route.ts`, and
`components/button.tsx`. This PR only changes `README.md`,
`docs/content/docs/installation.mdx`,
`docs/content/docs/quickstart.mdx`, and `wiki/*.md` files.

## Review Readiness

- [x] Draft PR; docs wording and install-path accuracy are ready for
review.
- [x] No code or runtime files changed.
- [x] Known docs type-check blocker is documented above.

## Test Output

```bash
$ git diff --check upstream/main...HEAD
# no output
```

```text
$ npm --prefix docs run types:check
[MDX] generated files
✓ Types generated successfully
app/(home)/layout.tsx(2,29): error TS2307: Cannot find module @/lib/layout.shared or its corresponding type declarations.
...
components/button.tsx(4,20): error TS2307: Cannot find module @/lib/cn or its corresponding type declarations.
```

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

## Screenshots (if applicable)

Not applicable.

## Additional Notes

The PR remains a draft while docs verification is limited by the
existing docs app `@/lib/*` resolution issue.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 21:06:30 +00:00
akothari-godaddy
7ddcbcb616
perf: surface optimization overhead diagnostics (#1212)
## Summary
- add overhead diagnostics to perf JSON output
- report optimization p50/p95/p99, slow request percentage, per-stage
totals/percentiles, and top slow requests
- update text report and recommendations to point at the slowest stage
and HEADROOM_COMPRESSION_TIMEOUT_SECONDS when optimization is
consistently slow

## Verification
- python -m py_compile headroom/perf/analyzer.py
tests/test_cli_perf_format.py
- pytest tests/test_cli_perf_format.py could not run locally because
pytest is not installed in this Python environment
2026-07-15 21:04:21 +00:00
TUTU244
412db40a0b
fix(code): pin tree-sitter-language-pack <1.0.0 in [code] extra (#1219)
## Description

<!-- Briefly explain the change and why it is needed. -->

Closes #

## Type of Change

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

## Changes Made

- 

## Testing

<!-- Check what you actually ran, then paste the real command output
below. -->

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

### Test Output

```text
# Paste relevant command output or artifact links here
```

## Real Behavior Proof

- Environment:
- Exact command / steps:
- Observed result:
- Not tested:

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

Add screenshots to help explain your changes.

## Additional Notes

<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:54:25 +00:00
John Xu
1c50eca8b3
fix(proxy): skip Responses memory tools for ChatGPT auth (#1579)
## Description

Fix ChatGPT/Codex session-auth Responses proxy handling so the ChatGPT
backend always receives an explicit `store=false`, while keeping
Responses memory tools limited to the regular API-key path where stored
responses are supported.

## Type of Change

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

## Changes Made

- Detect ChatGPT auth before Responses memory-tool injection and force
`store=false` for ChatGPT-auth Responses payloads.
- Skip Responses memory tools and transparent memory-tool continuation
handling for ChatGPT auth across HTTP, WebSocket first frames, WebSocket
follow-up `response.create` frames, and WS-to-HTTP fallback.
- Preserve API-key behavior after the current main merge: API-key
requests that explicitly set `store=false` skip Responses memory tools,
while API-key requests that receive injected memory tools are forced to
`store=true` for continuation support.
- Address Copilot formatter comments by making
`_allow_responses_memory_tools` call sites formatter-stable.

## 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 --extra dev ruff format --check headroom/proxy/handlers/openai.py
1 file already formatted

$ uv run --extra dev ruff check headroom/proxy/handlers/openai.py tests/test_openai_codex_routing.py tests/test_openai_codex_ws_timings.py tests/test_ws_http_fallback.py
All checks passed!

$ uv run --extra dev python -m pytest -q tests/test_openai_codex_routing.py tests/test_openai_codex_ws_timings.py tests/test_ws_http_fallback.py
37 passed in 0.34s
```

## Real Behavior Proof

- Environment: Local checkout of `fix/codex-store-false-memory-tools`
using `uv run --extra dev`.
- Exact command / steps: Ran the focused formatter, lint, and pytest
commands listed in `Testing`.
- Observed result: Formatting is stable, lint passes, and the focused
OpenAI/Codex routing and fallback tests pass.
- Not tested: Full test suite, `mypy headroom`, and a fresh live ChatGPT
backend probe after the formatter-only follow-up. The original PR
validation recorded that valid ChatGPT subscription backend requests
return `200` with `store=false`, while identical `store=true` or omitted
`store` requests return `400 Store must be set to false`.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A.

## Additional Notes

- Post-deploy monitoring terms: `Responses: forced store=false for
ChatGPT auth`, `WS Responses: forced store=false for ChatGPT auth`,
`chatgpt_store_false`, `Memory: forced store=true for Responses memory
tool continuation`, and upstream 400s containing `Store must be set to
false`.
- Expected healthy signals: ChatGPT-auth Responses requests keep
`store=false` and no longer fail with `Store must be set to false`;
API-key memory-tool flows still inject memory tools and can continue via
`previous_response_id`.
- Rollback trigger: any increase in ChatGPT-auth 400s, API-key
memory-tool continuation failures, or missing memory tool injection on
API-key Responses requests.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:52:01 +00:00
Aashish Tamsya
420dc9077b
feat(grok-build): add Grok Build wrap command and MCP integration (#1629)
## Description

Adds first-class Grok Build support to Headroom so Grok CLI sessions can
route through the local proxy for context compression and savings
tracking.

This PR introduces `headroom wrap grok-build` / `headroom unwrap
grok-build`, a `grok_build` provider slice, Grok MCP registrar support,
and install/telemetry wiring so Grok traffic is attributed correctly in
the proxy and dashboard.

Review follow-up (`9368c413`): when users already own
`[model.grok-build]` in `~/.grok/config.toml`, wrap rewrites `base_url`
in that table in place instead of appending a duplicate header (invalid
TOML).

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- Added `headroom/providers/grok_build/` with runtime helpers,
reversible `~/.grok/config.toml` injection, and install env builders.
- Added `headroom wrap grok-build` and `headroom unwrap grok-build` CLI
commands.
- Added `GrokRegistrar` for Headroom MCP registration in Grok config.
- Wired `grok_build` into install planner/registry, agent savings,
telemetry, and proxy client detection (`grok/` user agent).
- **Review fix:** rewrite `base_url` inside an existing user-owned
`[model.grok-build]` table in place (`# was: …` metadata).
- Added regression tests + docs (`grok-build.mdx`, `proxy.mdx`) and
CHANGELOG entry.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest -q tests/test_provider_grok_build.py tests/test_mcp_registry/test_grok_registrar.py
============================== 12 passed in 1.13s ==============================
```

See **Screenshots** below for terminal captures (pytest, review-fix
in-place rewrite, proxy `/readyz`, unwrap).

## Real Behavior Proof

- Environment: macOS, Python 3.11.12 venv, feat/grok-build @ `9368c413`,
isolated `GROK_HOME` temp dirs, proxy port 8799
- Exact command / steps: see screenshot evidence (wrap/unwrap, in-place
table rewrite, `/readyz`)
- Observed result: see screenshots — 12 tests pass; single
`[model.grok-build]` table after wrap on pre-existing config; proxy
healthy; unwrap restores backup
- Not tested: Live interactive Grok chat with xAI auth through the proxy

## Review Readiness

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

## Checklist

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

Terminal captures from local verification (`9368c413`). Assets hosted on
fork prerelease only — **not** in the source tree.

**1. Pytest — 12 passed (incl. review-fix regression)**

![pytest 12
passed](https://github.com/aashishtamsya/headroom/releases/download/pr-1629-evidence/01-pytest.png)

**2. Review fix — in-place `[model.grok-build]` rewrite (single table,
`# was:` metadata)**

![review fix in-place
rewrite](https://github.com/aashishtamsya/headroom/releases/download/pr-1629-evidence/02-review-fix-in-place.png)

**3. Proxy health — `/readyz` healthy on port 8799**

![proxy readyz
healthy](https://github.com/aashishtamsya/headroom/releases/download/pr-1629-evidence/03-proxy-health.png)

**4. Unwrap — restores pre-wrap backup**

![unwrap restores
backup](https://github.com/aashishtamsya/headroom/releases/download/pr-1629-evidence/04-unwrap.png)

## Additional Notes

Screenshot assets:
https://github.com/aashishtamsya/headroom/releases/tag/pr-1629-evidence
(temporary prerelease; safe to delete after merge).

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:51:52 +00:00
EC0RP
ecf086f1f8
docs(compose): document the memory-stack docker-compose services (#1242)
## What

Adds explanatory comments throughout `docker-compose.yml` so the "memory
stack" is self-documenting for new users.

Covers:
- The **headroom-proxy** service — OpenAI-compatible endpoint, why it
binds to `0.0.0.0`, the `/readyz` healthcheck, and the `depends_on`
start-order caveat.
- **Qdrant** (vector search) and **Neo4j** (relationship graph) — their
roles, exposed ports, and named volumes for persistence.
- A header block with quick-start steps, the full list of host-exposed
ports, and a note that the proxy can run standalone without the
datastores.
- A callout that the `NEO4J_AUTH` default is **local-dev only** and must
be overridden before any non-local use.

## Why

The compose file previously had only minimal inline comments, making it
unclear which services are optional and which port maps to what. These
are documentation-only changes — no behavior, image, or configuration
values changed.

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

Co-authored-by: Cason Clark <casonclark@Casons-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:38:55 +00:00
panamarob30-jpg
3f5067022f
[codex] Simulate Codex read maturation risk (#1395)
## Summary
- add Codex support to `audit-reads --codex --simulate-maturation`
- classify Codex edit targets from `apply_patch`, `sed -i`, `tee`, and
shell redirects so maturation risk can count edits
- include focused tests for Codex maturation metrics, CLI JSON/text
output, and edit-risk buckets
- refresh `uv.lock` to match the current `pyproject.toml` version/extras

## Validation
- `uv run ruff check headroom/audit/codex.py
headroom/audit/maturation.py headroom/audit/__init__.py
headroom/cli/audit.py tests/test_audit_codex.py`
- `uv run pytest tests/test_audit_codex.py tests/test_audit_reads.py
tests/test_read_maturation.py
tests/test_read_maturation_handler_nobust.py -q`
- live local run: `uv run headroom audit-reads --codex --path
/home/robert-briscoe/.codex/sessions --simulate-maturation`

Co-authored-by: Robert Briscoe <robert@briscoe.dev>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:36:34 +00:00
yaowei
ac7ee4e0bf
fix(proxy): support Codex WS compatible gateways (#1281)
Adds opt-in compatibility for OpenAI-compatible WebSocket gateways used
behind Codex /v1/responses.

- HEADROOM_OPENAI_WS_FLATTEN_RESPONSE_CREATE=1 flattens Codex
response.create frames before upstream send.
- HEADROOM_OPENAI_WS_PROPAGATE_UPSTREAM_CLOSE=1 propagates upstream
close code/reason back to the client.
- Default behavior is unchanged.

Tested:
python -m pytest tests/test_openai_codex_ws_lifecycle.py -q
18 passed

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:36:27 +00:00
monkeygold
02c77640a9
fix(transforms): guard Log fallback against invalid JSON + fix MIXED false-positive on source code (#1347)
## Summary

Three related fixes in the content router/detector, addressing data-loss
and misrouting bugs found via chaotic audit:

- **SMART_CRUSHER → Log fallback guard (#1306):** Truncated/invalid JSON
tool outputs were tagged `json_array` by the native magika detector
(classifies by shape, not parseability), routed to SmartCrusher (no-op),
Kompress (no-op), then collapsed by LogCompressor to a single
CCR-retrieval marker — **99.9% data loss** when CCR retrieval isn't
configured. A JSON-validity guard (`_content_is_valid_json`) now skips
the Log fallback for content that fails `json.loads`; valid JSON arrays
still reach it (LogCompressor is a no-op on them).
- **MIXED false-positive on source code:** `is_mixed_content` regex
heuristics misclassify Python with dict/list literals (`{`, `[` at line
start → `has_json_blocks`) + docstrings (`has_prose`) as MIXED, wasting
1–1.4s latency with 0% compression. When the native detector confidently
says `SOURCE_CODE` (confidence ≥ 0.8), `_determine_strategy` now trusts
it over the regex heuristics.
- **PASSTHROUGH for code when CodeAware disabled:** When
`prefer_code_aware_for_code=False` (default), source code now uses
`PASSTHROUGH` instead of `KOMPRESS`, honouring the config's "let code
pass through unmangled" intent. KOMPRESS can destroy code semantics (98%
compression, 11% fact recall on large blobs).
- **RecursionError hardening:** Caught in both `_try_detect_json` and
`_content_is_valid_json` so deeply nested JSON (`[[[[...]]]]` with 10k+
levels) no longer crashes the detector/router — also serves as a DoS
mitigation.

#### Test plan
- [x] `tests/test_transforms_content_router.py` — 36 passed (8 new
tests)
- [x] `tests/test_transforms_content_detection.py` — 9 passed
- [x] `tests/test_cache_aligner_detector_only.py` — 22 passed
- [x] `tests/test_compression_decision.py`,
`test_compression_policy.py`, `test_compress_api.py`,
`test_compression_safety_rails.py` — 137 passed, 5 skipped
- [x] `ruff check` on changed files — all checks passed
- [x] `mypy` on changed files — no issues found

New tests cover:
- Invalid JSON skips Log fallback (content preserved verbatim)
- Valid JSON arrays still reach Log fallback
- MIXED false-positive overridden by high-confidence SOURCE_CODE
detection
- Low-confidence SOURCE_CODE does NOT override MIXED (safety)
- Genuine mixed content (PLAIN_TEXT detection) still uses MIXED
- PASSTHROUGH preserves code verbatim, never invokes Kompress
- CodeAware explicitly enabled still uses CODE_AWARE

#### Risks / rollback
- Behaviour change: code blobs previously routed through MIXED→KOMPRESS
now use PASSTHROUGH. This is the documented intent of
`prefer_code_aware_for_code=False`; if a deployment relied on the
accidental KOMPRESS compression of code, set
`prefer_code_aware_for_code=True` to restore CODE_AWARE.
- The JSON-validity guard adds one `json.loads` call in the narrow "no
savings" fallback path only — negligible overhead.
- Revert is a single-commit revert; no schema/migration changes.

Generated with [Devin](https://devin.ai)

Co-authored-by: monkeygold <monkeygold@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:36:20 +00:00
Peter Lodri
a51bbfb6a5
fix(opencode): use type=local + environment field for MCP config (#1380) (#1388)
## Summary

Fixes #1380 — OpenCode MCP config was written with the wrong schema in
both `mcp install` and `wrap opencode`.

### Root Cause

`_spec_to_entry` and `build_opencode_config_content` both generated:
```json
{
  "type": "remote",
  "url": "http://127.0.0.1:<port>/mcp",
  "env": {...}
}
```

OpenCode's local-stdio MCP schema requires:
```json
{
  "type": "local",
  "command": ["headroom", "mcp", "serve"],
  "environment": {...}
}
```

The proxy does not expose `/mcp`; it returns 404. The `env` field is
also the wrong key — OpenCode expects `environment`.

### Changes

- **`headroom/mcp_registry/opencode.py`** — `_spec_to_entry`:
`type=local`, remove `url`, command always a list, env vars under
`environment`; `_entry_to_spec`: read `environment` first, fall back to
legacy `env` for existing configs
- **`headroom/providers/opencode/runtime.py`** —
`build_opencode_config_content`: local stdio entry with
`HEADROOM_PROXY_URL` env var pointing to the proxy port (headroom mcp
serve picks it up at startup)
- **Tests** — updated two assertions to match corrected schema; added 3
regression tests for `type=local`, `environment` field, and legacy `env`
fallback

## Test Plan

- [x] `pytest tests/test_mcp_registry_opencode.py` — 64 passed
- [x] `pytest tests/test_cli/test_wrap_opencode.py` — 64 passed
- [x] All previously-passing tests remain green

## Remaining items from #1380

- `--no-mcp` still writes persistent MCP via
`inject_opencode_provider_config()` — tracked in the issue, separate PR
- `mcp uninstall/status` symmetry — tracked in the issue, larger scope
- `--target opencode` CLI addition — tracked in the issue

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

## Real behavior proof

**Setup:** macOS 14, Python 3.12, headroom-ai 0.27.0-dev, OpenCode 0.1.x

**Steps after patch:**
```bash
headroom mcp install --agent opencode
cat ~/.config/opencode/opencode.json | python3 -m json.tool
```

**After-fix evidence — written config:**
```json
{
  "mcp": {
    "headroom": {
      "type": "local",
      "command": ["headroom", "mcp", "serve"],
      "enabled": true
    }
  }
}
```
Before fix: `type: "remote"`, `url: "http://127.0.0.1:8787/mcp"` (404 on
proxy), `env` key (wrong field name). OpenCode failed to start headroom
MCP server.

After fix: `type: "local"` — OpenCode launches the MCP server as a
subprocess and the MCP session connects.

**What I did not test:** Windows config paths, `OPENCODE_HOME` env
override on Linux.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 20:36:14 +00:00
JerrettDavis
d50c73f2a1 test: align savings schema assertions 2026-07-15 15:15:24 -05:00
Sneha Roy
e8bff1cfe3
feat: add CrewAI and AutoGen tool compression integrations (#1384)
## Description

Add CrewAI and AutoGen tool compression integrations, following the same
patterns as the existing LangChain agent integration
(`HeadroomToolWrapper` / `wrap_tools_with_headroom`). Both delegate
compression to `compress_tool_result()` from the MCP integration, with
per-tool metrics tracking via `ToolCompressionMetrics` /
`ToolMetricsCollector`.

Closes #1379

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- Add `headroom/integrations/crewai/` — `HeadroomToolWrapper` subclasses
CrewAI `BaseTool`, wraps `_run()` with compression
- Add `headroom/integrations/autogen/` — `HeadroomToolWrapper` wraps
AutoGen `FunctionTool` (sync and async) with compression
- Wire both into `headroom/integrations/__init__.py` with aliased
re-exports (avoids name collision with LangChain's
`HeadroomToolWrapper`)
- Add `[crewai]` and `[autogen]` optional dependency extras to
`pyproject.toml`
- Add 24 unit tests (12 per framework) under `tests/test_integrations/`
- Add `.mdx` doc pages for both frameworks under `docs/content/docs/`
- Update `CHANGELOG.md` with entries under `### Added`

## 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
$ ruff check headroom/integrations/crewai headroom/integrations/autogen tests/test_integrations/crewai tests/test_integrations/autogen
All checks passed!

$ pytest tests/test_integrations/autogen -v
12 passed

$ pytest tests/test_integrations/crewai -v
12 passed
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.11, crewai 1.14.7, autogen-agentchat
0.7.5
- Exact command / steps: Ran standalone adapter demos and benchmark
runner across 4 task types
- Observed result:

| Task | Tokens (raw) | Tokens (compressed) | Savings |
|------|-------------|-------------------|---------|
| Inventory JSON (80 items) | 5,044 | 1,532 | 69.6% |
| Server logs (150 lines) | 8,712 | 314 | 96.4% |
| Analytics query (100 rows) | 10,762 | 10,762 | 0% |
| API docs (20 endpoints) | 8,043 | 8,043 | 0% |

Compression results are identical across CrewAI and AutoGen — expected
since both route through the same `compress_tool_result()` pipeline.

- Not tested: Full end-to-end with a live LLM agent loop (demos test the
compression pipeline standalone). LangGraph not included — headroom
already has `headroom/integrations/langchain/langgraph.py`.

## Review Readiness

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

## Checklist

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

## Additional Notes

- LangGraph integration is intentionally excluded — headroom already has
one at `headroom/integrations/langchain/langgraph.py`
- Re-exports in `__init__.py` are aliased (`CrewAIToolWrapper`,
`AutoGenToolWrapper`) to avoid collision with the existing LangChain
`HeadroomToolWrapper`
- Both integrations follow the exact same conventions as the existing
LangChain agents module: optional dep guard, `compress_tool_result()`
delegation, metrics with 1000-entry cap, Google-style docstrings
- `mypy` not checked due to Rust build dependency (`maturin`) that
requires Application Control policy changes on this machine

---------

Co-authored-by: Sneha27feb <sroy27.ai@gmail.com>
2026-07-15 19:58:54 +00:00
Gen Li
3dd9660d91
feat: 3-layer context compression pipeline (L1+L2+L3) (#1405)
## Description

> **Default behavior is unchanged:** only L1 (annotation-key stripping)
is on by default. L2 (description truncation) and L3 (system-prompt
compression) are **opt-in** via `HEADROOM_TOOL_DESC_MAX_CHARS` and
`HEADROOM_SYSTEM_COMPACT=1` respectively — instruction-level compression
never runs unless an operator explicitly enables it. Verified in
`system_compact.py`: `system_compact_enabled()` returns `False` when the
env var is unset.

Reduces MCP-injected context overhead (~40K tokens / 20% of a 200K
window) through a progressive 3-layer compression pipeline. Each layer
is independently controlled, fail-safe, and additive — operators can
enable L1 only (default) or opt into L2/L3 for deeper savings.

### Layer 1: Tool Schema Annotation Key Stripping (default on)
- Strip JSON Schema annotation keys (`$schema`, `title`, `examples`,
`deprecated`, `default`, `readOnly`, `writeOnly`) from tool definitions
- Normalise whitespace in `description` fields
- Zero risk — removes only non-constraint metadata that models ignore
- ~8% savings on tool schema size

### Layer 2: Tool Description Truncation (opt-in:
`HEADROOM_TOOL_DESC_MAX_CHARS`)
- Truncate verbose tool/parameter descriptions to configurable length
- Preserves first complete sentence (critical for model tool selection)
- Optionally appends second sentence within 1.5× budget
- Hard-truncates with `...` if a single sentence exceeds limit
- Recursively processes nested `description` fields in
`input_schema`/`parameters`
- ~43% savings on description text (estimated ~17K tokens)

### Layer 3: System Prompt CCR Compression (opt-in:
`HEADROOM_SYSTEM_COMPACT`)
- Compress `system[]` content blocks using existing
`ContentRouter.compress()`
- Only compresses blocks exceeding `HEADROOM_SYSTEM_COMPACT_MIN_CHARS`
(default 500)
- Preserves `cache_control` markers and non-text blocks
- Fail-safe: leaves block unchanged if compression fails or doesn't save
size
- ~14.5% savings on system prompt (estimated ~3.5K tokens)

**Combined savings (all 3 layers enabled): ~40K → ~17K tokens (~58%
reduction)**

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

## Changes Made

- `headroom/proxy/tool_schema_compaction.py` — New shared module: L1
annotation stripping + L2 description truncation with
`strip_annotation_keys()` and `truncate_descriptions()`
- `headroom/proxy/system_compaction.py` — New module: L3 system prompt
CCR compression with `compact_system_blocks()`
- `headroom/proxy/handlers/anthropic.py` — Add L1+L2+L3 call sites
(after tool assembly, before PRE_SEND)
- `headroom/proxy/handlers/openai.py` — Add L1+L2+L3 call sites
(parallel to Anthropic handler)
- `tests/test_tool_schema_compaction.py` — 42 unit tests covering edge
cases, nested schemas, fail-safe behavior
- `tests/test_system_compaction.py` — Tests for L3 compression,
cache_control preservation, min-chars gating

## 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_tool_schema_compaction.py tests/test_system_compaction.py tests/test_anthropic_compaction_transforms.py -v
===== 49 passed in 4.96s =====

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

$ uv run mypy headroom/proxy/tool_schema_compaction.py headroom/proxy/system_compaction.py headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/openai.py
Success: no issues found in 4 source files

# Manual verification with HEADROOM_TOOL_DESC_MAX_CHARS=120
# Single tool schema: 548→434 bytes (L1, 20.8% saved) → 315 bytes (L2, 27.4% saved)
# Combined: 548→315, 42.5% saved
# Full request with proxy: orig=39179 opt=31988 saved=7191 (18.4% compression)
```

## Real Behavior Proof

- Environment: macOS, Python 3.13, headroom proxy v0.28.0, Claude Code
CLI
- Exact command / steps:
1. Start proxy with `HEADROOM_TOOL_DESC_MAX_CHARS=120
HEADROOM_SYSTEM_COMPACT=1 headroom proxy`
  2. Route Claude Code traffic through proxy
  3. Check `/stats` endpoint for `transforms_applied` and byte savings
- Observed result: L1/L2/L3 transforms applied correctly, ~58% token
reduction on MCP-heavy context
- Not tested: Windows, production deployment

## Review Readiness

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

## Checklist

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

- L2 and L3 are **opt-in** via env vars. Default behavior is unchanged
(only L1 active).
- All layers have fail-safe fallbacks — if compaction fails or doesn't
reduce size, the original payload passes through unchanged.
- The Anthropic handler now appends `anthropic:tool_schema_compaction`
(L1), `anthropic:tool_desc_compaction` (L2), and
`anthropic:system_compact` (L3) to `transforms_applied`, so `/stats` and
transformation accounting are no longer blind to compression that
changed the request. Covered by handler-level e2e regression in
`tests/test_anthropic_compaction_transforms.py` (positive + negative
cases). The earlier follow-up #1423 is superseded — no longer needed.

---------

Signed-off-by: lg320531124 <lg320531124@users.noreply.github.com>
Co-authored-by: lg320531124 <lg320531124@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:58:51 +00:00
Zhenjia ZHOU
4035c04187
feat(text-crusher): CJK-aware segmentation + relevance via ICU (#1504)
## Description

`TextCrusher` (the native extractive prose compressor added in #1171)
only handled ASCII: `split_segments` split on `.!?`+whitespace and
`tokens` split on whitespace/alphanumeric runs. CJK
(Chinese/Japanese/Korean) has neither spaces nor ASCII terminators, so a
whole CJK paragraph collapsed into **one segment / one token** — it
passed through at ~0% compression, and BM25 relevance + salience scored
zero terms.

This makes `TextCrusher` CJK-aware. CJK-bearing content takes an ICU
(`icu_segmenter`, UAX#29 sentence + dictionary word) segmentation path,
with a length fallback for terminator-sparse runs, a local BM25 over the
ICU word tokens, and ICU-token salience. Dispatch is on **content
only**, so pure-ASCII text is byte-identical to before — the shared
`BM25Scorer` and the ASCII path are untouched.

It also adds a committed, reproducible answer-retention eval
(`benchmarks/i18n_compression_eval.py`) with a deterministic zh/ja/ko CI
regression gate, so the improvement below is permanently verifiable
rather than a one-off measurement.

Extends #1171.

## Type of Change

- [x] Bug fix (CJK passed through near-uncompressed)
- [x] New feature (CJK segmentation / relevance support)
- [x] Performance improvement (CJK now compresses; ICU segmenters
cached, not rebuilt per call)

## Changes Made

- `is_cjk` predicate gates a CJK path (ideographs, kana, Hangul, CJK
punctuation, full/half-width forms).
- `split_segments` → ICU `SentenceSegmenter` for CJK + a mandatory
length fallback (whitespace / CJK punctuation / hard cap) for
terminator-sparse runs; ASCII path unchanged.
- `tokens` → ICU `WordSegmenter` (dictionary) for CJK; ASCII path
unchanged.
- `relevance_cjk`: a local BM25 over ICU word tokens — the shared ASCII
`BM25Scorer` scores zero terms for CJK and is parity-locked, so this is
an intentional separate scorer (documented in code).
- CJK salience uses ICU tokens (whitespace-split gave one giant "word" →
zero salience).
- `count_tokens`: CJK-aware so `compression_ratio` isn't nonsense for
space-free text.
- ICU segmenters resolved once in `static LazyLock` (compiled_data is
static) instead of rebuilt per call.
- New dep `icu_segmenter` 2.2, `compiled_data` only (see Dependency
below).
- `benchmarks/i18n_compression_eval.py` +
`tests/test_transforms/test_text_crusher_cjk_eval.py`: a zh/ja/ko
answer-retention eval — a deterministic needle CI gate (always-runs, no
external data), real-transcript fidelity with CJK-aware salient, and
optional `multi-wiki-qa` natural-data retention (loaded via the
`[evals]` `datasets` extra, skipped if absent; data never vendored —
CC-BY-NC-SA).

## Testing

- [x] Unit tests pass (`pytest` + `cargo test`)
- [x] Linting passes (`ruff check`/`format` on the new eval + test —
clean)
- [ ] Type checking passes (`mypy headroom`) — N/A, the only Python
added is a benchmark + test, not `headroom/` source
- [x] New tests added for new functionality
- [x] Manual testing performed (see Real Behavior Proof)

### Test Output

```text
$ cargo test -p headroom-core --lib text_crusher
running 12 tests
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 841 filtered out

$ .venv/bin/python -m pytest tests/test_transforms/test_text_crusher*.py
15 passed

$ .venv/bin/python -m pytest tests/test_transforms/test_text_crusher_cjk_eval.py
6 passed   # deterministic zh/ja/ko needle CI gate

$ cargo clippy -p headroom-core && ruff check benchmarks/i18n_compression_eval.py   # both clean
```

## Real Behavior Proof

- Environment: macOS (Darwin 25.3.0), Python in a uv venv,
`headroom-core` built via `uv pip install -e .` (maturin), branch
`feat/cjk-text-compression`.
- Exact command / steps: built `_core`, then ran a mixed
Chinese+Japanese doc (no spaces, `。` terminators) through
`TextCrusher().compress(doc, "认证令牌缓存策略", 0.3)`; separately evaluated
answer-retention on the public CMRC2018 Chinese QA dev set (bury the
gold-answer paragraph among 25 distractors, query = the question,
compress to 30%, check the gold answer survives), and end-to-end through
`ContentRouter`.
- Observed result: a mixed Chinese+Japanese doc compressed 189 → 78
tokens (ratio 0.41, kept 3/8 segments) with the query-relevant sentence
surviving — before this change the same doc was a single segment → 100%
passthrough. On the public CMRC2018 Chinese QA dev set, answer-retention
under 30% compression rose 34% → 93% (multiple seeds). End-to-end
through `ContentRouter` on real CJK content, aggregate savings rose 16%
→ 40%. Pure-ASCII (English) output stayed byte-identical (the English
parity fixtures did not move). Demo terminal output:

    ```text
    ORIGINAL  tokens= 189  chars=189
    COMPRESS  tokens=  78  ratio=0.41  segments kept 3/8
    QUERY-RELEVANT sentence survived: True
    --- compressed output (verbatim kept CJK sentences) ---
    认证令牌的缓存策略采用最近最少使用淘汰算法来管理过期条目。
    请求重试使用指数退避并设置最大次数上限。
    数据备份每天凌晨执行并保留最近三十天的快照。
    ```
The committed eval now demonstrates this across all three CJK languages.
The deterministic needle gate (in CI via
`tests/test_transforms/test_text_crusher_cjk_eval.py`, 6 passed) has
TextCrusher keep the query-relevant needle while truncate/random drop it
in zh, ja, and ko. On real `multi-wiki-qa` natural data (n=80/lang),
query-aware answer-retention is **zh 74% / ja 70% / ko 50%** vs
**25–41%** for the truncate/random baselines:

    ```text
=== Part A: multi-wiki-qa answer-retention (n=80/lang, target_ratio=0.3)
===
      lang    text_crusher  truncate  random
      zh-cn           74%       25%     38%
      ja              70%       31%     39%
      ko              50%       26%     41%
    ```
Korean is measurably weaker (ICU has no Korean dictionary and falls back
to UAX#29 word-breaking) — still well above baselines, and scoped as a
follow-up.
- Not tested: the live proxy HTTP path (validated at the `ContentRouter`
/ `TextCrusher` layer, not via a running proxy); no-space Korean
(standard Korean is space-delimited and is covered); non-CJK SE-Asian
scripts (out of scope).

## Dependency (per CONTRIBUTING supply-chain policy)

`icu_segmenter` 2.2 (ICU4X), `features = ["compiled_data"]`:

- **Why this package (vs. ourselves / existing deps):** CJK needs
dictionary/UAX#29 segmentation. A hand-rolled char-bigram scored
slightly worse on real data (CMRC2018 answer-retention: 92.5% ICU vs 91%
bigram, 4 seeds); jieba/lindera are ZH-only or 13–207 MB dicts. ICU4X
covers zh/ja/ko in one crate. The existing `unicode-segmentation` does
UAX#29 only (no CJK dictionary), so it can't word-segment space-free
CJK.
- **Who maintains it:** the official `unicode-org` ICU4X project; active
release cadence (2.2 in 2025); no known CVEs.
- **Install surface:** ~13 new pure-Rust crates, no build scripts, no
native code, no build/runtime network. `compiled_data` bundles locale
data at compile time (hermetic). `auto`/`lstm` deliberately NOT enabled
— LSTM covers SE-Asian scripts (Thai/Lao), not CJK, and would pull in
`libm` for nothing.
- **Why this version:** 2.x is the stabilized ICU4X API (1.x used a
different data-provider model); floored at 2.2 (Cargo.lock pins the
patch) since segmenter boundaries are observable in output and bumps
should be deliberate.

## Review Readiness

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

## Checklist

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

## Additional Notes

- **Parity:** the shared `BM25Scorer` (byte-exact parity-locked with
`headroom/relevance/bm25.py`) is untouched. `relevance_cjk` is a
separate local scorer because the shared one's tokenizer is ASCII-only.
The whole CJK path lives in Rust (`text_crusher.py` is a thin wrapper
over `_core`), so there is no Python mirror to keep in sync; the parity
fixtures stay green (only the CJK `unicode` fixture was re-recorded,
intentionally; English fixtures unchanged).
- **Known by-design gap (not a bug):** CJK content + a pure-ASCII query
yields no token overlap, so relevance falls back to recency + salience
(cross-script query matching is unsupported).
- The Python added is a benchmark
(`benchmarks/i18n_compression_eval.py`) plus its test, not `headroom/`
runtime source — both are `ruff`-clean; `mypy headroom` is unaffected.
- **License:** the optional Part A pulls `alexandrainst/multi-wiki-qa`
(CC-BY-NC-SA-4.0) at run time via the `[evals]` extra and is skipped if
absent — the dataset is never vendored into the repo, and the always-run
CI gate (Part C) uses only our own deterministic data.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:58:48 +00:00
mstattma
3757a7cef3
fix(core): avoid unidiff panic on bash xtrace (#1506)
## Summary
- preflight unified diff inputs before calling the Rust unidiff parser
- catch parser panics so malformed-but-diff-looking input falls back to
non-diff
- add regressions for Bash xtrace lines like `+++ test.sh` and `+++
dirname test.sh`

## Repro
`detect_content_type("+++ test.sh")` could panic through the Rust
detector because unidiff treated the lone `+++` line as a target header
without a preceding source header.

## Tests
- `cargo fmt --all --check`
- `cargo test -p headroom-core --lib
transforms::unidiff_detector::tests`
- `cargo test -p headroom-core --lib`

Co-authored-by: Michael Stattmann <mstattma@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:58:45 +00:00
vscunha
05932d7165
fix(proxy): compress OpenCode tool schemas and embedded JSON (#1535)
## Description

Fixes two remaining OpenCode/OpenAI Chat compression gaps after `main`
incorporated the original savings-profile threading and user
content-block work from this PR.

OpenCode requests can still report very low savings when most input
tokens live in verbose `tools` schemas rather than messages. They can
also route poorly when a short instruction wraps a valid JSON block but
does not satisfy the existing long-prose heuristic.

Closes #1534

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

- Compact OpenAI Chat Completions `tools` schemas whenever request
compression is active, reusing the existing OpenAI Responses schema
compactor. The outbound tool invocation shape is preserved while
non-semantic annotations such as `$schema`, `title`, and `examples` are
removed.
- Include the tool-schema token delta in Headroom's savings accounting
and expose `openai:chat:tool_schema_compaction` in the applied
transforms.
- Detect valid JSON blocks surrounded by prose or log text as mixed
content, so short OpenCode instructions route through mixed/SmartCrusher
handling instead of falling through or producing a no-op.
- Adapt the mixed-content change to the new
`headroom.transforms.mixed_content` module introduced on `main` by
#1939.

## Why the Focus Changed

The original headline fix—threading savings-profile kwargs into
`/v1/chat/completions`—is now already present on `main`, as is the user
content-block opt-in behavior. Those duplicate changes were removed
during the merge.

The branch also no longer changes developer/system role protection or
forced-Kompress semantics. It follows `main` for both, so the earlier
instruction-role safety concern is outside the current diff.

The resulting PR is limited to two OpenCode-specific compression gaps
that remain reproducible on current `main`.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check` on changed files)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for the fixed behavior
- [ ] Manual live-upstream testing performed after the latest rebase

### Test Output

```text
59 passed, 1 warning in 83.53s
All checks passed!  # ruff check
4 files already formatted  # ruff format --check
python -m py_compile: passed
git diff --check: passed
```

Focused test coverage includes:

- OpenAI Chat tool-schema compaction, transform reporting, outbound
schema shape, and positive token savings.
- Embedded JSON mixed-content detection, SmartCrusher routing, positive
savings, and preservation of a critical sentinel value.
- Current `main` regressions for savings-profile threading, user content
blocks, turn hooks, and forced-Kompress behavior.

## Real Behavior Proof

- Environment: Linux ARM64, Python 3.13.12, current `main` at `9bacf481`
merged into the branch.
- Exact command / steps: focused pytest run across the OpenAI
cache-stability, content-router, mixed-content, savings-profile,
user-block, turn-hook, and forced-Kompress suites.
- Observed result: 59 tests passed; the chat request test forwarded
compacted tools and reported positive savings, while the embedded-JSON
fixture used mixed routing and preserved `CRITICAL_NEEDLE_42`.
- Not tested: full repository suite and a live external OpenCode request
after the latest merge; those remain for CI/live follow-up.

## 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 the non-obvious behavior
- [ ] I have made corresponding documentation changes — N/A; internal
routing behavior only
- [x] My changes generate no new warnings
- [x] I have added tests that prove the fixes are effective
- [x] New and existing focused tests pass locally
- [ ] I have updated the changelog — N/A; release automation handles fix
entries

## Screenshots

N/A — proxy/transform behavior only.

## Additional Notes

- Current diff versus `main`: 4 files, 172 insertions, no role-policy or
forced-Kompress changes.
- The mixed-content conflict was resolved by extending the new isolated
parser module rather than reintroducing parsing code into
`ContentRouter`.
2026-07-15 19:58:42 +00:00
Manmit Singh
942e916368
feat(cli): add headroom inspect to view original vs compressed content (#1595)
## Description

Headroom exposes plenty of *quantitative* compression telemetry (token
counts, ratios, `headroom perf`, `/metrics`) but no way to actually
**see what the compressor changed** in the content. That makes it hard
to trust compression or debug a quality regression ("did it drop
something I cared about?").

This adds a `headroom inspect` command (the issue's Option 1). It reads
the proxy's existing loopback `/transformations/feed` endpoint — which
already carries the pre/post-compression message snapshots when the
proxy runs with `--log-messages` — and renders, per request, the
original vs compressed content for each message with the changed
segments highlighted. No new dependencies (stdlib `difflib`).

```
headroom inspect                 # inspect the most recent request
headroom inspect --last 5        # the 5 most recent
headroom inspect --full          # include unchanged messages
headroom inspect --format json   # raw feed for offline tooling
```

Per request it shows the model, per-request token counts + savings, the
transforms applied, and a colorized unified diff of each changed message
(red = removed, green = added). Clear errors when no proxy is reachable
or when the proxy wasn't started with `--log-messages`.

Side-by-side / interactive rendering (the fuller form of Option 1) can
follow as a polish pass; this lands the core "see what changed"
capability on data Headroom already captures.

Closes #1267

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- `headroom/cli/inspect.py`: new `inspect` command +
content-flattening/diff-render helpers.
- `headroom/cli/__init__.py`, `headroom/cli/main.py`: register the
command.
- `tests/test_cli_inspect.py`: unit tests (content extraction, the
no-proxy / no-`--log-messages` / empty-feed paths, text render, json
output).
- `wiki/cli.md`: document the command + options.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] New tests added

### Test Output

```text
$ pytest tests/test_cli_inspect.py -q
7 passed

$ ruff check headroom/cli/ tests/test_cli_inspect.py
All checks passed!
```

## Real Behavior Proof

- Environment: repo main @ HEAD, local venv
- Exact command / steps: invoked the `inspect` command against a mocked
`/transformations/feed` payload (one request, a user message with a line
removed by SmartCrusher).
- Observed result: header shows `req-1 gpt-4o`, `tokens 100 → 40 (saved
60, 60.0%)`, `transforms: SmartCrusher`, and a unified diff with the
removed line on the original side; no-proxy and missing-`--log-messages`
cases raise actionable errors; `--format json` emits the raw feed.
- Not tested: live end-to-end against a real proxy with `--log-messages`
(the data source — the feed endpoint — is exercised via the mocked
payload that mirrors its shape).

## 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 the feature works
2026-07-15 19:58:38 +00:00
Manmit Singh
4cbd5da673
feat(proxy): opt-in compression for catch-all passthrough routes (#1699)
## Description

Requests whose path doesn't match a built-in API route fall through to
`handle_passthrough`, which forwarded the body verbatim — bypassing
ContentRouter/Kompress/TOIN entirely. Wrapper-proxy setups that front
Headroom on custom paths (e.g. `/api/codex-proxy/<key>/v1/responses`)
got zero compression on coding-agent traffic and hit context-limit 400s
in long sessions. This adds an opt-in flag that routes OpenAI
Responses-shaped passthrough bodies through the same compression path
the native `/v1/responses` handler uses.

Closes #1546

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

- Added `ProxyConfig.compress_passthrough` (default `False`) +
`--compress-passthrough` CLI flag + `HEADROOM_COMPRESS_PASSTHROUGH=1`
env.
- `handle_passthrough`: when enabled, POST requests whose path ends in
`/responses` with an OpenAI Responses-shaped body are compressed via the
existing `_compress_openai_responses_payload_in_executor` before
forwarding; stale `Content-Length` is dropped so httpx recomputes it.
- New `_maybe_compress_passthrough_responses` helper — fail-open:
non-JSON, non-Responses payloads, unmodified results, and any compressor
error forward the original body unchanged.
- Documented the flag in `docs/content/docs/proxy.mdx`.

## 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/python -m pytest tests/test_compress_passthrough.py -q
collected 6 items
tests/test_compress_passthrough.py ......                                [100%]
============================== 6 passed in 0.35s ===============================

$ .venv/bin/ruff check headroom/proxy/handlers/openai.py headroom/proxy/models.py headroom/proxy/server.py tests/test_compress_passthrough.py
All checks passed!
```

## Real Behavior Proof

- Environment: macOS arm64, Python 3.14, repo `.venv`.
- Exact command / steps: `.venv/bin/python -m pytest
tests/test_compress_passthrough.py -q` — covers a Responses-shaped body
being compressed, non-JSON passthrough, non-Responses (`messages`)
payload untouched, unmodified-result short-circuit, compressor-error
fail-open, and `ProxyConfig().compress_passthrough is False` default.
Plus import smoke: `ProxyConfig(compress_passthrough=True)`,
server/handler modules import, helper present.
- Observed result: 6 passed; flag defaults off; enabled path reuses the
native Responses compressor and never raises out to the request.
- Not tested: live end-to-end through a real second proxy to a real
upstream (no external wrapper proxy / upstream credentials in sandbox);
the compression call is the same one `/v1/responses` already exercises
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

## Additional Notes

Scoped to OpenAI Responses-shaped bodies (the reporter's exact case).
Anthropic `/messages` and OpenAI `/chat/completions` passthrough
compression are natural follow-ups — deliberately left out to keep this
change focused and fail-safe. CHANGELOG is release-managed, left
unchecked.
2026-07-15 19:58:34 +00:00
Ashish
6469fcd018
feat(transforms): language-aware stack-trace collapse for Go/Rust/.NET/Java/Node (#1791)
## Description

Stack-trace handling covered only Python tracebacks and a generic ` at
symbol(` pattern. Go panics and Rust panics flowed to prose compression;
.NET traces were unrecognized; Java chained exceptions split into
separate traces at every `Caused by:` (so later chain heads fell off the
`max_stack_traces` cliff); and oversized traces were blindly
head-truncated — keeping runtime scheduler noise while dropping the app
frames and chain heads an agent actually needs.

This PR adds language-aware trace flavors (Go, Rust, .NET, Java chains,
Node async) to the Rust core and both Python mirrors, and replaces blind
truncation with a runtime-frame collapse: message lines, chain heads,
the trace head, and app-code frames survive; contiguous runtime/stdlib
frames fold into `[... N frames collapsed]` markers. A 147-line Go panic
dump compresses to 19 lines with the panic message, signal line, and app
frame intact.

Note one intentional behavior change: now that Go/Rust panics are
*detected*, panics ≤8KB in tool outputs gain the existing error-output
protection (`protect_error_outputs`) they previously missed — small
panics stay verbatim, exactly like small Python tracebacks already do.

## Type of Change

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

## Changes Made

- `crates/headroom-core/src/transforms/log_compressor.rs`:
- New `TraceFlavor::GoPanic` (`panic:` / `fatal error:` / `goroutine N
[state]:` openers, tab-indented `.go:` frame lines, `created by` /
call-line continuation, blank-separated goroutine blocks) and
`TraceFlavor::DotNet` (`Unhandled exception.`, `at … ) in file:line N`
frames — checked before Java since those frames also satisfy the Java
shape — plus `--->` inner-exception heads and `--- End of` separators).
- Renamed the misnamed `Go` flavor to `RustBacktrace` (its `is_go_frame`
matched `N: 0x<hex>` — the Rust backtrace shape) and gave it real
openers (`thread '…' panicked at`, `stack backtrace:`); the free-text
panic-message line after the opener stays in the trace (`terminates` now
receives `lines_so_far`).
- Java: continues across `Caused by:` / `Suppressed:` / `... N more`,
and `is_java_at_frame` admits `/` so JPMS module frames (`at
java.base/…`) pass the opener re-check — without this, modern JDK traces
fragmented at the parse cap into ≤20-line groups.
- Frame collapse (`collapse_trace_frames`): for traces over
`stack_trace_max_lines`, keeps message/chain-head lines, first
`trace_head_frames` frames, up to `trace_app_frames` app frames; runtime
frames (prefix + path marker tables per language) fold into `[... N
frames collapsed]` markers that occupy the run's first line slot and
carry score 0.8 so the global cap doesn't drop them first. Collapsed
frame indices are excluded from the context-line pass (otherwise ±3
context re-added them), and the parse cap re-opens on continuation lines
so selection sees one contiguous trace. New config:
`collapse_runtime_frames=true`, `trace_head_frames=3`,
`trace_app_frames=5`; new sidecar stat `runtime_frames_collapsed`.
- `crates/headroom-py/src/lib.rs`: the three new knobs on the
`LogCompressorConfig` PyO3 signature.
- `headroom/transforms/log_compressor.py`: dataclass fields +
constructor pass-through; `_parse_lines` opener patterns mirrored per
the documented contract.
- `headroom/transforms/content_detector.py`: `_LOG_PATTERNS` additions
(Go panic/goroutine/frame lines, Rust panic/backtrace/numbered frames,
.NET, Java chain heads, Node `at async`); JS/Java `at` pattern admits
JPMS module paths.
- `tests/test_transforms_stack_traces.py` (new, 10 tests) + 7 new Rust
unit tests (flavor open/continue/terminate, chain grouping, collapse
keeps chain heads/app frames, collapse-off comparison, small traces
untouched).

## Testing

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

### Test Output

```
$ cargo test -p headroom-core
928 passed; 3 ignored

$ python -m pytest tests/test_transforms_stack_traces.py tests/test_log_compressor.py \
    tests/test_transforms_log_compressor.py tests/test_transforms_content_detection.py \
    tests/test_transforms_content_router.py tests/test_transforms/test_content_router.py \
    tests/test_lossless_mode.py tests/test_compression_fidelity_regression.py -q
191 passed
```

## Real Behavior Proof

- Environment: macOS arm64, Python 3.13, repo main @ e8151f05,
`headroom._core` rebuilt via scripts/build_rust_extension.sh
- Exact command / steps: fed a 147-line Go panic + 24-goroutine dump and
a 66-line Java chained exception through
`LogCompressor(LogCompressorConfig(enable_ccr=False)).compress(...)`
- Observed result: Go dump 147 → 19 lines with `panic: runtime error…`,
`[signal SIGSEGV…]`, and the `main.handler` app frame kept, scheduler
frames as `[... 4 frames collapsed]` per goroutine; Java output keeps
`Caused by: java.io.IOException`, `com.example.Disk.read`, and `... 17
more` — all three were lost under blind truncation (verified by the
collapse-off comparison test)
- Not tested: Windows; PHP/Ruby traces (out of scope); interplay with
Kompress relevance-split on mixed log+trace payloads beyond the existing
suite

## Review Readiness

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 19:58:30 +00:00
Ashish
737b332129
feat(config): fold TOML array-of-tables to csv-schema via SmartCrusher (#1799)
## Description

Adds a schema-fold tier to the structured-config compressor (introduced
in #1784). TOML files containing an `[[array-of-tables]]` are parsed
with the stdlib `tomllib` reference parser and bridged to SmartCrusher's
lossless `csv-schema` renderer, which folds the repeated per-record keys
into a single schema over the rows. On lockfiles and override-lists —
where the repeated keys dominate the byte count — this is a large win.

**Stacked on #1784 — review only the top commit** (`feat(config): fold
TOML array-of-tables to csv-schema via SmartCrusher`). The base commit
is #1784's config-compressor PR; this PR will collapse to the single new
commit once #1784 merges.

Faithfulness is guaranteed by construction, not by a heuristic:
- `tomllib` is the reference TOML parser, so the extracted records are
ground-truth.
- `csv-schema` is a lossless JSON renderer (`smart_crusher.py` documents
it as such), so the model reads a faithful, reformatted view of the
exact parsed data.
- Byte-exact recovery rides the existing CCR path — the original is
persisted to the `CompressionStore` and a `Retrieve original: hash=…`
marker is emitted. The fold is only emitted when that store write
succeeds, so nothing is ever unrecoverable.

Scope is deliberately **TOML-only**: `tomllib` is stdlib, whereas PyYAML
is only a *transitive* dependency (not declared in `pyproject.toml`),
and INI record-sections would need a bespoke dict-of-dicts→records
transform. Those flavors can follow in a separate PR with an explicit
dependency decision.

## Type of Change

- [x] New feature (non-breaking change which adds functionality)

## Changes Made

- `headroom/transforms/config_compressor.py`: added Tier 3
(`_schema_fold`) — TOML `[[array-of-tables]]` → `tomllib` → JSON →
`SmartCrusher(csv-schema)`. New `enable_schema_fold` config flag
(default on; auto-off in lossless mode since it rides `enable_ccr`). The
fold competes with the reversible text tiers and is adopted only when
strictly smaller. Added `_load_toml` (stdlib parser with tomli backport)
and `_json_default` (TOML date/time → ISO; bail on any other
non-serializable value).
- Recovery reuses the existing `CompressionStore` + `Retrieve original:
hash=` marker; no new CCR plumbing.
- `tests/test_transforms_config_compressor.py`: 14 new tests covering
the fold, big-win assertion, byte-exact CCR round-trip, lossless-mode
disable, flag-off, non-TOML skip, no-array skip, small-array
`passthrough` decline, store-failure fallback, savings-floor rejection,
unparseable/non-serializable bails, `_load_toml`/`_json_default` units,
and a datetime-valued fold.

## Testing

- [x] New and existing unit tests pass locally
- [x] New tests added for the new behavior

### Test Output

```
tests/test_transforms_config_compressor.py ............................. [ 59%]
headroom/transforms/config_compressor.py     127      0     36      0   100%
============================== 49 passed in 0.61s ==============================
```

Must-stay-green suites (`test_lossless_mode`,
`test_lossless_excluded_compaction`,
`test_transforms_content_detection`,
`test_compression_fidelity_regression`) — 48 passed.
Router/tabular/smart_crusher regression — 73 + 82 passed. `mypy
--strict` clean on the changed module.

## Real Behavior Proof

- Environment: local, Python 3.11.0, macOS (darwin), `HF_HUB_OFFLINE=1`
- Exact command / steps: parsed a 25-record `[[tool.mypy.overrides]]`
TOML through
`ConfigCompressor(ConfigCompressorConfig(enable_ccr=True)).compress()`,
then retrieved the CCR hash from the `CompressionStore`.
- Observed result: `strategy=config_schema_fold`, 2765 → 840 chars (30%
of original); the marker hash resolved to the byte-exact original
(`recovered == original` True); with `enable_ccr=False` (lossless mode)
the fold did not run and no marker was emitted; a 3-record long-valued
`[[package]]` array correctly declined (SmartCrusher `passthrough`).
- Not tested: the live proxy end-to-end path and non-TOML flavors
(YAML/INI schema folding is intentionally out of scope for this PR).

## Review Readiness

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 19:58:27 +00:00
gglucass
12a9710665
feat(stats): per-bucket output-shaping savings in /stats-history (#1819)
## Description

Adds per-bucket **output-shaping savings** to `/stats-history`. Today
output-shaping savings exist only as a single global aggregate
(`savings.by_layer.output_shaping`), so downstream consumers can't chart
them over time. This threads a per-request output-savings estimate into
the existing rollup so every `series` bucket carries
`output_tokens_saved_delta` + `output_savings_usd_delta`, symmetric with
the existing `compression_savings_usd_delta`.

Motivation: on Claude Code subscription traffic, input is ~99%
cache-discounted (the compressible live zone is a fraction of a
percent), while output shaping is a ~36% reduction on full-price output
tokens — so it's the dominant, honestly-attributable saving, and
currently the only one a dashboard can't render per day.

Closes #1816

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- `output_savings.py`: new read-only
`SavingsRecorder.estimate_request_savings(labels, output_tokens)` →
per-request synthetic-control estimate `max(0, baseline_mean(stratum) -
output_tokens)` for treatment requests; 0 for control / unknown stratum
/ no label. Does **not** mutate the ledger, so it composes with
`record_from_labels` without double-counting. `record_from_labels`'s
`bool` contract is unchanged.
- `outcome.py`: in the funnel, capture that estimate and pass it to
`record_request(output_tokens_saved=...)`.
- `savings_tracker.py`: `record_request` gains `output_tokens_saved`;
accumulates lifetime cumulative `output_tokens_saved` /
`output_savings_usd` (priced via new `_estimate_output_savings_usd`,
output-rate), writes them into each checkpoint, and now checkpoints when
**either** compression **or** output savings occurred (so output-only
requests aren't dropped). `_build_rollup` diffs the cumulative into
`output_tokens_saved_delta` / `output_savings_usd_delta` per bucket;
`_normalize_history_entry` and the CSV export carry the fields.
- Additive + backward-compatible: checkpoints predating the feature
default the new fields to 0.

## Testing

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

### Test Output

```text
$ uv run --extra dev pytest tests/test_output_shaping_rollup.py tests/test_output_savings.py \
    tests/test_output_savings_cli.py tests/test_proxy_savings_history.py tests/test_request_outcome.py -q
... 103 passed

$ uv run --extra dev ruff check headroom/proxy/savings_tracker.py headroom/proxy/output_savings.py \
    headroom/proxy/prometheus_metrics.py headroom/proxy/outcome.py tests/test_output_shaping_rollup.py
All checks passed!

$ uv run --extra dev mypy headroom/proxy/savings_tracker.py headroom/proxy/output_savings.py
Success: no issues found in 2 source files
```

New tests (`tests/test_output_shaping_rollup.py`): output savings bucket
into the daily series; an output-only request (no compression) still
checkpoints; pre-feature requests default to 0;
`estimate_request_savings` returns the baseline-relative saving for
treatment and 0 for control / unknown / over-baseline.

## Real Behavior Proof

- Environment: macOS, CPython 3.10.18, this branch (rebased on latest
`main`), litellm pricing available.
- Exact command / steps: seed a baseline (as `learn --verbosity` would),
then drive 3 requests through the real, unmocked chain
`SavingsRecorder.estimate_request_savings` →
`SavingsTracker.record_request` → `history_response()`, and print
`series.daily`. Full script + raw output:

```text
$ uv run python proof.py   # seeds baseline ~1000 out-tok; 3 treatment requests (out=600/550/700), one with no compression
[
  { "timestamp": "2026-07-05T00:00:00Z", "tokens_saved": 120,
    "compression_savings_usd_delta": 0.0006,
    "output_tokens_saved_delta": 850, "output_savings_usd_delta": 0.02125 },
  { "timestamp": "2026-07-06T00:00:00Z", "tokens_saved": 80,
    "compression_savings_usd_delta": 0.0004,
    "output_tokens_saved_delta": 300, "output_savings_usd_delta": 0.0075 }
]
```

- Observed result: output-shaping savings appear per day and independent
of the compression axis. 2026-07-05 = 850 (400+450 saved by two
treatment requests vs the ~1000-token baseline, including one request
with zero compression — proving the output-only checkpoint path),
2026-07-06 = 300, each priced at the model's output rate. Matches
expectations.
- Not tested: the full live proxy over HTTP with a real learned baseline
and organic traffic — I exercised the same code path minus the
HTTP/streaming layer. The measured-vs-estimated `method` gating is
unchanged by this PR.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A — backend-only change (no UI surface in this repo). The runtime
effect is the `/stats-history` `series.daily` JSON with the new
`output_tokens_saved_delta` / `output_savings_usd_delta` fields, shown
under **Real Behavior Proof** above. The downstream chart that renders
them lives in the separate Headroom desktop app.

## Additional Notes

- Per CONTRIBUTING's issue-first policy for features, I opened #1816
first with the spec; happy to adjust the API surface (field names /
gating) to whatever you prefer. A downstream consumer (Headroom desktop
chart) is already implemented against this exact contract and stacks the
segment only when `output_reduction.method == "measured"`.
- Docs checkbox left unchecked: I didn't find a `/stats-history` schema
doc to update; point me at one if it exists.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:58:24 +00:00
Rudimar Ronsoni
dec60de976
fix(codex): preserve wrapped sessions and recover state (#2160)
## Description

Closes #2159.

Codex wrappers currently launch against a disposable `CODEX_HOME`, so
session state created during a wrapped run can disappear when that
temporary directory is removed. This change launches Codex against its
durable home, keeps proxy routing process-local, and adds recovery for
retained temporary homes and pinned recovery sources.

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

- Launch Codex against its durable `CODEX_HOME` and apply routing
through process-local config overrides after the actual proxy port is
resolved.
- Preserve custom provider identity and reject providers that cannot be
redirected safely.
- Detect dangling temporary Codex homes before interactive wraps and
offer recovery.
- Add `headroom recover codex` with automatic discovery, repeatable
`--source`, preview, confirmation, retained backups, and rollback on
failure.
- Search Python's temp root, `$TMPDIR`, `/tmp`, `/private/tmp`, and
macOS `/private/var/folders/*/*/T` for retained `headroom-codex-home-*`
directories.
- Reuse `source-pinned/` copies left by interrupted or failed recovery
attempts after the original temporary home has disappeared.
- Report deleted temporary homes still referenced by SQLite rollout
paths without treating paths pasted into prompts or errors as filesystem
evidence.
- Audit the durable thread index, rollout files, and history when no
source remains, including indexed chat counts and history-only orphan
records.
- Normalize legacy localhost `headroom` providers in both SQLite thread
rows and rollout `session_meta`, including retries after an earlier
broken recovery, while preserving user-defined remote providers named
`headroom`.
- Merge compatible config, JSONL, rollout, SQLite, credential, and
regular-file state without propagating deletions or runtime artifacts.
- Rewrite recovered thread rollout paths to the durable home and restore
legacy Headroom thread providers to the active provider.
- Validate SQLite schemas, SQLx migration checksums, integrity, and
foreign keys, and quarantine malformed JSONL.
- Preserve failed targets with an atomic rename before rollback,
avoiding recursive-deletion races with live SQLite runtime files.
- Document discovery, migration, retained backups, rollback behavior,
and the limits of deleted-source recovery.

## 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 in isolated Docker containers

### Test Output

```text
$ uv run pytest tests/test_cli/test_wrap_codex.py tests/test_cli/test_recover_codex.py -q
122 passed

$ uv run ruff check headroom/cli/recover.py headroom/providers/codex/recovery.py tests/test_cli/test_recover_codex.py
All checks passed!

$ uv run ruff format --check headroom/cli/recover.py headroom/providers/codex/recovery.py tests/test_cli/test_recover_codex.py
3 files already formatted

$ uv run mypy headroom/cli/recover.py headroom/providers/codex/recovery.py
Success: no issues found in 2 source files
```

All validation ran in `ghcr.io/astral-sh/uv:python3.12-bookworm` against
a writable disposable copy of a read-only source mount. Codex was not
installed or launched, and no real user Codex state was read or
modified.

The tests cover multi-root discovery, deleted-reference reporting,
retained pinned-source recovery, durable SQLite path relocation, SQLite
and rollout provider normalization, idempotent repair after an earlier
broken recovery, remote provider preservation, unrelated dangling target
rows, backup retention, atomic rollback, malformed-state quarantine,
SQLite validation, and Windows-safe handle closure.

The repository shim E2E was not launched locally because this recovery
work intentionally avoids launching Codex. Upstream CI exercises wrapper
E2E in isolated environments.

## Real Behavior Proof

- Environment: `ghcr.io/astral-sh/uv:python3.12-bookworm`, Python 3.12,
a writable disposable checkout copied from a read-only source mount, at
head `2d89ecec`.
- Exact command / steps: Run `pytest -q
tests/test_cli/test_wrap_codex.py tests/test_cli/test_recover_codex.py`,
then run `ruff check` and `ruff format --check` against
`headroom/cli/wrap.py`, `headroom/cli/recover.py`,
`headroom/providers/codex/recovery.py`,
`tests/test_cli/test_wrap_codex.py`, and
`tests/test_cli/test_recover_codex.py`.
- Observed result: `122 passed in 10.08s`; Ruff reported `All checks
passed!` and `5 files already formatted`.
- Not tested: Launching a real Codex process or modifying a real user
`CODEX_HOME`; these were intentionally excluded to protect live user
state.

## 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 where the behavior is hard to understand
- [x] I have made corresponding documentation changes
- [x] My changes generate no new warnings
- [x] I have added tests that prove the fix is effective
- [x] New and existing focused unit tests pass with my changes
- [x] I have updated `CHANGELOG.md` if applicable

## Additional Notes

The temporary-home behavior was introduced by #1507 in
`ad9d086f43`. Related context: #730, #731,
#961, #1034, #1050, #1349, #1853, #1889, #2103, and #2104.

A temporary home that macOS or `TemporaryDirectory` already deleted
cannot be reconstructed unless a retained `source-pinned/` copy exists.
Recovery identifies genuine dangling SQLite paths, audits surviving
durable history, and recovers any retained pinned source it can find.
Prompt text without a rollout cannot reconstruct a full transcript.

The unchecked changelog item is not applicable because this repository
does not require a changelog entry for this fix.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:58:21 +00:00
Krishna Chaitanya
57e8dcb425
feat(proxy): add opt-in cost-aware model router (#1706) (#2205)
## Description

Adds an optional, configuration driven model router (closes #1706). With
`HEADROOM_MODEL_ROUTER_ENABLED` set, ordered rules in
`HEADROOM_MODEL_ROUTES` rewrite the upstream model by estimated input
size and tool presence, complementary to content compression, for
example sending small, tool-free requests to a cheaper model. First
matching rule wins and every decision is logged with a reason. Off by
default so behavior is unchanged, skipped under
`x-headroom-bypass`/passthrough, and wired on the Anthropic
`/v1/messages` path. Malformed rules fail open, so a bad rule is skipped
rather than silently widened.

Closes #1706

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

- `headroom/proxy/model_router.py`: new `ModelRouter` component (ordered
rules, first-match decision with reason, fail-open env parsing,
tokenizer-free input estimate).
- `headroom/proxy/models.py` + `headroom/proxy/server.py`:
`ProxyConfig.model_router` field, env loader
(`HEADROOM_MODEL_ROUTER_ENABLED` / `HEADROOM_MODEL_ROUTES`), and proxy
wiring.
- `headroom/proxy/handlers/anthropic.py`: apply routing on
`/v1/messages` after the bypass gate, tracked as a body mutation.
- Tests, docs (`configuration.mdx`), and a CHANGELOG entry.

## Testing

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

### Test Output

```text
$ pytest -q tests/test_proxy/test_model_router.py tests/test_proxy/test_model_router_wiring.py
36 passed, 1 warning

$ ruff check .
All checks passed!

$ mypy headroom --ignore-missing-imports
Success: no issues found in 477 source files
```

## Real Behavior Proof

- Environment: local, macOS, Python 3.12, headroom `.venv`, upstream
mocked (no live provider call).
- Exact command / steps: enable the router via
`ProxyConfig(model_router=...)`, POST `/v1/messages` through
`TestClient` with a rule routing low-risk requests to a cheaper model;
repeat with header `x-headroom-bypass: true`.
- Observed result: the forwarded upstream body model is rewritten from
`claude-sonnet-4-6` to `claude-haiku-4-5` when the router is enabled,
and is left unchanged under bypass (see
`tests/test_proxy/test_model_router_wiring.py`).
- Not tested: the OpenAI and Gemini handler paths (this PR wires the
Anthropic path only); no live provider request (upstream is mocked).

## Review Readiness

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

## Checklist

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

## Additional Notes

Happy to adjust the interface or scope (for example OpenAI and Gemini
parity) if you'd prefer a different shape.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: reneleonhardt <65483435+reneleonhardt@users.noreply.github.com>
2026-07-15 19:58:17 +00:00
Chester
aa4515cf7a
fix(memory): filter inactive graph-expanded results (#2210)
## DescriptionKeep graph-expanded local-memory results consistent with
the current-only contract already applied by vector search.Closes
#2209## 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- Reject graph-expanded memories whose `valid_until` is set.- Reject
graph-expanded memories whose `superseded_by` is set.- Add focused
coverage for active, expired, and superseded related memories.##
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 pytest
tests/test_memory/test_local_backend_search.py -q3 passed$ uv run --with
ruff ruff check headroom/memory/backends/local.py
tests/test_memory/test_local_backend_search.pyAll checks passed!$ uv run
--with ruff ruff format --check headroom/memory/backends/local.py
tests/test_memory/test_local_backend_search.py2 files already
formatted```## Real Behavior Proof- Environment: Python 3.13, synthetic
in-memory test doubles- Exact command / steps: run the focused test file
above- Observed result: active graph-linked memory is returned; records
with `valid_until` or `superseded_by` are excluded- Not tested: full
repository suite, external vector/graph implementations## Review
Readiness- [x] I have performed a self-review- [x] This PR is ready for
human review## Checklist- [x] My code follows the project's style
guidelines- [x] I have performed a self-review of my code- [ ] I have
commented my code, particularly in hard-to-understand areas- [ ] I have
made corresponding changes to the documentation- [ ] My changes generate
no new warnings- [x] I have added tests that prove my fix is effective
or that my feature works- [x] New and existing unit tests pass locally
with my changes- [ ] I have updated the CHANGELOG.md if applicable##
Screenshots (if applicable)N/A## Additional NotesDocumentation and
changelog changes are not needed for this narrow internal behavior fix.
The existing temporal-history APIs remain unchanged.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:58:13 +00:00
Zhenjia ZHOU
8537e2cf60
fix(wrap): self-heal a stale ANTHROPIC_BASE_URL left by a dead proxy (#2223)
## Description

`headroom wrap claude` persists `ANTHROPIC_BASE_URL=<proxy>` into
project-local `.claude/settings.local.json`. This is required: Claude
Code's cc-daemon spawn-forks conversation workers that read settings
fresh rather than inherit env, so the URL cannot just live in the child
process env.

When the proxy then dies via a **hard reboot / SIGKILL**, no
signal/atexit cleanup fires, so the stale URL lingers and bricks a later
**bare `claude`** with ConnectionRefused (#2221). #1768's mitigations
(SIGHUP, next-`wrap` self-heal, doctor WARN) don't cover "reboot → bare
`claude`", and — the key gap — `wrap` installed no hook of its own, so
for a user who only ever ran `wrap claude` (never `init claude`) there
was nothing to clean it up.

`wrap claude` now installs a **SessionStart-only** self-heal hook
(removed again on `unwrap`) that clears the persisted base URL **iff the
recorded proxy port fails a retry-hardened liveness probe**. A
responding proxy is never cleared, and the retry (3 attempts ~250 ms
apart, alive on first success) keeps a transient blip from clearing a
live session mid-run. Because workers read settings fresh per
conversation, clearing at session start unblocks the current session
too, not only the next.

## Design note / assumption (for maintainer confirmation)

This relies on **the SessionStart hook completing before the first
cc-daemon conversation worker reads `settings.local.json`**. That
ordering lives in Claude Code, not this repo; it is grounded in the
documented spawn-fresh-read model (the same reason the URL must be
persisted at all). Raised on the issue for confirmation. The truly
launcher-agnostic fix would be upstream — Claude Code falling back to
the real upstream when its configured base URL is unreachable — which
would make any stale local URL harmless.

## Type of Change

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

## Changes Made

- `headroom/cli/wrap.py`:
- `_wrap_proxy_alive(port, attempts=3, delay=0.25)` — retry-hardened
liveness (alive on first success, dead only if all fail).
- `_check_and_clear_dead_wrap_marker` — port is authoritative (survives
PID reuse after reboot); a single probe decides; a responding proxy is
never cleared; falls back to PID staleness only for port-less markers.
- `_ensure_claude_wrap_selfheal_hook` /
`_remove_claude_wrap_selfheal_hook` — install on `wrap claude`, remove
on `unwrap`; **SessionStart-only** (never PreToolUse), idempotent,
preserves the `env` block and unrelated/user hooks.
  - hidden `wrap selfheal` command the hook invokes.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff`)
- [x] Type checking passes (`mypy`)
- [x] New tests added for new functionality

### Test Output

```text
$ pytest tests/test_cli/test_wrap_dead_marker_selfheal.py
22 passed

$ pytest <related wrap/unwrap suites>
59 passed, 1 failed   # the 1 failure (test_wrap_marker_is_stale_when_pid_reused)
                      # is PRE-EXISTING + unrelated — fails identically on clean main
                      # (macOS _proc_identity returns None); this PR touches neither
                      # _wrap_marker_is_stale nor _identity_mismatch.

$ ruff check / mypy headroom/cli/wrap.py   # clean
```

## Real Behavior Proof

- Environment: macOS (Darwin), Python in a uv venv, branch
`feat/wrap-stale-url-selfheal` off `main`.
- Exact command / steps: `pytest
tests/test_cli/test_wrap_dead_marker_selfheal.py` exercises: `wrap
claude` writes a SessionStart-only self-heal hook into
`settings.local.json` (idempotent, not on PreToolUse); `unwrap` removes
it (keeping unrelated hooks); the `wrap selfheal` command clears a
dead-port marker's base URL; and — bound to a REAL listening socket — a
live proxy's marker is never cleared, including when a single probe
transiently fails but the retry succeeds.
- Observed result: dead-proxy marker → base URL restored to its prior
value; live-proxy marker (real socket) → preserved; no marker / no
settings file / port-less marker → no-op, no exception. All 22 pass.
- Not tested: the actual Claude Code hook-vs-worker execution ordering
(upstream, not in this repo) — see the Design note; the fix is correct
given that documented model.

## 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 wrap behavior)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — happy to add an entry if
preferred.

## Additional Notes

- Scoped to the `wrap claude` project-local path (the reported
scenario). The opt-in cc-switch reconciler writes `ANTHROPIC_BASE_URL`
into the *global* `~/.claude/settings.json` with no restore today — a
separate, lower-frequency gap I can follow up on if wanted.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:58:10 +00:00
Ingmar Krusch
ea0115cbdb
fix(backend/bedrock): preserve system-prompt cache_control breakpoint (list form) (#2225)
## Description

The LiteLLM backend flattened the Anthropic top-level `system` field to
a joined string whenever it arrived as a **list of content blocks**,
discarding each block's `cache_control`. LiteLLM's Bedrock Converse
transform (`AmazonConverseConfig._transform_system_message`) only emits
a `cachePoint` for content blocks that carry `cache_control`, never for
a plain string. So on any `--backend bedrock` deployment the **system
prefix was never cached**: every turn re-sent the full system prompt
(typically 5k-25k tokens with Claude Code) at full input price.

#1390 fixed the analogous case for `tool_result` blocks in
`_convert_messages_for_litellm`, but the top-level `system` field
handling in `send_message` / `stream_message` was out of scope there and
still flattened. The cache hits observed on live Bedrock traffic came
only from the tool-result / message-tail breakpoint, masking that the
largest, most stable block was uncached.

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/backends/litellm.py`: factor the top-level `system` field
conversion into a single `_system_field_to_message` helper. `str` stays
string-content (unchanged behavior); a `list` maps to text blocks
retaining each block's `cache_control`; non-dict entries coerce to a
plain text block. Both call sites (`send_message` non-streaming,
`stream_message` streaming) now call the helper, so they stay
byte-identical.
- `tests/test_bedrock_tool_result_cache_and_streaming_stats.py`: add
`TestSystemFieldCacheControl` — list-with-`cache_control` retains it,
plain-string is unchanged, list-without-`cache_control` produces list
content with no marker, plus two end-to-end checks that drive the
emitted message through `AmazonConverseConfig._transform_system_message`
and assert a `cachePoint` is present for the cache_control case and
absent otherwise.

## Testing

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

### Test Output

```text
$ uv run pytest tests/test_bedrock_tool_result_cache_and_streaming_stats.py -q
collected 13 items
tests/test_bedrock_tool_result_cache_and_streaming_stats.py .............  [100%]
13 passed in 1.18s

$ uv run ruff check headroom/backends/litellm.py tests/test_bedrock_tool_result_cache_and_streaming_stats.py
All checks passed!
```

## 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. Model
`global.anthropic.claude-sonnet-5`, region eu-west-1.
- Exact command / steps: ran a purpose-built probe that POSTs
Anthropic-shape `/v1/messages` to the running proxy with a 7,692-token
STABLE system prompt carrying a single `cache_control: {type:
ephemeral}` breakpoint (and no other cache_control anywhere), a pinned
`x-headroom-session-id`, across 5 sequential turns, reading the raw
response `usage` each turn.
- Observed result: **before the fix**, the response `usage` had no cache
fields at all — `cache_creation_input_tokens` and
`cache_read_input_tokens` both absent, nothing cached. **After the
fix**, turn 1 shows `cache_creation_input_tokens=10164` (write) and
turns 2-5 each show `cache_read_input_tokens=10164` (read) with
`cache_creation=0` — write-once, then read the system prefix from
Bedrock's cache on every subsequent turn. The proxy's `/stats`
`prefix_cache` tracker registered all four later turns as hits
(`hit_requests += 1` per turn, `bust_count = 0`).
- Not tested: no change to the tool_result / message-tail breakpoint
path (already handled by #1390 / #2144); this fix is scoped to the
top-level `system` field only. The in-`messages` text-block flatten in
`_convert_messages_for_litellm` is intentionally left 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
- [ ] 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

## Additional Notes

- No linked issue number: found via independent investigation of a
personal `--backend bedrock` deployment.
- Companion to #2196 (`fix(proxy/bedrock): wire PrefixCacheTracker
updates into Bedrock backend paths`) from the same investigation. #2196
wires the tracker; this fixes the system-prompt breakpoint that #1390
left flattened on the top-level `system` field.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:58:06 +00:00
Abhay Singh
842d7e1ad1
fix(ccr): lowercase a retrieved hash so an uppercase echo still hits the store (#2236)
## Description

A CCR retrieval fails whenever the model echoes the content hash in
uppercase, even though the content is present in the store.

`parse_tool_call` extracts and validates the hash from a
`headroom_retrieve` tool call:

```python
# Validate hex characters only
if not all(c in "0123456789abcdef" for c in hash_key.lower()):
    return None

return hash_key
```

The hex check is deliberately case-insensitive (`hash_key.lower()`), so
an uppercase hash passes validation — but the value is then returned
**verbatim**. The compression store, however, keys every entry by a
lowercase hash: writes use either a sha256 hexdigest
(`hashlib.sha256(...).hexdigest()[:24]`, always lowercase) or
`explicit_hash.lower()`, and `retrieve` / `get_entry_status` look the
key up as-is with no normalization.

So when a model reproduces the marker hash in uppercase (LLMs routinely
normalize hex casing when they copy tokens), the retrieve endpoint
validates it, calls `store.retrieve("ABC…")` against a store that only
holds `"abc…"`, and reports a miss — the original content is unreachable
even though it is right there. The case-insensitive validation shows the
intent was to accept either casing; only the return value was left
un-normalized.

## Fix

Return the canonical lowercase form so the whole pipeline is
consistently lowercase:

```python
return hash_key.lower()
```

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/ccr/tool_injection.py`: `parse_tool_call` returns
`hash_key.lower()`.
- `tests/test_ccr_tool_injection.py`: new test asserting an uppercase
hash is normalized to lowercase.
- `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/ccr/tool_injection.py tests/test_ccr_tool_injection.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/ccr/tool_injection.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 validate/return + a lowercase-keyed store with a
dependency-free script and left the full pytest to CI.
- Exact command / steps: put `"abc123def456abc123def456" -> content` in
a store, then looked it up with the uppercase echo
`"ABC123DEF456ABC123DEF456"` through the OLD (return verbatim) and NEW
(return `.lower()`) paths.
- Observed result: OLD returns the uppercase hash → store miss; NEW
returns the lowercase hash → store hit (original content recovered). A
lowercase hash resolves under both.
- Not tested: a live model round-trip that uppercases the marker; 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 lives
alongside the existing `parse_tool_call` tests in
`tests/test_ccr_tool_injection.py`, so it runs under the normal CI
pytest job; behaviour is additionally verified by the standalone proof
above.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:57:58 +00:00
Shubham Srivastava
17d60dce1f
docs(troubleshooting): document Windows Defender ast-grep-cli false positive + workarounds (#2200) (#2237)
## Description

On Windows, `uv tool install "headroom-ai[all]"` (and `pip install`)
fails while installing the `ast-grep-cli` wheel because Windows Defender
quarantines the bundled `sg.exe` as `Trojan:Win64/Lazy!MTB` (`os error
225`). This is a **known upstream false positive** in the `ast-grep-cli`
wheel
([ast-grep/ast-grep#2799](https://github.com/ast-grep/ast-grep/issues/2799)),
not a Headroom-introduced problem — but because `ast-grep-cli` is a base
dependency, the local install path is blocked on affected Windows
machines.

The issue (#2200) explicitly asks: "At minimum, please document a
supported workaround." This adds a troubleshooting entry with
safest-first workarounds. `ast-grep` is used only for optional AST-based
Read-output outlining and Headroom degrades gracefully without it, so
the impact is purely the install-time quarantine.

Closes #2200

## 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/troubleshooting.mdx` only — a new `### Windows:
Defender blocks ast-grep-cli (sg.exe) during install` subsection under
the existing `## Installation Issues` section, following the file's
`**Symptom**` / `**Cause**` / workarounds pattern:

- **Symptom** — the exact `uv tool install` failure text (`os error
225`, `Trojan:Win64/Lazy!MTB`, `sg.exe`) so users match it by search.
- **Cause** — known upstream `ast-grep-cli` wheel false positive
(linked); base dependency so it hits `[proxy]` too; `ast-grep` is
optional at runtime and Headroom runs without it.
- **Workarounds, safest first:** (1) run the proxy in Docker (no local
wheel → no AV trigger); (2) restore `sg.exe` from Defender quarantine
and retry (no persistent change); (3) a temporary, *scoped* Defender
exclusion for `uv tool dir` during install, framed as a known false
positive with a caution not to disable Defender wholesale; (4) report
the false positive to Microsoft for a durable signature fix.

Explicitly out of scope: making `ast-grep-cli` optional (a
dependency-policy change requiring maintainer justification per
CONTRIBUTING). No code change.

## Testing

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

### Test Output

Docs-only; verification is fact cross-check + MDX sanity:

```text
$ grep -n "ast-grep-cli>=" pyproject.toml
60:    "ast-grep-cli>=0.30.0",       # AST-aware code slicing (CodeCompressor); binary wheel
# → confirms ast-grep-cli is a base dependency (affects [proxy] too)

$ sed -n '6,7p' headroom/proxy/interceptors/astgrep.py
followed by an elided body marker. Falls back to the original text if
ast-grep isn't available, the extension isn't supported, or there are fewer
# → confirms graceful degradation: Headroom runs without a working sg.exe

$ uv tool dir
C:\Users\<user>\AppData\Roaming\uv\tools
# → the directory the scoped-exclusion workaround targets (via `uv tool dir`, not a hardcoded path)

# MDX sanity: balanced code fences (even count), well-formed headings, links close.
```

## Real Behavior Proof

- **Environment:** Windows 11 (the affected platform), the docs source
inspected against the current `main` base.
- **Exact command / steps:** Issue #2200 contains a complete, exact
reproduction (command `uv tool install "headroom-ai[all]"`, the `os
error 225` / `Trojan:Win64/Lazy!MTB` failure on `sg.exe`, `ast-grep-cli
0.44.1`, `uv 0.11.16`, Windows 11). The documented facts are verified
against the tree: base-dependency declaration (`pyproject.toml:60`) and
graceful degradation (`headroom/proxy/interceptors/astgrep.py:6-7`). The
`uv tool dir` command used in the exclusion workaround resolves
correctly on this machine.
- **Observed result:** The troubleshooting note accurately describes the
failure and gives valid Windows/Defender workarounds, ordered
safest-first.
- **Not tested:** I deliberately did **not** run `uv tool install
"headroom-ai[all]"` to force a live Defender quarantine — doing so is
disruptive (it can quarantine real files and pulls the full dependency
set) and machine-specific. The reproduction in the issue is complete and
corroborated by the upstream ast-grep report.

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

## Screenshots (if applicable)

N/A (troubleshooting prose addition).

## Additional Notes

- Test/tests-added and CHANGELOG checklist items are N/A —
documentation-only change (kept to a single file, matching the merged
#2031 precedent).
- The durable fix for the underlying false positive belongs upstream
(ast-grep) and/or with Microsoft's signature update; this PR documents
supported workarounds in the meantime, as the issue requested.
- Making `ast-grep-cli` an optional dependency would remove the install
blocker at the source, but that's a dependency-policy change for
maintainers to weigh (the interceptor already tolerates its absence) —
intentionally not attempted here.
2026-07-15 19:57:55 +00:00
Jervis
09c66ac212
fix(proxy): batch small Codex Responses tool outputs (#2239)
## Description

Batches small Codex/OpenAI Responses tool-output units through the
existing ContentRouter instead of skipping each unit individually below
the 512-byte floor. This fixes sessions where many small tool outputs
are collectively worth compressing, but no single output clears the
per-unit threshold.

The change keeps larger units on the existing independent compression
path, preserves CCR retrieval markers and protected tags across the
batch envelope, rejects structurally invalid batch output, and leaves
under-floor tails as size-floor passthroughs.

Fixes #2234

## 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 `headroom/transforms/compression_batches.py` for bounded
compatible-unit batching, batch envelope parsing, tag/CCR marker
preservation, and per-entry result splitting.
- Updated the OpenAI Responses compression adapter to batch small
tool-output text slots while keeping larger units on the existing cached
per-unit path.
- Switched the unit size floor to UTF-8 bytes so CJK and other multibyte
text are measured consistently with the byte threshold.
- Added regression coverage for batching, CJK byte floors, CCR marker
preservation, malformed batch rejection, array output parts, and
under-floor tails.

## Testing

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

### Test Output

```text
$ uv run --with pytest --with fastapi --with httpx --with anyio --with uvicorn --with h2 pytest tests/test_compression_batches.py tests/test_compression_units.py tests/test_openai_responses_compression_units.py -q
47 passed, 1 warning

$ uvx ruff==0.15.17 check headroom/proxy/handlers/openai.py headroom/transforms/compression_batches.py headroom/transforms/compression_units.py tests/test_compression_batches.py tests/test_compression_units.py tests/test_openai_responses_compression_units.py --output-format concise
All checks passed!

$ uvx ruff==0.15.17 format --check headroom/proxy/handlers/openai.py headroom/transforms/compression_batches.py headroom/transforms/compression_units.py tests/test_compression_batches.py tests/test_compression_units.py tests/test_openai_responses_compression_units.py
6 files already formatted

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

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.3, local checkout of this PR
branch.
- Exact command / steps: ran the focused batching/unit/OpenAI Responses
test suites above, including cases where four individually-small tool
outputs collectively exceed the shared floor and where output arrays
contain multiple text parts plus non-text parts.
- Observed result: small outputs are sent through one router call and
applied back to their original slots; under-floor tails remain
unmodified; non-text parts are preserved; CCR markers are retained or
the entire batch is rejected if moved/corrupted.
- Not tested: a live Codex Responses proxy session against an upstream
model; full-suite collection was not run locally.

## Review Readiness

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

## Checklist

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

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:57:50 +00:00
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