Skip to main content

matrixcode_core/compress/
config.rs

1//! Compression configuration and bias settings.
2
3use anyhow::Result;
4
5// ============================================================================
6// Constants
7// ============================================================================
8
9/// Compression trigger threshold (percentage of context window).
10/// Lowered to 0.5 to compress earlier for long conversations (128K context -> 64K threshold)
11pub const DEFAULT_COMPRESSION_THRESHOLD: f64 = 0.5;
12
13/// Minimum messages to keep after compression.
14/// Increased to preserve more recent context for continuity
15pub const MIN_MESSAGES_TO_KEEP: usize = 20;
16
17/// Target ratio after compression (keep this fraction of tokens).
18pub const DEFAULT_TARGET_RATIO: f64 = 0.4;
19
20/// Default model for summarization.
21pub const DEFAULT_COMPRESSOR_MODEL: &str = "claude-3-5-haiku-20241022";
22
23// ============================================================================
24// Circuit Breaker (NEW - from Claude Code)
25// ============================================================================
26
27/// Maximum consecutive compression failures before stopping retries.
28/// Claude Code: "1,279 sessions had 50+ consecutive failures, wasting ~250K API calls/day"
29pub const MAX_CONSECUTIVE_FAILURES: u32 = 3;
30
31/// Token buffers for threshold levels (from Claude Code).
32pub const AUTOCOMPACT_BUFFER_TOKENS: u32 = 13_000;
33pub const WARNING_THRESHOLD_BUFFER_TOKENS: u32 = 20_000;
34pub const ERROR_THRESHOLD_BUFFER_TOKENS: u32 = 20_000;
35pub const MANUAL_COMPACT_BUFFER_TOKENS: u32 = 3_000;
36
37/// Time-based microcompact threshold (minutes since last assistant message).
38/// When gap exceeds this, server cache has expired - clear old tool results.
39pub const TIME_BASED_MC_GAP_THRESHOLD_MINUTES: u32 = 5;
40
41/// Message to replace cleared tool result content (from Claude Code).
42pub const TIME_BASED_MC_CLEARED_MESSAGE: &str = "[Old tool result content cleared]";
43
44// ============================================================================
45// Threshold Levels (NEW)
46// ============================================================================
47
48/// Threshold level for compression warnings.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ThresholdLevel {
51    /// Normal - no action needed
52    Normal,
53    /// Warning - approaching limit, warn user
54    Warning,
55    /// Error - near limit, strongly suggest compact
56    Error,
57    /// Blocking - must compact before continuing
58    Blocking,
59}
60
61// ============================================================================
62// Helper Functions
63// ============================================================================
64
65/// Format token count for display.
66pub fn format_tokens(n: u32) -> String {
67    if n < 1_000 {
68        n.to_string()
69    } else if n < 10_000 {
70        format!("{:.1}K", n as f64 / 1_000.0)
71    } else {
72        format!("{:.0}K", n as f64 / 1_000.0)
73    }
74}
75
76// ============================================================================
77// Compression Bias
78// ============================================================================
79
80/// Compression bias - controls what to prioritize during compression.
81#[derive(Debug, Clone, Default)]
82pub struct CompressionBias {
83    /// Preserve tool calls and their results.
84    pub preserve_tools: bool,
85    /// Preserve thinking blocks.
86    pub preserve_thinking: bool,
87    /// Preserve user questions.
88    pub preserve_user_questions: bool,
89    /// Compact long outputs instead of removing.
90    pub compact_long_outputs: bool,
91    /// Aggressive mode - remove more content.
92    pub aggressive: bool,
93    /// Custom keywords to preserve.
94    pub preserve_keywords: Vec<String>,
95}
96
97impl CompressionBias {
98    /// Default bias - balanced preservation.
99    pub fn balanced() -> Self {
100        Self {
101            preserve_tools: true,
102            preserve_thinking: false,
103            preserve_user_questions: true,
104            compact_long_outputs: false,
105            aggressive: false,
106            preserve_keywords: vec![
107                "决定".to_string(),
108                "decision".to_string(),
109                "重要".to_string(),
110                "important".to_string(),
111                "关键".to_string(),
112                "key".to_string(),
113            ],
114        }
115    }
116
117    /// Preserve all important content.
118    pub fn preserve_important() -> Self {
119        Self {
120            preserve_tools: true,
121            preserve_thinking: true,
122            preserve_user_questions: true,
123            compact_long_outputs: true,
124            aggressive: false,
125            preserve_keywords: vec![
126                "决定".to_string(),
127                "decision".to_string(),
128                "重要".to_string(),
129                "important".to_string(),
130                "关键".to_string(),
131                "key".to_string(),
132                "完成".to_string(),
133                "done".to_string(),
134                "成功".to_string(),
135                "success".to_string(),
136            ],
137        }
138    }
139
140    /// Aggressive compression.
141    pub fn aggressive() -> Self {
142        Self {
143            preserve_tools: false,
144            preserve_thinking: false,
145            preserve_user_questions: false,
146            compact_long_outputs: false,
147            aggressive: true,
148            preserve_keywords: vec![],
149        }
150    }
151
152    /// Focus on preserving tool operations.
153    pub fn tool_focused() -> Self {
154        Self {
155            preserve_tools: true,
156            preserve_thinking: false,
157            preserve_user_questions: false,
158            compact_long_outputs: false,
159            aggressive: false,
160            preserve_keywords: vec![
161                "工具".to_string(),
162                "tool".to_string(),
163                "执行".to_string(),
164                "execute".to_string(),
165                "文件".to_string(),
166                "file".to_string(),
167            ],
168        }
169    }
170
171    /// Parse bias from a string specification.
172    pub fn parse(spec: &str) -> Result<Self> {
173        let spec = spec.trim().to_lowercase();
174
175        if spec == "balanced" || spec == "default" || spec.is_empty() {
176            return Ok(Self::balanced());
177        }
178        if spec == "aggressive" {
179            return Ok(Self::aggressive());
180        }
181        if spec == "preserve_important" || spec == "important" {
182            return Ok(Self::preserve_important());
183        }
184        if spec == "tool_focused" || spec == "tools" {
185            return Ok(Self::tool_focused());
186        }
187
188        // Parse custom specification
189        let mut bias = Self::default();
190
191        for part in spec.split_whitespace() {
192            if let Some(preserve_list) = part.strip_prefix("preserve:") {
193                for item in preserve_list.split(',') {
194                    match item.trim() {
195                        "tools" | "tool" => bias.preserve_tools = true,
196                        "thinking" | "think" => bias.preserve_thinking = true,
197                        "user" | "questions" => bias.preserve_user_questions = true,
198                        "compact" | "long" => bias.compact_long_outputs = true,
199                        _ => {}
200                    }
201                }
202            } else if let Some(keyword_list) = part.strip_prefix("keywords:") {
203                bias.preserve_keywords = keyword_list
204                    .split(',')
205                    .map(|k| k.trim().to_string())
206                    .filter(|k| !k.is_empty())
207                    .collect();
208            } else if part == "aggressive" {
209                bias.aggressive = true;
210            }
211        }
212
213        Ok(bias)
214    }
215
216    /// Format bias for display.
217    pub fn format(&self) -> String {
218        let mut parts: Vec<String> = Vec::new();
219
220        if self.preserve_tools {
221            parts.push("tools".to_string());
222        }
223        if self.preserve_thinking {
224            parts.push("thinking".to_string());
225        }
226        if self.preserve_user_questions {
227            parts.push("user".to_string());
228        }
229        if self.compact_long_outputs {
230            parts.push("compact".to_string());
231        }
232        if self.aggressive {
233            parts.push("aggressive".to_string());
234        }
235
236        if !self.preserve_keywords.is_empty() {
237            parts.push(format!("keywords:{}", self.preserve_keywords.join(",")));
238        }
239
240        if parts.is_empty() {
241            "default".to_string()
242        } else {
243            parts.join(", ")
244        }
245    }
246}
247
248// ============================================================================
249// Compression Configuration
250// ============================================================================
251
252/// Configuration for context compression.
253#[derive(Debug, Clone)]
254pub struct CompressionConfig {
255    /// Threshold (0.0-1.0) at which to trigger compression.
256    pub threshold: f64,
257    /// Maximum tokens to target after compression.
258    pub target_ratio: f64,
259    /// Minimum recent messages to always preserve.
260    pub min_preserve_messages: usize,
261    /// Whether to use AI summarization.
262    pub use_summarization: bool,
263    /// Optional model name for summarization.
264    pub compressor_model: Option<String>,
265    /// Compression bias.
266    pub bias: CompressionBias,
267}
268
269impl Default for CompressionConfig {
270    fn default() -> Self {
271        Self {
272            threshold: DEFAULT_COMPRESSION_THRESHOLD,
273            target_ratio: DEFAULT_TARGET_RATIO,
274            min_preserve_messages: MIN_MESSAGES_TO_KEEP,
275            use_summarization: true,
276            compressor_model: None,
277            bias: CompressionBias::balanced(),
278        }
279    }
280}
281
282impl CompressionConfig {
283    /// Get the compressor model name.
284    pub fn compressor_model_name(&self) -> &str {
285        self.compressor_model
286            .as_deref()
287            .unwrap_or(DEFAULT_COMPRESSOR_MODEL)
288    }
289
290    /// Calculate threshold level based on token usage.
291    /// Returns the level and percentage of context remaining.
292    pub fn calculate_threshold_level(
293        token_usage: u32,
294        context_window: u32,
295    ) -> (ThresholdLevel, u32) {
296        let percent_left = if context_window > 0 {
297            ((context_window - token_usage) as f64 / context_window as f64 * 100.0) as u32
298        } else {
299            0
300        };
301
302        // Calculate thresholds
303        let auto_threshold = context_window.saturating_sub(AUTOCOMPACT_BUFFER_TOKENS);
304        let warning_threshold = auto_threshold.saturating_sub(WARNING_THRESHOLD_BUFFER_TOKENS);
305        let error_threshold = auto_threshold.saturating_sub(ERROR_THRESHOLD_BUFFER_TOKENS);
306        let blocking_threshold = context_window.saturating_sub(MANUAL_COMPACT_BUFFER_TOKENS);
307
308        let level = if token_usage >= blocking_threshold {
309            ThresholdLevel::Blocking
310        } else if token_usage >= error_threshold {
311            ThresholdLevel::Error
312        } else if token_usage >= warning_threshold {
313            ThresholdLevel::Warning
314        } else {
315            ThresholdLevel::Normal
316        };
317
318        (level, percent_left.max(0))
319    }
320}
321
322// ============================================================================
323// Circuit Breaker State (NEW)
324// ============================================================================
325
326/// State for circuit breaker to prevent infinite retry loops.
327#[derive(Debug, Clone, Default)]
328pub struct CircuitBreakerState {
329    /// Number of consecutive compression failures.
330    pub consecutive_failures: u32,
331    /// Whether circuit breaker has tripped.
332    pub is_tripped: bool,
333    /// Last failure timestamp (for reset timeout).
334    pub last_failure_time: Option<u64>,
335}
336
337impl CircuitBreakerState {
338    /// Create a new circuit breaker state.
339    pub fn new() -> Self {
340        Self::default()
341    }
342
343    /// Record a failure. Returns true if circuit breaker should trip.
344    pub fn record_failure(&mut self) -> bool {
345        self.consecutive_failures += 1;
346        self.last_failure_time = Some(std::time::SystemTime::now()
347            .duration_since(std::time::UNIX_EPOCH)
348            .unwrap_or_default()
349            .as_secs());
350
351        if self.consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
352            self.is_tripped = true;
353            return true;
354        }
355        false
356    }
357
358    /// Record a success. Resets failure count.
359    pub fn record_success(&mut self) {
360        self.consecutive_failures = 0;
361        self.is_tripped = false;
362        self.last_failure_time = None;
363    }
364
365    /// Check if compression should be skipped due to circuit breaker.
366    pub fn should_skip(&self) -> bool {
367        self.is_tripped
368    }
369
370    /// Reset the circuit breaker (manual override).
371    pub fn reset(&mut self) {
372        self.consecutive_failures = 0;
373        self.is_tripped = false;
374        self.last_failure_time = None;
375    }
376}