lean-ctx 3.1.5

Context Runtime for AI Agents with CCP. 42 MCP tools, 10 read modes, 90+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing + diaries, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24 AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use regex::Regex;
use std::sync::OnceLock;

static TIMESTAMP_RE: OnceLock<Regex> = OnceLock::new();

fn timestamp_re() -> &'static Regex {
    TIMESTAMP_RE.get_or_init(|| {
        Regex::new(r"^\[?\d{4}[-/]\d{2}[-/]\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]]*\]?\s*").unwrap()
    })
}

pub fn compress(output: &str) -> Option<String> {
    let lines: Vec<&str> = output.lines().collect();
    if lines.len() <= 10 {
        return None;
    }

    let mut deduped: Vec<(String, u32)> = Vec::new();
    let mut error_lines = Vec::new();

    for line in &lines {
        let stripped = timestamp_re().replace(line, "").trim().to_string();
        if stripped.is_empty() {
            continue;
        }

        let lower = stripped.to_lowercase();
        if lower.contains("error")
            || lower.contains("fatal")
            || lower.contains("panic")
            || lower.contains("exception")
        {
            error_lines.push(stripped.clone());
        }

        if let Some(last) = deduped.last_mut() {
            if last.0 == stripped {
                last.1 += 1;
                continue;
            }
        }
        deduped.push((stripped, 1));
    }

    let result: Vec<String> = deduped
        .iter()
        .map(|(line, count)| {
            if *count > 1 {
                format!("{line} (x{count})")
            } else {
                line.clone()
            }
        })
        .collect();

    let mut parts = Vec::new();
    parts.push(format!("{} lines → {} unique", lines.len(), deduped.len()));

    if !error_lines.is_empty() {
        parts.push(format!("{} errors:", error_lines.len()));
        for e in error_lines.iter().take(5) {
            parts.push(format!("  {e}"));
        }
        if error_lines.len() > 5 {
            parts.push(format!("  ... +{} more errors", error_lines.len() - 5));
        }
    }

    if result.len() > 30 {
        let tail = &result[result.len() - 15..];
        parts.push(format!("last 15 unique lines:\n{}", tail.join("\n")));
    } else {
        parts.push(result.join("\n"));
    }

    Some(parts.join("\n"))
}