Skip to main content

lean_ctx_proxy/
cache_safety.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Cache-preservation telemetry for the proxy's frozen-region prose rewrites
3//! (#710).
4//!
5//! The proxy only ever rewrites prose inside the cache-safe frozen window
6//! `[cached_prefix_len, boundary)` — never inside the client-cached prefix and
7//! never in the live tail. This module turns that invariant into a *measurable*
8//! production signal: every request that performs a frozen-region prose rewrite
9//! reports whether the rewrite stayed cache-safe, and `/status` surfaces the
10//! resulting ratio (`1.0` = every rewrite was provably cache-safe, the
11//! healthy steady state). A value below `1.0` is a regression signal.
12
13use std::sync::atomic::{AtomicU64, Ordering};
14
15use serde::{Deserialize, Serialize};
16
17/// Total prose segments (text fields) compressed across all requests.
18static PROSE_SEGMENTS: AtomicU64 = AtomicU64::new(0);
19/// Requests that performed at least one frozen-region prose rewrite.
20static PROSE_REQUESTS: AtomicU64 = AtomicU64::new(0);
21/// Of those, the requests whose every rewrite was cache-safe.
22static CACHE_SAFE_REQUESTS: AtomicU64 = AtomicU64::new(0);
23/// Deliberate cold-prefix repacks (#480): requests where the proxy predicted the
24/// client-cached prefix was already cold and rewrote it on purpose. Tracked
25/// separately so an *intentional* prefix rewrite never dilutes the
26/// `cache_safe_ratio`, whose job is to catch *accidental* #448 regressions.
27static COLD_PREFIX_REPACKS: AtomicU64 = AtomicU64::new(0);
28/// Prompt-cache breakpoints the proxy actively injected (#939): requests where a
29/// client set no `cache_control` and the proxy added one on `system` so an
30/// otherwise-uncached prefix bills at the cached rate. Pure win signal.
31static BREAKPOINTS_INJECTED: AtomicU64 = AtomicU64::new(0);
32/// Requests whose unanchored system prompt carried at least one volatile,
33/// cache-busting field (#940, cache-aligner telemetry). A measurement-only
34/// signal — the body is never mutated — that quantifies how much cache the
35/// client's system prompt leaks before any opt-in relocate.
36static VOLATILE_SYSTEM_REQUESTS: AtomicU64 = AtomicU64::new(0);
37/// Cumulative volatile fields detected across those requests (#940).
38static VOLATILE_FIELDS_DETECTED: AtomicU64 = AtomicU64::new(0);
39/// Requests where the opt-in relocate (#974) actively moved volatile fields out
40/// of the cacheable prefix into the uncached tail. A pure cache-win signal.
41static VOLATILE_RELOCATE_REQUESTS: AtomicU64 = AtomicU64::new(0);
42/// Cumulative volatile fields relocated across those requests (#974).
43static VOLATILE_FIELDS_RELOCATED: AtomicU64 = AtomicU64::new(0);
44
45/// Record one request's frozen-region prose activity.
46///
47/// `segments` is how many prose fields were compressed this request; `all_safe`
48/// is `true` when *every* rewrite landed strictly inside the cache-safe frozen
49/// window. A no-op request (`segments == 0`) is not counted, so the ratio
50/// reflects only requests that actually mutated prose.
51pub fn record(segments: u64, all_safe: bool) {
52    if segments == 0 {
53        return;
54    }
55    PROSE_SEGMENTS.fetch_add(segments, Ordering::Relaxed);
56    PROSE_REQUESTS.fetch_add(1, Ordering::Relaxed);
57    if all_safe {
58        CACHE_SAFE_REQUESTS.fetch_add(1, Ordering::Relaxed);
59    }
60}
61
62/// Record one deliberate cold-prefix repack (#480). Counted on its own gauge,
63/// never against [`record`]'s cache-safe ratio.
64pub fn record_cold_repack() {
65    COLD_PREFIX_REPACKS.fetch_add(1, Ordering::Relaxed);
66}
67
68/// Record one actively-injected prompt-cache breakpoint (#939).
69pub fn record_breakpoint_injected() {
70    BREAKPOINTS_INJECTED.fetch_add(1, Ordering::Relaxed);
71}
72
73/// Record one unanchored-system scan that found `fields` volatile fields (#940).
74/// A no-op when none were found, so the gauges count only cache-leaking requests.
75pub fn record_volatile_system(fields: u64) {
76    if fields == 0 {
77        return;
78    }
79    VOLATILE_SYSTEM_REQUESTS.fetch_add(1, Ordering::Relaxed);
80    VOLATILE_FIELDS_DETECTED.fetch_add(fields, Ordering::Relaxed);
81}
82
83/// Record one request whose system prompt had `fields` volatile values relocated
84/// to the uncached tail (#974). A no-op when none moved, so the gauges count only
85/// requests the relocate actually rewrote.
86pub fn record_volatile_relocated(fields: u64) {
87    if fields == 0 {
88        return;
89    }
90    VOLATILE_RELOCATE_REQUESTS.fetch_add(1, Ordering::Relaxed);
91    VOLATILE_FIELDS_RELOCATED.fetch_add(fields, Ordering::Relaxed);
92}
93
94/// Cache-preservation ratio: `safe / total`, or `1.0` when nothing has been
95/// rewritten yet (the trivially-safe empty state). Pure, so it is unit-tested
96/// independently of the global counters.
97#[must_use]
98pub fn ratio(safe: u64, total: u64) -> f64 {
99    if total == 0 {
100        return 1.0;
101    }
102    safe as f64 / total as f64
103}
104
105/// Point-in-time view of the cache-safety counters for `/status`.
106#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
107pub struct CacheSafety {
108    /// Prose segments compressed in the frozen region (cumulative).
109    pub prose_segments_compressed: u64,
110    /// Requests that performed at least one frozen-region prose rewrite.
111    pub prose_requests: u64,
112    /// Fraction of those requests whose every rewrite was cache-safe (`1.0` is
113    /// the healthy steady state; the proxy only rewrites inside the cache-safe
114    /// window by construction).
115    pub cache_safe_ratio: f64,
116    /// Deliberate cold-prefix repacks (#480), cumulative. Non-zero only when the
117    /// opt-in mode fired on a predicted-cold session resume — expected, not a
118    /// regression.
119    #[serde(default)]
120    pub cold_prefix_repacks: u64,
121    /// Prompt-cache breakpoints the proxy actively injected (#939), cumulative.
122    /// Non-zero only when the opt-in `cache_breakpoint` mode added a `system`
123    /// breakpoint for a client that set none — a pure cache win, not a regression.
124    #[serde(default)]
125    pub breakpoints_injected: u64,
126    /// Requests whose unanchored system prompt leaked at least one volatile field
127    /// (#940), cumulative. Measurement-only (the body is never mutated); non-zero
128    /// only when the opt-in `cache_aligner` telemetry is enabled.
129    #[serde(default)]
130    pub volatile_system_requests: u64,
131    /// Volatile fields detected across those requests (#940), cumulative.
132    #[serde(default)]
133    pub volatile_fields_detected: u64,
134    /// Requests where the opt-in relocate (#974) moved volatile fields out of the
135    /// cacheable prefix into the uncached tail, cumulative. Non-zero only with
136    /// `cache_align_relocate` enabled — a pure cache win, not a regression.
137    #[serde(default)]
138    pub volatile_relocate_requests: u64,
139    /// Volatile fields relocated across those requests (#974), cumulative.
140    #[serde(default)]
141    pub volatile_fields_relocated: u64,
142}
143
144#[must_use]
145pub fn snapshot() -> CacheSafety {
146    let prose_requests = PROSE_REQUESTS.load(Ordering::Relaxed);
147    let safe = CACHE_SAFE_REQUESTS.load(Ordering::Relaxed);
148    CacheSafety {
149        prose_segments_compressed: PROSE_SEGMENTS.load(Ordering::Relaxed),
150        prose_requests,
151        cache_safe_ratio: ratio(safe, prose_requests),
152        cold_prefix_repacks: COLD_PREFIX_REPACKS.load(Ordering::Relaxed),
153        breakpoints_injected: BREAKPOINTS_INJECTED.load(Ordering::Relaxed),
154        volatile_system_requests: VOLATILE_SYSTEM_REQUESTS.load(Ordering::Relaxed),
155        volatile_fields_detected: VOLATILE_FIELDS_DETECTED.load(Ordering::Relaxed),
156        volatile_relocate_requests: VOLATILE_RELOCATE_REQUESTS.load(Ordering::Relaxed),
157        volatile_fields_relocated: VOLATILE_FIELDS_RELOCATED.load(Ordering::Relaxed),
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn ratio_is_one_when_empty() {
167        assert_eq!(ratio(0, 0), 1.0);
168    }
169
170    #[test]
171    fn ratio_reflects_unsafe_rewrites() {
172        assert_eq!(ratio(3, 3), 1.0);
173        assert_eq!(ratio(2, 4), 0.5);
174        assert_eq!(ratio(0, 2), 0.0);
175    }
176}