use crate::compact::{
calculate_token_warning_state as core_calculate_token_warning_state,
get_auto_compact_threshold as core_get_auto_compact_threshold,
get_effective_context_window_size as core_get_effective_context_window_size,
CompactionResult, TokenWarningState,
};
use crate::types::Message;
use crate::utils::env_utils::is_env_truthy;
#[derive(Debug, Clone, Default)]
pub struct RecompactionInfo {
pub is_recompaction_in_chain: bool,
pub turns_since_previous_compact: i32,
pub previous_compact_turn_id: Option<String>,
pub auto_compact_threshold: usize,
pub query_source: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct AutoCompactResult {
pub was_compacted: bool,
pub compaction_result: Option<CompactionResult>,
pub consecutive_failures: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct AutoCompactTrackingState {
pub compacted: bool,
pub turn_counter: usize,
pub turn_id: String,
pub consecutive_failures: usize,
}
impl AutoCompactTrackingState {
pub fn new() -> Self {
Self {
compacted: false,
turn_counter: 0,
turn_id: uuid::Uuid::new_v4().to_string(),
consecutive_failures: 0,
}
}
}
pub use crate::compact::{
AUTOCOMPACT_BUFFER_TOKENS, ERROR_THRESHOLD_BUFFER_TOKENS, MANUAL_COMPACT_BUFFER_TOKENS,
MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES,
};
pub fn get_effective_context_window_size(model: &str) -> usize {
core_get_effective_context_window_size(model) as usize
}
pub fn get_auto_compact_threshold(model: &str) -> usize {
core_get_auto_compact_threshold(model) as usize
}
pub fn calculate_token_warning_state(token_usage: usize, model: &str) -> TokenWarningState {
core_calculate_token_warning_state(token_usage as u32, model)
}
pub fn is_auto_compact_enabled() -> bool {
if is_env_truthy(Some("DISABLE_COMPACT")) {
return false;
}
if is_env_truthy(Some("DISABLE_AUTO_COMPACT")) {
return false;
}
true
}
fn is_forked_agent_query_source(query_source: Option<&str>) -> bool {
matches!(query_source, Some("session_memory") | Some("compact"))
}
fn is_marble_origami_query_source(query_source: Option<&str>) -> bool {
matches!(query_source, Some("marble_origami"))
}
pub fn should_auto_compact(
messages: &[Message],
model: &str,
query_source: Option<&str>,
snip_tokens_freed: usize,
) -> bool {
if is_forked_agent_query_source(query_source) {
return false;
}
if !is_auto_compact_enabled() {
return false;
}
let token_count = estimate_token_count(messages).saturating_sub(snip_tokens_freed);
let threshold = get_auto_compact_threshold(model);
let effective_window = get_effective_context_window_size(model);
log::debug!(
"autocompact: tokens={} threshold={} effective_window={}{}",
token_count,
threshold,
effective_window,
if snip_tokens_freed > 0 {
format!(" snipFreed={}", snip_tokens_freed)
} else {
String::new()
}
);
let state = calculate_token_warning_state(token_count, model);
state.is_above_auto_compact_threshold
}
pub async fn auto_compact_if_needed(
messages: &[Message],
model: &str,
query_source: Option<&str>,
tracking: Option<&AutoCompactTrackingState>,
snip_tokens_freed: usize,
) -> AutoCompactResult {
if is_env_truthy(Some("DISABLE_COMPACT")) {
return AutoCompactResult::default();
}
if let Some(t) = tracking {
if t.consecutive_failures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES as usize {
return AutoCompactResult::default();
}
}
let should_compact = should_auto_compact(messages, model, query_source, snip_tokens_freed);
if !should_compact {
return AutoCompactResult::default();
}
let recompaction_info = RecompactionInfo {
is_recompaction_in_chain: tracking.map(|t| t.compacted).unwrap_or(false),
turns_since_previous_compact: tracking.map(|t| t.turn_counter as i32).unwrap_or(-1),
previous_compact_turn_id: tracking.map(|t| t.turn_id.clone()),
auto_compact_threshold: get_auto_compact_threshold(model),
query_source: query_source.map(|s| s.to_string()),
};
log::debug!(
"autocompact: triggering compaction with recompaction_info: {:?}",
recompaction_info
);
let prev_failures = tracking.map(|t| t.consecutive_failures).unwrap_or(0);
let next_failures = prev_failures + 1;
if next_failures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES as usize {
log::warn!(
"autocompact: circuit breaker tripped after {} consecutive failures — skipping future attempts this session",
next_failures
);
}
AutoCompactResult {
was_compacted: false,
compaction_result: None,
consecutive_failures: Some(next_failures),
}
}
fn estimate_token_count(messages: &[Message]) -> usize {
messages.iter().map(|m| m.content.len() / 4).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::MessageRole;
#[test]
fn test_get_effective_context_window_size() {
let window = get_effective_context_window_size("claude-sonnet-4-6");
assert!(window > 0);
}
#[test]
fn test_get_auto_compact_threshold() {
let threshold = get_auto_compact_threshold("claude-sonnet-4-6");
let effective = get_effective_context_window_size("claude-sonnet-4-6");
assert!(threshold < effective);
}
#[test]
fn test_calculate_token_warning_state() {
let state = calculate_token_warning_state(50_000, "claude-sonnet-4-6");
assert!(!state.is_above_warning_threshold);
assert!(!state.is_above_error_threshold);
assert!(!state.is_above_auto_compact_threshold);
assert!(state.percent_left > 50.0);
}
#[test]
fn test_calculate_token_warning_state_at_threshold() {
let threshold = get_auto_compact_threshold("claude-sonnet-4-6");
let state = calculate_token_warning_state(threshold as usize, "claude-sonnet-4-6");
assert!(state.is_above_auto_compact_threshold);
}
#[test]
fn test_is_auto_compact_enabled_default() {
let result = is_auto_compact_enabled();
assert!(result || !result); }
#[test]
fn test_should_auto_compact_empty_messages() {
let messages: Vec<Message> = vec![];
let result = should_auto_compact(&messages, "claude-sonnet-4-6", None, 0);
assert!(!result);
}
#[test]
fn test_should_auto_compact_forked_agent_guards() {
let messages: Vec<Message> = vec![];
let result = should_auto_compact(&messages, "claude-sonnet-4-6", Some("session_memory"), 0);
assert!(!result);
let result = should_auto_compact(&messages, "claude-sonnet-4-6", Some("compact"), 0);
assert!(!result);
}
#[test]
fn test_auto_compact_tracking_state() {
let state = AutoCompactTrackingState::new();
assert!(!state.compacted);
assert_eq!(state.turn_counter, 0);
assert!(!state.turn_id.is_empty());
assert_eq!(state.consecutive_failures, 0);
}
#[test]
fn test_recompaction_info_default() {
let info = RecompactionInfo::default();
assert!(!info.is_recompaction_in_chain);
assert_eq!(info.turns_since_previous_compact, 0);
assert!(info.previous_compact_turn_id.is_none());
}
#[test]
fn test_auto_compact_result_default() {
let result = AutoCompactResult::default();
assert!(!result.was_compacted);
assert!(result.compaction_result.is_none());
}
}