Skip to main content

ai_agent/services/compact/
reactive_compact.rs

1// Source: ~/claudecode/openclaudecode/src/services/compact/reactiveCompact.ts
2//! Reactive compact module for error recovery.
3//!
4//! When the API rejects a request due to context size (413 / media too large),
5//! reactive compact tries to recover by compacting the oldest group of messages
6//! that contains the overflow, then retrying the request.
7
8use crate::compact::estimate_token_count;
9use crate::services::compact::grouping::group_messages_by_api_round;
10use crate::types::Message;
11
12/// Result of a reactive compact attempt.
13#[derive(Debug, Clone)]
14pub struct ReactiveCompactResult {
15    /// The compacted message list to retry with.
16    pub messages: Vec<Message>,
17    /// Whether a compact actually happened.
18    pub compacted: bool,
19}
20
21/// Attempt reactive compact to reduce context size for retry.
22///
23/// Groups messages by API round, then drops the oldest groups until the
24/// total token count falls below the effective context window size.
25pub fn run_reactive_compact(
26    messages: &[Message],
27    model: &str,
28) -> Result<ReactiveCompactResult, String> {
29    let effective_window = crate::compact::get_effective_context_window_size(model) as usize;
30    let token_count = estimate_token_count(messages, effective_window as u32) as usize;
31
32    if token_count <= effective_window {
33        return Ok(ReactiveCompactResult {
34            messages: messages.to_vec(),
35            compacted: false,
36        });
37    }
38
39    let groups = group_messages_by_api_round(messages);
40    if groups.len() <= 1 {
41        return Ok(ReactiveCompactResult {
42            messages: messages.to_vec(),
43            compacted: false,
44        });
45    }
46
47    // Drop oldest groups until under the window
48    let mut remaining: Vec<Message> = messages.to_vec();
49    for group in &groups {
50        if remaining.len() <= 4 {
51            break;
52        }
53
54        let new_len = remaining.len().saturating_sub(group.len());
55        if new_len < 4 {
56            break;
57        }
58
59        // Remove this group from remaining
60        remaining = remove_group(&remaining, group);
61
62        let new_tokens = estimate_token_count(&remaining, effective_window as u32) as usize;
63        if new_tokens < effective_window {
64            break;
65        }
66    }
67
68    Ok(ReactiveCompactResult {
69        messages: remaining,
70        compacted: true,
71    })
72}
73
74/// Remove a group of messages from the full list (by position matching).
75fn remove_group(all: &[Message], group: &[Message]) -> Vec<Message> {
76    if group.len() >= all.len() {
77        return Vec::new();
78    }
79
80    // Find the group as a contiguous slice in all
81    for i in 0..=all.len().saturating_sub(group.len()) {
82        if all[i..i + group.len()]
83            .iter()
84            .zip(group.iter())
85            .all(|(a, b)| a.content == b.content && a.role == b.role)
86        {
87            let mut result = all[..i].to_vec();
88            result.extend_from_slice(&all[i + group.len()..]);
89            return result;
90        }
91    }
92
93    // Fallback: drop the oldest N messages where N = group size
94    let keep = all.len().saturating_sub(group.len());
95    all[keep..].to_vec()
96}
97
98/// Check if reactive compact is available.
99pub fn is_reactive_compact_enabled() -> bool {
100    !crate::utils::env_utils::is_env_truthy(Some("DISABLE_REACTIVE_COMPACT"))
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_reactive_compact_below_threshold() {
109        let messages = vec![Message {
110            role: crate::types::MessageRole::User,
111            content: "Hello".to_string(),
112            ..Default::default()
113        }];
114        let result = run_reactive_compact(&messages, "claude-sonnet-4-6");
115        assert!(result.is_ok());
116        assert!(!result.unwrap().compacted);
117    }
118
119    #[test]
120    fn test_reactive_compact_enabled() {
121        assert!(is_reactive_compact_enabled());
122    }
123}