use std::sync::Arc;
use std::time::Duration;
use mentedb::MenteDb;
use mentedb::consolidation::archival::ArchivalDecision;
use mentedb_extraction::{ExtractionConfig, ExtractionLlmJudge, HttpExtractionProvider};
use tracing::{info, warn};
const MIN_CLUSTER_SIZE: usize = 2;
const CANONICALIZE_LIMIT: usize = 32;
const SIMILARITY_THRESHOLD: f32 = 0.85;
const CACHE_MAX_AGE_US: u64 = 24 * 3600 * 1_000_000;
const MAX_FORGETS_PER_SWEEP: usize = 500;
#[derive(Debug, Default)]
pub struct MaintenanceReport {
pub consolidated: usize,
pub decayed: usize,
pub forgotten: usize,
}
pub fn run_sweep(db: &MenteDb) -> MaintenanceReport {
let mut report = MaintenanceReport::default();
match db.find_consolidation_candidates(MIN_CLUSTER_SIZE, SIMILARITY_THRESHOLD) {
Ok(candidates) => {
for c in candidates {
if db.consolidate_cluster(&c.memories).is_ok() {
report.consolidated += 1;
}
}
}
Err(e) => warn!("maintenance: consolidation failed: {e}"),
}
match db.apply_decay_global() {
Ok(n) => report.decayed = n,
Err(e) => warn!("maintenance: decay failed: {e}"),
}
match db.evaluate_archival_global() {
Ok(decisions) => {
for (id, decision) in decisions {
if report.forgotten >= MAX_FORGETS_PER_SWEEP {
break;
}
if decision == ArchivalDecision::Delete && db.forget(id).is_ok() {
report.forgotten += 1;
}
}
}
Err(e) => warn!("maintenance: archival failed: {e}"),
}
db.evict_stale_speculative(CACHE_MAX_AGE_US);
if let Err(e) = db.flush() {
warn!("maintenance: flush failed: {e}");
}
report
}
pub async fn canonicalize_topics(db: &MenteDb, config: &Option<ExtractionConfig>) -> usize {
let Some(cfg) = config.clone() else {
return 0;
};
let provider = match HttpExtractionProvider::new(cfg) {
Ok(p) => p,
Err(e) => {
warn!("maintenance: topic canonicalization skipped, provider build failed: {e}");
return 0;
}
};
let judge = ExtractionLlmJudge::new(provider);
let n = db
.canonicalize_trajectory_topics(judge, CANONICALIZE_LIMIT)
.await;
if n > 0 {
info!(count = n, "maintenance: trajectory topics canonicalized");
}
n
}
pub fn spawn_scheduler(
db: Arc<MenteDb>,
interval_hours: u64,
extraction_config: Option<ExtractionConfig>,
) {
if interval_hours == 0 {
info!("maintenance scheduler disabled (--maintenance-interval-hours 0)");
return;
}
info!("maintenance scheduler enabled: sweep every {interval_hours}h");
tokio::spawn(async move {
let interval = Duration::from_secs(interval_hours * 3600);
loop {
tokio::time::sleep(interval).await;
let db_sweep = Arc::clone(&db);
match tokio::task::spawn_blocking(move || run_sweep(&db_sweep)).await {
Ok(r) => info!(
consolidated = r.consolidated,
decayed = r.decayed,
forgotten = r.forgotten,
"maintenance sweep complete"
),
Err(e) => warn!("maintenance sweep task panicked: {e}"),
}
canonicalize_topics(&db, &extraction_config).await;
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use mentedb::prelude::*;
use mentedb_embedding::hash_provider::HashEmbeddingProvider;
#[test]
fn sweep_runs_on_a_populated_db_without_error() {
let path = std::env::temp_dir().join(format!("mentedb_maint_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
let db = MenteDb::open_with_embedder(&path, Box::new(HashEmbeddingProvider::new(256)))
.expect("open db");
let agent = AgentId::new();
for i in 0..5 {
let content = format!("fact number {i} about the project");
let emb = db.embed_text(&content).unwrap().unwrap();
db.store(MemoryNode::new(agent, MemoryType::Semantic, content, emb))
.unwrap();
}
let report = run_sweep(&db);
assert!(report.forgotten <= MAX_FORGETS_PER_SWEEP);
drop(db);
let _ = std::fs::remove_dir_all(&path);
}
}