use crate::{
memory_index::{MemoryMetadata, MemoryScope},
memory_index_manager::MemoryIndexManager,
vector_sync_manager::VectorSyncManager,
Result,
};
use std::sync::Arc;
use tracing::{info, warn};
#[derive(Debug, Clone)]
pub struct MemoryCleanupConfig {
pub interval_hours: u64,
pub archive_threshold: f32,
pub delete_threshold: f32,
}
impl Default for MemoryCleanupConfig {
fn default() -> Self {
Self {
interval_hours: 24,
archive_threshold: 0.1,
delete_threshold: 0.02,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CleanupStats {
pub archived: usize,
pub deleted: usize,
pub total_scanned: usize,
}
pub struct MemoryCleanupService {
index_manager: Arc<MemoryIndexManager>,
config: MemoryCleanupConfig,
vector_sync: Option<Arc<VectorSyncManager>>,
}
impl MemoryCleanupService {
pub fn new(
index_manager: Arc<MemoryIndexManager>,
config: MemoryCleanupConfig,
vector_sync: Option<Arc<VectorSyncManager>>,
) -> Self {
Self {
index_manager,
config,
vector_sync,
}
}
pub async fn run_cleanup(
&self,
scope: &MemoryScope,
owner_id: &str,
) -> Result<CleanupStats> {
let mut stats = CleanupStats::default();
let index = self
.index_manager
.load_index(scope.clone(), owner_id.to_string())
.await?;
let memory_ids: Vec<String> = index.memories.keys().cloned().collect();
stats.total_scanned = memory_ids.len();
let mut to_archive: Vec<String> = Vec::new();
let mut to_delete: Vec<String> = Vec::new();
for (id, metadata) in &index.memories {
let strength = metadata.compute_strength();
if metadata.archived && strength < self.config.delete_threshold {
to_delete.push(id.clone());
} else if !metadata.archived && strength < self.config.archive_threshold {
to_archive.push(id.clone());
}
}
if !to_archive.is_empty() {
let mut index = self
.index_manager
.load_index(scope.clone(), owner_id.to_string())
.await?;
for id in &to_archive {
if let Some(meta) = index.memories.get_mut(id) {
let strength = meta.compute_strength();
info!(
"Archiving memory '{}' (strength={:.3}, key='{}')",
id, strength, meta.key
);
meta.archived = true;
}
}
self.index_manager.save_index(&index).await?;
stats.archived = to_archive.len();
}
if !to_delete.is_empty() {
let mut index = self
.index_manager
.load_index(scope.clone(), owner_id.to_string())
.await?;
for id in &to_delete {
if let Some(meta) = index.memories.remove(id) {
warn!(
"Deleting archived memory '{}' (strength < {:.3}, key='{}')",
id, self.config.delete_threshold, meta.key
);
if let Some(ref vs) = self.vector_sync {
let file_uri = format!(
"cortex://{}/{}/{}",
scope, owner_id, meta.file
);
if let Err(e) = vs
.sync_file_change(
&file_uri,
crate::memory_events::ChangeType::Delete,
)
.await
{
warn!(
"Failed to delete vectors for memory '{}': {}",
id, e
);
}
}
}
}
self.index_manager.save_index(&index).await?;
stats.deleted = to_delete.len();
}
info!(
"Cleanup complete for {}/{}: scanned={}, archived={}, deleted={}",
scope, owner_id, stats.total_scanned, stats.archived, stats.deleted
);
self.index_manager.invalidate_cache(scope, owner_id).await;
Ok(stats)
}
pub async fn run_cleanup_batch(
&self,
entries: &[(MemoryScope, String)],
) -> Result<CleanupStats> {
let mut total = CleanupStats::default();
for (scope, owner_id) in entries {
match self.run_cleanup(scope, owner_id).await {
Ok(stats) => {
total.total_scanned += stats.total_scanned;
total.archived += stats.archived;
total.deleted += stats.deleted;
}
Err(e) => {
warn!("Cleanup failed for {}/{}: {}", scope, owner_id, e);
}
}
}
Ok(total)
}
}
pub fn compute_memory_strength(metadata: &MemoryMetadata) -> f32 {
metadata.compute_strength()
}