zeph 0.21.1

Lightweight AI agent with hybrid inference, skills-first architecture, and multi-channel I/O
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::cli::MemoryCommand;

pub(crate) async fn handle_memory_command(
    cmd: MemoryCommand,
    config_path: Option<&std::path::Path>,
) -> anyhow::Result<()> {
    use crate::bootstrap::resolve_config_path;
    use zeph_memory::store::SqliteStore;

    let config_file = resolve_config_path(config_path);
    let config = zeph_core::config::Config::load(&config_file).unwrap_or_default();
    let sqlite = SqliteStore::new(crate::db_url::resolve_db_url(&config))
        .await
        .map_err(|e| anyhow::anyhow!("failed to open SQLite: {e}"))?;

    match cmd {
        MemoryCommand::Export { path } => cmd_export(&sqlite, &config, &path).await?,
        MemoryCommand::Import { path } => cmd_import(&sqlite, &path).await?,
        MemoryCommand::ForgettingSweep => cmd_forgetting_sweep(&sqlite, &config).await?,
        MemoryCommand::Trajectory => cmd_trajectory(&sqlite).await?,
        MemoryCommand::Tree => cmd_tree(&sqlite).await?,
    }

    Ok(())
}

async fn cmd_export(
    sqlite: &zeph_memory::store::SqliteStore,
    config: &zeph_core::config::Config,
    path: &std::path::Path,
) -> anyhow::Result<()> {
    let snapshot = zeph_memory::export_snapshot(sqlite)
        .await
        .map_err(|e| anyhow::anyhow!("export failed: {e}"))?;
    let json = serde_json::to_string_pretty(&snapshot)
        .map_err(|e| anyhow::anyhow!("serialization failed: {e}"))?;
    zeph_common::fs_secure::write_private(path, json.as_bytes())
        .map_err(|e| anyhow::anyhow!("failed to write {}: {e}", path.display()))?;
    let convs = snapshot.conversations.len();
    let msgs: usize = snapshot
        .conversations
        .iter()
        .map(|c| c.messages.len())
        .sum();
    println!(
        "Exported {convs} conversation(s) with {msgs} message(s) to {}",
        path.display()
    );
    if config.memory.redact_credentials {
        eprintln!(
            "Warning: snapshot may contain sensitive conversation data predating \
             redaction. Store the file securely and restrict access."
        );
    }
    Ok(())
}

async fn cmd_import(
    sqlite: &zeph_memory::store::SqliteStore,
    path: &std::path::Path,
) -> anyhow::Result<()> {
    let json = std::fs::read_to_string(path)
        .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", path.display()))?;
    let snapshot: zeph_memory::MemorySnapshot =
        serde_json::from_str(&json).map_err(|e| anyhow::anyhow!("invalid snapshot format: {e}"))?;
    let stats = zeph_memory::import_snapshot(sqlite, snapshot)
        .await
        .map_err(|e| anyhow::anyhow!("import failed: {e}"))?;
    println!(
        "Imported: {} conversation(s), {} message(s), {} summary(ies), {} skipped",
        stats.conversations_imported,
        stats.messages_imported,
        stats.summaries_imported,
        stats.skipped,
    );
    Ok(())
}

async fn cmd_forgetting_sweep(
    sqlite: &zeph_memory::store::SqliteStore,
    config: &zeph_core::config::Config,
) -> anyhow::Result<()> {
    let forgetting_cfg = zeph_memory::ForgettingConfig {
        enabled: true,
        decay_rate: config.memory.forgetting.decay_rate,
        forgetting_floor: config.memory.forgetting.forgetting_floor,
        sweep_interval_secs: config.memory.forgetting.sweep_interval_secs,
        sweep_batch_size: config.memory.forgetting.sweep_batch_size,
        replay_window_hours: config.memory.forgetting.replay_window_hours,
        replay_min_access_count: config.memory.forgetting.replay_min_access_count,
        protect_recent_hours: config.memory.forgetting.protect_recent_hours,
        protect_min_access_count: config.memory.forgetting.protect_min_access_count,
    };
    let result = zeph_memory::forgetting::run_forgetting_sweep(sqlite, &forgetting_cfg)
        .await
        .map_err(|e| anyhow::anyhow!("forgetting sweep failed: {e}"))?;
    println!(
        "Forgetting sweep complete: downscaled={} replayed={} pruned={}",
        result.downscaled, result.replayed, result.pruned
    );
    Ok(())
}

async fn cmd_trajectory(sqlite: &zeph_memory::store::SqliteStore) -> anyhow::Result<()> {
    let total = sqlite
        .count_trajectory_entries()
        .await
        .map_err(|e| anyhow::anyhow!("failed to count trajectory entries: {e}"))?;
    let procedural = sqlite
        .load_trajectory_entries(Some("procedural"), 1000)
        .await
        .map_err(|e| anyhow::anyhow!("failed to load procedural entries: {e}"))?
        .len();
    let episodic = sqlite
        .load_trajectory_entries(Some("episodic"), 1000)
        .await
        .map_err(|e| anyhow::anyhow!("failed to load episodic entries: {e}"))?
        .len();
    println!("Trajectory memory statistics:");
    println!("  Total entries:  {total}");
    println!("  Procedural:     {procedural}");
    println!("  Episodic:       {episodic}");
    Ok(())
}

async fn cmd_tree(sqlite: &zeph_memory::store::SqliteStore) -> anyhow::Result<()> {
    let total = sqlite
        .count_tree_nodes()
        .await
        .map_err(|e| anyhow::anyhow!("failed to count tree nodes: {e}"))?;
    let leaves = sqlite
        .load_tree_leaves_unconsolidated(10000)
        .await
        .map_err(|e| anyhow::anyhow!("failed to load leaves: {e}"))?
        .len();
    println!("Memory tree statistics:");
    println!("  Total nodes:             {total}");
    println!("  Unconsolidated leaves:   {leaves}");
    Ok(())
}