1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Context-window memory-protection model.
//!
//! Why: a Claude Code session that silently runs into its context limit loses
//! work and produces degraded output. trusty-mpm tracks token usage per
//! session and acts at configurable thresholds (warn / alert / auto-compact).
//! Centralizing the threshold math here keeps the daemon, TUI gauge, and
//! Telegram alert in agreement on what "85%" means.
//! What: `MemoryConfig` (the three thresholds), `MemoryUsage` (a token count
//! snapshot), and `MemoryPressure` (the derived warn/alert/compact level).
//! Test: `cargo test -p trusty-mpm-core` checks threshold classification at
//! the boundaries and that the config rejects nonsensical ordering.
use serde::{Deserialize, Serialize};
/// Memory-protection thresholds, as fractions of the context window.
///
/// Why: every deployment may want different headroom; defaults match the
/// task spec (warn 70%, alert 85%, auto-compact 90%).
/// What: three `f32` ratios in `0.0..=1.0`, plus a validity check.
/// Test: `default_config_is_valid` and `rejects_disordered_thresholds`.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct MemoryConfig {
/// Fraction at which a non-blocking warning is surfaced.
pub warn_at: f32,
/// Fraction at which an alert (Telegram push) fires.
pub alert_at: f32,
/// Fraction at which the daemon triggers an automatic compaction.
pub compact_at: f32,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
warn_at: 0.70,
alert_at: 0.85,
compact_at: 0.90,
}
}
}
impl MemoryConfig {
/// True if the thresholds are sane: each in `(0,1]` and strictly ordered.
///
/// Why: a misconfigured `compact_at < warn_at` would compact constantly.
/// What: validates range and `warn_at < alert_at < compact_at`.
/// Test: `rejects_disordered_thresholds`.
pub fn is_valid(&self) -> bool {
let in_range = |x: f32| x > 0.0 && x <= 1.0;
in_range(self.warn_at)
&& in_range(self.alert_at)
&& in_range(self.compact_at)
&& self.warn_at < self.alert_at
&& self.alert_at < self.compact_at
}
}
/// A point-in-time token-usage snapshot for one session.
///
/// Why: hook events (`TokenUsageUpdate`, `PostCompact`) report token counts;
/// the daemon stores the latest snapshot per session to drive the gauge.
/// What: used tokens vs. the model's context window size.
/// Test: `fraction_is_ratio` checks the derived ratio.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct MemoryUsage {
/// Tokens currently occupying the context window.
pub used_tokens: u64,
/// Total context window size for the session's model.
pub window_tokens: u64,
}
impl MemoryUsage {
/// Fraction of the context window currently used, clamped to `0.0..=1.0`.
///
/// Why: the TUI gauge and threshold classifier both need this ratio.
/// What: `used / window`, guarding against a zero window.
/// Test: `fraction_is_ratio`.
pub fn fraction(&self) -> f32 {
if self.window_tokens == 0 {
return 0.0;
}
(self.used_tokens as f32 / self.window_tokens as f32).clamp(0.0, 1.0)
}
/// Classify this usage against a `MemoryConfig` into a pressure level.
///
/// Why: the daemon needs one place that decides warn vs. alert vs. compact.
/// What: returns the highest threshold the current fraction has crossed.
/// Test: `pressure_classification_at_boundaries`.
pub fn pressure(&self, config: &MemoryConfig) -> MemoryPressure {
let f = self.fraction();
if f >= config.compact_at {
MemoryPressure::Compact
} else if f >= config.alert_at {
MemoryPressure::Alert
} else if f >= config.warn_at {
MemoryPressure::Warn
} else {
MemoryPressure::Ok
}
}
}
/// Derived memory-pressure level for a session.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum MemoryPressure {
/// Below the warn threshold — healthy.
Ok,
/// At/above warn — surface a non-blocking warning.
Warn,
/// At/above alert — push a Telegram alert.
Alert,
/// At/above compact — trigger automatic compaction.
Compact,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_is_valid() {
assert!(MemoryConfig::default().is_valid());
}
#[test]
fn rejects_disordered_thresholds() {
let bad = MemoryConfig {
warn_at: 0.9,
alert_at: 0.8,
compact_at: 0.95,
};
assert!(!bad.is_valid());
}
#[test]
fn fraction_is_ratio() {
let u = MemoryUsage {
used_tokens: 50,
window_tokens: 200,
};
assert!((u.fraction() - 0.25).abs() < f32::EPSILON);
let empty = MemoryUsage {
used_tokens: 10,
window_tokens: 0,
};
assert_eq!(empty.fraction(), 0.0);
}
#[test]
fn pressure_classification_at_boundaries() {
let cfg = MemoryConfig::default();
let at = |frac: f32| MemoryUsage {
used_tokens: (frac * 1000.0) as u64,
window_tokens: 1000,
};
assert_eq!(at(0.50).pressure(&cfg), MemoryPressure::Ok);
assert_eq!(at(0.70).pressure(&cfg), MemoryPressure::Warn);
assert_eq!(at(0.85).pressure(&cfg), MemoryPressure::Alert);
assert_eq!(at(0.90).pressure(&cfg), MemoryPressure::Compact);
assert_eq!(at(0.99).pressure(&cfg), MemoryPressure::Compact);
}
#[test]
fn pressure_levels_are_ordered() {
assert!(MemoryPressure::Ok < MemoryPressure::Warn);
assert!(MemoryPressure::Warn < MemoryPressure::Alert);
assert!(MemoryPressure::Alert < MemoryPressure::Compact);
}
}