daat-locus 0.2.0

A long-running local agent runtime with memory, workflows, apps, and sleep-time self-improvement.
use std::path::{Path, PathBuf};

use miette::{Result, miette};

use crate::{
    daat_locus_paths::{DaatLocusPaths, daat_locus_paths},
    daemon::connect_daemon_status,
    reasoning::compiled::COMPILED_DIR_NAME,
};

async fn get_daat_locus_home() -> PathBuf {
    daat_locus_paths().await.root().to_path_buf()
}

async fn reject_if_daemon_running(reset_name: &str) -> Result<()> {
    if connect_daemon_status().await.is_ok() {
        return Err(miette!(
            "{reset_name} refused while DaatLocus daemon is running; run `daat-locus daemon stop` first"
        ));
    }
    Ok(())
}

pub async fn run_memory_reset() -> Result<()> {
    let home = get_daat_locus_home().await;
    reject_if_daemon_running("memory reset").await?;
    clear_memory_state(&home).await?;

    println!(
        "[memory-reset] reset memory persistence under {}",
        home.display()
    );
    println!("[memory-reset] cleared: runtime_conversation");
    println!("[memory-reset] cleared: runtime_error_cases.jsonl, reasoning_traces.jsonl");
    println!("[memory-reset] cleared: current plan");
    println!("[memory-reset] preserved: config/, state/, artifacts/, logs/");

    Ok(())
}

async fn clear_memory_state(home: &Path) -> Result<()> {
    let paths = DaatLocusPaths::from_root(home.to_path_buf());
    clear_files(&[
        paths.memory_file("runtime_conversation.json"),
        paths.memory_file("runtime_conversation"),
        paths.memory_file("plan"),
        paths.journal_file("runtime_error_cases.jsonl"),
        paths.journal_file("reasoning_traces.jsonl"),
    ])
    .await?;

    Ok(())
}

pub async fn run_state_reset() -> Result<()> {
    let home = get_daat_locus_home().await;
    reject_if_daemon_running("state reset").await?;
    let cleared = clear_state_files(&home).await?;

    println!("[state-reset] reset runtime state under {}", home.display());
    if cleared.is_empty() {
        println!("[state-reset] nothing to remove");
    } else {
        println!("[state-reset] cleared: {}", cleared.join(", "));
    }
    println!("[state-reset] preserved: config/, memory/, artifacts/, logs/");

    Ok(())
}

async fn clear_state_files(home: &Path) -> Result<Vec<String>> {
    let paths = DaatLocusPaths::from_root(home.to_path_buf());
    let files = [
        "events",
        "pending_work_queue",
        "telegram_transport_state",
        "telegram_update_offset",
    ];
    clear_named_files(paths.state_dir(), &files).await
}

pub async fn run_compile_reset() -> Result<()> {
    let home = get_daat_locus_home().await;
    reject_if_daemon_running("compile reset").await?;
    let cleared = clear_compiled_artifacts(&home).await?;

    println!(
        "[compile-reset] cleared compile/evaluation artifacts under {}",
        home.display()
    );
    if cleared.is_empty() {
        println!("[compile-reset] nothing to remove");
    } else {
        println!("[compile-reset] cleared: {}", cleared.join(", "));
    }
    println!("[compile-reset] preserved: config/, state/, memory, logs/");

    Ok(())
}

async fn clear_compiled_artifacts(home: &Path) -> Result<Vec<String>> {
    let mut cleared = Vec::new();
    let paths = DaatLocusPaths::from_root(home.to_path_buf());

    for dir_name in [COMPILED_DIR_NAME, "evaluations"] {
        let path = paths.artifact_dir(dir_name);
        if path.exists() {
            tokio::fs::remove_dir_all(&path)
                .await
                .map_err(|err| miette!("failed to remove {}: {err}", path.display()))?;
            cleared.push(dir_name.to_string());
        }
    }

    Ok(cleared)
}

pub async fn run_reset_all() -> Result<()> {
    let home = get_daat_locus_home().await;
    reject_if_daemon_running("reset all").await?;
    clear_memory_state(&home).await?;
    let state_cleared = clear_state_files(&home).await?;
    let artifact_cleared = clear_compiled_artifacts(&home).await?;
    let log_cleared = clear_log_dirs(&home).await?;

    println!("[reset] reset all state under {}", home.display());
    if state_cleared.is_empty() {
        println!("[reset] cleared state: none");
    } else {
        println!("[reset] cleared state: {}", state_cleared.join(", "));
    }
    println!(
        "[reset] cleared memory: runtime_conversation, runtime_error_cases.jsonl, reasoning_traces.jsonl"
    );
    if artifact_cleared.is_empty() {
        println!("[reset] cleared compile artifacts: none");
    } else {
        println!(
            "[reset] cleared compile artifacts: {}",
            artifact_cleared.join(", ")
        );
    }
    if log_cleared.is_empty() {
        println!("[reset] cleared logs: none");
    } else {
        println!("[reset] cleared logs: {}", log_cleared.join(", "));
    }
    println!("[reset] preserved: config.toml, telegram_acl.json");

    Ok(())
}

async fn clear_log_dirs(home: &Path) -> Result<Vec<String>> {
    let mut cleared = Vec::new();
    let paths = DaatLocusPaths::from_root(home.to_path_buf());
    let path = paths.logs_dir();
    if path.exists() {
        tokio::fs::remove_dir_all(&path)
            .await
            .map_err(|err| miette!("failed to remove {}: {err}", path.display()))?;
        cleared.push("logs".to_string());
    }

    Ok(cleared)
}

async fn clear_named_files(dir: PathBuf, file_names: &[&str]) -> Result<Vec<String>> {
    let mut cleared = Vec::new();
    for file_name in file_names {
        let path = dir.join(file_name);
        if path.exists() {
            tokio::fs::remove_file(&path)
                .await
                .map_err(|err| miette!("failed to remove {}: {err}", path.display()))?;
            cleared.push((*file_name).to_string());
        }
    }
    Ok(cleared)
}

async fn clear_files(paths: &[PathBuf]) -> Result<()> {
    for path in paths {
        if path.exists() {
            tokio::fs::remove_file(path)
                .await
                .map_err(|err| miette!("failed to remove {}: {err}", path.display()))?;
        }
    }
    Ok(())
}