Commit graph

137 commits

Author SHA1 Message Date
github-actions[bot]
660fa8cfb6
chore: release main (#1574)
Some checks failed
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Blocked by required conditions
Docker / promote-latest (push) Blocked by required conditions
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 7s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 6s
Init E2E / docker-init-e2e (push) Failing after 9s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 6s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 6s
Merge Conflicts / merge-conflicts (push) Failing after 5s
Release Please / release-please (push) Failing after 4s
Security / CodeQL (javascript-typescript) (push) Failing after 30s
Security / CodeQL (python) (push) Failing after 18s
Wrap E2E / docker-wrap-e2e (push) Failing after 11s
Security / Dependency audit (pip-audit) (push) Failing after 54s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 5s
Security / Secret scan (gitleaks) (push) Failing after 27s
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 14m26s
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
🤖 I have created a release *beep* *boop*
---


<details><summary>0.29.0</summary>

##
[0.29.0](https://github.com/headroomlabs-ai/headroom/compare/v0.28.0...v0.29.0)
(2026-07-03)


### Features

* **proxy:** add --lossless no-CCR mode with format-native compaction
([#1721](https://github.com/headroomlabs-ai/headroom/issues/1721))
([c75ebde](c75ebdee6d))
* **stats:** surface Codex WS compression counters in /stats summary
([#1680](https://github.com/headroomlabs-ai/headroom/issues/1680))
([2fe19c3](2fe19c39e4))
* **transforms:** adaptive Otsu KEEP/DROP threshold (+ land relevance
split on main)
([#1726](https://github.com/headroomlabs-ai/headroom/issues/1726))
([eea667a](eea667a720))


### Bug Fixes

* **bedrock:** fail fast when session-token auth lacks botocore
([#1553](https://github.com/headroomlabs-ai/headroom/issues/1553))
([54cfa36](54cfa361d3))
* **bedrock:** route ARNs via converse, named AWS profiles, and au. re…
([#1456](https://github.com/headroomlabs-ai/headroom/issues/1456))
([7d87aa2](7d87aa2f1c))
* **ccr:** honor workspace dir for sqlite store
([#1564](https://github.com/headroomlabs-ai/headroom/issues/1564))
([96e1dfe](96e1dfe395))
* **claude:** surface Remote Control proxy incompatibility
([#1610](https://github.com/headroomlabs-ai/headroom/issues/1610))
([4bf7f92](4bf7f92417))
* **cli:** stop advertising unwired compression tuning env vars in
banner
([#1634](https://github.com/headroomlabs-ai/headroom/issues/1634))
([d5bf98d](d5bf98df31))
* **codex:** avoid duplicate headroom provider config
([#1431](https://github.com/headroomlabs-ai/headroom/issues/1431))
([ddd4adf](ddd4adf911))
* **compression:** reject lossy unmarked tool output in unit router path
([#1479](https://github.com/headroomlabs-ai/headroom/issues/1479))
([de24cd5](de24cd5fc0))
* **cortex-code:** migrate to current Cortex REST API endpoints + add
e2e benchmarks
([#1474](https://github.com/headroomlabs-ai/headroom/issues/1474))
([f00ace6](f00ace6da5))
* **dashboard:** align token savings headline denominator
([#1653](https://github.com/headroomlabs-ai/headroom/issues/1653))
([646e705](646e705514))
* **dashboard:** derive per-project setup URL from live origin
([#1511](https://github.com/headroomlabs-ai/headroom/issues/1511))
([e035aef](e035aefce2))
* **detection:** contain unidiff panic on orphaned +++ target line
([#1548](https://github.com/headroomlabs-ai/headroom/issues/1548))
([e386c09](e386c097d6))
* **evals:** CJK-aware F1 tokenization + token estimation
([#1527](https://github.com/headroomlabs-ai/headroom/issues/1527))
([99a8540](99a8540e65))
* **install:** close parent log fd in start_detached_agent
([#1576](https://github.com/headroomlabs-ai/headroom/issues/1576))
([816cb85](816cb85fa8))
* **install:** use Windows-safe PID liveness probe in runtime_status
([#1544](https://github.com/headroomlabs-ai/headroom/issues/1544))
([#1560](https://github.com/headroomlabs-ai/headroom/issues/1560))
([6b227b9](6b227b9c90))
* **learn:** aggregate verbosity baselines across projects instead of
overwriting
([#1288](https://github.com/headroomlabs-ai/headroom/issues/1288))
([27a5468](27a5468349))
* **mcp:** show lifetime totals and label rolling session scope in
headroom_stats
([#1428](https://github.com/headroomlabs-ai/headroom/issues/1428))
([1c0e152](1c0e15243e))
* **memory:** cap local embedder CPU thread oversubscription
([#198](https://github.com/headroomlabs-ai/headroom/issues/198))
([#1559](https://github.com/headroomlabs-ai/headroom/issues/1559))
([b84afbf](b84afbfb83))
* **memory:** singleflight LocalBackend init to stop cold-start races
([#1691](https://github.com/headroomlabs-ai/headroom/issues/1691))
([bec47a1](bec47a1898))
* **openclaw:** detect uv-installed headroom binary in ~/.local/bin
([#1459](https://github.com/headroomlabs-ai/headroom/issues/1459))
([adaeb88](adaeb88a4d))
* **opencode:** preserve custom OpenAI gateway paths
([#1596](https://github.com/headroomlabs-ai/headroom/issues/1596))
([c19347c](c19347c310))
* **opencode:** route native providers + load transport plugin, fix
Serena context
([#1573](https://github.com/headroomlabs-ai/headroom/issues/1573))
([ad0034f](ad0034f981))
* preserve anthropic passthrough tool order
([#1427](https://github.com/headroomlabs-ai/headroom/issues/1427))
([a932247](a9322477e3))
* **proxy/auth:** match real Anthropic OAuth token prefix (sk-ant-oat)
([#1672](https://github.com/headroomlabs-ai/headroom/issues/1672))
([8cddf9b](8cddf9b58e))
* **proxy:** expose persistent savings metrics
([#1647](https://github.com/headroomlabs-ai/headroom/issues/1647))
([5fe4e7b](5fe4e7b195))
* **proxy:** fail open when kompress saturation would exhaust
pre-upstream budget
([#1430](https://github.com/headroomlabs-ai/headroom/issues/1430))
([15ac650](15ac650d40))
* **proxy:** handle streaming CCR retrieval
([#1451](https://github.com/headroomlabs-ai/headroom/issues/1451))
([d337e3b](d337e3b828))
* **proxy:** include system/tools/sampling in cache key
([#1473](https://github.com/headroomlabs-ai/headroom/issues/1473))
([312129a](312129a8e7))
* **proxy:** preserve Responses passthrough bytes
([#1598](https://github.com/headroomlabs-ai/headroom/issues/1598))
([2a34a82](2a34a822f2))
* **proxy:** strip Codex lite header on the HTTP /responses path
([#1663](https://github.com/headroomlabs-ai/headroom/issues/1663))
([9fbd47b](9fbd47ba6b))
* **proxy:** wire --compression-max-workers /
HEADROOM_COMPRESSION_MAX_WORKERS
([#1632](https://github.com/headroomlabs-ai/headroom/issues/1632))
([814ffa3](814ffa36a4))
* **savings:** count cache-read tokens in input cost estimate
([#1429](https://github.com/headroomlabs-ai/headroom/issues/1429))
([72ade37](72ade37112))
* skip Magika backend on x86 CPUs without AVX2
([#1162](https://github.com/headroomlabs-ai/headroom/issues/1162))
([64783d8](64783d8824))
* **transforms/content-router:** route grep/log output away from HTML
extractor
([#1719](https://github.com/headroomlabs-ai/headroom/issues/1719))
([0d18ef2](0d18ef26f4))
* **transforms:** bound native content detection with a Windows watchdog
([#575](https://github.com/headroomlabs-ai/headroom/issues/575))
([#1563](https://github.com/headroomlabs-ai/headroom/issues/1563))
([95abca3](95abca3abd))
* Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL
([#1393](https://github.com/headroomlabs-ai/headroom/issues/1393))
([cff7247](cff7247efd))
* **wrap:** detach the shared proxy on Windows so it survives an
ungraceful agent close
([#1464](https://github.com/headroomlabs-ai/headroom/issues/1464))
([6cba441](6cba4419d0))
* **wrap:** preserve custom Vertex base URL
([#1477](https://github.com/headroomlabs-ai/headroom/issues/1477))
([75427bb](75427bbd4a))
* **wrap:** remove rtk instructions from Codex AGENTS.md on unwrap
([#1604](https://github.com/headroomlabs-ai/headroom/issues/1604))
([c9d717c](c9d717c13c))
</details>

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 22:54:04 -07:00
github-actions[bot]
aea3c35177
chore: release main (#1441)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.28.0</summary>

##
[0.28.0](https://github.com/headroomlabs-ai/headroom/compare/v0.27.0...v0.28.0)
(2026-06-29)


### Features

* add --disable-kompress-fallback to restore legacy PASSTHROUGH fallback
([#1185](https://github.com/headroomlabs-ai/headroom/issues/1185))
([f309244](f309244a77))
* add first-class OpenCode support (wrap, learn, mcp install)
([#559](https://github.com/headroomlabs-ai/headroom/issues/559))
([91cd210](91cd2102d7))
* add HEADROOM_KEEPALIVE_EXPIRY to keep upstream connections warm
([#1124](https://github.com/headroomlabs-ai/headroom/issues/1124))
([85786b3](85786b33a3))
* **azure-foundry:** derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE
([#1138](https://github.com/headroomlabs-ai/headroom/issues/1138))
([e5031b0](e5031b0121))
* **cache:** attribute prompt-cache misses to TTL lapse vs prefix change
([#1313](https://github.com/headroomlabs-ai/headroom/issues/1313))
([#1343](https://github.com/headroomlabs-ai/headroom/issues/1343))
([4658721](4658721ea0))
* **code:** add Perl support to code-aware compressor
([#1125](https://github.com/headroomlabs-ai/headroom/issues/1125))
([f39858c](f39858c233))
* headroom wrap opencode / unwrap opencode CLI
([#1105](https://github.com/headroomlabs-ai/headroom/issues/1105))
([b4571cc](b4571cc346))
* **learn:** weight loops in Headroom Learn + RTK-loop eval
([#1160](https://github.com/headroomlabs-ai/headroom/issues/1160))
([14e8dc4](14e8dc4c84))
* **learn:** write per-project learnings to CLAUDE.local.md by default
([#1115](https://github.com/headroomlabs-ai/headroom/issues/1115))
([ced75e4](ced75e4718))
* **proxy:** add request timeout config
([#738](https://github.com/headroomlabs-ai/headroom/issues/738))
([c0745d4](c0745d4161))
* **proxy:** pilot hardening — inbound auth, security headers, audit
log, air-gap switch
([#1537](https://github.com/headroomlabs-ai/headroom/issues/1537))
([546ab55](546ab553dc))
* **proxy:** support glob patterns in exclude_tools
([#870](https://github.com/headroomlabs-ai/headroom/issues/870))
([#1259](https://github.com/headroomlabs-ai/headroom/issues/1259))
([a2159c0](a2159c0b66))
* **read-maturation:** activity-based hold-back Read maturation
(Mechanism B)
([#1068](https://github.com/headroomlabs-ai/headroom/issues/1068))
([723b80c](723b80c091))
* **savings:** durable savings ledger + headroom savings command
([#1127](https://github.com/headroomlabs-ai/headroom/issues/1127))
([978ffa0](978ffa0a6a))
* **wrap:** add --1m to preserve the 1M context window on wrap claude
([#1158](https://github.com/headroomlabs-ai/headroom/issues/1158))
([#1351](https://github.com/headroomlabs-ai/headroom/issues/1351))
([b50d9c1](b50d9c17ce))
* **wrap:** make tokensave the primary coding-task compressor, Serena
the backup
([#1230](https://github.com/headroomlabs-ai/headroom/issues/1230))
([dca9853](dca9853ed9))


### Bug Fixes

* **agent-evals:** Phase 0 — coding-agent accuracy A/B framework
([#1037](https://github.com/headroomlabs-ai/headroom/issues/1037))
([84f9871](84f9871e30))
* **agno:** tolerate streaming tool-call SDK objects in parser
([#1312](https://github.com/headroomlabs-ai/headroom/issues/1312))
([#1336](https://github.com/headroomlabs-ai/headroom/issues/1336))
([5986c22](5986c2260f))
* **bedrock:** add boto3 1.41 + CRT for aws login credentials
([#1486](https://github.com/headroomlabs-ai/headroom/issues/1486))
([4db3bc9](4db3bc91d9))
* bump codebase-memory-mcp to v0.8.1
([#1284](https://github.com/headroomlabs-ai/headroom/issues/1284))
([530318b](530318b425))
* **ccr:** make headroom_retrieve a hash-only full-content lookup
([#1532](https://github.com/headroomlabs-ai/headroom/issues/1532))
([c2fc4d3](c2fc4d3753))
* **ccr:** propagate --no-ccr-marker flag to all compressors
([#1022](https://github.com/headroomlabs-ai/headroom/issues/1022))
([#1197](https://github.com/headroomlabs-ai/headroom/issues/1197))
([0c9b42a](0c9b42a919))
* **ccr:** skip Anthropic marker emission when tool injection is
deferred
([#1273](https://github.com/headroomlabs-ai/headroom/issues/1273))
([2cae13d](2cae13dd79))
* **ci:** extend gitleaks allowlist to cover test fixtures + verified
examples
([#1539](https://github.com/headroomlabs-ai/headroom/issues/1539))
([d2565a6](d2565a6983))
* **ci:** guarantee model present in test shards to end cache-miss
flakiness
([#1399](https://github.com/headroomlabs-ai/headroom/issues/1399))
([2e29c72](2e29c7223f))
* **ci:** normalize Windows CRLF line endings in PR governance script
([#1012](https://github.com/headroomlabs-ai/headroom/issues/1012))
([5194388](5194388b66))
* **cli:** add explicit UTF-8 encoding to file I/O in wrap commands
([#1126](https://github.com/headroomlabs-ai/headroom/issues/1126))
([#1164](https://github.com/headroomlabs-ai/headroom/issues/1164))
([a0cb798](a0cb7982e3))
* **cli:** fall back gracefully when embedding-server sidecar is absent
([#1206](https://github.com/headroomlabs-ai/headroom/issues/1206))
([38f1404](38f1404432))
* **cli:** harden all CLI surfaces + fix docs accuracy
([#1491](https://github.com/headroomlabs-ai/headroom/issues/1491))
([bd76235](bd76235f5c))
* **cli:** wire --http2/--no-http2 (HEADROOM_HTTP2) into proxy command
([#1373](https://github.com/headroomlabs-ai/headroom/issues/1373))
([e06b616](e06b61671f))
* **cli:** wire --rpm/--tpm and HEADROOM_RPM/HEADROOM_TPM to the Click
proxy command
([#1375](https://github.com/headroomlabs-ai/headroom/issues/1375))
([8aab8f2](8aab8f22cb))
* **code:** slice tree-sitter byte offsets as UTF-8
([#1332](https://github.com/headroomlabs-ai/headroom/issues/1332))
([8238402](82384022bd))
* **code:** validate Python compressed syntax
([#1302](https://github.com/headroomlabs-ai/headroom/issues/1302))
([cbd361d](cbd361de2a))
* **code:** verify a real parse in tree-sitter availability check
([#1231](https://github.com/headroomlabs-ai/headroom/issues/1231))
([#1299](https://github.com/headroomlabs-ai/headroom/issues/1299))
([5e0bb69](5e0bb69725))
* **codex:** retag threads on init so Codex Desktop history stays
visible ([#961](https://github.com/headroomlabs-ai/headroom/issues/961))
([#1349](https://github.com/headroomlabs-ai/headroom/issues/1349))
([e6bbc40](e6bbc40b11))
* **codex:** stop pinning Codex memory MCP to one project db
([#1269](https://github.com/headroomlabs-ai/headroom/issues/1269))
([ad7993b](ad7993bf15))
* **dashboard:** include RTK stats in the historical tab
([#1324](https://github.com/headroomlabs-ai/headroom/issues/1324))
([35939c3](35939c3536))
* **deps:** remediate dependency CVEs and publish SBOM
([#1509](https://github.com/headroomlabs-ai/headroom/issues/1509))
([5771a80](5771a8020e))
* **docker:** persist session history across container revisions
([#1118](https://github.com/headroomlabs-ai/headroom/issues/1118))
([5912d65](5912d65674))
* **gemini:** offload compression to the executor
([#1382](https://github.com/headroomlabs-ai/headroom/issues/1382))
([615848e](615848eba4))
* **gemini:** resolve Google model capabilities through ModelRegistry
([#1276](https://github.com/headroomlabs-ai/headroom/issues/1276))
([17ecad9](17ecad9d89))
* **install:** guard install_agent_ensure against duplicate runtime
spawns
([#1301](https://github.com/headroomlabs-ai/headroom/issues/1301))
([8da0b4e](8da0b4e565))
* **install:** repair macOS launchd restart/start lifecycle
([#1290](https://github.com/headroomlabs-ai/headroom/issues/1290))
([da1a397](da1a3973ed))
* **install:** stop duplicating ENTRYPOINT in persistent-docker runtime
command ([#833](https://github.com/headroomlabs-ai/headroom/issues/833))
([#1348](https://github.com/headroomlabs-ai/headroom/issues/1348))
([feedead](feedead077))
* **io:** use UTF-8 with locale fallback and preserve line endings on
config/text I/O
([#1498](https://github.com/headroomlabs-ai/headroom/issues/1498))
([1baa04e](1baa04ef65))
* **kompress:** hard override keeps must-keep tokens regardless of model
score ([#1400](https://github.com/headroomlabs-ai/headroom/issues/1400))
([42612c8](42612c86df))
* **langchain:** disable streaming on wrapped model during ainvoke()
([#1287](https://github.com/headroomlabs-ai/headroom/issues/1287))
([3590046](359004646b))
* **mcp:** register managed installs with a resolvable headroom command
([#1386](https://github.com/headroomlabs-ai/headroom/issues/1386))
([22def93](22def93177))
* **mcp:** report correct savings_percent in headroom_compress
([#1106](https://github.com/headroomlabs-ai/headroom/issues/1106))
([f216e43](f216e43055))
* **opencode:** write local MCP config
([#1381](https://github.com/headroomlabs-ai/headroom/issues/1381))
([6c83790](6c83790680))
* **packaging:** move hnswlib to optional [vector] extra so [all] needs
no C++ toolchain
([#1499](https://github.com/headroomlabs-ai/headroom/issues/1499))
([80fa086](80fa086660))
* patch rtk hook script to use absolute path after register_claude_hooks
([#571](https://github.com/headroomlabs-ai/headroom/issues/571))
([b618d2d](b618d2d11a))
* **perf:** surface RTK/CLI context-tool savings in perf and the session
card ([#1433](https://github.com/headroomlabs-ai/headroom/issues/1433))
([9362747](93627471b7))
* **proxy:** add --protect-tool-results to prevent lossy compression of
exact-output Bash results
([#1374](https://github.com/headroomlabs-ai/headroom/issues/1374))
([51d4bcf](51d4bcfc11))
* **proxy:** add an Anthropic buffered read-timeout override
([#1331](https://github.com/headroomlabs-ai/headroom/issues/1331))
([3be2526](3be2526b76))
* **proxy:** add versionless Vertex AI routes for Claude Code
compatibility
([#1321](https://github.com/headroomlabs-ai/headroom/issues/1321))
([bb3e040](bb3e040a46))
* **proxy:** bind before eager preload so a hung compressor load can't
block startup
([#1500](https://github.com/headroomlabs-ai/headroom/issues/1500))
([d5ac07f](d5ac07fc45))
* **proxy:** build SSL contexts for custom CA bundles
([#1134](https://github.com/headroomlabs-ai/headroom/issues/1134))
([561ba17](561ba17ec2))
* **proxy:** forward request-id headers on the streaming path
([#1100](https://github.com/headroomlabs-ai/headroom/issues/1100))
([#1258](https://github.com/headroomlabs-ai/headroom/issues/1258))
([3d59df7](3d59df7be8))
* **proxy:** gate CCR retrieve/compress endpoints to loopback
([#1338](https://github.com/headroomlabs-ai/headroom/issues/1338))
([acafb2d](acafb2d0f6))
* **proxy:** honor force_kompress routing profile
([#996](https://github.com/headroomlabs-ai/headroom/issues/996))
([b4682d6](b4682d6f91))
* **proxy:** keep large compression results on the critical path
([#296](https://github.com/headroomlabs-ai/headroom/issues/296))
([#1352](https://github.com/headroomlabs-ai/headroom/issues/1352))
([90734b6](90734b691a))
* **proxy:** offload /v1/compress to the compression executor to stop
blocking the loop
([#1501](https://github.com/headroomlabs-ai/headroom/issues/1501))
([27e010e](27e010e38f))
* **proxy:** preserve Responses memory continuations with store=false
([#1103](https://github.com/headroomlabs-ai/headroom/issues/1103))
([cdfeeac](cdfeeacc63))
* **proxy:** queue mid-turn user messages on non-Bedrock streaming path
([#1377](https://github.com/headroomlabs-ai/headroom/issues/1377))
([b09f027](b09f027062))
* **proxy:** register interceptor in explicit transforms list when
HEADROOM_INTERCEPT_ENABLED
([#1376](https://github.com/headroomlabs-ai/headroom/issues/1376))
([55c700c](55c700c686))
* **proxy:** report real input tokens on streaming message_start
([#1132](https://github.com/headroomlabs-ai/headroom/issues/1132))
([#1305](https://github.com/headroomlabs-ai/headroom/issues/1305))
([70cc96a](70cc96a386))
* **proxy:** retry upstream 429 with Retry-After on both forwarders
([#1329](https://github.com/headroomlabs-ai/headroom/issues/1329))
([90bee89](90bee89243))
* **proxy:** retry upstream 529 overloaded like 429 on both forwarders
([#1495](https://github.com/headroomlabs-ai/headroom/issues/1495))
([547b15d](547b15dab2))
* **proxy:** stop re-compressing headroom_retrieve output and emitting
unredeemable markers
([#1323](https://github.com/headroomlabs-ai/headroom/issues/1323))
([43494ff](43494ff526))
* **proxy:** strip Codex lite header from OpenAI WebSockets
([#1543](https://github.com/headroomlabs-ai/headroom/issues/1543))
([5d3803a](5d3803a21c))
* **read-lifecycle:** persist STALE Read originals in the CCR store
([#1488](https://github.com/headroomlabs-ai/headroom/issues/1488))
([9157173](9157173018))
* recover persistent proxy feature checks and reject non-Copilot
exchange URL
([#1465](https://github.com/headroomlabs-ai/headroom/issues/1465))
([16c638b](16c638bc21))
* remove agents.md
([#1540](https://github.com/headroomlabs-ai/headroom/issues/1540))
([a7d3360](a7d3360a05))
* respect COPILOT_PROVIDER_TYPE env var when provider_type is auto
([#549](https://github.com/headroomlabs-ai/headroom/issues/549))
([24cf256](24cf256e50))
* restore token-mode compression on frozen prefixes
([#1489](https://github.com/headroomlabs-ai/headroom/issues/1489))
([8e0dadf](8e0dadfe02))
* **router:** degrade to pure-Python detection on native panic
([#1123](https://github.com/headroomlabs-ai/headroom/issues/1123))
([#1260](https://github.com/headroomlabs-ai/headroom/issues/1260))
([a00fb67](a00fb6761e))
* **rtk:** stop hook registration timing out on a forked daemon
([#1314](https://github.com/headroomlabs-ai/headroom/issues/1314))
([9758817](9758817979))
* **smart-crusher:** honor enable_ccr_marker on the opaque-blob path
([#1130](https://github.com/headroomlabs-ai/headroom/issues/1130))
([27d6f8e](27d6f8e2a7))
* **subscription:** only reset 5h contribution on real rollover, not API
jitter
([#1255](https://github.com/headroomlabs-ai/headroom/issues/1255))
([8d6c175](8d6c175d60))
* **subscription:** run transcript token scan off the event loop
([#1263](https://github.com/headroomlabs-ai/headroom/issues/1263))
([f03021f](f03021f1b6))
* surface output reduction without a restart, and explain $0.00 savings
on Python 3.14
([#1296](https://github.com/headroomlabs-ai/headroom/issues/1296))
([c30ec4c](c30ec4cda8))
* **tests:** reset whole headroom logger subtree so caplog stays
deterministic
([#1117](https://github.com/headroomlabs-ai/headroom/issues/1117))
([fda4670](fda4670ef8))
* **tls:** add HEADROOM_TLS_STRICT=0 toggle for corporate SSL inspection
([#1308](https://github.com/headroomlabs-ai/headroom/issues/1308))
([#1341](https://github.com/headroomlabs-ai/headroom/issues/1341))
([52068dd](52068dd650))
* **tokenizers:** price CJK/Kana/Hangul at ~1 token per char in
EstimatingTokenCounter
([#1093](https://github.com/headroomlabs-ai/headroom/issues/1093))
([a35fe86](a35fe86e87))
* **transforms:** gate tool string output from lossy compression
([#1307](https://github.com/headroomlabs-ai/headroom/issues/1307))
([#1387](https://github.com/headroomlabs-ai/headroom/issues/1387))
([c6c921a](c6c921a7c1))
* **websocket:** harden responses websocket origin handling
([#1481](https://github.com/headroomlabs-ai/headroom/issues/1481))
([c632023](c632023cc1))
* **windows:** pin UTF-8 encoding on text-mode subprocess calls
([#1311](https://github.com/headroomlabs-ai/headroom/issues/1311))
([d633e81](d633e8172c))
* **wrap:** add Copilot unwrap command
([#1251](https://github.com/headroomlabs-ai/headroom/issues/1251))
([b4fde0c](b4fde0c3a4))
* **wrap:** isolate proxy stdio from proxy.log on Windows
([#1191](https://github.com/headroomlabs-ai/headroom/issues/1191))
([959ab0d](959ab0de47))
* **wrap:** keep agent savings opt-in
([#1294](https://github.com/headroomlabs-ai/headroom/issues/1294))
([b829ceb](b829ceba84))
* **wrap:** show the dashboard URL when the proxy is already running
([#1313](https://github.com/headroomlabs-ai/headroom/issues/1313))
([b0146c4](b0146c4ccd))


### Performance Improvements

* **compression:** take large cold-start contexts off the synchronous
kompress path
([#1171](https://github.com/headroomlabs-ai/headroom/issues/1171))
([#1298](https://github.com/headroomlabs-ai/headroom/issues/1298))
([6c68ff4](6c68ff4e9f))
</details>

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 12:53:17 -07:00
Kenneth Wong
4db3bc91d9
fix(bedrock): add boto3 1.41 + CRT for aws login credentials (#1486)
## Description

`pip install headroom-ai[bedrock]` cannot serve users who authenticate
with `aws login` (IAM Identity Provider / console-login, DPoP).
Resolving those credentials requires the AWS Common Runtime (CRT);
without `awscrt`, botocore raises `MissingDependencyException`.

The AWS docs state the requirement as: **"Boto3 version 1.41.0 or later
with AWS Common Runtime (CRT)"** — i.e. both a modern boto3 floor and
CRT (installed via the `[crt]` extra).

## Type of Change

- [x] Bug fix (non-breaking)

## Changes Made

- `pyproject.toml` `bedrock` extra: bump `boto3>=1.28.0` →
`boto3>=1.41.0`, add `botocore[crt]>=1.41.0` (installs `awscrt`).
- `uv.lock`: regenerated — adds `awscrt`, resolves `boto3` to 1.42.x.

No code changes — the bedrock backend already passes `aws_profile_name`
through to the LiteLLM calls (via #1456); this just makes the installed
dependencies actually able to resolve `aws login` credentials.

## Impact

- **`aws login` (IAM Identity Provider / DPoP):** now works — awscrt
present.
- **`aws sso login` (classic Identity Center):** unaffected (already
worked).
- **static keys (`~/.aws/credentials`):** unaffected.
- Bumping the boto3 floor only affects the optional `[bedrock]` extra;
bedrock users benefit from a current boto3 regardless.

## Testing

Dependency-only change. `uv lock` resolves cleanly (257 packages, awscrt
0.29.2, boto3 1.42.38). No runtime code path altered, so existing
bedrock tests are unaffected.

## Checklist

- [x] Self-review performed
- [x] No new warnings
- [x] Linting passes

## Additional Notes

Focused on the dependency gap only. ARN routing / named-profile wiring /
docs are handled in #1456; pricing in #1485.
2026-06-28 14:51:52 -07:00
Parideboy
80fa086660
fix(packaging): move hnswlib to optional [vector] extra so [all] needs no C++ toolchain (#1499)
## Description

`pip install "headroom-ai[all]"` aborts on any machine without a C++
toolchain.
`[all]` pulls `[memory]`, which was the only extra carrying
`hnswlib>=0.8.0`. hnswlib
compiles from source where no wheel matches the target, and that build
failure rolls
back the **entire** `[all]` install.

hnswlib is already fully optional at runtime: `MemoryConfig` defaults to
`VectorBackend.AUTO` → **sqlite-vec** (pure Python, no compiler), and
only falls back
to HNSW. So `[memory]` does not need hnswlib to function. This moves
hnswlib into a
dedicated optional `[vector]` extra, exactly like `[pytorch-mps]` is
already kept out
of `[all]`.

Closes #1368

## Type of Change

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

## Changes Made

- `pyproject.toml`:
  - Removed `hnswlib>=0.8.0` from `[memory]` (keeps `sqlite-vec` +
`sentence-transformers`; the default sqlite-vec backend still works).
- Added `vector = ["hnswlib>=0.8.0"]` for users who opt into the HNSW
backend.
- `[all]` still references `[memory]` (now hnswlib-free) and does
**not** add
    `[vector]`, so it resolves with no compiler.
- `[dev]` keeps `hnswlib`, so CI still installs and exercises the HNSW
backend tests.
- Docs: documented the new `[vector]` extra in `installation.mdx` and
the README, and
noted it is excluded from `[all]`; fixed the `[memory]` row that claimed
to bundle
  hnswlib.

No application code changed.

## Testing

- [x] Linting passes (`ruff check .`)
- [x] Manual testing performed (TOML resolution check — see proof)
- [ ] Unit tests pass (`pytest`) — no app code changed; existing
memory/HNSW tests are
unaffected (the HNSW backend dependency moved extras but `[dev]`/CI
still install it).

### Test Output

```text
$ python - <<'PY'  # resolve [all] transitively and check hnswlib placement
memory has hnswlib: False
vector has hnswlib: True
dev has hnswlib:    True
[all] resolved has hnswlib: False
[all] has sqlite-vec: True
[all] has sentence-transformers: True
PY

$ ruff check headroom/ tests/
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.11; `tomllib` + a small
transitive-extra
  resolver over the edited `pyproject.toml`.
- Exact command / steps: parse `pyproject.toml`, expand
`headroom-ai[...]`
self-references in `[all]` recursively, then check which extras carry
`hnswlib`.
- Observed result: the resolved `[all]` set contains no hnswlib while
`[vector]` and `[dev]` do. Full output:
  ```text
  memory has hnswlib: False
  vector has hnswlib: True
  dev has hnswlib:    True
  [all] resolved has hnswlib: False
  [all] has sqlite-vec: True
  [all] has sentence-transformers: True
  ```
`[all]` now resolves with **no** hnswlib (so no compiler needed), while
the HNSW
  backend stays installable via `[vector]` and still tested via `[dev]`.
- Not tested: a real `pip install` on a compiler-less host (the failure
is a build-time
rollback that the resolver check captures deterministically); the
native-wrapper e2e
jobs that this `pyproject.toml` change triggers run `wrap` e2e, not the
memory HNSW
  path, so dropping hnswlib from `[all]` does not affect them.

## Review Readiness

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

## Additional Notes

- Editing `pyproject.toml` trips the `e2e` path filter, so the
Windows/macOS/Docker
native-wrapper jobs also run on this PR. They install + run the `wrap`
e2e flow (not
  the memory HNSW backend), so the extras change is safe for them.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 13:16:55 -07:00
Tejas Chopra
5771a8020e
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description

Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.

This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).

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

## Changes Made

**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).

**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).

**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.

**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.

**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.

## Testing

- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof

### Test Output

```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found

# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME        INSTALLED  TYPE    VULNERABILITY        SEVERITY
sqlitedict  2.1.0      python  GHSA-g4r7-86gm-pgqc  High      # [benchmark]-only, unpatchable, accepted
nltk        3.9.4      python  GHSA-p4gq-832x-fm9v  High      # [benchmark]-only, unpatchable, accepted

# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
    Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised

# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out

# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit)         -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit)       -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit)      -> found 0 vulnerabilities / No vulnerabilities found
```

## Real Behavior Proof

- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)

## Additional Notes

**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.

Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.

**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
github-actions[bot]
95b2333ee5
chore: release main (#1274)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.27.0</summary>

##
[0.27.0](https://github.com/chopratejas/headroom/compare/v0.26.0...v0.27.0)
(2026-06-22)


### Features

* **cli:** add headroom doctor setup diagnostics
([#926](https://github.com/chopratejas/headroom/issues/926))
([e45cf4e](e45cf4e061))
* **cli:** add headroom update command and release banner
([#1088](https://github.com/chopratejas/headroom/issues/1088))
([26be2c3](26be2c39cb))
* compression extraction — Rust knob exposure, CCR hardening, traffic
audits ([#818](https://github.com/chopratejas/headroom/issues/818))
([b7be381](b7be3814f1))
* measure and surface token throughput (tokens/sec) through the proxy
([#983](https://github.com/chopratejas/headroom/issues/983))
([0d89c67](0d89c674cd))
* output-token reduction — verbosity shaper, per-user learning,
counterfactual savings
([#965](https://github.com/chopratejas/headroom/issues/965))
([a99dc61](a99dc61424))
* **policy:** decay P_alive from idle time near cache TTL
([#856](https://github.com/chopratejas/headroom/issues/856) P3b)
([#1028](https://github.com/chopratejas/headroom/issues/1028))
([fe4f9ee](fe4f9ee478))
* **providers:** add Cortex Code (Snowflake CoCo) as a supported agent
([#1190](https://github.com/chopratejas/headroom/issues/1190))
([d9d0bf4](d9d0bf4b79))
* **proxy:** cc-switch reconciler — keep Headroom in the request path
alongside cc-switch
([#1030](https://github.com/chopratejas/headroom/issues/1030))
([e8fc8a0](e8fc8a0d18))
* **proxy:** hot-reload live env knobs so a reused proxy picks them up
without a restart
([#1090](https://github.com/chopratejas/headroom/issues/1090))
([6904d47](6904d47a01))
* **proxy:** make COMPRESSION_TIMEOUT_SECONDS configurable via env
([#946](https://github.com/chopratejas/headroom/issues/946))
([#991](https://github.com/chopratejas/headroom/issues/991))
([addebdb](addebdb29c))
* **transforms:** tabular + spreadsheet (.xlsx/.xls) compression
([#1128](https://github.com/chopratejas/headroom/issues/1128))
([d789a7c](d789a7c528))
* **vertex:** turnkey Claude Code + Vertex compression (+ fixes from the
Vertex review)
([#1113](https://github.com/chopratejas/headroom/issues/1113))
([0e05915](0e0591506c))


### Bug Fixes

* **ccr:** accept 12-char SmartCrusher hashes in tool injection
([#1095](https://github.com/chopratejas/headroom/issues/1095))
([#1141](https://github.com/chopratejas/headroom/issues/1141))
([9f7f3ad](9f7f3adfea))
* **ccr:** return stored content when headroom_retrieve query matches
nothing ([#1213](https://github.com/chopratejas/headroom/issues/1213))
([#1236](https://github.com/chopratejas/headroom/issues/1236))
([08fb845](08fb845fe3))
* **content-router:** honor target_ratio in compression cache + add
proxy --target-ratio flag
([#1108](https://github.com/chopratejas/headroom/issues/1108))
([8894ee0](8894ee0c18))
* **dashboard:** light-mode backgrounds + aligned savings tables
([#1064](https://github.com/chopratejas/headroom/issues/1064))
([5eae32b](5eae32ba47))
* **deps:** make litellm optional on Python 3.14
([#956](https://github.com/chopratejas/headroom/issues/956))
([#993](https://github.com/chopratejas/headroom/issues/993))
([b2f04e4](b2f04e4ef7))
* **e2e:** align Codex wrap e2e with global-only RTK guidance
([#1240](https://github.com/chopratejas/headroom/issues/1240))
([#1254](https://github.com/chopratejas/headroom/issues/1254))
([bc12ace](bc12acef59))
* **init:** set ENABLE_TOOL_SEARCH=true so Claude Code keeps deferring
tools ([#746](https://github.com/chopratejas/headroom/issues/746))
([#995](https://github.com/chopratejas/headroom/issues/995))
([500ec2b](500ec2b7fa))
* **kompress:** never block the request path on the cold-cache model
download ([#1161](https://github.com/chopratejas/headroom/issues/1161))
([3fc2a78](3fc2a78a5e))
* **memory:** use ONNX embedder for `wrap --memory` sync
([#1092](https://github.com/chopratejas/headroom/issues/1092))
([#1262](https://github.com/chopratejas/headroom/issues/1262))
([4f9feda](4f9fedaa7a))
* **openclaw:** wrap plugin export as {register} object for OpenClaw
2026.x compatibility
([#1218](https://github.com/chopratejas/headroom/issues/1218))
([2e6c442](2e6c442dc8))
* **providers:** update DeepSeek V3 context limit from 128K to 1M
([#1038](https://github.com/chopratejas/headroom/issues/1038))
([#1137](https://github.com/chopratejas/headroom/issues/1137))
([bcabc5c](bcabc5cb11))
* **proxy:** allow disabling periodic TOIN stats logging
([#1265](https://github.com/chopratejas/headroom/issues/1265))
([b5f63d8](b5f63d8fa9))
* **proxy:** honor HEADROOM_EXCLUDE_TOOLS for Codex /v1/responses tool
outputs ([#940](https://github.com/chopratejas/headroom/issues/940))
([#1053](https://github.com/chopratejas/headroom/issues/1053))
([f03e77b](f03e77bec0))
* **proxy:** preserve byte-faithful Anthropic tool forwarding
([#1222](https://github.com/chopratejas/headroom/issues/1222))
([1f18d59](1f18d59809))
* **proxy:** route Codex OAuth image requests
([#1215](https://github.com/chopratejas/headroom/issues/1215))
([381d771](381d771e46))
* **proxy:** scope CORS to loopback + gate operator/content endpoints
([#1226](https://github.com/chopratejas/headroom/issues/1226))
([bd55a42](bd55a426bc))
* **proxy:** stamp X-Client: codex on Responses endpoint for
unidentified callers
([#1036](https://github.com/chopratejas/headroom/issues/1036))
([b0cd032](b0cd0329c7))
* **proxy:** treat NODE_EXTRA_CA_CERTS as additive, not replacement
([#998](https://github.com/chopratejas/headroom/issues/998))
([#1031](https://github.com/chopratejas/headroom/issues/1031))
([c987283](c98728363a))
* **telemetry:** switch anonymous telemetry to opt-in (off by default)
([#1223](https://github.com/chopratejas/headroom/issues/1223))
([b998697](b99869778b))
* **tokenizers:** bound tiktoken vocab load so a stalled download cannot
hang requests
([#956](https://github.com/chopratejas/headroom/issues/956))
([#994](https://github.com/chopratejas/headroom/issues/994))
([7e86baf](7e86bafb90))
* **unwrap:** remove ANTHROPIC_BASE_URL + ENABLE_TOOL_SEARCH and init
hooks on unwrap
([#992](https://github.com/chopratejas/headroom/issues/992))
([5b84691](5b84691770))
* **wrap:** keep Codex RTK guidance global
([#1240](https://github.com/chopratejas/headroom/issues/1240))
([7c26a54](7c26a54d53))
* **wrap:** percent-encode non-ASCII cwd names in X-Headroom-Project
header ([#1071](https://github.com/chopratejas/headroom/issues/1071))
([9f712cc](9f712ccbd7))
* **wrap:** write env.ANTHROPIC_BASE_URL to settings.json so
daemon-spawned conversations inherit proxy
([#951](https://github.com/chopratejas/headroom/issues/951))
([#1078](https://github.com/chopratejas/headroom/issues/1078))
([a554c3a](a554c3a0e6))
</details>

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-21 22:28:55 -07:00
Tufan ALIN
f11a271229
fix(code): pin tree-sitter-language-pack <1.0 so code compression works (#1234)
## Description

The `[code]` extra requires `tree-sitter-language-pack>=0.10.0` with no
upper bound, so it now resolves to the 1.x line.
tree-sitter-language-pack 1.0 (2026-03-21) is a breaking rewrite whose
`get_language()` / `get_parser()` return the pack's own binding types
instead of standalone `tree_sitter.Language` / `tree_sitter.Parser`. As
a result `headroom/transforms/code_compressor.py::_get_parser()` raises,
the exception is caught upstream, and AST code compression silently
falls back to passthrough (0% reduction, no error surfaced) on a fresh
`pip install headroom-ai[code]`. This caps the dependency below the
breaking rewrite and pins the matching tree-sitter range, which is the
line the existing code is written against.

Closes #1232

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

- Pin `tree-sitter-language-pack>=0.10.0,<1.0` in the `[code]` extra
(was `>=0.10.0`).
- Add an explicit `tree-sitter>=0.25.2,<0.26` pin to document the
supported range (0.13.0 already requires `tree-sitter>=0.25.2`).
- Add an inline comment explaining why the `<1.0` cap is required, to
prevent a future re-bump.

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

I did not run the full pytest / ruff / mypy suite for this change (it is
a dependency-constraint pin); I verified the actual runtime behavior the
pin restores. See Real Behavior Proof.

### Test Output

```text
# BEFORE (resolved tree-sitter-language-pack 1.9.1): code compression no-ops
CodeAwareCompressor().compress(<real .py>) -> compression_ratio = 1.0  (0% on every file sampled)

# AFTER (tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2), headroom code unchanged:
is_tree_sitter_available(): True
# 60 varied real Python files (headroom, litellm, pydantic, openai), default CodeCompressorConfig:
  compressed OK (valid + reduced): 31   (52%)
  rejected for invalid syntax:     17   (28%)  -> returns original, never serves broken code
  no reduction / too small:        12   (20%)
  reduction when it worked: min 4.4%  median 37.2%  max 88.8%
# All compressed outputs re-parsed clean with ast.parse().
```

## Real Behavior Proof

- Environment: Python 3.12, headroom-ai 0.26.0. Before:
tree-sitter-language-pack 1.9.1 (what `[code]` resolves today). After:
tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2 (what this pin
resolves).
- Exact command / steps: `pip install "headroom-ai[code]"`; then run
`CodeAwareCompressor(CodeCompressorConfig()).compress(src)` over a
sample of real `.py` files and re-tokenize before/after with tiktoken
(cl100k_base), re-parsing each output with `ast.parse`.
- Observed result: with the unpinned (1.x) resolution, every sampled
file returned `compression_ratio == 1.0` (0%, silent passthrough). With
the pinned (0.x) resolution and no code changes,
`is_tree_sitter_available()` is True and 31/60 files compressed validly
at a ~37% median (up to ~89%); all compressed outputs re-parsed clean.
- Not tested: the full pytest / ruff / mypy suite; per-language rates
for JS/TS/Go/Rust/Java/C/C++ (they share the same `_get_parser()` path,
so the fix applies, but I measured Python specifically); the ~28%
invalid-syntax rejections are a separate pre-existing robustness issue
tracked in #1233, not addressed here.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A (dependency-constraint change).

## Additional Notes

- This is the minimal fix to restore functionality. The proper
longer-term fix is to migrate `_get_parser()` and the AST walker to the
tree-sitter-language-pack 1.x API, after which the `<1.0` cap can be
lifted; happy to follow up with that if preferred.
- Unchecked checklist items, with rationale: no docs change needed
(constraint-only); no new tests added (a corpus-based
compress-and-reparse regression test would be valuable but belongs with
the robustness work in #1233); I did not run the full local unit-test
suite for a dependency pin; CHANGELOG appears to be release-please
managed, so I left it untouched.
- I am not a maintainer; this came out of an independent evaluation of
the `[code]` path. Pinning `<1.0` parks the project on the
now-superseded 0.x pack, which is the tradeoff for a one-line fix today.

Co-authored-by: mitralone <5514599+mitralone@users.noreply.github.com>
2026-06-21 10:06:59 -07:00
Ashish
d789a7c528
feat(transforms): tabular + spreadsheet (.xlsx/.xls) compression (#1128)
## Description

Adds a content-type-aware path for **tabular data** — CSV/TSV, markdown
tables, fixed-width text, and binary `.xlsx`/`.xls` spreadsheets — by
routing them through the existing, battle-tested `SmartCrusher` instead
of letting them fall through to `PLAIN_TEXT → Kompress`.

The pipeline already compressed tables losslessly when handed a JSON
array of records. This wires up the missing front door: detect tabular
text (and ingest binary spreadsheets), convert to JSON records, and
reuse `SmartCrusher.crush()`. No new compression algorithm.

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

- **Detection** (`content_detector.py`): new `ContentType.TABULAR` +
`_try_detect_tabular()` for CSV/TSV, markdown tables, and fixed-width
columns. Ordered after search/log (which also look "delimited") and
before code, with a prose-rejection guard so it never steals
`file:line:content` search output, `key: value` logs, or sentences with
incidental commas. Rust backend returns `plain_text` for unknown types
and the router already falls back to the Python detector, so **no Rust
change**.
- **Bridge** (`tabular_ingest.py`): stdlib parsers + `to_records()` + a
`TabularCompressor` that parses → JSON records → `SmartCrusher`
(lossless `csv-schema` first; lossy row-drop with reversible
`<<ccr:HASH>>` markers stays SmartCrusher's built-in fallback). Only
adopts a result when it actually saves bytes.
- **Spreadsheets** (`spreadsheet_ingest.py`): `.xlsx`/`.xls` → per-sheet
CSV text at the SDK boundary. Optional deps (`pip install
headroom-ai[spreadsheet]`) fail loudly with an install hint, never
silently degrade.
- **Routing** (`content_router.py`): `CompressionStrategy.TABULAR`,
`enable_tabular_compressor` flag, lazy getter, apply branch, strategy
maps, Kompress fallback eligibility.
- **SDK** (`compress.py`): `compress_spreadsheet(path, ...)` helper (one
message per sheet).
- **Packaging** (`pyproject.toml`): new `[spreadsheet]` extra;
`openpyxl` added to `[dev]` so the xlsx path is exercised in CI.
- **Docs/demo**: `examples/tabular_compression_demo.py` + README entry.

### Design note: lossless-only

Compact, all-unique tables with no query yield ~0 savings — this is
correct, not a bug. SmartCrusher returns
`skip:unique_entities_no_signal` and won't drop unique rows without a
duplicate/relevance signal. Real wins come from verbose/redundant tables
and query-driven selection. A pressure-driven lossy row sampler was
considered and intentionally not added.

## Testing

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

### Test Output

```text
$ python -m pytest tests/test_transforms_tabular.py -q
collected 20 items
tests/test_transforms_tabular.py ....................                    [100%]
============================== 20 passed in 7.15s ==============================

$ ruff check headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
All checks passed!

$ mypy headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
Success: no issues found in 2 source files
```

`tests/test_transforms_tabular.py` (20 tests): detection true positives
+ no-misroute negatives (search/log/JSON/prose), parser units (incl.
fixed-width), the CSV→SmartCrusher bridge, router routing + disable
flag, and `.xlsx` ingestion (skipif openpyxl missing) + error paths.
`spreadsheet_ingest` 100% / `tabular_ingest` 90% line coverage.

## Real Behavior Proof

- **Environment:** local checkout of `feat/tabular-compression`, Python
3.x, `pip install -e ".[dev]"`.
- **Exact command / steps:** `python
examples/tabular_compression_demo.py` (no API key required).
- **Observed result:**
  ```text
  === Raw tabular text (ContentRouter, char-level) ===
compact unique CSV strat=tabular chars 1306 -> 1072 ( 17.9% saved)
redundant CSV strat=tabular chars 2661 -> 1350 ( 49.3% saved)
verbose markdown strat=tabular chars 2019 -> 1580 ( 21.7% saved)

  === Full pipeline (real tokenizer) ===
  redundant CSV            tokens       768 ->    394  ( 48.7% saved)

  === Binary spreadsheet (.xlsx) ===
  2-sheet workbook         tokens      1092 ->    683  ( 37.5% saved)
  ```
- **Not tested:** legacy `.xls` binary path (needs optional `xlrd` +
binary fixture; `# pragma: no cover`); base64-embedded `.xlsx` inside
multimodal blocks (out of scope, noted as a 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 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 are intentionally untouched: this repo uses
**release-please**, which bumps the version and CHANGELOG via automated
`chore: release main` PRs, not per-feature PRs.
- The `.xls` path is `# pragma: no cover` (legacy, needs optional `xlrd`
+ a binary fixture).
- Follow-up (out of scope): base64-embedded `.xlsx` inside
tool-result/multimodal blocks; porting tabular parsers into the Rust
core for parity.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:30:20 -05:00
Eyal Mizrachi
b2f04e4ef7
fix(deps): make litellm optional on Python 3.14 (#956) (#993)
## Description

`litellm` is a hard dependency and its metadata caps `Requires-Python
>=3.10,<3.14`, so `pip install headroom-ai` is unsatisfiable on Python
3.14. But litellm is only used for model registry / pricing / non-core
providers — all lazily imported behind `ImportError` guards — never on
the core compression or Anthropic proxy path. Refs #956 (install half).

## Type of Change

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

## Changes Made

- Add a `python_version < '3.14'` marker to both litellm declarations
(core deps + dev extra); installs unchanged on <=3.13, skipped on 3.14
(matches the existing rapidocr/tomli marker pattern).

## Testing

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

### Test Output

```text
$ pytest tests/test_litellm_optional.py -q
2 passed in 0.10s

$ python3.14 -m pip install dist/headroom_ai-0.25.0-cp310-abi3-linux_x86_64.whl
Successfully installed headroom-ai-0.25.0 ...   # litellm NOT installed
$ python3.14 -c "import importlib.util as u; print(u.find_spec('litellm') is not None)"
False
```

## Real Behavior Proof

- Environment: fresh venv on CPython 3.14.5, Linux
- Exact command / steps: built the abi3 wheel, `pip install` it on
Python 3.14, then `import headroom` + start the proxy + send a
compressible request
- Observed result: install exits 0 with litellm skipped; `import
headroom` works; the proxy compresses (29913 -> 27626 tokens). Stock
0.25.0 cannot install on 3.14 at all.
- Not tested: litellm-backed features on 3.14 (intentionally unavailable
there until litellm supports 3.14)

## Review Readiness

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 21:11:12 -05:00
github-actions[bot]
b81a4a7a16
chore: release main (#931)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.26.0</summary>

##
[0.26.0](https://github.com/chopratejas/headroom/compare/v0.25.0...v0.26.0)
(2026-06-16)


### Features

* add Copilot BYOK provider wrapper utilities and CLI support
([#1041](https://github.com/chopratejas/headroom/issues/1041))
([e67ee2a](e67ee2af65))
* add dashboard agent usage stats
([#814](https://github.com/chopratejas/headroom/issues/814))
([6d3f39f](6d3f39f213))
* Add support for Mistral Vibe CLI
([#935](https://github.com/chopratejas/headroom/issues/935))
([0932b8b](0932b8bef4))
* attribute reread waste to over-compression via marker check
([#901](https://github.com/chopratejas/headroom/issues/901))
([f928576](f9285766dd))
* **bedrock:** cross-region + Converse compression; bundle proxy binary
in images ([#999](https://github.com/chopratejas/headroom/issues/999))
([0dc2e1c](0dc2e1cb3f))
* **dashboard:** surface compression-vs-cache net impact in Prefix Cache
panel ([#913](https://github.com/chopratejas/headroom/issues/913))
([2a4d300](2a4d300841))
* **evals:** adversarial-input robustness grid for compressors
([#918](https://github.com/chopratejas/headroom/issues/918))
([5939004](5939004185))
* **parser:** detect re-issued identical tool calls as reread waste
([#909](https://github.com/chopratejas/headroom/issues/909))
([7d4ae86](7d4ae86ec0))
* **policy:** batch deep edits through one cache-bust
([#856](https://github.com/chopratejas/headroom/issues/856) P3a)
([#1015](https://github.com/chopratejas/headroom/issues/1015))
([c2e52fe](c2e52fe743))
* **policy:** consume net-cost mutation gate in ContentRouter
([#856](https://github.com/chopratejas/headroom/issues/856) P2)
([#905](https://github.com/chopratejas/headroom/issues/905))
([553ade4](553ade4ec6))
* **proxy:** compress AWS Bedrock InvokeModel requests via configurable
upstream ([#720](https://github.com/chopratejas/headroom/issues/720))
([7edb27a](7edb27ab24))


### Bug Fixes

* **anthropic:** strip styled Claude model ids
([#651](https://github.com/chopratejas/headroom/issues/651))
([0c5c89d](0c5c89d05c))
* **anyllm:** forward openai api_base/api_key to the any-llm backend
([#942](https://github.com/chopratejas/headroom/issues/942))
([#954](https://github.com/chopratejas/headroom/issues/954))
([a7ee8a6](a7ee8a60a7))
* **cache:** guard None exemplar embeddings in dynamic detector
([#950](https://github.com/chopratejas/headroom/issues/950))
([1ec9320](1ec9320888))
* **cache:** name the missing piece in semantic detector guard
([#1018](https://github.com/chopratejas/headroom/issues/1018))
([3b0bcee](3b0bceecf4))
* **ci:** check out repo in PR Governance label job
([#1021](https://github.com/chopratejas/headroom/issues/1021))
([4558bc2](4558bc2465))
* **ci:** make PR governance advisory
([#1047](https://github.com/chopratejas/headroom/issues/1047))
([74dff94](74dff94fb8))
* **codex:** compute waste signals on the OpenAI Responses path
([#898](https://github.com/chopratejas/headroom/issues/898))
([b9e2761](b9e27614c6))
* **codex:** poll /wham/usage for subscription limits (handshake no
longer sends x-codex-* headers)
([#924](https://github.com/chopratejas/headroom/issues/924))
([8c00f71](8c00f7103c))
* **codex:** PR health label check state
([#986](https://github.com/chopratejas/headroom/issues/986))
([99c874d](99c874d423))
* **codex:** retag thread providers so history menu stays whole across
the proxy boundary
([#1034](https://github.com/chopratejas/headroom/issues/1034))
([74ae781](74ae781644))
* **codex:** write canonical hooks feature flag and migrate deprecated
codex_hooks ([#743](https://github.com/chopratejas/headroom/issues/743))
([dff6a19](dff6a19946))
* **compression:** convert tree-sitter byte offsets to char offsets
([#892](https://github.com/chopratejas/headroom/issues/892))
([b1f700f](b1f700fc27))
* **compression:** correct JSON array item counting and entropy gate
([#887](https://github.com/chopratejas/headroom/issues/887))
([d6f0f0f](d6f0f0f642))
* **compression:** keep container bodies compressible in code handler
([#890](https://github.com/chopratejas/headroom/issues/890))
([16ed73b](16ed73bca6))
* **compression:** measure short-value threshold on payload, not token
([#889](https://github.com/chopratejas/headroom/issues/889))
([65b0e8c](65b0e8c58d))
* **compression:** use thread-local tree-sitter parsers in code handler
([#893](https://github.com/chopratejas/headroom/issues/893))
([6cdb846](6cdb846200))
* **gemini:** surface functionResponse payloads to waste-signal
detection ([#897](https://github.com/chopratejas/headroom/issues/897))
([9b0c840](9b0c840dd7))
* **learn:** decode directory names with spaces in Windows project paths
([#997](https://github.com/chopratejas/headroom/issues/997))
([#1027](https://github.com/chopratejas/headroom/issues/1027))
([2d3701b](2d3701b59e))
* **learn:** scan subagent and workflow transcripts
([#1045](https://github.com/chopratejas/headroom/issues/1045))
([0ddd4ed](0ddd4ed9e9))
* **openclaw:** declare headroom_retrieve tool contract
([#947](https://github.com/chopratejas/headroom/issues/947))
([7c8c909](7c8c909c85))
* **policy:** correct warm-cache penalty in net_mutation_gain to (S +
dT) ([#903](https://github.com/chopratejas/headroom/issues/903))
([0632eba](0632eba6c3))
* **proxy:** add native Bedrock converse-stream route
([#917](https://github.com/chopratejas/headroom/issues/917))
([b08ec15](b08ec15b0d))
* **proxy:** keep codex image-generation WS turns alive through the
relay ([#1000](https://github.com/chopratejas/headroom/issues/1000))
([7dbbb40](7dbbb4077e))
* **proxy:** make budget enforcement actually work
([#885](https://github.com/chopratejas/headroom/issues/885))
([a14ab45](a14ab45cf0))
* **proxy:** read RTK gain stats globally by default
([#957](https://github.com/chopratejas/headroom/issues/957))
([b70fccb](b70fccbe17))
* route v1internal code assist requests to cloudcode-pa.googleapis…
([#821](https://github.com/chopratejas/headroom/issues/821))
([e20f16b](e20f16b1a6))
* **serena:** stop the Serena dashboard popup and make --no-serena
actually disable Serena
([#1003](https://github.com/chopratejas/headroom/issues/1003))
([919379a](919379a8a1))
* support Copilot Business subscription auth
([#641](https://github.com/chopratejas/headroom/issues/641))
([0b4a4bd](0b4a4bd483))
* wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy
entrypoint ([#943](https://github.com/chopratejas/headroom/issues/943))
([9b7b436](9b7b436b04))
* **wrap:** avoid duplicate top-level keys when injecting codex provider
([#884](https://github.com/chopratejas/headroom/issues/884))
([dd22cfd](dd22cfd72a))


### Code Refactoring

* DRY cache logic, add thread safety, fix Bash exclusion
([#704](https://github.com/chopratejas/headroom/issues/704))
([e36fccd](e36fccd8cf))
</details>

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-16 15:35:00 -07:00
github-actions[bot]
8a53c8ec3b
chore: release main (#891)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.25.0</summary>

##
[0.25.0](https://github.com/chopratejas/headroom/compare/v0.24.0...v0.25.0)
(2026-06-12)


### Features

* add differential network capture harness
([#761](https://github.com/chopratejas/headroom/issues/761))
([11ab5f8](11ab5f83a1))
* add light mode for dashboard
([#834](https://github.com/chopratejas/headroom/issues/834))
([c425893](c425893d12))
* add OAuth2 client-credentials upstream-auth proxy extension
([#778](https://github.com/chopratejas/headroom/issues/778))
([#784](https://github.com/chopratejas/headroom/issues/784))
([eb2e50f](eb2e50feb2))
* add Vertex AI proxy routing
([#793](https://github.com/chopratejas/headroom/issues/793))
([3c77e52](3c77e52ce4))
* **cli:** comprehensive help text, validation, and exception handling
improvements
([#640](https://github.com/chopratejas/headroom/issues/640))
([028efab](028efabb4e))
* compression safety rails — error-output protection, pipeline circuit
breaker, library inflation guard
([#851](https://github.com/chopratejas/headroom/issues/851))
([c0cadcc](c0cadccff9))
* **dashboard:** per-model savings breakdown and expected-vs-actual cost
on historical charts
([#807](https://github.com/chopratejas/headroom/issues/807))
([34dafe6](34dafe69d9))
* detect re-served tool results as over-compression waste signal
([#854](https://github.com/chopratejas/headroom/issues/854))
([5f1d88a](5f1d88ad27))
* **evals:** add zero-cost tool schema compaction integrity eval
([#817](https://github.com/chopratejas/headroom/issues/817))
([53a08c6](53a08c63bf))
* gated Markdown-KV compaction formatter (serialization-aware output)
([#859](https://github.com/chopratejas/headroom/issues/859))
([06b2625](06b2625b17))
* **kompress:** warn on unrecognized HEADROOM_KOMPRESS_BACKEND +
document backend selection
([#204](https://github.com/chopratejas/headroom/issues/204))
([6367d0b](6367d0b722))
* **memory:** add opt-in Apple-GPU (MPS) embedding runtime
([#766](https://github.com/chopratejas/headroom/issues/766))
([c71592d](c71592d421))
* net-cost cache mutation formula on CompressionPolicy
([#856](https://github.com/chopratejas/headroom/issues/856) P1)
([#857](https://github.com/chopratejas/headroom/issues/857))
([d5f5802](d5f58026e2))
* **plugins:** Hermes agent headroom_retrieve plugin
([#824](https://github.com/chopratejas/headroom/issues/824))
([058bced](058bcedab8))
* probe-based retention scoring of recorded compression events
([#862](https://github.com/chopratejas/headroom/issues/862))
([c2106cb](c2106cbdab))
* **proxy:** add CLI opt-outs for CCR injection (compression-only mode)
([#823](https://github.com/chopratejas/headroom/issues/823))
([693d9d2](693d9d20e2))
* **proxy:** attribute savings history rollups per provider
([#791](https://github.com/chopratejas/headroom/issues/791))
([0b8b8d9](0b8b8d92de))
* **proxy:** log compressed messages alongside original request
([#261](https://github.com/chopratejas/headroom/issues/261))
([2269e40](2269e40bde))
* **proxy:** per-project savings breakdown on the dashboard (claude,
codex, aider, copilot, cursor)
([#803](https://github.com/chopratejas/headroom/issues/803))
([914a60a](914a60a2b0))
* support Python 3.14+ via pyo3 abi3 stable ABI
([#516](https://github.com/chopratejas/headroom/issues/516))
([19eac8e](19eac8e00d))
* switch Kompress default to kompress-v2-base with weight-only int8 ONNX
([#799](https://github.com/chopratejas/headroom/issues/799))
([74392b2](74392b238e))
* **transforms:** attribute read_lifecycle + smart_crush tags
([#249](https://github.com/chopratejas/headroom/issues/249))
([8f37426](8f374263d3))


### Bug Fixes

* **anthropic:** CCR exception must re-raise, not silently swallow
([#838](https://github.com/chopratejas/headroom/issues/838))
([8db5efc](8db5efc6f9))
* **ccr:** key Rust search/diff/log markers with explicit_hash
([#852](https://github.com/chopratejas/headroom/issues/852))
([bfcb07d](bfcb07d78e))
* **ccr:** make retrieval TTL configurable
([#715](https://github.com/chopratejas/headroom/issues/715))
([2533f77](2533f7703e))
* **ccr:** skip CCR when model calls headroom_retrieve alongside user
tools ([#839](https://github.com/chopratejas/headroom/issues/839))
([30078f8](30078f8465))
* **ccr:** use shared compression store
([#875](https://github.com/chopratejas/headroom/issues/875))
([249af6c](249af6cc7b))
* **ci:** correct comments, timeouts, and pip reliability in native e2e
workflows ([#878](https://github.com/chopratejas/headroom/issues/878))
([b716c8c](b716c8c2ee))
* **ci:** pin cosign-installer to v3 (v4 does not exist)
([#774](https://github.com/chopratejas/headroom/issues/774))
([199d693](199d693f98))
* **codex:** respect CODEX_HOME for wrap config
([#731](https://github.com/chopratejas/headroom/issues/731))
([96abf38](96abf38b09))
* **content_router:** guard against empty compression output causing
Anthropic 400
([#771](https://github.com/chopratejas/headroom/issues/771))
([2f9ff07](2f9ff07e6c))
* **copilot:** use responses API for subscription reasoning models
([#647](https://github.com/chopratejas/headroom/issues/647))
([84ac332](84ac332d14))
* correct preserved-entry index mapping in Gemini content round-trip
([#836](https://github.com/chopratejas/headroom/issues/836))
([0ffe2b6](0ffe2b6ea4))
* **dashboard:** stable 'Proxy $ Saved' hero tile under --workers &gt; 1
([#481](https://github.com/chopratejas/headroom/issues/481))
([fd73b88](fd73b88368))
* don't inject empty tools:[] when client omitted the tools field
([#772](https://github.com/chopratejas/headroom/issues/772))
([574bbae](574bbae2cb))
* harden Copilot API auth token handling
([#557](https://github.com/chopratejas/headroom/issues/557))
([6b0c09f](6b0c09ffd5))
* **health:** readyz verifies upstream connectivity, not just process
liveness ([#744](https://github.com/chopratejas/headroom/issues/744))
([5dfb446](5dfb446da1))
* **init:** guard persistent task startup
([#616](https://github.com/chopratejas/headroom/issues/616))
([9252d85](9252d852c5))
* **init:** normalize Windows hook paths to forward slashes
([#788](https://github.com/chopratejas/headroom/issues/788))
([6ea6e31](6ea6e31f09))
* **init:** suppress hook recovery output
([#760](https://github.com/chopratejas/headroom/issues/760))
([b439599](b4395993ae))
* **learn:** claude-cli streams output with idle timeout
([#373](https://github.com/chopratejas/headroom/issues/373))
([9bff575](9bff5752bb))
* make headroom wrap readiness probe timeout configurable for slow ML
imports ([#581](https://github.com/chopratejas/headroom/issues/581))
([163677b](163677b405))
* **parser:** detect waste signals in Anthropic tool_result content
blocks ([#815](https://github.com/chopratejas/headroom/issues/815))
([929698a](929698af10))
* **proxy:** F4 — trust X-Forwarded-* only behind allow-listed gateway
([d10bd5f](d10bd5f59c))
* **proxy:** lazy-import server to avoid fastapi crash
([#442](https://github.com/chopratejas/headroom/issues/442))
([93c6937](93c69372e6))
* **proxy:** make CCR multi-worker warning conditional on backend
([#770](https://github.com/chopratejas/headroom/issues/770))
([d76a729](d76a7296df))
* **proxy:** make Kompress eager preload cache-only so a cold cache
can't block startup
([#783](https://github.com/chopratejas/headroom/issues/783))
([841663d](841663da16))
* **proxy:** restore Codex usage headers on WS and streaming SSE
transports ([#577](https://github.com/chopratejas/headroom/issues/577))
([#794](https://github.com/chopratejas/headroom/issues/794))
([0ce68de](0ce68dedd7))
* schema compaction must not drop property names that match DROP_KEYS
([#785](https://github.com/chopratejas/headroom/issues/785))
([ae2122f](ae2122fda8))
* **security:** block DNS-rebinding on /debug/* and /stats/reset via
Host-header allowlist
([#605](https://github.com/chopratejas/headroom/issues/605))
([b4b5025](b4b50253f1))
* **ssl:** upstream httpx client inherits SSL_CERT_FILE,
REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS
([#745](https://github.com/chopratejas/headroom/issues/745))
([e50fbb3](e50fbb3e0d))
* suppress LiteLLM provider banner before import
([#874](https://github.com/chopratejas/headroom/issues/874))
([f9384ef](f9384ef4b7))
* **transforms:** use thread-local tree-sitter parsers to prevent pyo3
Unsendable panic
([#604](https://github.com/chopratejas/headroom/issues/604))
([2ad300a](2ad300aff8))
* **wrap:** track shared proxy clients with markers
([#877](https://github.com/chopratejas/headroom/issues/877))
([05bd56b](05bd56bcb6))


### Code Refactoring

* extract litellm model resolution to shared utility
([ec7d006](ec7d0065cc))
</details>

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-11 22:18:46 -08:00
Logan Kang
c71592d421
feat(memory): add opt-in Apple-GPU (MPS) embedding runtime (#766)
## Description

On Apple-Silicon Macs — especially fanless models like the MacBook Air
(M5) — running the proxy with memory context injection can pin the CPU
while embedding. The embedding work runs an uncapped session on the CPU,
saturating multiple cores, which starves the proxy's asyncio loop and
leads to request timeouts.

This PR adds an **opt-in** runtime that offloads the memory embedder to
the Apple GPU (MPS). Setting `HEADROOM_EMBEDDER_RUNTIME=pytorch_mps`
routes embedding through the torch `sentence-transformers` backend on
MPS instead of the default ONNX CPU embedder, moving the work off the
CPU and keeping the proxy responsive.
The default behavior is unchanged — the feature is strictly opt-in,
env-var only, and falls through to the existing default embedder
selection (with a warning) whenever MPS or the torch dependencies are
unavailable.

Fixes: N/A — no tracking issue (surfaced while running codex auto-review
through
the proxy on a fanless MacBook Air (M5)).

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

- **Runtime selection** (`headroom/proxy/memory_handler.py`): read
`HEADROOM_EMBEDDER_RUNTIME`; when set to `pytorch_mps` **and** MPS is
actually available, route the memory embedder to the torch
`sentence-transformers` backend (Apple GPU).
- If MPS is unavailable or torch/sentence-transformers is not installed,
log a warning and fall through to the existing default embedder
selection (ONNX when available, else the pre-existing local
sentence-transformers fallback) — no crash. The default (env var unset)
is unchanged. Env-var only
- **MPS serialization** (`headroom/memory/adapters/embedders.py`):
`LocalEmbedder` now funnels every `encode()` through a dedicated
single-worker `ThreadPoolExecutor` when the resolved device is MPS.
torch-MPS is not thread-safe, and the existing `run_in_executor(None,
...)` dispatch would otherwise let concurrent proxy requests call MPS
from multiple threads. CPU/CUDA keep the shared default executor
(behavior unchanged). `close()` also drops the cached model so re-use
after close re-initializes cleanly.
- **Packaging** (`pyproject.toml`): new `pytorch-mps` extra (`torch` +
`sentence-transformers`), **platform-gated to macOS** (`; sys_platform
== 'darwin'`) since MPS is Apple-Silicon-only. Deliberately left out of
`[all]` (its deps already arrive via `[ml]`/`[memory]`).
- **Tests** (`tests/test_memory/test_embedder_mps_serialization.py`):
regression coverage for the serialized executor, concurrency safety (no
SIGABRT), CPU-path default behavior, and close/re-use re-initialization.
- **Docs**: `wiki/{configuration,memory,macos-deployment}.md`,
`docs/content/docs/{configuration,installation,memory}.mdx`,
`README.md`, `CHANGELOG.md`.

## 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 (CPU-offload + concurrency profiling on
Apple Silicon)

## Test Output

```
$ pytest -v tests/test_memory/test_embedder_mps_serialization.py
collected 4 items
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor PASSED            [ 25%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_creates_single_worker_executor PASSED  [ 50%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_concurrent_embeds_do_not_crash PASSED  [ 75%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_reembed_after_close_recreates_executor PASSED [100%]
============================== 4 passed in 6.92s ===============================

$ ruff check headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py tests/test_memory/test_embedder_mps_serialization.py
All checks passed!

$ mypy headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py
Success: no issues found in 2 source files

$ pytest -q tests/test_memory/ tests/test_memory_handler_concurrent_init.py tests/test_memory_handler_native_ops.py
553 passed, 1 skipped in 13.51s
```

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

**Why MPS (and not CoreML or a thread cap):** measured on an
Apple-Silicon Mac, the default uncapped CPU embedding session saturates
the cores; the same model on MPS runs at a fraction of the CPU (≈8x
lower sustained CPU utilization in profiling) while producing
**byte-identical embeddings** (cosine distance ≈ 0 across runtimes), so
relevance/ranking is unchanged. A CoreML execution-provider path was
evaluated and rejected: the default optimized ONNX model uses fused ops
that fall back to CPU under CoreML (no offload), and a full-precision
re-export was impractical (very low throughput + multi-GB memory). MPS
via `sentence-transformers` was the only practical GPU offload.

**Why serialization is mandatory:** torch-MPS is not thread-safe —
concurrent encode calls from a multi-worker executor abort with
`-[IOGPUMetalCommandBuffer validate]: failed assertion 'commit an
already committed command buffer'` (reproduced deterministically; a
single-worker executor resolves it).
Under concurrent load the serialized single-GPU-stream throughput meets
or exceeds the parallel CPU path while using a fraction of the cores.

**Scope / boundary:** this targets the Python **memory** embedder, which
is live on the proxy request path (memory context injection). The
Rust-backed SmartCrusher compression path is unaffected and remains
non-configurable from Python by design.

**Safety:** default behavior is unchanged (ONNX, no torch). The feature
is opt-in, env-var only, macOS-gated at the packaging layer, and
degrades gracefully (warn + the existing default embedder selection)
when MPS or the dependencies are unavailable.
2026-06-11 12:59:20 -05:00
Patrick A
2ad300aff8
fix(transforms): use thread-local tree-sitter parsers to prevent pyo3 Unsendable panic (#604)
## Problem

pyo3 marks `_native::Parser` as `#[pyclass(unsendable)]`, which causes a
hard thread-assertion panic when a parser created on one thread is
accessed from another:

```
thread '<unnamed>' panicked at pyo3-0.28.3/src/impl_/pyclass.rs:1055:9:
assertion `left == right` failed: _native::Parser is unsendable, but sent to another thread
  left: ThreadId(2)
 right: ThreadId(1)
```

The prior implementation stored parsers in a module-level `dict[str,
Any]` (`_tree_sitter_languages`). When `_run_compression_in_executor`
dispatched compression work to a `ThreadPoolExecutor`, pool workers
grabbed parsers from that shared dict that were originally created on
the main asyncio thread and panicked.

This produces a 500 on every request where code compression is attempted
via a pool thread.

## Fix

Replace the global dict with `threading.local()` so each thread creates
and owns its own parser instances. No cross-thread parser access is
possible.

```python
# before
_tree_sitter_languages: dict[str, Any] = {}  # shared — crosses threads

# after
_tree_sitter_local = threading.local()  # per-thread — isolated
```

`is_tree_sitter_loaded()` and `unload_tree_sitter()` updated to operate
on the current thread's local cache (semantics unchanged for
single-threaded callers).

## Tests

9 regression tests added in
`tests/test_transforms/test_tree_sitter_thread_safety.py`:

- Thread isolation: two threads get distinct parser instances
- Within-thread reuse: same thread gets the same cached instance
- Thread pool: parsers usable from `ThreadPoolExecutor` workers without
panic
- Concurrent workers: each distinct pool thread owns a unique parser
- `is_tree_sitter_loaded` / `unload_tree_sitter` lifecycle

Also adds a `filterwarnings` entry for
`PytestUnraisableExceptionWarning`: pyo3 emits this when short-lived
test threads drop parsers at teardown; it does not occur in production
where pool threads are long-lived.

## Relation to #564

PR #564 proposes the same `threading.local()` approach but was blocked
on missing tests (`CHANGES_REQUESTED`). This PR includes the full test
suite.
2026-06-10 18:30:00 -05:00
Frank Borkin
19eac8e00d
feat: support Python 3.14+ via pyo3 abi3 stable ABI (#516)
## Description

Sets pyo3 params to support python above 3.13

Fixes #(408

## 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 Cargo.toml

## Testing

Describe the tests you ran to verify your changes:

- [ ] 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

```
Compiles
```

## Checklist

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

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 06:33:16 -08:00
Patrick A
9579567b7d
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What

Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.

## Why

Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:

- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`

## How

Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.

| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |

## Breaking changes

None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-08 22:06:24 -08:00
github-actions[bot]
01762b1ec7
chore: release main (#607)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 11:21:23 -07:00
Patrick A
fa558c5647
fix(deps): move gunicorn to [proxy-prod] extra, add Windows guard (#537)
* fix(deps): add missing runtime deps to [code] and [proxy] extras

- Add gunicorn>=21.0.0 to the [proxy] extra

The proxy docs (docs/content/docs/proxy.mdx and wiki/proxy.md) show
gunicorn as the recommended production deployment server:

  pip install gunicorn
  gunicorn headroom.proxy.server:app --worker-class uvicorn.workers.UvicornWorker

Users installing headroom-ai[proxy] for production get uvicorn (already
declared) but had to discover and install gunicorn manually. Adding it
to [proxy] removes that friction.

Investigation notes:
- [code] only needs tree-sitter-language-pack (already declared).
  code_compressor.py has zero numpy imports. The kompress fallback
  inside code_compressor.py is guarded by ImportError and requires [ml].
- numpy is correctly declared in [relevance] (numpy>=1.24.0) and pulled
  transitively by sentence-transformers in [memory]. It is NOT needed
  under [code].
- tree-sitter is a transitive dep of tree-sitter-language-pack (requires
  tree-sitter>=0.25.2) so it does not need an explicit entry.

* docs(changelog): add entry for gunicorn proxy dep fix

style(tests): ruff format test_provider_proxy_routes.py (blank lines after docstrings)

* fix(deps): move gunicorn to [proxy-prod] extra, add Windows guard

- Remove gunicorn from [proxy] so dev, CI, and Windows users are not
  forced to install a Unix-only package that does nothing on Windows
- Add new [proxy-prod] extra that includes [proxy] + gunicorn with a
  sys_platform != 'win32' environment marker
- Production users: pip install 'headroom-ai[proxy,proxy-prod]'
- Update CHANGELOG to reflect the new extra name

* fix(devcontainer): bump uv floor to >=0.11.0 for lockfile compatibility

uv 0.6.17 (previously pinned) cannot parse lockfiles generated by
uv >= 0.11.x. The validate CI job (triggered by pyproject.toml
changes) was failing with 'Failed to parse uv.lock'. Loosening the
pin to >=0.11.0 picks up the matching format parser while keeping the
Docker layer cacheable with a range rather than an exact pin.

* fix(devcontainer): skip gitpython wheel filename check in uv sync

gitpython 3.1.47 on PyPI has wheel gitpython-3.1.46-py3-none-any.whl
(wrong filename). uv >=0.11.19 strict filename validation rejects this
lockfile entry. UV_SKIP_WHEEL_FILENAME_CHECK=1 bypasses the check until
the upstream lockfile is regenerated with a corrected entry.

* fix(deps): correct gitpython version in uv.lock to match actual wheel

gitpython 3.1.47 on PyPI was uploaded with sdist/wheel files named
gitpython-3.1.46.*. The version field in uv.lock said 3.1.47 but all
download URLs reference 3.1.46 files, causing uv >=0.11.19 to refuse
to parse the lockfile with a version-mismatch error.

Change the version field to 3.1.46 so the entry is internally
consistent. Also revert the now-unnecessary UV_SKIP_WHEEL_FILENAME_CHECK
workaround from post-create.sh.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-07 23:49:14 -07:00
github-actions[bot]
f7c2552264
chore: release main 2026-06-04 14:05:56 +00:00
github-actions[bot]
cc71e07d01
chore: release main 2026-05-26 03:54:25 +00:00
chopratejas
8e4ab187e9 ci(release): align manifest + pyproject + package.json to 0.22.3
The repo had drifted: pyproject.toml said 0.9.1 but PyPI's latest
published headroom-ai was 0.22.3. release_version.py papered over
this by taking max(canonical, latest_tag) at release time;
release-please does NOT do that — it trusts the manifest verbatim.

Left as-is, release-please would propose 0.9.2 on the next merge
and PyPI would reject it ("400 Cannot publish version lower than
latest"), looping the bot forever.

Fix: align every version-bearing file to 0.22.3 (the truth on
PyPI). Done via `scripts/version-sync.py --version 0.22.3`:

- .release-please-manifest.json
- pyproject.toml
- sdk/typescript/package.json
- plugins/openclaw/package.json (+ headroom-ai dep range -> ^0.22.3)
- .claude-plugin/marketplace.json
- .github/plugin/marketplace.json
- plugins/headroom-agent-hooks/.claude-plugin/plugin.json
- plugins/headroom-agent-hooks/.github/plugin/plugin.json

After this lands, the bot's next release PR will propose 0.22.4
(patch) or 0.23.0 (minor) depending on conventional-commit traffic
since v0.22.3.
2026-05-25 18:41:38 -07:00
chopratejas
20dc1f28f3 fix(proxy): Strands MCP bundle + backend path fixes + Codex fail-closed protection
Three logically-related sets of proxy changes ship in this branch:

1. Strands integration on the Bedrock path (HeadroomBundle + 4 OpenAI
   handler fixes + LiteLLM cache stats + dep pin)
2. /stats MCP aggregation (cross-process events log → proxy summary)
3. Codex compression-failure fail-closed (WS + HTTP /v1/responses)

== 1. Strands integration on the Bedrock path ==

* HeadroomBundle (headroom/integrations/strands/bundle.py): single-helper
  MCP wiring for a Strands Agent — Headroom MCP server (headroom_compress
  / headroom_retrieve / headroom_stats) plus optional Serena MCP and
  optional in-process compression hook. Constructor builds unstarted
  MCPClient instances per server; Strands' Agent owns the subprocess
  lifecycle. Default config: MCP enabled, Serena enabled, hook OFF
  (proxy is the single source of truth for compression). User-side
  integration is two lines in any Strands app.

* headroom/proxy/handlers/openai.py — backend path now:
  - calls PrefixCacheTracker.update_from_response (was direct-OpenAI only)
  - intercepts CCR headroom_retrieve tool_calls server-side, mirroring
    the Anthropic handler pattern; NO silent fallback, re-raises on
    CCR errors (per feedback_no_silent_fallbacks)
  - works for both non-streaming and streaming paths

* headroom/proxy/handlers/streaming.py: _stream_openai_via_backend now
  accepts prefix_tracker + optimized_messages, parses cache stats from
  the SSE final-usage frame (cache_creation_input_tokens added to the
  state machine), records CCR retrieve feedback via a new
  _record_ccr_feedback_from_openai_sse helper. Streaming CCR intercept
  is intentionally out of scope (mirrors Anthropic streaming behaviour).

* headroom/backends/litellm.py: send_openai_message response usage block
  now carries cache_read_input_tokens / cache_creation_input_tokens
  (Anthropic/Bedrock dialect) and prompt_tokens_details.cached_tokens
  (OpenAI dialect). Backwards-compatible — cold-start callers see the
  same 3-key shape; cache keys appear only when the underlying provider
  returns them. Pinned by test_no_cache_fields_means_no_cache_keys.

* headroom/proxy/auth_mode.py: ("strands-agents/", "strands") added to
  CLIENT_UA_MAP. Production callers should also set X-Client: strands
  since the default openai-python UA carries no Strands signal.

* pyproject.toml: huggingface-hub>=1.5.0,<2.0 pinned in [ml] so a sibling
  install (e.g. strands-agents) can't drag the version below the floor
  transformers 5.x requires (otherwise Kompress silently goes
  "unavailable").

== 2. /stats MCP aggregation ==

* headroom/proxy/cost.py: _aggregate_mcp_events() reads the cross-process
  shared events file the Headroom MCP server already writes to and
  surfaces summary.mcp with three new keys:
    - compressions       (count of headroom_compress invocations)
    - tokens_removed     (sum of input - output across those)
    - retrievals         (count of headroom_retrieve — the load-bearing
                          over-compression alarm; if it grows linearly
                          with turn count, lossy compressors are
                          dropping info the model actually needs)
  Defensive on every axis — missing MCP SDK, missing file, malformed
  events, read errors — never blocks /stats.

* examples/strands_bundle_demo.py: stats panel prints the new fields so
  the demo shows the full proxy-HTTP + MCP-tool story in one view.

== 3. Codex compression-failure fail-closed protection ==

Reported by Camille (2026-05-21): Codex threads were locking with
"ran out of room in the model's context window" after Headroom's
compression timed out on an oversized response.create frame and
forwarded the original ~1.7 MB frame to the upstream, which then
rejected it. Codex's auto-compact heuristic gates on the upstream-
reported total_usage_tokens (which Headroom had been shrinking on
earlier turns), so its compaction never fired and the thread locked.

Validated against open Codex issues (CLI + Desktop share codex-rs/core):
* #16068 — confirms compaction gates on total_usage_tokens,
  estimated_token_count is computed but only logged
* #19806 — confirms image token estimator unbounded, contributes to
  the same ContextManager.get_total_token_usage → auto-compaction chain

* headroom/proxy/helpers.py: decide_compression_failure_action() with a
  unit-tested decision matrix:
    - asyncio.TimeoutError                              → refuse, always
    - non-timeout failure + frame > 256 KiB (configurable) → refuse
    - non-timeout failure + small frame                 → forward (legacy)
  Operator escape hatches:
    - HEADROOM_WS_FAIL_OPEN_ON_COMPRESSION_FAILURE=1 restores legacy
    - HEADROOM_WS_COMPRESSION_FAIL_THRESHOLD_BYTES tunes the threshold

* headroom/proxy/handlers/openai.py (WS /v1/responses): consults the
  helper after compression failure. On refuse: close client websocket
  code 1009 with "headroom: compression <reason> — please compact
  context and retry" reason; set termination_cause for the outer
  lifecycle finally; return.

* headroom/proxy/handlers/openai.py (HTTP /v1/responses): same helper.
  On refuse: raise HTTPException(413) with a structured error body so
  FastAPI's HTTPException handler emits a clean 413. The existing
  `except HTTPException: raise` guard in this handler already ensures
  the 413 propagates without being swallowed by the 502 catch-all.

Anthropic /v1/messages NOT changed in this branch: no equivalent bug
report on Anthropic-protocol clients, Claude Code (Anthropic-owned)
handles context overflow via its own cache_control/ephemeral
primitives, and Cursor/Aider don't maintain the local-Y estimate the
Codex bug requires. Deferred until a real report lands; the patch is
a one-liner reusing the same helper.

== Tests + verification ==

* tests/test_backends/test_litellm_cache_stats.py — 3 tests pinning
  cache-stat surfacing across Anthropic/OpenAI dialects + backwards-
  compat for no-cache responses.
* tests/test_proxy/test_openai_backend_path.py — 5 tests (Bedrock cache
  fields, OpenAI fallback shape, CCR intercept with provider="openai",
  CCR re-raise on exception, streaming signature contract).
* tests/test_proxy/test_mcp_stats_aggregation.py — 5 tests pinning the
  aggregator across compress+retrieve mixes, empty events, unknown event
  types, missing token fields, and read failures.
* tests/test_proxy/test_compression_failure_action.py — 12 tests pinning
  the fail-closed decision matrix (timeout always refuses, small
  transient passes through, oversize refuses, env override variants,
  custom threshold, invalid threshold falls back, 0/negative ignored).

* examples/strands_bedrock_demo.py — model_id bumped from deprecated
  Claude 3 Haiku to Sonnet 4.5 (the deprecated model now errors on
  account access).
* examples/strands_via_proxy_demo.py — proxy + Bedrock cache + streaming
  smoke test.
* examples/strands_mcp_dispatch_test.py — pure MCP round-trip probe.
* examples/strands_bundle_demo.py — full Strands + HeadroomBundle E2E
  demo (this is the shape a real Strands user copies into their app).

Full pytest: 5327 passed, 178 skipped. The previously-failing
test_core_operations.py::TestAddBatch::test_add_batch_basic passes now
that the huggingface-hub pin in pyproject.toml unblocks transformers
imports.

E2E verified live against AWS Bedrock (Sonnet 4.5):
* cache_write=10,438 on turn A → cache_read=10,438 on turn B
* streaming SSE final usage frame carries cache_read_input_tokens
* 78.7% reduction on a 50 KB JSON tool_result via SmartCrusher (
  dispatched per-content-type by ContentRouter)
* Strands Agent + HeadroomBundle: model autonomously called
  headroom_compress + headroom_retrieve via MCP; CompressionStore
  round-trip succeeded; final answer correct.
2026-05-21 11:00:14 -07:00
chopratejas
c1d2eec588 docs: improve discoverability for AI agents and search crawlers
Several signals AI agents and search engines use to discover and
install a project were misaligned or missing:

* ``docs/app/layout.tsx`` set ``metadataBase`` to
  ``https://chopratejas.github.io/headroom/`` while the live docs run
  on Vercel — every page's ``og:url`` and ``twitter:url`` resolved to
  a URL that returns 404 for ``/llms.txt``. Now points at the live
  Vercel host (overridable via ``NEXT_PUBLIC_SITE_URL`` for a future
  custom domain). Adds explicit ``openGraph`` and ``twitter`` metadata
  so social shares render a card with the project's pitch.
* No ``llms.txt`` at the GitHub repo root. AI agents crawling
  ``github.com/chopratejas/headroom/`` saw only the README. The new
  ``llms.txt`` follows the llmstxt.org convention: 1-line pitch,
  canonical docs links, copy-paste install commands (pip / npm /
  Docker / proxy / ``headroom wrap``), and entry points for the
  library, proxy, MCP server, and SDK integrations. Points at the
  Fumadocs-generated ``/llms.txt`` and ``/llms-full.txt`` for the
  full picture.
* ``pyproject.toml`` ``Documentation`` URL pointed at the GitHub
  README anchor. Updated to point at the docs site so PyPI visitors
  land on searchable docs, and adds an ``AI / LLM Index`` URL
  pointing at the Fumadocs ``/llms.txt``.
* No explicit AI-bot allow list. Added ``docs/app/robots.ts`` (Next
  13+ App Router convention) with explicit allows for GPTBot,
  ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot,
  ChatGPT-User, Cohere-AI, CCBot, and Applebot-Extended. Wildcard
  allow as the catch-all. Advertises the sitemap.
* No ``sitemap.xml`` route. Added ``docs/app/sitemap.ts`` that pulls
  every Fumadocs page out of ``source`` (same source backing
  ``/llms.txt``, search, and OG images) so search and AI crawlers
  can enumerate doc pages without scraping HTML.
* README didn't tell AI agents where to look. Added a 2-line
  pointer near the top nav row: read ``/llms.txt`` here, or fetch
  the live index / full docs blob.

Also tightened the GitHub repo description and added five topics
(``claude-code``, ``cursor``, ``tokens``, ``prompt-engineering``,
``typescript``) via ``gh repo edit`` — that's already live on the
repo, not part of this commit.

No Python or Rust code changes; ``make ci-precheck`` was run to
confirm the test slice still passes.
2026-05-13 17:36:06 -07:00
JerrettDavis
2fd48260f6 ci: skip live evals without credentials 2026-05-11 13:35:24 -05:00
Tejas Chopra
9ff28e9803
Merge pull request #422 from chopratejas/fix-user-experience-and-feature-clarity
fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness
2026-05-07 16:46:55 -07:00
chopratejas
265554d4ad fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness
Address user-reported UX gaps across the CLI surface:

- code-aware: add --code-aware/--no-code-aware (+ HEADROOM_CODE_AWARE_ENABLED env)
  to the Click CLI. PR #411 had added these only to the orphaned argparse main;
  the user-facing CLI couldn't reach the flag. Banner status text "remove
  --no-code-aware to enable" referenced a flag that didn't exist — fix to point
  at the actual flag/env. Surface code-aware in the click banner and add
  print_banner=False plumbing to run_server so the click path doesn't print
  two banners back-to-back.

- --mode: hide alias clutter via metavar=[token|cache] and rewrite help to
  lead with the two real modes. Legacy aliases (token_mode/token_savings/...)
  still validate.

- perf --hours: was documented but ignored. Records are now actually filtered,
  the report shows the actual time-range covered, and the count of records
  filtered out (so users can tell when raising --hours helps).

- perf TOIN: replace the hash-keyed pattern dump with a strategy-distribution
  view + recommendation-eligibility from the live store — actionable signal
  rather than opaque rows.

- code-graph: clarify in --help that it indexes cwd / project root.

- wrap: spell out supported tools, wrap-vs-proxy distinction, and that
  `headroom wrap opencode` isn't a thing (use `proxy` directly for opencode;
  openclaw is not opencode).

- mcp: note that mcp__headroom__headroom_retrieve is correct MCP namespacing,
  not a doubled-prefix bug. Renaming would break the proxy's tool injection.

- LLMLingua cleanup: remove [llmlingua] extra from pyproject (no live code
  uses it). Delete wiki/llmlingua.md and clean retired flag/class references
  in 6 other wiki pages. Point at [ml] (Kompress) where ML compression is
  documented.

- init -g openclaw: strip mcpServers from existing plugin entries before
  re-writing — newer openclaw schemas reject it, leaving stale entries from
  older installs unhealable. Pinned with regression test.

Tests: mock_run_server signatures in two existing tests accept **kwargs
(needed for the new print_banner plumbing). New test for the openclaw
mcpServers strip. Full suite: 4847 passed, 262 skipped, 0 failed.
2026-05-07 16:43:35 -07:00
chopratejas
183d51c8a8 fix(ci): include NOTICE in sdist + assert License-File metadata matches tarball
Every release since v0.20.16 has uploaded 12 wheels but no sdist. The
underlying failure is a 400 from PyPI:

    400 License-File NOTICE does not exist in distribution file
    headroom_ai-X.Y.Z.tar.gz at headroom_ai-X.Y.Z/NOTICE

Two-part regression:

1. The hatch -> maturin migration in 2a91cbb (single-wheel maturin build
   backend, May 4) replaced `[tool.hatch.build.targets.sdist].include`,
   which listed both `LICENSE` and `NOTICE`, with maturin's own include
   directive that only carried `LICENSE` over. Maturin's PEP 639 license
   auto-discovery still emits `License-File: NOTICE` into the sdist's
   PKG-INFO (because NOTICE exists at the project root and matches the
   default glob), so the sdist tarball declares a license file it
   doesn't physically contain. PyPI's PEP 639 validator rejects with
   400. Wheels were unaffected because maturin auto-injects both files
   into `*.dist-info/licenses/`.

2. CI showed "publish-pypi" green for ~22 releases despite this break
   because twine was bailing earlier with `400 File already exists` on
   the wheels (the version detector kept computing the same v0.21.5).
   PR #412 added `skip-existing: true` (May 6) to make wheel re-uploads
   idempotent. With wheels now silently skipping, twine proceeded to
   upload the sdist for the first time in three weeks - and the
   dormant License-File error surfaced as a hard 400.

Fix:

- Add `NOTICE` alongside `LICENSE` in `[tool.maturin].include` for the
  `sdist` format. Both files now ship in the tarball, matching what
  PEP 639 already declares in PKG-INFO.
- Replace the existing "verify sdist contains LICENSE" check with a
  generic "every License-File entry in PKG-INFO resolves to a real
  tarball member" check. This catches the same bug class for any
  future addition (COPYING, AUTHORS, etc.) without another bespoke
  literal.

Verified locally:

    $ maturin sdist --out dist
    Including license file `LICENSE`
    Including license file `NOTICE`
    Including files matching "LICENSE"
    Including files matching "NOTICE"
    Built source distribution to dist/headroom_ai-0.9.1.tar.gz

    $ tar -tzf dist/headroom_ai-0.9.1.tar.gz | grep -E '(LICENSE|NOTICE)$'
    headroom_ai-0.9.1/LICENSE
    headroom_ai-0.9.1/NOTICE

    $ twine check dist/headroom_ai-0.9.1.tar.gz
    Checking dist/headroom_ai-0.9.1.tar.gz: PASSED
2026-05-07 16:29:05 -07:00
Daniel Munoz
9492386398 fix: harden release gating and clarify pipx compatibility 2026-05-06 12:41:17 +02:00
chopratejas
b154e17853 fix: PR #372 — restore [image] extra on Python 3.13 via rapidocr 3.x adapter
Root cause: `headroom-ai[all]==0.20.16` fails to install on Python 3.13
because `rapidocr-onnxruntime` 1.4.0–1.4.4 wheels declare
`requires-python: <3.13,>=3.6`. After 1.4.x the rapidocr ecosystem
split: `rapidocr-onnxruntime` (bundled-ORT, capped at <3.13) vs
`rapidocr` 3.x (engine-agnostic core, supports 3.13+, returns
RapidOCROutput dataclass instead of v1's tuple).

Fix:

1. pyproject.toml — environment-marker hybrid in [image]:
   - rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13'
   - rapidocr>=3.0,<4; python_version>='3.13'
   - onnxruntime>=1.7,<2; python_version>='3.13'
   ORT remains the engine on every Python version; bundle and speed
   unchanged, just split into two packages on 3.13+.

2. headroom/image/compressor.py — runtime adapter:
   _resolve_rapidocr() tries v1 first, falls back to v3 when v1 is
   missing, returns (None, None) when neither installed. Cached at
   module scope. Detection at runtime (not Python-version-based) so
   users can install either package on any Python version.

   _ocr_extract branches on resolved api_version:
   - v1: (list[(box, text, score)], elapsed) tuple — unchanged
   - v3: RapidOCROutput dataclass with .txts / .scores / .boxes
     attrs (each may be None when nothing detected)

   Defensive None-handling, length-mismatch detection, structured
   log events for both branches.

Smoke test (real install verified before commit):
    pip install rapidocr onnxruntime pillow
    → result type: RapidOCROutput
    → fields: txts (None when empty), scores (None when empty), boxes
    Confirms the v3 None-coercion is necessary.

Tests: 11 new unit tests in tests/test_image_ocr_api_compat.py covering:
- Resolver: v1 preferred, v3 fallback, both missing
- v1 path: tuple parses, low-confidence None, empty result None
- v3 path: dataclass parses, low-confidence None, None attrs handled,
  mismatched lengths logged + None
- Backend missing: returns None gracefully

All 11 pass; `make ci-precheck` PASSED.

Closes #372.
2026-05-04 08:20:01 -07:00
chopratejas
e325d1b866 fix(ci): pin public PyPI in pyproject.toml + scrub Netflix URLs from uv.lock
Devcontainer validate jobs were failing on PR #360 with:

    × Failed to fetch:
      https://pypi.netflix.net/packages/.../nvidia_nvshmem_cu12-3.4.5-...whl
    ├─▶ Request failed after 3 retries
    ╰─▶ operation timed out

`pypi.netflix.net` is Netflix's internal PyPI mirror. It got into the
lockfile because my local `~/.config/uv/uv.toml` had:

    index-url = "https://pypi.netflix.net/simple"

Running `uv lock` from that machine baked Netflix-internal URLs into
uv.lock for every package. Public CI runners (and any external
contributor) can't resolve them.

Two fixes:

1. Add `[[tool.uv.index]]` block to pyproject.toml pinning public PyPI
   as the project's default index. uv now ignores user-level config when
   resolving for this project, regardless of who runs `uv lock`. This
   prevents the same contamination from any developer in the future.

2. Regenerate uv.lock against public PyPI. All package URLs now point
   at `https://files.pythonhosted.org/...` and `https://pypi.org/simple/`.
   Zero references to `pypi.netflix.net` remain in the lockfile.

Verified: `grep -c "pypi.netflix" uv.lock` returns 0.
2026-05-03 13:52:16 -07:00
chopratejas
2a91cbb4b4 refactor: single-wheel maturin build backend (fixes #355)
Eliminates the dual-package architecture that was the root cause of #355.
`pip install headroom-ai` now produces ONE wheel containing both the Python
source (headroom/*.py) and the compiled Rust extension (headroom/_core.so).
No more separate `headroom-core-py` package, no more chicken-and-egg with
PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action
plumbing in CI.

This is the canonical pattern used by cryptography, polars, ruff,
pydantic-core, and other Rust-as-core Python packages. Honors the
"Rust as core engine" direction.

## What changed

- pyproject.toml: `[build-system]` swapped from hatchling to maturin.
  `[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at
  `crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."`
  picks up the root `headroom/` package directly (dashboard HTML
  templates and other non-Python files included automatically).
- crates/headroom-py/pyproject.toml: deleted. The crate is no longer a
  separate published package; its Cargo.toml stays as the cdylib build
  target invoked via `[tool.maturin] manifest-path`.
- crates/headroom-py/python/: deleted (placeholder layout for the old
  separate package).

## CI updates

- ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust
  toolchain set up before `pip install -e .` (which now invokes maturin
  via build-system). Removed the "build wheel + symlink .so" dance.
  `build` job swapped from `python -m build` (hatch) to
  `maturin build` + `maturin sdist`.
- release.yml: collapsed dual-package matrix into one. New `build-wheels`
  matrix produces cross-platform wheels for cp310/11/12/13 ×
  {linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New
  `collect-dist` aggregator merges artifacts. publish-pypi consumes the
  merged dist.
- init-native-e2e.yml: dropped windows-latest from the matrix —
  upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting
  MSVC C runtime libraries, so the Rust extension cannot build for
  win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS.
- headroom-e2e-setup: composite action now sets up Rust toolchain +
  Swatinem/rust-cache before `pip install -e .[proxy]`.
- eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before
  install. rust.yml's wheels job builds from root pyproject.toml (no
  more `-m crates/headroom-py/Cargo.toml`).
- e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in
  the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the
  install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false`
  from wrap-e2e — the image now ships the full Rust core.
- Dockerfile (main): simplified — no more Layer 2/3 dance with
  `headroom-core-py` install + symlink. Single `uv pip install` builds
  + installs everything.
- .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin
  added so `uv sync` builds the extension inside the devcontainer.

## Lockfile + script

- uv.lock: regenerated. No `headroom-core-py` entries remain.
- scripts/build_rust_extension.sh: simplified from a symlink-into-tree
  workaround to a thin wrapper around `pip install -e .`. The maturin
  build-backend handles placement automatically.

## Local validation (all green on macOS aarch64)

1. Clean venv `pip install -e .` → `from headroom._core import …` works.
2. `maturin build --release` → 13.8 MB wheel, 336 files including
   `headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and
   `headroom/dashboard/templates/dashboard.html`.
3. `pip install <wheel>` in fresh venv → import works.
4. Wheel contents verified via `unzip -l`.
5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed.
6. `pytest tests/test_relevance.py` — 30 passed.
7. `cargo build --workspace` + `cargo test --workspace` — all green.
8. `make ci-precheck` — 176 Python tests + Rust + commitlint green.

## Migration notes

Users on `pip install headroom-ai` get the Rust core automatically
(linux + macos wheels). sdist installs require rust toolchain available
locally — pip will build via maturin.

Closes #355
Supersedes #357 (workarounds-based fix abandoned in favor of
architectural fix)
2026-05-03 13:16:41 -07:00
chopratejas
dcbc921d63 fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:

  1. test_anthropic_cache_hit_across_two_turns          — A2/A6/E
       Identical cache_control'd system+messages on two turns must
       eventually produce cache_read_input_tokens > 0. Guards the cache
       hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
       Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
       eventually-consistent prompt-cache write latency without masking
       a real "proxy broke cache stability" regression.

  2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
       Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
       cache_read on turn 2 must still be > 0 AND the proxy must emit
       compression headers — proving the live-zone block dispatcher
       ran on the new tail without disturbing the cached prefix.

  3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
       Wraps proxy._retry_request to snapshot the upstream-bound body
       and assert cache_control on system blocks survives verbatim,
       and user content is not flattened from list to string form.

  4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
       Three-turn conversation through /v1/chat/completions; each
       turn returns valid content, prior assistant turns survive in
       the messages list (proxy doesn't drop them).

  5. test_openai_streaming_sse_chunks_arrive_in_order   — A8 (SSE wire)
       Streams /v1/chat/completions; asserts each event is
       'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
       reassembled content non-empty, no malformed events.

  6. test_gemini_multi_turn_through_proxy               — Gemini reach
       Two-turn conversation through native
       /v1beta/models/{model}:generateContent. Proves Gemini handler
       wiring stayed intact through the megamerge.

  7. test_ccr_marker_round_trip_live                    — B7 (CCR)
       Pre-populates compression_store with a fixture entry, embeds
       a CCR marker on a tool_result, verifies (a) headroom_retrieve
       tool is injected into the upstream tools array (PR-B7
       always-on), and (b) /v1/retrieve returns the original bytes
       by hash with all rows intact. Pre-populating the Python store
       (vs. driving SmartCrusher's internal Rust store) matches the
       established pattern in tests/test_proxy_ccr.py and exercises
       the surface served by /v1/retrieve.

  8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
       Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
       seeds LocalBackend, captures upstream-bound body. Asserts:
       (a) system prompt byte-identical to input; (b) memory text
       lands on latest user message tail; (c) earlier messages
       untouched. Guards the live-zone-only injection contract.

  9. test_classify_auth_mode_routes_payg_vs_oauth       — Phase F-prep / B5
       NOT a live API call. Sends three header shapes through the
       proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
       sk-ant-api03-...), captures dispatcher headers via a wrap on
       _retry_request, and asserts the canonical auth-mode classifier
       maps each correctly. Codifies the Phase F contract.

Conventions:

  * file-level pytestmark = pytest.mark.live → excluded by default
    via 'pytest -m "not live"'. Adds a 'live' marker registration in
    pyproject.toml's [tool.pytest.ini_options].markers.
  * each test skipif's on the relevant API key — no silent fallbacks,
    no real-API runs against fake keys.
  * uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
    rather than re-implementing env loading.
  * model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
    (no hardcodes); Anthropic primary/fallback resolves at runtime per
    key entitlement.
  * assertions are direction-only (cache_read > 0, tokens_after <=
    tokens_before) — never tied to upstream pricing/tokenizer drift.
  * shared module-scoped TestClient fixture for performance; CCR and
    memory tests build dedicated proxies for their config-specific paths.

Verification:
  * pytest tests/test_realignment_live_multi_turn.py -v
      → 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
  * pytest -m "not live" --tb=short -q
      → 4694 passed, 265 skipped, 9 deselected — same baseline as today
  * make ci-precheck → green (rust + python + commitlint)

Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:39:19 -07:00
chopratejas
fb9be139a5 feat(python): switch relevance scorer to fastembed (BAAI/bge-small-en-v1.5)
Replace `sentence-transformers` (PyTorch-backed) with `fastembed` (ONNX-
backed) so Python and Rust call into the same library + same model for
relevance scoring. Both sides run BAAI/bge-small-en-v1.5 (33M params,
384 dims, ~30 MB int8-quantized ONNX) auto-downloaded from HF Hub.

Cross-language verification on ('authentication failed for user',
'login error'): Python=0.7505, Rust=0.7507, delta ~0.0002 — well below
the relevance_threshold (0.3) buffer SmartCrusher uses for keep/drop
decisions, so the two implementations agree on every observable
SmartCrusher output. (True byte-equal would require both to load the
identical ONNX weights file — Rust's `fastembed` crate and Python's
`fastembed` package can pick different upstream artifacts; deferred.)

Why fastembed:
- removes torch from the relevance/ path (Phase 6: drop torch from
  Python).
- ~2-3x faster than sentence-transformers' all-MiniLM-L6-v2 for the
  same input shape.
- bge-small-en-v1.5 outranks all-MiniLM-L6-v2 on MTEB by ~6 points.
- self-contained: no longer reads ML_MODEL_DEFAULTS.sentence_transformer
  from utils config.
2026-04-26 23:36:22 -07:00
chopratejas
7ed2b0ba34 Sync plugins to 0.9.2, pyproject canonical at 0.9.1 [skip ci] 2026-04-21 20:36:51 -07:00
JerrettDavis
56f6307665 fix: support py310 version sync scripts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:42:56 -05:00
chopratejas
7cd67ef06d fix: bundle ast-grep/difftastic/scc + generic tool_result interceptor framework
What this does, in plain terms:

Headroom's proxy now ships with three CLI tools (ast-grep, difftastic,
scc) that it can use to shrink tool_result payloads before they reach
the model. The goal is simple: when Claude Code (or Codex, Aider, etc.)
asks the model to reason about a big file or diff, we swap the verbose
output for a compact, same-meaning version. Fewer tokens per turn, same
answers, lower bill.

Today a single interceptor is wired: ast-grep on Read. When an agent
reads a large code file, the proxy replaces the file body with an
outline of its top-level functions/classes plus docstrings. In live
tests that cut prompt tokens 74–76% on both OpenAI and Anthropic,
same answer either way.

How it works:
- `pip install headroom-ai` now installs ast-grep via a PyPI wheel
  (core dep). difftastic and scc are fetched once at proxy startup
  from pinned upstream GitHub releases and cached per-user.
- A generic registry (`headroom/proxy/interceptors/`) lets us add more
  tool-aware rewrites in one file each: declare `matches()` and
  `transform()`, call `register()`, done. No proxy or metrics plumbing
  per tool.
- Safety rails built in: pass-through when a Read specifies a line
  range; second Read of the same file in a conversation returns full
  content (progressive disclosure); any failing interceptor logs and
  skips, never crashes a request.

Opt-in for now:
- Off by default while this ships. Turn on with
  `headroom proxy --intercept-tool-results` or
  `HEADROOM_INTERCEPT_ENABLED=1`, so we can measure before flipping
  defaults.

What users see after turning it on:
- First `headroom wrap claude` boot is ~5s longer (binaries fetched).
  Every subsequent run is cache-only.
- Existing `transforms_applied` field in metrics gets entries like
  `interceptor:ast-grep`, so savings show up in current dashboards
  and HTML reports with no UI change.

Other housekeeping in this PR:
- uv.lock moved to .gitignore — regenerated locally per environment.
- 35 unit + integration tests, ruff + mypy clean.
- Dead-code audit done: removed `binaries.run()`, `needs_filesystem`
  plumbing, unused `_kind` tuple elements, unused `tool_output`
  parameter, and the never-set HEADROOM_SKIP_TOOLS_BOOTSTRAP env.
2026-04-20 17:36:44 -07:00
Adryan Eka Vandra
b71b659e1b
fix(ci): restore repro harness test in dev installs
Add websockets to the dev extra so the repro harness smoke test can import
its websocket client dependency in the CI test matrix. Also apply ruff
formatting to the files the formatter check was rejecting so the 3.12 lint
job passes.
2026-04-20 22:12:01 +07:00
chopratejas
3373dabce0 chore(proxy): add third-party extension point
Adds `headroom/proxy/extensions.py` — a generic entry-point hook under
the `headroom.proxy_extension` group. External packages register an
`install(app, config)` callable that runs once at proxy startup and is
free to add ASGI middleware, routes, mutate config, or raise to
fail-closed (e.g., license check failure aborts startup).

Why: Headroom OSS keeps a deliberately minimal surface, but several
high-value capabilities — PII redaction + tool-call vaulting, data-
residency routing, multi-tenant RBAC, compliance audit, vision/voice
privacy — only make sense as separately-distributed packages. A small,
stable plugin contract lets those live outside this repo without
requiring the OSS to know about them.

Contract:

    [project.entry-points."headroom.proxy_extension"]
    my_extension = "my_pkg.extension:install"

    def install(app: FastAPI, config: ProxyConfig) -> None: ...

An extension that raises from install() is a deliberate fail-closed
signal and aborts startup. Entry-point load failures are logged and
skipped so one broken third-party package cannot take the proxy down.

Changes:
  * New: headroom/proxy/extensions.py (~55 LOC)
  * headroom/proxy/server.py: invoke install_all(app, config) in
    create_app() immediately after CORS middleware registration
  * pyproject.toml: register slow and real_llm pytest markers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:01:27 -07:00
chopratejas
e306d6b338 Bump version to 0.5.25 for PyPI release
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:32:13 -07:00
chopratejas
b3a73641a0 Bump to 0.5.24 2026-04-12 09:17:49 -07:00
chopratejas
af3cd7f07f Fix #149: memory crash in Docker — missing vector index + pthread error
Three fixes for Docker-native install with --memory:

1. Add sqlite-vec to proxy extras — memory's vector index now installs
   with pip install headroom-ai[proxy]. No separate pip install needed.

2. Fix AUTO vector backend fallback — was: SQLITE_VEC → HNSW → crash.
   Now: SQLITE_VEC → HNSW → clear error message listing install options.

3. Fix ONNX pthread_setaffinity_np error in Docker containers — set
   intra/inter thread count to 1 in SessionOptions. Prevents the
   "Invalid argument" error on containers with limited CPU affinity.
2026-04-12 09:17:27 -07:00
chopratejas
ccc763a7b0 Bump version to 0.5.23 2026-04-11 19:45:08 -07:00
chopratejas
00a6ab2dee Bump version to 0.5.22 2026-04-11 19:32:53 -07:00
chopratejas
3c0c540e1b Auto-download codebase-memory-mcp binary, add watchdog to proxy deps
No manual install needed. headroom wrap claude --code-graph:
1. Checks if binary exists (PATH or ~/.local/bin/)
2. Downloads from GitHub releases if not found (~15MB, one-time)
3. Indexes project + starts file watcher

Same pattern as rtk auto-download. Platform-aware:
darwin-arm64, darwin-amd64, linux-arm64, linux-amd64, windows-amd64.

Also: add watchdog>=4.0.0 to proxy extras.
2026-04-11 19:30:38 -07:00
chopratejas
1fee4ac8fd Revert version to 0.5.21, fix changelog header to 0.5.22 2026-04-11 11:05:10 -07:00
chopratejas
0f7f7b0f69 Release v0.6.0: cross-agent memory, plugin learn, code graph 2026-04-11 10:44:27 -07:00
JerrettDavis
a1beb08d53 feat: add reproducible devcontainers
Add a default devcontainer and a compose-backed memory-stack profile, validate them in CI, and document the contributor workflow.

Also lock the memory-stack dependencies, pin related container tooling, and sync the latest healthcheck shutdown fix for stubbed memory handlers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 12:58:35 -05:00
JerrettDavis
ebd482c0a0 feat: add OTEL observability core
Add a shared observability layer for OTEL metrics and Langfuse tracing.
Instrument the shared compression pipeline once so SDK and proxy paths can
reuse the same metrics and spans without scattering telemetry concerns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 21:20:34 -05:00
chopratejas
65e4a35546 Add OCR routing via RapidOCR + measure actual tokens after compression
Transcode technique now runs RapidOCR to extract text from images.
Falls back to full_low if OCR confidence < 70% or no text detected.

Token counting is now done AFTER compression by measuring the actual
output — no hardcoded estimates. OCR text counted by char length,
resized images re-estimated from new dimensions.

RapidOCR added to headroom-ai[image] extra (~15MB ONNX models, ~180ms CPU).
4 new OCR tests (extraction, blank image, confidence threshold, full pipeline).
29 total image compression tests passing.
2026-04-09 16:22:18 -07:00
chopratejas
c52b0f1e57 Add sentencepiece to image extra — required by SigLIP tokenizer (#103)
SiglipTokenizer from transformers hard-requires sentencepiece at runtime.
Without it, the image router silently falls back to PRESERVE (no compression).
Users on pip install headroom-ai[image] or [all] were missing this dependency.
2026-04-08 11:33:28 -07:00
chopratejas
9c51f63c48 Bump to v0.5.21 2026-04-08 10:31:32 -07:00