remem-ai 0.6.42

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::{Context, Result};
use tokio::time::{Duration, Instant};

use crate::{db, maintenance};

const CLEANUP_PROBE_INTERVAL: Duration = Duration::from_secs(60);

pub(super) struct CleanupProbeSchedule {
    once: bool,
    attempted_once: bool,
    next_daemon_probe_at: Instant,
}

impl CleanupProbeSchedule {
    pub(super) fn new(once: bool, now: Instant) -> Self {
        Self {
            once,
            attempted_once: false,
            next_daemon_probe_at: now,
        }
    }

    pub(super) fn is_due(&self, now: Instant) -> bool {
        if self.once {
            !self.attempted_once
        } else {
            now >= self.next_daemon_probe_at
        }
    }

    pub(super) fn record_probe(&mut self, now: Instant) {
        self.attempted_once = true;
        self.next_daemon_probe_at = now + CLEANUP_PROBE_INTERVAL;
    }
}

pub(super) fn enqueue_if_due(
    conn: &rusqlite::Connection,
    schedule: &mut CleanupProbeSchedule,
    now: Instant,
) -> Result<Option<db::CleanupEnqueueDecision>> {
    if !schedule.is_due(now) {
        return Ok(None);
    }
    schedule.record_probe(now);
    db::maybe_enqueue_cleanup_job(conn)
        .map(Some)
        .context("schedule automatic lifecycle cleanup")
}

pub(super) async fn execute_claimed(
    job: &db::Job,
    lease_owner: &str,
) -> Result<maintenance::CleanupExecution> {
    anyhow::ensure!(
        job.job_type == db::JobType::Cleanup,
        "dedicated cleanup executor received {} job",
        job.job_type.as_str()
    );
    let job_id = job.id;
    let lease_owner = lease_owner.to_string();
    tokio::task::spawn_blocking(move || {
        let conn = db::open_db().context("open database for automatic lifecycle cleanup")?;
        maintenance::execute_automatic_cleanup_job(
            &conn,
            job_id,
            &lease_owner,
            chrono::Utc::now().timestamp(),
        )
    })
    .await
    .context("join automatic lifecycle cleanup task")?
}

pub(super) fn safe_failure_message(error: &anyhow::Error) -> String {
    maintenance::safe_cleanup_error(error)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn once_schedule_probes_only_once() {
        let started = Instant::now();
        let mut schedule = CleanupProbeSchedule::new(true, started);

        assert!(schedule.is_due(started));
        schedule.record_probe(started);
        assert!(!schedule.is_due(started + CLEANUP_PROBE_INTERVAL));
    }

    #[test]
    fn daemon_schedule_probes_once_per_minute() {
        let started = Instant::now();
        let mut schedule = CleanupProbeSchedule::new(false, started);

        assert!(schedule.is_due(started));
        schedule.record_probe(started);
        assert!(!schedule.is_due(started + CLEANUP_PROBE_INTERVAL - Duration::from_millis(1)));
        assert!(schedule.is_due(started + CLEANUP_PROBE_INTERVAL));
    }

    #[test]
    fn cleanup_failure_message_is_bounded_and_redacted() {
        let error = anyhow::anyhow!("api_key=super-secret-{}", "x".repeat(2_000));

        let message = safe_failure_message(&error);

        assert!(message.contains("[REDACTED]"));
        assert!(!message.contains("super-secret"));
        assert!(message.len() <= 1_000);
    }
}