lean-ctx 3.6.0

Context Runtime for AI Agents with CCP. 63 MCP tools, 10 read modes, 95+ 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 std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;

use anyhow::{Context, Result};

use crate::ipc;

fn data_dir() -> PathBuf {
    dirs::data_local_dir()
        .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/share"))
        .join("lean-ctx")
}

pub fn daemon_pid_path() -> PathBuf {
    data_dir().join("daemon.pid")
}

pub fn daemon_addr() -> ipc::DaemonAddr {
    ipc::DaemonAddr::default_for_current_os()
}

pub fn is_daemon_running() -> bool {
    let pid_path = daemon_pid_path();
    let Ok(contents) = fs::read_to_string(&pid_path) else {
        return false;
    };
    let Ok(pid) = contents.trim().parse::<u32>() else {
        return false;
    };
    if ipc::process::is_alive(pid) {
        return true;
    }
    let _ = fs::remove_file(&pid_path);
    ipc::cleanup(&daemon_addr());
    false
}

pub fn read_daemon_pid() -> Option<u32> {
    let contents = fs::read_to_string(daemon_pid_path()).ok()?;
    contents.trim().parse::<u32>().ok()
}

pub fn start_daemon(args: &[String]) -> Result<()> {
    if is_daemon_running() {
        let pid = read_daemon_pid().unwrap_or(0);
        anyhow::bail!("Daemon already running (PID {pid}). Use --stop to stop it first.");
    }

    ipc::cleanup(&daemon_addr());

    if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
        crate::config_io::cleanup_legacy_backups(&data_dir);
    }

    let exe = std::env::current_exe().context("cannot determine own executable path")?;

    let mut cmd_args = vec!["serve".to_string()];
    for arg in args {
        if arg == "--daemon" || arg == "-d" {
            continue;
        }
        cmd_args.push(arg.clone());
    }
    cmd_args.push("--_foreground-daemon".to_string());

    let log_dir = data_dir();
    let _ = fs::create_dir_all(&log_dir);
    let stderr_log = log_dir.join("daemon-stderr.log");
    let stderr_file = fs::OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(&stderr_log);
    let stderr_cfg = match stderr_file {
        Ok(f) => std::process::Stdio::from(f),
        Err(_) => std::process::Stdio::inherit(),
    };

    let child = Command::new(&exe)
        .args(&cmd_args)
        .stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::null())
        .stderr(stderr_cfg)
        .spawn()
        .with_context(|| format!("failed to spawn daemon: {}", exe.display()))?;

    let pid = child.id();
    write_pid_file(pid)?;

    std::thread::sleep(std::time::Duration::from_millis(200));

    if !ipc::process::is_alive(pid) {
        let _ = fs::remove_file(daemon_pid_path());
        let stderr_content = fs::read_to_string(&stderr_log).unwrap_or_default();
        let stderr_trimmed = stderr_content.trim();
        if stderr_trimmed.is_empty() {
            anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
        }
        anyhow::bail!("Daemon process exited immediately:\n{stderr_trimmed}");
    }

    let addr = daemon_addr();
    if crate::core::protocol::meta_visible() {
        eprintln!(
            "lean-ctx daemon started (PID {pid})\n  Endpoint: {}\n  PID file: {}",
            addr.display(),
            daemon_pid_path().display()
        );
    }

    Ok(())
}

pub fn stop_daemon() -> Result<()> {
    let pid_path = daemon_pid_path();

    let Some(pid) = read_daemon_pid() else {
        eprintln!("No daemon PID file found. Nothing to stop.");
        return Ok(());
    };

    if !ipc::process::is_alive(pid) {
        eprintln!("Daemon (PID {pid}) is not running. Cleaning up stale files.");
        ipc::cleanup(&daemon_addr());
        let _ = fs::remove_file(&pid_path);
        return Ok(());
    }

    // Step 1: Try graceful HTTP shutdown.
    let http_shutdown_ok = try_http_shutdown();

    // Step 2: Wait up to 3s for process exit.
    if http_shutdown_ok {
        for _ in 0..30 {
            std::thread::sleep(std::time::Duration::from_millis(100));
            if !ipc::process::is_alive(pid) {
                break;
            }
        }
    }

    // Step 3: OS-level graceful termination if still alive.
    if ipc::process::is_alive(pid) {
        let _ = ipc::process::terminate_gracefully(pid);
        for _ in 0..20 {
            std::thread::sleep(std::time::Duration::from_millis(100));
            if !ipc::process::is_alive(pid) {
                break;
            }
        }
    }

    // Step 4: Force kill as last resort.
    if ipc::process::is_alive(pid) {
        eprintln!("Daemon (PID {pid}) did not stop gracefully, force killing.");
        let _ = ipc::process::force_kill(pid);
        std::thread::sleep(std::time::Duration::from_millis(100));
    }

    let _ = fs::remove_file(&pid_path);
    ipc::cleanup(&daemon_addr());
    eprintln!("lean-ctx daemon stopped (PID {pid}).");
    Ok(())
}

fn try_http_shutdown() -> bool {
    let Ok(rt) = tokio::runtime::Runtime::new() else {
        return false;
    };

    rt.block_on(async {
        crate::daemon_client::daemon_request("POST", "/v1/shutdown", "")
            .await
            .is_ok()
    })
}

pub fn daemon_status() -> String {
    let addr = daemon_addr();
    if let Some(pid) = read_daemon_pid() {
        if ipc::process::is_alive(pid) {
            let listening = addr.is_listening();
            return format!(
                "Daemon running (PID {pid})\n  Endpoint: {} ({})\n  PID file: {}",
                addr.display(),
                if listening { "ready" } else { "missing" },
                daemon_pid_path().display()
            );
        }
        return format!("Daemon not running (stale PID file for PID {pid})");
    }
    "Daemon not running".to_string()
}

fn write_pid_file(pid: u32) -> Result<()> {
    let pid_path = daemon_pid_path();
    if let Some(parent) = pid_path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| format!("cannot create dir: {}", parent.display()))?;
    }
    let mut f = fs::File::create(&pid_path)
        .with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
    write!(f, "{pid}")?;
    Ok(())
}

/// Write the current process's PID. Called from the foreground-daemon process.
pub fn init_foreground_daemon() -> Result<()> {
    let pid = std::process::id();
    write_pid_file(pid)?;
    Ok(())
}

/// Cleanup PID file and IPC endpoint on shutdown.
pub fn cleanup_daemon_files() {
    let _ = fs::remove_file(daemon_pid_path());
    ipc::cleanup(&daemon_addr());
}