use anyhow::Result;
use rusqlite::{Connection, params};
pub const TTL_ENV_VAR: &str = "AI_MEMORY_OBSERVATIONS_TTL_DAYS";
pub const DEFAULT_TTL_DAYS: i64 = 7;
#[must_use]
pub fn ttl_days() -> i64 {
std::env::var(TTL_ENV_VAR)
.ok()
.and_then(|v| v.parse::<i64>().ok())
.filter(|d| *d > 0)
.unwrap_or(DEFAULT_TTL_DAYS)
}
pub fn prune(conn: &Connection) -> Result<usize> {
let cutoff = (chrono::Utc::now() - chrono::Duration::days(ttl_days())).to_rfc3339();
let n = conn.execute(
"DELETE FROM recall_observations WHERE observed_at < ?1",
params![cutoff],
)?;
Ok(n)
}
pub fn prune_before(conn: &Connection, cutoff_rfc3339: &str) -> Result<usize> {
let n = conn.execute(
"DELETE FROM recall_observations WHERE observed_at < ?1",
params![cutoff_rfc3339],
)?;
Ok(n)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::observations::{Candidate, record_recall};
use rusqlite::Connection;
fn fresh() -> Connection {
crate::storage::open(std::path::Path::new(":memory:")).expect("open in-memory db")
}
fn seed_memory(conn: &Connection, id: &str) {
conn.execute(
"INSERT INTO memories \
(id, tier, namespace, title, content, created_at, updated_at) \
VALUES (?1, 'long', 'test', ?2, 'content', '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z')",
params![id, format!("title-{id}")],
)
.expect("seed memory");
}
#[test]
fn ttl_days_falls_back_when_env_unset() {
unsafe {
std::env::remove_var(TTL_ENV_VAR);
}
assert_eq!(ttl_days(), DEFAULT_TTL_DAYS);
}
#[test]
fn prune_before_deletes_only_old_rows() {
let conn = fresh();
seed_memory(&conn, "m1");
seed_memory(&conn, "m2");
record_recall(
&conn,
"r1",
&[Candidate {
memory_id: "m1",
retriever: "hybrid",
rank: 1,
score: 0.9,
}],
)
.unwrap();
conn.execute(
"UPDATE recall_observations SET observed_at = ?1 WHERE memory_id = 'm1'",
params!["2020-01-01T00:00:00Z"],
)
.unwrap();
record_recall(
&conn,
"r1",
&[Candidate {
memory_id: "m2",
retriever: "hybrid",
rank: 2,
score: 0.8,
}],
)
.unwrap();
let pruned = prune_before(&conn, "2024-01-01T00:00:00Z").unwrap();
assert_eq!(pruned, 1, "only the 2020-stamped row should be pruned");
let remaining: i64 = conn
.query_row("SELECT COUNT(*) FROM recall_observations", [], |r| r.get(0))
.unwrap();
assert_eq!(remaining, 1);
}
}