mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
1 commit
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a7b197c6ec |
refactor(proxy): MemoryRanker + ImageCompressionDecision + branch-aware version-sync
Three independent contract-pattern follow-ons bundled into one PR. Same frozen-dataclass + factory + apply_to_tags + Rust-portable shape that PR #473 / #477 / #483 established. ## (1) MemoryRanker + RecencyBoostRanker Pre-this-PR Headroom ranked memory candidates by pure cosine similarity. Every other memory system we surveyed (Letta, Mem0, Cognee, Supermemory) re-ranks beyond cosine. * ``MemoryRanker`` Protocol — pluggable re-ranker; future PRs add source-weight + access-count rankers behind the same interface. * ``RecencyBoostRanker`` — first concrete impl. Final score is ``cosine × exp(-age_days / decay_days)``. Default decay 30 days (half-life ~21 days; 60-day-old factor 0.135, 90-day-old 0.050). * ``MemoryCandidate`` — backend-agnostic frozen value type that flows through the ranker. ``MemoryCandidate.from_backend_result`` adapter converts the existing ``MemoryResult`` shape (with nested ``memory.created_at``) into the ranker's flatter form. * Wired into ``memory_handler.search_and_format_context`` as an optional ``ranker=`` kwarg — backwards-compat: ``None`` (default) preserves the pure-cosine path identically. Defensive: * ``created_at=None`` → factor 1.0 (recency-neutral, back-compat with legacy rows / migrating backends) * Negative age (clock skew) → clamped to factor 1.0 (a future-dated row can't outrank a real fresh memory) * Sort is stable on ties — same input → same output every turn, so consecutive turns inject memories in the same order (prefix-cache friendly) Performance: O(N) over candidates where N=top_k≈10. One ``math.exp`` per candidate. Sub-microsecond. Zero new I/O. ## (2) ImageCompressionDecision Mirror of :class:`CompressionDecision` for image compression. Two sites today (``openai.py:1203``, ``anthropic.py:868``) gate inline; both already respect bypass (no Gemini-class drift bug like text compression had), but consolidating into a value type: * Locks bypass-respect via AST contract test — future sites can't drift on it * Surfaces ``image_skip_reason`` in ``RequestOutcome.tags`` for dashboard slicing (same observability surface as ``passthrough_reason`` and ``memory_skip_reason``) * Same Rust-port shape as the other decision types Precedence: ``bypass_header`` > ``image_optimize_disabled`` > ``no_messages`` > ``should_compress=True``. Anthropic's extra ``is_cache_mode`` check stays inline because it's Anthropic-specific (openai/gemini don't have it). Documented in a code comment. ## (3) Branch-aware sync-plugin-versions hook Pre-this-fix the pre-commit ``sync-plugin-versions`` hook ran on every commit and bumped manifests to the predicted-next-release version. Every PR ended up carrying the prediction as collateral ("Why are we bumping ``.claude-plugin/marketplace.json`` — we should not, right??" - user, on PR #483). Fix: the hook is now a NO-OP unless EITHER: * We're on the ``main`` branch, OR * ``HEADROOM_SYNC_VERSIONS=1`` is set explicitly (release workflow) On feature branches the hook prints a single line explaining the skip and exits cleanly. The release workflow opts in via the env var; behaviour on main / at release time is unchanged. ## Test coverage * 16 new tests on ``MemoryRanker`` / ``RecencyBoostRanker`` (frozen, equal cosine wins by recency, decay configurable, NULL timestamp neutral, no-mutation contract, Rust-port shape) * 17 new tests on ``ImageCompressionDecision`` (frozen, all 3 skip reasons, precedence, observability fields, apply_to_tags) * 1 new AST invariant test (extends ``test_handler_outcome_tag_invariant.py``) — locks "no raw ``if self.config.image_optimize and messages and not _bypass:`` conjunction in any handler" All existing memory + cache-stability + handler tests pass (203 ✓). ``make ci-precheck`` clean. ## Rust portability All three new value types port cleanly to frozen Rust structs + pure functions. Same migration pattern as ``CompressionDecision`` (already locked in for the SmartCrusher Rust port). ## Zero-regression contract * Default ``ranker=None`` → memory_handler behaves identically to pre-this-PR (pure cosine; no perf change) * Image decision migration is identity at the bypass/optimize/messages gate — no behaviour change, just contract consolidation * Hook fix is no-op on feature branches (less churn) and unchanged on main (release flow preserved) |