Commit graph

5 commits

Author SHA1 Message Date
JD Davis
3077ac81e8
feat: add deterministic runtime rollout controls (#1490)
## Description

Establish one centrally resolved, observable, deterministic, versioned
runtime rollout-control mechanism for Headroom. Runtime rollout controls
which behaviors an already-built artifact may expose; it does not select
or qualify a Headroom release/version.

## Type of Change

- [x] New feature (non-breaking change that adds functionality)
- [x] Bug fix (non-breaking change that fixes rollout enforcement
regressions)
- [x] Documentation update
- [x] Code refactoring (no functional changes)

## Changes Made

- Added `RolloutChannel`, `HEADROOM_ROLLOUT_CHANNEL`,
`--rollout-channel`, and a versioned immutable `RolloutSnapshot` shared
by Python configuration boundaries.
- Added schema/policy versions, canonical registry and snapshot SHA-256
identities, per-feature decision reasons, disable precedence, unsafe
qualification poisoning, strict CLI validation, and fail-closed
environment handling.
- Added `headroom rollout status --json`, Python `/stats.rollout`, and
Rust `/rollout/status` runtime provenance.
- Added equivalent Rust snapshot semantics and shared Python/Rust policy
vectors while retaining language-specific feature registries.
- Enforced rollout policy at alternate Python server composition roots
so `HEADROOM_READ_MATURATION=1` cannot bypass its beta gate.
- Preserved typed rollout snapshots across multi-worker serialization
with schema, policy, registry, snapshot-digest, type, and feature-name
validation.
- Made loopback runtime output-shaper updates replace the immutable
snapshot atomically for request readers, retain explicit request/disable
provenance, preserve channel and kill-switch precedence, invalidate
cached stats, and return the effective rollout decision.
- Made `headroom learn --verbosity --apply` report a channel-blocked
update instead of claiming the shaper is live.
- Made explicit CLI feature flags fail loudly when their current channel
blocks them.
- Made persistent interceptor installation select canary automatically,
or reject an explicitly insufficient channel unless the break-glass
override is set.
- Updated architecture, proxy, rollout, learn, and output-shaper
documentation with required channels and hot-reload semantics.

## Testing

- [x] Unit tests pass
- [x] Linting passes (`ruff check .` and `ruff format --check .`)
- [x] Type checking passes (`mypy headroom --ignore-missing-imports`)
- [x] New regression tests added for every corrected behavior
- [x] Rust tests and production-target Clippy pass
- [x] Documentation build passes

### Test Output

```text
Focused rollout coverage suite
57 passed; headroom.rollout + rollout CLI: 98% coverage

Affected proxy/rollout/transform/governance suites
222 passed; 0 failed

Final changed regression suites
100 passed; 0 failed

Cross-module hot-reload isolation regression
6 passed; 0 failed

cargo test -p headroom-core -p headroom-proxy --quiet
headroom-core: 924 passed; 1 ignored
headroom-proxy and integration suites: all passed

cargo clippy -p headroom-core -p headroom-proxy --lib --bins -- -D warnings
cargo fmt --all -- --check
ruff check .
ruff format --check .
mypy headroom --ignore-missing-imports
git diff --check
All passed

cd docs && npm run build
Compiled successfully; 164 static pages generated
```

The unsharded Windows-only CI selection exposed unrelated baseline
failures, principally the existing `sqlite:///C:\\...` URL parser
producing an invalid `\\C:\\...` path. At commit `8e793a80`, all 52
completed GitHub checks passed; the only other conclusions are expected
skips and superseded governance jobs.

## Real Behavior Proof

- **Environment:** Windows checkout on Python 3.13.3 and the current
Rust workspace, based on upstream `main` at `93f2d7a2`.
- **Exact command / steps:** Exercised canary and beta feature requests
through CLI status, Python `/stats.rollout`, Rust `/rollout/status`,
multi-worker payload round trips, loopback `/admin/runtime-env`, real
proxy request shaping before/after hot reload, installer manifest
generation, and shared Python/Rust policy vectors.
- **Observed result:** Stable blocks unstable requests; disable wins
over explicit/default/legacy/unsafe paths; unsafe state reports
`qualification_eligible=false`; worker handoff rejects tampering;
running output shaping changes only when the effective beta policy
permits it; explicit blocked flags fail with actionable diagnostics.
- **Not tested:** Live production traffic requiring provider
credentials, or future artifact qualification/promotion automation
(intentionally out of scope).

## Runtime Rollout Safety

- **Rollout-managed features:** Python `tool_result_interceptors`,
`proxy_output_shaper`, `read_maturation`; Rust `native_bedrock`,
`openai_responses_streaming`, `canary_probe`.
- **Minimum rollout channel:** Registry-defined per feature; process
default is `stable`.
- **Stable/default behavior changed:** No unstable feature becomes
enabled by default. Explicit blocked CLI flags now fail instead of
silently doing nothing.
- **Kill switch / disable path:**
`HEADROOM_DISABLE_FEATURES=<comma-separated feature names>`; explicit
disable has highest precedence, including over the unsafe override.
- **Unsafe override required:** No.
`HEADROOM_UNSAFE_ALLOW_UNSTABLE_FEATURES=1` is break-glass only and
makes qualification evidence ineligible.
- **Qualification impact:** Adds machine-readable policy/snapshot
identities and eligibility; does not implement qualification itself.
- **Rollback path:** Set the named disable list for operational
rollback, lower the channel, or revert this PR.

## Review Readiness

- [x] I have performed a full diff 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 hard-to-understand areas
- [x] I have made corresponding documentation changes
- [x] My changes generate no new warnings
- [x] I added tests that reproduce and prevent every regression fixed
during review
- [x] New and existing affected tests pass locally
- [x] I did **not** edit `CHANGELOG.md`; release-please generates it
from the Conventional Commit PR title

## Additional Notes

Out of scope: artifact candidates, benchmark orchestration,
qualification manifests/gates, promotion automation, release branches,
publication guards, and release-risk classification. Those workflows can
consume the rollout registry digest, runtime snapshot digest, decision
reasons, and qualification eligibility through supported black-box
interfaces.

---------

Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
Co-authored-by: JD Davis <jd@JDH-AIR-00.local>
2026-08-12 23:16:54 -05:00
Rod Boev
38479fcda1
fix(codex): rerun memory lookup on every response.create WS frame (#2113)
## Description

Long-lived Codex `/v1/responses` WebSocket sessions only ran memory
decision, query construction, and context injection on the first
`response.create` frame because `handle_openai_responses_ws` kept that
logic outside the relay loop. This change extracts that path into a
local helper reused before compression for every eligible
`response.create`, while keeping sticky memory tools deduplicated by the
existing session helper.

Closes #2059

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

- Extracted the first-frame memory preparation path into a local async
helper inside `handle_openai_responses_ws`.
- Reused that helper for the initial frame and every later eligible
`response.create` before compression and shaping.
- Added focused two-turn WebSocket regressions for per-frame lookup,
bypass and disabled-memory handling, list-shaped later inputs,
memory-handler fail-open recovery, and sticky-tool replay.
- Raised the locked production floors for `click` and `pillow` to clear
the current `pip-audit` findings that now fail external PR merge
snapshots.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_codex_ws_per_frame_memory.py
tests/test_openai_codex_ws_lifecycle.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/proxy/handlers/openai.py
tests/test_codex_ws_per_frame_memory.py
tests/test_openai_codex_ws_lifecycle.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_codex_ws_per_frame_memory.py tests/test_openai_codex_ws_lifecycle.py -q

============================= test session starts =============================
platform win32 -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0
rootdir: D:\Repos\headroom-pr-2059-codex-ws-memory-lookup
configfile: pyproject.toml
plugins: anyio-4.12.1, langsmith-0.9.3, asyncio-1.3.0, cov-7.0.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 35 items

tests\test_codex_ws_per_frame_memory.py .........                        [ 25%]
tests\test_openai_codex_ws_lifecycle.py ..........................       [100%]

============================= 35 passed in 1.89s ==============================

uv run ruff check headroom/proxy/handlers/openai.py tests/test_codex_ws_per_frame_memory.py tests/test_openai_codex_ws_lifecycle.py
All checks passed!

uv run ruff format headroom/proxy/handlers/openai.py tests/test_codex_ws_per_frame_memory.py tests/test_openai_codex_ws_lifecycle.py --check
3 files already formatted
```

## Real Behavior Proof

- Environment: Windows, Python 3.12.13, in-process WebSocket harness
with a fake memory handler
- Exact command / steps: `uv run pytest
tests/test_codex_ws_per_frame_memory.py
tests/test_openai_codex_ws_lifecycle.py -q`, which opens one connection,
sends distinct `response.create` frames, and records each memory lookup
plus the forwarded tool set
- Observed result: the handler performs one lookup per eligible frame,
later-frame compression sees current-turn memory-prepared input, and
both forwarded turns carry the same deduplicated `memory_search` and
`memory_save` definitions
- Not tested: live Codex subscription WebSocket

## Review Readiness

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

## Checklist

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

## Additional Notes

- Security CI currently flags `click 8.3.1` and `pillow 12.2.0`, so this
PR carries the narrow floor bump to `click>=8.3.3` and `pillow>=12.3.0`
as a supply-chain unblock for the same final merge snapshot.
- `CHANGELOG.md` remains untouched because Headroom generates release
notes from conventional commits.
- This PR is scoped to the OpenAI Responses WebSocket relay lifecycle;
it does not change the HTTP `/v1/responses` path or the Anthropic
handler.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 14:42:27 -04:00
Ashish
23d73ae070
test(evals): add offline fidelity regression gate (recall-based, zero-model) (#1187)
## Description

Headroom's lossy compression drops rows/lines using statistical
heuristics but **never checks that meaning survived** — a dropped `OOM
killed worker 3` line can silently flip a model's answer with no signal
that compression caused it. The repo already ships a quality-metric
toolkit (`headroom/evals/metrics.py`) and a `weekly-suite` eval job, but
neither gates the compression path on a PR.

This adds a **per-PR fidelity regression gate**: compress vendored
golden tool-outputs through SmartCrusher's lossy path and assert the
evidence that answers each case's question survives. It is the first of
a planned trio (this is the "offline gate" half of the fidelity work);
query-aware retention and a hard token-budget API are documented
follow-ups.

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

- **Blocking gate** (`tests/test_compression_fidelity_regression.py`):
compresses each golden case via `smart_crush_tool_output(...,
with_compaction=False)` and scores with `compute_information_recall`.
Two assertions:
- **Per-case critical recall == 1.0** — every `answer_evidence` string
(placed in error/anomaly rows, the documented SmartCrusher retention
guarantee) must survive.
- **Aggregate recall ≥ committed baseline** (`baseline.json`, tol 0.02)
— catches softer regressions.
- **Vendored fixtures** (`tests/fixtures/fidelity_golden/`):
deterministic `_generate.py` emits `cases.json` (4 cases: OOM crash,
payment exception, latency anomaly, CI failure) + `baseline.json`.
- **Non-blocking weekly report** (`.github/workflows/eval.yml`): one
step in the existing `weekly-suite` job (schedule/manual only) reuses
the existing `evaluate_information_retention` runner for a recall report
on the production routing path.
- **Pure reuse**: scoring (`evals/metrics.py`), compressor
(`smart_crush_tool_output`), and the weekly runner
(`evaluate_information_retention`) all already existed.

### Design notes

- **Zero new CI setup.** The blocking gate runs in the existing `[dev]`
test shard — no new workflow, no new deps, **no model, no network, no
secrets** (verified under `HF_HUB_OFFLINE=1`). It deliberately uses
small hand-made structured fixtures rather than the repo's HuggingFace
dataset loaders, which would require a network download + ModernBERT and
don't belong in a fast PR gate.
- **Scope:** structured JSON tool-output (the dominant, deterministic,
model-free case). Real-dataset (HotpotQA/BFCL) recall — which needs
`[all]` + a local model — is a **documented follow-up PR**, and the
`weekly-suite` job (which genuinely runs every Monday) is its natural
home.

## Testing

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

### Test Output

```text
$ HF_HUB_OFFLINE=1 python -m pytest tests/test_compression_fidelity_regression.py -v
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[logs_oom] PASSED
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[payment_exception] PASSED
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[latency_anomaly] PASSED
tests/test_compression_fidelity_regression.py::test_critical_evidence_survives_compression[ci_test_failures] PASSED
tests/test_compression_fidelity_regression.py::test_aggregate_recall_not_regressed PASSED
============================== 5 passed in 0.18s ===============================
```

## Real Behavior Proof

- **Environment:** local checkout of `feat/fidelity-regression-gate`,
`pip install -e ".[dev]"`, `HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1`
(proves no model/network).
- **Exact command / steps:** `HF_HUB_OFFLINE=1 python -m pytest
tests/test_compression_fidelity_regression.py -q` → `5 passed in 0.14s`.
- **Negative control (proves the gate has teeth):** compressing
`logs_oom` and probing for a benign row that compression legitimately
drops returns `recall = 0.00, lost = ['heartbeat ping 25']` — i.e. the
gate fires when critical evidence is dropped, so it is not trivially
green.
- **Weekly (non-blocking) step verified locally:**
  ```text
  Information retention: 50/50 cases >=0.9 recall, avg compression 65.7%
  ```
- **Not tested:** real-dataset (HotpotQA/BFCL) recall and
prose/ModernBERT compression — intentionally deferred to a follow-up PR
targeting the weekly job.

## Review Readiness

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

## Checklist

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

## Additional Notes

- CHANGELOG/version intentionally untouched: repo uses
**release-please**.
- **Follow-up PR (planned):** wire the real HotpotQA/BFCL loaders
(`headroom/evals/datasets.py`) into the `weekly-suite` job for genuine
benchmark-scale recall coverage (model-allowed, non-blocking). Further
follow-ups from the same design: a live per-request fidelity guardrail,
query-aware lossy retention, and a hard `target_tokens` budget API.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:53:59 -05:00
chopratejas
c62d45eea8 fix(memory): expose memory IDs in auto-tail + memory_list tool + ID-usage guidance
Pre-this-PR the auto-injected memory block rendered rows as `1. <content>`
with no addressable handle. To UPDATE or DELETE a row the model first had
to call memory_search to discover its ID — two round trips, against the
model-as-judge architecture.

This PR adds three tightly-coupled affordances so the model can act on
memory directly:

1. Auto-tail rows now carry the memory ID:
     `1. [mem_alpha_001] User prefers Python`
   The bracketed token is the canonical ID — same identifier accepted by
   memory_update and memory_delete.

2. New `memory_list` tool — chronological browse (vs `memory_search`'s
   semantic lookup). Returns recent memories with their IDs. Backend
   dispatches to `Backend.list_memories` if available, else falls back
   to an empty-query `search_memories`. Caps at 100 entries.

3. ID-usage guidance text appended to the auto-tail block. Tells the
   model that bracketed IDs can go straight to memory_update /
   memory_delete with no intervening search. The guidance lives in the
   user-message tail (never system) — preserves cache-prefix byte
   stability (invariant I2).

`memory_update` and `memory_delete` tool descriptions also point at the
[id] block as a valid ID source — keeps tool docs consistent with the
new affordance.

Verification:
- 10/10 tests pass in tests/test_memory_auto_tail.py (incl. 2 new
  guidance tests + 2 new ID-format tests)
- 31/31 tests pass in tests/test_memory_handler_native_ops.py (incl. 4
  new memory_list dispatch tests + existing assertions updated for the
  [id] format change)
- Golden fixtures regenerated for the tool-description copy changes
  (tests/fixtures/memory_tool_definitions/{anthropic,openai}.json)
- Live end-to-end test against real Anthropic API
  (tests/test_proxy_memory_integration.py::TestMemoryIdAutoTailAndUpdate):
  seeded memory → auto-tail → Claude → memory_update with exact ID.
  PASSED.
2026-05-19 22:10:42 -05:00
chopratejas
8dcd474aca fix: A7 — memory tool injection session-sticky for both Anthropic and OpenAI
Closes the second half of P0-6: once memory injects memory_save / memory_search
into body["tools"] for a session, every subsequent turn injects the byte-equal
same definitions — even if memory is disabled mid-session. Toggling tool list
mid-session busts Anthropic prefix cache per guide §6.3 #2.

Adds in headroom/proxy/helpers.py:

  * SessionToolTracker — bounded LRU keyed by (provider, session_id) storing
    GOLDEN tool-definition bytes from the first injection. Tracker is
    provider-aware so the same session_id under Anthropic and OpenAI keeps
    independent state. Reentrant lock for concurrent access; LRU eviction at
    HEADROOM_TOOL_TRACKER_MAX_SESSIONS (default 1000).
  * apply_session_sticky_memory_tools — single coordination point with three
    paths: first-time inject (record golden bytes), sticky replay (always
    inject golden bytes regardless of inject_this_turn), and skip. Honors
    HEADROOM_TOOL_INJECTION_STICKY=disabled as a loud operator opt-in for
    rollback (NOT a fallback).
  * serialize_tool_definition_canonical — deterministic byte serialization
    via the same separators=(",",":")/ensure_ascii=False rules as
    serialize_body_canonical.
  * log_tool_injection_decision — structured per-decision log line; never
    logs the tool definition contents.

Wires the helper into all four memory tool injection sites:
  * handlers/anthropic.py — /v1/messages
  * handlers/openai.py — /v1/chat/completions
  * handlers/openai.py — /v1/responses
  * handlers/openai.py — Codex WS path

memory_handler.MemoryHandler gains compute_memory_tool_definitions(provider) —
a pure builder that returns the tool definitions without mutating a tools
list, so the proxy can route through the sticky tracker. The legacy
inject_tools(...) is preserved for callers without a session_id.

Tests: tests/test_memory_tool_session_sticky.py — 29 unit + integration
cases covering: turn-1→turn-2 byte-equality (Anthropic + OpenAI), sticky
replay after memory disabled, golden-fixture pin, LRU eviction, provider
isolation under shared session_id, thread-safe concurrent access, env-var
contract, disabled-mode passthrough, dedupe with client tools.

Golden fixtures pin canonical bytes:
  * tests/fixtures/memory_tool_definitions/anthropic.json
  * tests/fixtures/memory_tool_definitions/openai.json

No regex. No hardcodes (env-configurable: HEADROOM_TOOL_INJECTION_STICKY,
HEADROOM_TOOL_TRACKER_MAX_SESSIONS). No silent fallbacks. Per-decision
structured logging. Realignment build constraints satisfied.
2026-05-02 10:11:27 -07:00