## Description
Extracts the proxy memory ranking formulas into a pure
`memory_rank_policy` module and keeps `MemoryCandidate` /
`RecencyBoostRanker` as the public adapter-facing API. Also preserves
backend memory IDs when ranked candidates are rebuilt, so downstream
memory update/delete handles survive the ranking boundary.
Closes #
## Type of Change
- [ ] 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
- [x] Code refactoring (no functional changes)
## Changes Made
- Added `headroom.proxy.memory_rank_policy` for timestamp parsing,
recency factor calculation, and score boosting.
- Updated `RecencyBoostRanker` to delegate policy math while preserving
the existing public API.
- Preserved `MemoryCandidate.id` when rank output candidates are
rebuilt.
- Added focused policy tests plus an ID-preservation regression test.
- Included the current LiteLLM callback signature compatibility shim
required for repo-wide mypy on main-based slices.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
python -m pytest tests/test_memory_rank_policy.py tests/test_memory_ranker.py tests/test_litellm_callback.py -q
32 passed in 6.22s
python -m ruff check .
All checks passed!
python -m ruff format --check .
1095 files already formatted
python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files
gitleaks protect --staged --no-banner --redact
no leaks found
```
## Real Behavior Proof
- Environment: Windows, Python 3.13.13, local worktree based on
`headroomlabs/main`.
- Exact command / steps: targeted pytest, ruff, ruff format check,
repo-wide mypy, staged gitleaks scan.
- Observed result: memory rank policy/ranker/callback tests pass; static
checks pass; no staged secrets detected.
- Not tested: full provider/API integration; this slice only changes
pure policy delegation and candidate shape preservation.
## 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
## Screenshots (if applicable)
N/A
## Additional Notes
Documentation and changelog updates are not applicable for this internal
architecture slice. PR-specific GHAS checks will be monitored after
opening.
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)