lean-ctx 3.6.2

Context Runtime for AI Agents with CCP. 51 MCP tools, 10 read modes, 60+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, 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 super::types::SessionState;

pub(crate) fn normalize_loaded_session(mut session: SessionState) -> SessionState {
    if matches!(session.project_root.as_deref(), Some(r) if r.trim().is_empty()) {
        session.project_root = None;
    }
    if matches!(session.shell_cwd.as_deref(), Some(c) if c.trim().is_empty()) {
        session.shell_cwd = None;
    }

    if let (Some(ref root), Some(ref cwd)) = (&session.project_root, &session.shell_cwd) {
        let root_p = std::path::Path::new(root);
        let cwd_p = std::path::Path::new(cwd);
        let root_looks_real = has_project_marker(root_p);
        let cwd_looks_real = has_project_marker(cwd_p);

        if !root_looks_real && cwd_looks_real && is_agent_or_temp_dir(root_p) {
            session.project_root = Some(cwd.clone());
        }
    }

    if session.compression_level.is_empty() {
        if session.terse_mode {
            session.compression_level = "lite".to_string();
        } else if let Some(env_level) = crate::core::config::CompressionLevel::from_env() {
            session.compression_level = env_level.label().to_string();
            session.terse_mode = env_level.is_active();
        } else {
            let profile = crate::core::profiles::active_profile();
            if profile.compression.terse_mode_effective() {
                session.compression_level = "lite".to_string();
                session.terse_mode = true;
            } else {
                session.compression_level = "off".to_string();
            }
        }
    } else if !session.terse_mode {
        let level =
            crate::core::config::CompressionLevel::from_str_label(&session.compression_level)
                .unwrap_or_default();
        session.terse_mode = level.is_active();
    }

    session
}

pub(crate) fn session_matches_project_root(
    session: &SessionState,
    target_root: &std::path::Path,
) -> bool {
    if let Some(root) = session.project_root.as_deref() {
        let root_path =
            crate::core::pathutil::safe_canonicalize_or_self(std::path::Path::new(root));
        if root_path == target_root {
            return true;
        }
        if has_project_marker(&root_path) {
            return false;
        }
    }

    if let Some(cwd) = session.shell_cwd.as_deref() {
        let cwd_path = crate::core::pathutil::safe_canonicalize_or_self(std::path::Path::new(cwd));
        return cwd_path == target_root || cwd_path.starts_with(target_root);
    }

    false
}

fn has_project_marker(dir: &std::path::Path) -> bool {
    const MARKERS: &[&str] = &[
        ".git",
        ".lean-ctx.toml",
        "Cargo.toml",
        "package.json",
        "go.mod",
        "pyproject.toml",
        ".planning",
    ];
    MARKERS.iter().any(|m| dir.join(m).exists())
}

fn is_agent_or_temp_dir(dir: &std::path::Path) -> bool {
    let s = dir.to_string_lossy();
    s.contains("/.claude")
        || s.contains("/.codex")
        || s.contains("/var/folders/")
        || s.contains("/tmp/")
        || s.contains("\\.claude")
        || s.contains("\\.codex")
        || s.contains("\\AppData\\Local\\Temp")
        || s.contains("\\Temp\\")
}