Commit graph

5 commits

Author SHA1 Message Date
AxelRay
ef7e07e0f5
fix(policy): price net-cost mutations with the 1h cache-write tier (#2780)
## Description

This fixes the net-cost mutation gate for requests using Anthropic's
1-hour prompt-cache TTL.

The gate previously hardcoded the 5-minute cache-write multiplier of
1.25x. A 1-hour cache write costs 2.0x, so the old calculation
understated the true write penalty and could incorrectly recommend
mutation for 1-hour clients.

Closes #2773

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

- Added TTL-aware cache-write multiplier selection for 5-minute and
1-hour tiers.
- Threaded the resolved TTL through the content router and compression
policy helpers.
- Preserved the existing 5-minute behavior as the default.
- Added Python and Rust regression coverage for the 1-hour tier.
- Retuned the netcost gate fixtures so the 1-hour write tier flips the
decision in the full ContentRouter path.
- Did not edit CHANGELOG.md.

## Testing

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

### Test Output

```text
pytest tests/test_compression_policy.py -q
20 passed

cargo test -p headroom-core --lib compression_policy -- --nocapture
14 passed

pytest tests/test_netcost_gate.py -q
27 passed

Ruff checks and formatting passed.
git diff --check passed.
```

## Real Behavior Proof

- Environment: Linux x86_64 contributor checkout with Python and Rust
test environments.
- Exact command / steps:
  - Ran the Python compression policy test suite.
  - Ran the Rust compression policy unit tests.
- Ran the netcost gate suite, including the 1-hour env and
request-marker cases.
- Exercised the new 1-hour TTL golden case alongside the existing
5-minute cases.
- Observed result: The 1-hour case uses the 2.0x write multiplier and
skips the same candidate that still mutates under 5-minute pricing.
Existing 5-minute behavior remains covered and passing.
- Not tested: A live Anthropic request through the proxy and production
traffic.

## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] 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
- [x] I did **not** edit CHANGELOG.md - it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable for this backend policy fix.

## Additional Notes

Ready for review. CI is green on the current tip.
2026-08-16 15:09:50 -07:00
Focused Instability
0632eba6c3
fix(policy): correct warm-cache penalty in net_mutation_gain to (S + dT) (#903)
Fixes #906.

## What

Part of #904 (net-cost policy completion tracking). Follows up #856 /
#857 with the corrected gain term raised in [this #856
comment](https://github.com/chopratejas/headroom/issues/856#issuecomment-4679706939)
— prerequisite for P2 (pipeline consumption), which would otherwise wire
in a formula that is always-pro-mutation by exactly `P_alive·(w−r)·ΔT`.

## Why the corrected form is right

With a live cache, the ΔT tokens a mutation removes are **already
cache-written** — keeping them costs only reads (`ΔT·r·R`), so a
mutation cannot avoid a fresh write of them. Blending alive (`ΔT·r·R −
(w−r)·S`) and dead (`ΔT·(w + r·(R−1))`, no suffix penalty) cases over
`P_alive`:

```
gain = ΔT·(w + r·(R−1)) − P_alive·(w−r)·(S + ΔT)
```

Three independent confirmations:

1. **Direct cost check** (w=1.25, r=0.1, warm, ΔT=50K, S=10K, R=2):
keeping costs 60K·0.1·2 = 12,000 in reads; mutating costs 10K·1.25
(suffix rewrite, the first of the R touches) + 10K·0.1 (remaining read)
= 13,500 — mutation loses 1,500, matching the corrected gain of −1,500.
The old form said +56,000.
2. **The issue's own anchors**: corrected break-even is exactly `R =
11.5·S/ΔT` → 2K/50K = 287.5 (~290, as the issue says) and 50K/10K = 2.3
— the spec text's anchor numbers can only be derived from the corrected
penalty. The implemented form gave 276 and *negative*.
3. **Internal consistency**: `break_even_reads` already shipped with the
~11.5·S/ΔT shape; this PR reconciles `net_mutation_gain` with it (and
drops break_even's stray −1 term).

## Behavior changes (formula is still dead code — nothing consumes it
yet)

- 50K-shave/10K-suffix/R=3 golden: +61,000 → **+3,500** (tight win,
consistent with 2.3-read break-even).
- 2K-shave/50K-suffix/R=10 golden: −53,200 → **−55,500**.
- S=0 boundary: an edit of already-cached content with no suffix is
profitable whenever ≥1 read remains (`gain = ΔT·r·R`), and exactly 0 at
R=0 warm. Not-yet-cached (live-zone) content should bypass the formula —
now documented on both implementations.

Rust + Python goldens updated in lockstep: 13 Rust + 19 Python tests
green.

## Next (separate PRs)

- **P2**: flag-gated consumption (`HEADROOM_NET_COST_POLICY=1`) with
decision telemetry.
- **P3**: batch deep edits (reclaim threshold), idle-timer compaction
near TTL lapse.

Co-authored-by: integration-check <integration@local>
2026-06-12 17:14:30 -05:00
Focused Instability
d5f58026e2
feat: net-cost cache mutation formula on CompressionPolicy (#856 P1) (#857)
Closes #856

**P1 of the #856 phased plan** — pure functions, zero behavior change.
(Closing keyword links the issue; if P2 hasn't started when this merges,
reopen #856 or it remains the design record for the P2/P3 follow-up
PRs.)

## What

Adds the break-even decision rule for deep (pre-cache-marker) edits to
`CompressionPolicy`:

```
gain = ΔT · (w + r·(R−1)) − P_alive · (w − r) · S
```

- `net_mutation_gain()`, `should_mutate_deep()` (gain > 0),
`break_even_reads()` (R = ((w−r)/r)·(S/ΔT−1) ≈ 11.5·S/ΔT) on the Rust
struct (source of truth) and the Python hand-mirror, following the
existing F2.1/F2.2 parity pattern.
- `CACHE_WRITE_MULTIPLIER = 1.25` / `CACHE_READ_MULTIPLIER = 0.1` public
constants (Anthropic 5-minute tier).
- Inputs clamped (`expected_reads ≥ 0`, `p_alive ∈ [0,1]`); methods take
`&self`/`self` so a follow-up can add per-mode margins.
- The formula derives the existing Subscription live-zone policy as its
S=0 special case rather than contradicting it.

**No callers yet.** P2 (consuming this in `TransformPipeline` behind
`HEADROOM_NET_COST_POLICY`, replacing the binary `live_zone_only` gate,
with decision telemetry) is specified in #856 and awaits maintainer
direction — this PR just lands the audited arithmetic both dispatchers
will share.

## Tests

Golden-value parity: 6 new Rust unit tests and 7 new Python tests assert
the **identical scenario numbers** (loss −53 200 for a 2K shave under a
50K warm suffix at R=10; win +61 000 for a 50K shave under a 10K suffix
at R=3; S=0 always profitable; P_alive=0 always profitable — the
idle-timer window; clamping; break-even 276 reads for the 2K/50K
anchor). A drift on either side trips the pair loudly, same contract as
the existing field-map parity test.

- `cargo test -p headroom-core --lib compression_policy`: 12 passed (6
existing + 6 new)
- `pytest tests/test_compression_policy.py`: 17 passed (10 existing + 7
new)
- `cargo fmt --check`, `cargo clippy -p headroom-core` clean; `ruff
check` + `ruff format --check` clean

## Real behavior proof

Not applicable in the runtime sense — this PR intentionally adds **no
runtime behavior** (pure functions, no call sites). The arithmetic is
validated against the research anchors above in both languages' test
suites; live decision telemetry arrives with P2 where the formula first
gates real traffic.

## Out of scope

P2 (flag-gated pipeline consumption + telemetry), P3 (deep-edit
batching, idle-timer compaction near TTL lapse), retiring the deprecated
`volatile_token_threshold`/`max_lossy_ratio` fields — all tracked in
#856.

---------

Co-authored-by: Ash Rhodes <ashley.rhodes@king.com>
2026-06-11 13:06:09 -05:00
chopratejas
797dc63da7 fix(core): F2.2 c1/3 — extend CompressionPolicy with three per-mode tuning fields
Adds three per-mode tuning fields to the F2.1 CompressionPolicy struct
on both sides of the parity bridge:

- volatile_token_threshold (u32 / int) — per-mode threshold below which
  content is treated as cache-stable. PAYG=128 (relaxed), Subscription=32
  (strict). Plumbed but unconsumed in F2.2 — the volatile detector in
  cache_aligner.py is shape-based; wiring it is a follow-up.

- max_lossy_ratio (f32 / float, [0.0, 1.0]) — per-mode upper bound on
  lossy compression aggressiveness. PAYG=0.45, Subscription=0.25.
  Plumbed but unconsumed in F2.2 — distinct from the caller-driven
  target_ratio kwarg in ContentRouter.

- toin_read_only (bool) — TOIN learning gate. True = serve cached
  patterns but never write new observations from this request.
  PAYG/OAuth=false (network effect feeds on aggressive traffic),
  Subscription=true (consistency over learning).

OAuth stays identical to PAYG across all five fields; the canary parity
test (oauth_matches_payg_today) covers the full struct so a future
divergence on any field trips the assertion just as loudly as a flag flip.

Per-mode defaults are CONSERVATIVE pending F2.1 bake telemetry; F2.2-
followup will tune. Per the realignment build constraints, the
configuration IS the per-mode default — no separate env var per field.

What's NOT in this commit:
- TOIN gate wiring (next commit, c2/3)
- policy_selected log extension (next commit, c2/3)

Tests:
- Rust: 6 unit tests in compression_policy::tests (3 new). OAuth=PAYG
  canary now compares ALL fields.
- Python: 10 tests in tests/test_compression_policy.py. Hard-coded
  expected_fields set in TestRustParityFieldMap extended.

Refs: F2.1 (#400)
2026-05-06 14:37:33 -07:00
chopratejas
de8e245990 fix(transforms): Python parity port of CompressionPolicy + cache_aligner gate (F2.1 c4/5)
Phase F2.1, commit 4 of 5 (consolidated from the 6-commit plan after
finding the Rust dispatcher gate is a no-op in F2.1 — Subscription
still gets live-zone compression by design, so the only behaviour
change is on the Python detector side).

What lands:

- New `headroom/transforms/compression_policy.py` — hand-mirror of
  `headroom_core::compression_policy::CompressionPolicy`. Two fields,
  `policy_for_mode(AuthMode)` constructor, `policy_default_payg()`
  helper for the enforcement-flag-off path. Source of truth is the
  Rust crate; a parity test guards against silent drift.

- `CacheAligner.should_apply` now reads `kwargs["compression_policy"]`
  and returns `False` when `policy.cache_aligner_enabled is False`.
  This is THE behaviour change for F2.1: subscription requests stop
  triggering volatility warnings and the per-pipeline-instance
  `_previous_prefix_hash` is no longer updated by them.

  Hidden state caveat: the hash field is per-pipeline-instance, not
  per-request. Clearing it on skip would race with concurrent PAYG
  requests on the same pipeline, so we don't. The behaviour is "skip
  silently" — exactly what cache-stability-sensitive callers want.

- 6 new tests in `tests/test_compression_policy.py`:
    - per-mode field assertions (mirror Rust unit tests)
    - `oauth_matches_payg_today` canary for F2.2 divergence
    - immutability check (`@dataclass(frozen=True)`)
    - field-set parity guard against Rust + Python drift
- 2 new tests in `tests/test_cache_aligner_detector_only.py`:
    - subscription policy short-circuits should_apply
    - PAYG policy does NOT short-circuit (sanity)

Note: this commit does NOT yet plumb the policy from the proxy
handlers into `pipeline.apply(...)` kwargs. That happens in c5/5
alongside the flag default flip. Until c5/5 lands, the gate is
reachable but unfired — `kwargs["compression_policy"]` is absent
from every production call site, so `policy.get(...)` returns None
and current behaviour is preserved bit-for-bit.

Verified: 28/28 affected Python tests pass.
2026-05-05 16:49:50 -07:00