Skip to main content

lean_ctx/proxy/
cache_safety.rs

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