lean-ctx 3.1.3

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 similar::{ChangeTag, TextDiff};

use crate::core::cache::SessionCache;
use crate::core::protocol;
use crate::core::tokens::count_tokens;

pub fn handle(cache: &mut SessionCache, path: &str) -> String {
    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => return format!("Error: {e}"),
    };

    let short = protocol::shorten_path(path);
    let new_lines = content.lines().count();
    let new_tokens = count_tokens(&content);

    let cached_entry = match cache.get(path) {
        Some(entry) => entry,
        None => {
            cache.store(path, content.clone());
            return format!(
                "{short} [first read, {new_lines}L, {new_tokens} tok] — cached for future deltas"
            );
        }
    };
    let old_content = cached_entry.content.clone();
    let old_hash = cached_entry.hash.clone();

    let new_hash = compute_hash(&content);
    if old_hash == new_hash {
        return format!("{short} cached (no changes)");
    }

    let diff = TextDiff::from_lines(&old_content, &content);
    let mut hunks = Vec::new();
    let mut additions = 0usize;
    let mut deletions = 0usize;

    for group in diff.grouped_ops(3) {
        let mut hunk_lines = Vec::new();
        for op in &group {
            for change in diff.iter_changes(op) {
                let line_no = change.new_index().or(change.old_index()).map(|i| i + 1);
                let text = change.value().trim_end_matches('\n');
                match change.tag() {
                    ChangeTag::Insert => {
                        additions += 1;
                        if let Some(n) = line_no {
                            hunk_lines.push(format!("+{n}: {text}"));
                        }
                    }
                    ChangeTag::Delete => {
                        deletions += 1;
                        if let Some(n) = line_no {
                            hunk_lines.push(format!("-{n}: {text}"));
                        }
                    }
                    ChangeTag::Equal => {
                        if let Some(n) = line_no {
                            hunk_lines.push(format!(" {n}: {text}"));
                        }
                    }
                }
            }
        }
        if !hunk_lines.is_empty() {
            hunks.push(hunk_lines.join("\n"));
        }
    }

    cache.store(path, content);

    let delta_output = hunks.join("\n---\n");
    let delta_tokens = count_tokens(&delta_output);
    let savings = if new_tokens > 0 {
        ((new_tokens as f64 - delta_tokens as f64) / new_tokens as f64 * 100.0) as u32
    } else {
        0
    };

    format!(
        "{short} [delta] +{additions}/-{deletions} lines ({delta_tokens} tok, {savings}% saved vs full)\n{delta_output}"
    )
}

fn compute_hash(content: &str) -> String {
    use md5::{Digest, Md5};
    let mut hasher = Md5::new();
    hasher.update(content.as_bytes());
    format!("{:x}", hasher.finalize())
}