daat-locus 0.1.0

A long-running local agent runtime with memory, workflows, apps, and sleep-time self-improvement.
use super::*;

#[derive(Default)]
pub(super) enum SleepTrigger {
    #[default]
    Manual,
    Idle,
}

pub(crate) struct SleepTaskResult {
    trigger: SleepTrigger,
    result: Result<crate::reasoning::sleep::SleepSummary>,
}

pub(super) async fn maybe_start_forced_sleep(
    context: &mut Context,
    tx: &tokio::sync::watch::Sender<DashboardState>,
    sleep_result_tx: &tokio::sync::mpsc::UnboundedSender<SleepTaskResult>,
    sleep_running: &mut bool,
    sleep_status: &mut SleepStatusSnapshot,
) -> Option<String> {
    if *sleep_running {
        return None;
    }
    let error_backlog = sleep_status.unread_runtime_error_backlog;
    if error_backlog < FORCE_SLEEP_ERROR_BACKLOG_THRESHOLD {
        return None;
    }
    let status = format!(
        "backlog too high (runtime_errors={}): background sleep started",
        error_backlog
    );
    start_background_sleep(
        context,
        tx,
        sleep_result_tx,
        sleep_running,
        sleep_status,
        SleepTrigger::Idle,
        &status,
    )
    .await;
    Some(format!(
        "backlog too high (runtime_errors={}): background sleep started",
        error_backlog
    ))
}

pub(super) async fn maybe_start_idle_sleep(
    context: &mut Context,
    tx: &tokio::sync::watch::Sender<DashboardState>,
    sleep_result_tx: &tokio::sync::mpsc::UnboundedSender<SleepTaskResult>,
    sleep_running: &mut bool,
    sleep_status: &mut SleepStatusSnapshot,
) -> Option<String> {
    let idle_since = context.idle_since?;
    if idle_since.elapsed() < AUTO_SLEEP_IDLE_THRESHOLD {
        return None;
    }
    if context
        .last_idle_sleep_at
        .is_some_and(|last| last.elapsed() < AUTO_SLEEP_MIN_INTERVAL)
    {
        return None;
    }
    if *sleep_running {
        return Some("idle: background sleep is running".to_string());
    }
    context.last_idle_sleep_at = Some(std::time::Instant::now());
    start_background_sleep(
        context,
        tx,
        sleep_result_tx,
        sleep_running,
        sleep_status,
        SleepTrigger::Idle,
        "idle: background sleep started",
    )
    .await;
    Some("idle: background sleep started".to_string())
}

pub(super) async fn start_background_sleep(
    context: &mut Context,
    tx: &tokio::sync::watch::Sender<DashboardState>,
    sleep_result_tx: &tokio::sync::mpsc::UnboundedSender<SleepTaskResult>,
    sleep_running: &mut bool,
    sleep_status: &mut SleepStatusSnapshot,
    trigger: SleepTrigger,
    status: &str,
) {
    *sleep_running = true;
    sleep_status.mark_started(match trigger {
        SleepTrigger::Manual => "manual",
        SleepTrigger::Idle => "automatic",
    });
    if let Err(err) = persist_sleep_status_snapshot(sleep_status).await {
        tracing::warn!("failed to persist sleep status start: {err:?}");
    }
    set_runtime_status(Some(tx), RuntimeStatusLevel::Info, status.to_string());
    sync_dashboard_state(context, tx, sleep_status, None);
    let config = context.config.clone();
    let compiled_prompts = context.compiled_prompts.clone();
    let sleep_result_tx = sleep_result_tx.clone();
    tokio::spawn(async move {
        let mut sleep_context = build_eval_context_with_compiled(config, compiled_prompts).await;
        let result = run_sleep(&mut sleep_context).await;
        sleep_context.shutdown().await;
        let _ = sleep_result_tx.send(SleepTaskResult { trigger, result });
    });
}

pub(crate) async fn handle_sleep_task_result(
    context: &mut Context,
    tx: &tokio::sync::watch::Sender<DashboardState>,
    sleep_status: &mut SleepStatusSnapshot,
    result: SleepTaskResult,
) {
    match result.result {
        Ok(summary) => {
            if let Ok(store) = load_compiled_prompts_only(&context.config).await {
                context.compiled_prompts = store;
            }
            let prefix = match result.trigger {
                SleepTrigger::Manual => "sleep completed",
                SleepTrigger::Idle => "background sleep completed",
            };
            sleep_status.apply_summary(&summary);
            let summary_text = summarize_sleep_summary(&summary);
            sleep_status.mark_completed(summary_text.clone());
            set_runtime_status(
                Some(tx),
                RuntimeStatusLevel::Info,
                format!("{prefix}: {summary_text}"),
            );
        }
        Err(err) => {
            let prefix = match result.trigger {
                SleepTrigger::Manual => "sleep failed",
                SleepTrigger::Idle => "background sleep failed",
            };
            sleep_status.mark_completed(err.to_string());
            set_runtime_status(
                Some(tx),
                RuntimeStatusLevel::Error,
                format!("{prefix}: {err}"),
            );
        }
    }
    refresh_sleep_status_queues(sleep_status).await;
    if let Err(err) = persist_sleep_status_snapshot(sleep_status).await {
        tracing::warn!("failed to persist sleep status result: {err:?}");
    }
    sync_dashboard_state(context, tx, sleep_status, None);
}