Commit graph

1553 commits

Author SHA1 Message Date
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
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
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
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
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
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
JerrettDavis
1d440023b6 Merge upstream/main into fix/copilot-oauth-runtime
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:44:58 -05:00
JerrettDavis
94cf57ac4d test: sync release workflow assertions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:38:01 -05:00
JerrettDavis
0a8a6dca1e test: derive canonical release version
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:32:10 -05:00
Tejas Chopra
5738339524
Merge pull request #219 from JerrettDavis/fix/python-github-packages-publish
ci: publish Python distributions to GitHub releases
2026-04-21 21:30:16 -07:00
JerrettDavis
af784465df test: restore cli package state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:23:58 -05:00
JerrettDavis
f5b959a470 test: isolate copilot oauth suites
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:01:31 -05:00
JerrettDavis
d60cf7914c fix: support live copilot oauth runtime
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 22:48:57 -05:00
Tejas Chopra
1f978f9235
Merge pull request #229 from JerrettDavis/feat/copilot-oauth
fix: support GitHub Copilot OAuth sessions
2026-04-21 20:14:36 -07:00
JerrettDavis
7989581350 fix: support copilot oauth sessions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 22:11:30 -05:00
Tejas Chopra
da54d4a80a
Merge pull request #224 from gglucass/codex/compact-stats-history-default
Compact /stats-history responses by default
2026-04-21 20:09:15 -07:00
Tejas Chopra
ed7d4942aa
Merge pull request #226 from JerrettDavis/feat/init-agent-hooks
feat(init): Add durable headroom init command for agent hooks
2026-04-21 20:08:49 -07:00
JerrettDavis
9ba9a59f78 test: isolate windows init branches from os globals
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 21:11:03 -05:00
JerrettDavis
c1b648664e test: raise init command branch coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:59:24 -05:00
JerrettDavis
a278a7b0ba test: cover init install flows end to end
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:15:11 -05:00
JerrettDavis
c5d795c2af build: sync agent hook manifests to repo semver
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:03:14 -05:00
JerrettDavis
3a999d1562 feat: add durable init command for agent hooks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 19:39:06 -05:00
Tejas Chopra
80920ed0e8
Merge pull request #212 from Kayzo/fix/onnx-memory-retention
fix(onnx): reduce retained cpu memory
2026-04-21 13:06:08 -07:00
Kayzo
1961cebd00 chore: format proxy route tests with ruff 2026-04-21 19:38:32 +00:00
Garm
a858a0fd4b style: format test_proxy_savings_history.py for CI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 18:59:24 +02:00
Garm
ea0f024b67 Compact /stats-history responses by default 2026-04-21 18:08:33 +02:00
JD Davis
e854ba1771
Merge branch 'main' into fix/python-github-packages-publish 2026-04-21 07:10:17 -05:00
Tejas Chopra
0d6ba9972a
Merge pull request #220 from JerrettDavis/feat/transformations-live-feed-v3
feat(dashboard): live feed sidebar with message transformations
2026-04-21 00:15:50 -07:00
Tejas Chopra
329956e197
Merge pull request #218 from JerrettDavis/fix/commitlint-merge-commits
fix: skip commitlint for PR merge commits
2026-04-21 00:15:36 -07:00
Garm
66f12b4f68 Merge branch 'main' into feat/track-embedded-installs
# Conflicts:
#	headroom/proxy/server.py
2026-04-21 09:12:04 +02:00
JerrettDavis
c15f9836b6 test(dashboard): fix playwright importorskip placement
Move importorskip after playwright import so module-level
import error triggers skip rather than collection error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 00:10:05 -05:00