Commit graph

2630 commits

Author SHA1 Message Date
JerrettDavis
ba4505074e fix: fall back from stale persistent wrap manifests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 19:00:39 -05:00
JerrettDavis
88dc15ea85 Merge upstream/main into feat/canonical-pipeline
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 18:47:41 -05:00
Garm
58282bbc5e test(proxy): cover turn_id branches flagged by codecov
Patch coverage on helpers.py was 87% — 5 lines of compute_turn_id
were untested. Add cases for: non-dict / non-user messages in the
reverse scan, empty-string user content (should keep scanning),
mixed text+tool_result content (agent-loop continuation, not a
turn boundary), and system=None (hashes without the system segment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:34:41 +02:00
Garm
d88c1abde9 style: ruff format compute_turn_id body
CI's `ruff format --check` (stricter than `ruff check`) collapsed two
generator expressions to single-line. Apply the autofix; semantics
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:30:26 +02:00
Garm
e8835affb8 docs(changelog): record turn_id feature; fix import order in new test
Follow-up to b2536e6: add the Unreleased changelog entry describing the
prompt-turn identifier, and pick up the ruff-fixed import layout in the
new test file (ruff --fix of I001).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:16:38 +02:00
Garm
c4464c066f feat(proxy): emit turn_id linking agent-loop API calls from one user prompt
Adds compute_turn_id() helper that hashes (model, system, messages prefix
up to the last user text message). An agent loop sends the same user-text
prefix across every iteration plus a growing tool chain, so this id is
stable across the turn but rolls over when the user sends a new prompt.

Stamps the id onto RequestLog at all three call sites (anthropic handler
bedrock + direct branches, and the streaming handler) and surfaces it as
turn_id in /transformations/feed so downstream consumers can aggregate
savings per user prompt rather than per API call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:15:55 +02:00
Tejas Chopra
4c9c29c421
Merge pull request #233 from Kayzo/fix/dashboard-stats-snapshot-cache
fix: cache dashboard stats snapshots
2026-04-22 12:33:19 -07:00
Kayzo
af9af3d2b5 chore(merge): resolve upstream main conflicts 2026-04-22 19:22:40 +00:00
Tejas Chopra
e71ebd5dfb
Merge pull request #230 from JerrettDavis/fix/copilot-oauth-runtime
fix: support live copilot oauth runtime
2026-04-22 11:47:37 -07:00
Tejas Chopra
40f11d8dff
Merge pull request #232 from gglucass/fix/learn-preserve-prior-recommendations
fix(learn): preserve prior recommendations across runs
2026-04-22 11:47:20 -07:00
Tejas Chopra
f5e8beadb2
Merge pull request #236 from gglucass/fix/streaming-anthropic-logger
fix(proxy): log streaming Anthropic requests so /transformations/feed populates
2026-04-22 11:38:08 -07:00
Garm
aad799d76e test(learn): cover _parse_prior_recommendations edge cases
Closes the codecov gap flagged on PR 232 (88.89% → near 100% on the patch):

- A file with no marker block returns no prior recommendations.
- A marker block with nothing between the markers yields an empty list
  (the re.split fast-path with zero sections).
- A stray `### ` with no heading text inside the block is silently
  skipped (the `if not heading: continue` branch, previously
  unexercised in tests) — a real section after it still parses cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:13:49 +02:00
JerrettDavis
87a13ef2cd Merge upstream/main into feat/canonical-pipeline
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 13:10:53 -05:00
Garm
b2536e602a test(learn): cover flush_to_file, backend edge cases, and hydrate/bump error paths
Adds 17 targeted tests to close the coverage gap on the new traffic_learner
paths (codecov flagged ~51%). Exercises:

- `flush_to_file` end-to-end with a fake learn plugin + writer: verifies
  anchored patterns are bucketed per project, recommendations are passed
  to the writer, writer exceptions are swallowed, and each early-return
  branch (no plugin, no patterns, discover_projects failure, un-anchored
  patterns) is hit without raising.
- `_resolve_backend_db_path` on None backend, backend without
  `_config`, and backend with empty `db_path`.
- `_collect_all_patterns` merging persisted + accumulator patterns by
  content_hash with summed evidence_count, plus the missing-DB branch.
- `_hydrate_persisted_state` with backend=None and with a backend
  pointing at a non-existent DB file (both no-ops).
- `_bump_persisted_evidence` with no backend, missing DB, and
  unknown memory id (all silent no-ops so the proxy hot path never
  blows up on malformed state).
- `stop()` cancelling the flush task cleanly.

All new tests use the existing `_FakeBackend` + `_init_db` helpers so
they exercise real SQLite paths, not mocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:06:44 +02:00
Garm
bde7807421 test(proxy): warm up TestClient before measuring /livez backpressure latency
The test drained the anthropic pre-upstream semaphore and asserted that 20
subsequent /livez calls stayed under 100 ms. With only 20 samples, the p99
computation falls through to max(latencies) — one cold-start outlier was
enough to fail the test. Observed on CI py3.10 runners where the first
TestClient request paid ~330 ms of one-time ASGI lifespan / import /
route-resolution cost while every subsequent request was sub-ms.

Add 3 warm-up requests before timing starts. Preserves the test's
signal (if /livez were actually blocked on the drained semaphore, all
post-warmup samples would still be slow) while removing the
runner-speed flake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:58:55 +02:00
Garm
934345d81b Merge remote-tracking branch 'upstream/main' into fix/streaming-anthropic-logger 2026-04-22 19:51:59 +02:00
Garm
aa5e5ae817 Merge remote-tracking branch 'upstream/main' into fix/learn-preserve-prior-recommendations 2026-04-22 19:51:58 +02:00
Garm
c571fd331b Merge remote-tracking branch 'upstream/main' into fix/traffic-learner-evidence-count 2026-04-22 19:44:06 +02:00
JerrettDavis
3f30474fb7 Merge upstream/main into fix/copilot-oauth-runtime
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 12:43:51 -05:00
Tejas Chopra
706c5e948d
Merge pull request #235 from gglucass/fix/cli-submodule-binding
fix: unblock CI on main (cli submodule binding + test sys.modules hygiene)
2026-04-22 10:27:28 -07:00
Garm
3e290b734b fix(learn): persist real evidence_count and bump on re-sighting
Before this change, every persisted traffic_learner row in memory.db
landed with evidence_count=1, causing two user-visible problems:

1. The live flush gate (evidence_count >= 2) filtered out every row, so
   CLAUDE.md / MEMORY.md never received the patterns the learner saw
   repeatedly.
2. _saved_hashes is in-memory only and reset on each proxy restart, so
   a pattern seen once in session A then twice in session B would insert
   a *duplicate* DB row instead of bumping the existing one. Users
   accumulated many rows stuck at 1 instead of a few rows with high
   evidence.

Root cause chain:
- _accumulate tracks a running count in the _pattern_counts tuple but
  enqueues the ExtractedPattern dataclass with its default
  evidence_count=1 intact.
- _save_worker writes pattern.evidence_count into metadata verbatim.
- After save, the hash goes into _saved_hashes and further sightings
  are early-returned — never bumped.
- Next process start has empty _saved_hashes, so the same content goes
  through the accumulator as fresh and gets re-saved.

Fix:
- _accumulate now sets pattern.evidence_count = count before enqueuing,
  so DB rows reflect the real number of sightings at save time.
- _save_worker captures the Memory.id returned by save_memory and
  records content_hash → id in a new _persisted_ids map.
- _accumulate's saved-hash branch now awaits
  _bump_persisted_evidence(memory_id), which runs an atomic
  json_set('$.evidence_count', existing + 1) UPDATE via
  asyncio.to_thread to keep the proxy hot path non-blocking.
- start() calls a new _hydrate_persisted_state() that reads existing
  traffic_learner rows' (id, content) pairs from the DB and pre-seeds
  _saved_hashes + _persisted_ids. Cross-session re-sightings bump the
  seeded row instead of inserting a duplicate.
- _load_persisted_patterns_from_sqlite and _hydrate_persisted_state
  query by json_extract(metadata, '$.source') = 'traffic_learner'
  instead of the prior LIKE on raw JSON — the bump path uses json_set,
  which rewrites the metadata string without the default ": " spacing,
  which would otherwise make the LIKE blind to bumped rows.

Adds TestEvidencePersistence with three cases:
- save persists the actual accumulated count (not the default 1)
- re-sightings bump the persisted row instead of creating duplicates
- a fresh learner hydrates _saved_hashes from DB, so cross-session
  re-sightings bump the pre-existing row

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:02:35 +02:00
Garm
d9138a3ed8 feat(learn): live flush of traffic patterns to agent-native context files
Replaces the previous shutdown-only flush with a debounced, near-real-time
dirty-flag flush worker that writes patterns into the correct CLAUDE.md /
MEMORY.md bucket as traffic accumulates.

- New FLUSH_DEBOUNCE_SECONDS gate (10s) prevents context-file thrash on
  bursty traffic while keeping updates "live" from the user's perspective.
- TrafficLearner.start() now spawns a _flush_worker alongside the save
  worker; _accumulate() sets a dirty flag; _flush_worker() calls
  flush_to_file() when dirty and past the debounce window.
- flush_to_file() now reads *both* persisted rows (memory.db) and the
  in-memory accumulator via _load_persisted_patterns_from_sqlite and
  _collect_all_patterns, so patterns survive proxy restarts and the
  agent-native files converge toward the full learned set.
- Patterns are bucketed per-project via the learn plugin registry
  (plugin.discover_projects()) and anchored to project roots through
  longest-matching-path on content or entity_refs
  (_project_for_pattern). Un-anchored patterns are dropped.
- Patterns are routed by PatternCategory to either CONTEXT_FILE
  (CLAUDE.md) or MEMORY_FILE (MEMORY.md) via
  _patterns_to_recommendations + _CATEGORY_TO_TARGET.
- Live flushes require evidence_count >= 2; shutdown flushes accept
  single-evidence rows to avoid losing last-session signal.

Adds tests for project routing, persisted-pattern loading, category
routing, and the debounced flush worker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:00:15 +02:00
Garm
9bde245c43 fix(proxy): log streaming Anthropic requests so feed populates
`_finalize_stream_response` recorded metrics, cost, and prefix-cache stats
but never appended a `RequestLog` to the request logger. Because the
streaming Anthropic path is what Claude Code uses, this meant
`/stats.recent_requests` and `/transformations/feed` were permanently
empty for typical traffic — even when the proxy was started with
`--log-messages`. Only the non-streaming Anthropic path
(`anthropic.py:1167, 1599`) and the Bedrock streaming finalizer
(`streaming.py:_stream_response_bedrock`) were logged.

Wire the same `RequestLog` shape the other paths build, plumbing `tags`
through both `_finalize_stream_response` call sites in `_stream_response`
and respecting `config.log_full_messages` for `request_messages`.

Adds `tests/test_proxy_streaming_request_logger.py` covering the happy
path, both `log_full_messages` branches, the zero-original-tokens edge,
and the logger-disabled no-op.
2026-04-22 14:16:35 +02:00
Kayzo
3045b36b52 fix: stabilize cli test isolation 2026-04-22 11:28:30 +00:00
Kayzo
d17aea2fe2 Merge remote-tracking branch 'upstream/main' into fix/dashboard-stats-snapshot-cache 2026-04-22 10:46:37 +00:00
Garm
1788d907f0 fix(tests): scope sys.modules mutation and unpin hardcoded version
Three independent pre-existing test-hygiene regressions on main, all
surfaced as cascading CI failures:

1. tests/test_cli/test_wrap_copilot.py (from #229) mutated
   sys.modules["headroom.cli.main"] with a fake click.Group() at
   module-import time and never restored it. Any later test that did
   `from headroom.cli.main import main` got an empty group with no
   version option and no registered subcommands, breaking ~20
   test_cli/* and test_cli_proxy_env.py tests. Rewrite to import the
   real `main` directly — the fake-group indirection served no
   purpose.

2. tests/test_proxy_copilot_auth_hooks.py (from #229) installed fake
   httpx / fastapi.responses / headroom.proxy.* modules into
   sys.modules inside a helper called from test functions, never
   cleaned up. Later tests that imported ASGITransport or JSONResponse
   hit the fakes and failed with ImportError. Switch the helper to
   monkeypatch.setitem so the fakes are scoped to the owning test.

3. tests/test_release_version.py hardcoded canonical=0.5.25 in the
   subprocess-output assertion; the project version in pyproject.toml
   has since bumped to 0.9.1. Compute the expected value dynamically
   via get_canonical_version(ROOT) so the test tracks pyproject.
2026-04-22 12:37:22 +02:00
Garm
796afd085b chore: apply ruff format to tests/test_release_workflows.py
Drive-by: main is currently failing `ruff format --check .` because of
two missing blank lines between two top-level functions in this file
(introduced in 8bf11d2). Fixing it here so this PR's CI can go green —
no other way to unblock the format check without landing a separate PR
first.
2026-04-22 12:06:45 +02:00
Kayzo
bb5b676dd7 fix: satisfy dashboard stats cache typing 2026-04-22 10:04:15 +00:00
Garm
3680707e60 fix(cli): eagerly bind subcommand submodules to headroom.cli
Tests that do `patch("headroom.cli.<sub>.<attr>")` resolve the target by
walking attributes on the `headroom.cli` package object. That lookup
fails when `tests/test_cli/test_wrap_copilot.py` pops `headroom.cli`
from `sys.modules` at import time and re-imports it with a fake
`headroom.cli.main` — the re-imported package only has `.wrap` bound
because `_register_commands()` in `main.py` never runs against the fake.

Eagerly importing the subcommand submodules from `__init__.py` binds
them as package attributes regardless of how `main.py` is loaded, so
the patch lookup survives that kind of sys.modules mutation.

Fixes #234
2026-04-22 11:56:59 +02:00
Kayzo
2b1ab269ca fix: cache dashboard stats snapshots 2026-04-22 09:30:19 +00:00
Garm
72ae0a9e03 chore: apply ruff format + add CHANGELOG entry 2026-04-22 11:28:11 +02:00
Garm
0123e49939 fix(learn): preserve prior recommendations across runs (#231)
`headroom learn` built the marker block from only the current run's
recommendations and wholesale-replaced any prior block via
`_MARKER_PATTERN.sub`. Sections learned weeks earlier that didn't
re-surface in a later run were silently dropped.

Fix: in `_merge_into_file`, parse recommendations out of the prior
block and union them with the new run's recommendations. Sections
re-surfaced by the new run take precedence (latest analysis wins);
sections not re-surfaced are carried forward so learnings accumulate
instead of getting clobbered.

To fully rebuild the block, delete it manually and re-run.

Tests: existing wholesale-replace test rewritten as a carry-forward
assertion. Added tests for same-section override, MEMORY.md
carry-forward, and round-trip of sections without a tokens annotation.

Closes #231
2026-04-22 10:22:36 +02:00
JerrettDavis
dcb3e8bdcb test: cover copilot auth branches
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 00:23:28 -05:00
JerrettDavis
71f38cbcba test: isolate copilot auth hook stubs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 00:09:01 -05:00
JerrettDavis
7cfc891317 ci: scope codecov patch coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 00:05:47 -05:00
JerrettDavis
42a8cefdf1 fix: normalize release workflow test formatting
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:55:01 -05:00
JerrettDavis
470bb6cfb9 fix: resolve rebased ci regressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
1b377d8c43 test: add focused pipeline coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
93a1f2113f refactor: move install init logic into provider slices
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
ce8a2f3cf8 test: expand provider pipeline coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
364a07228f fix: preserve lazy proxy backend imports
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
846fca21b0 chore: normalize compress line endings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
8e72270432 fix: restore pipeline compatibility regressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
7c86fa6b22 fix: avoid proxy registry import cycle
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
49a8f80e9b chore: normalize proxy compatibility line endings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
7d74730606 fix: restore proxy compatibility surface
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
5413e7af47 chore: normalize provider slice line endings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
b17c6d81cc refactor: extract provider logic into slices
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
cd9c2e1d01 feat: introduce canonical pipeline lifecycle contract
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00
JerrettDavis
64fe9763f5 test: skip rtk in BYOK copilot assertion
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:53:30 -05:00