diff --git a/src/agent/context.rs b/src/agent/context.rs index 96ef79d..9391ae0 100644 --- a/src/agent/context.rs +++ b/src/agent/context.rs @@ -101,11 +101,23 @@ pub fn trim_entries( } } - // Phase 2b: if memories > 50% of entries, evict oldest memories + // Phase 2b: if memories > 50% of context, evict lowest-scored first if cur_mem > conv_tokens && trimmed > max_tokens { - for i in 0..deduped.len() { - if drop[i] { continue; } - if !deduped[i].is_memory() { continue; } + let mut mem_indices: Vec = (0..deduped.len()) + .filter(|&i| !drop[i] && deduped[i].is_memory()) + .collect(); + mem_indices.sort_by(|&a, &b| { + let sa = match &deduped[a] { + ConversationEntry::Memory { score, .. } => score.unwrap_or(0.0), + _ => 0.0, + }; + let sb = match &deduped[b] { + ConversationEntry::Memory { score, .. } => score.unwrap_or(0.0), + _ => 0.0, + }; + sa.partial_cmp(&sb).unwrap_or(std::cmp::Ordering::Equal) + }); + for i in mem_indices { if cur_mem <= conv_tokens { break; } if trimmed <= max_tokens { break; } drop[i] = true; @@ -114,7 +126,7 @@ pub fn trim_entries( } } - // Phase 2b: drop oldest entries until under budget + // Phase 2c: if still over, drop oldest conversation entries for i in 0..deduped.len() { if trimmed <= max_tokens { break; } if drop[i] { continue; } diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 4d2a223..d8e8e38 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -28,6 +28,49 @@ use crate::subconscious::learn; pub use dmn::{SubconsciousSnapshot, Subconscious}; +use crate::agent::context::ConversationEntry; + +/// Load persisted memory scores from disk and apply to Memory entries. +fn load_memory_scores(entries: &mut [ConversationEntry], path: &std::path::Path) { + let data = match std::fs::read_to_string(path) { + Ok(d) => d, + Err(_) => return, + }; + let scores: std::collections::BTreeMap = match serde_json::from_str(&data) { + Ok(s) => s, + Err(_) => return, + }; + let mut applied = 0; + for entry in entries.iter_mut() { + if let ConversationEntry::Memory { key, score, .. } = entry { + if let Some(&s) = scores.get(key.as_str()) { + *score = Some(s); + applied += 1; + } + } + } + if applied > 0 { + dbglog!("[scoring] loaded {} scores from {}", applied, path.display()); + } +} + +/// Save all memory scores to disk. +fn save_memory_scores(entries: &[ConversationEntry], path: &std::path::Path) { + let scores: std::collections::BTreeMap = entries.iter() + .filter_map(|e| { + if let ConversationEntry::Memory { key, score: Some(s), .. } = e { + Some((key.clone(), *s)) + } else { + None + } + }) + .collect(); + if let Ok(json) = serde_json::to_string_pretty(&scores) { + let _ = std::fs::write(path, json); + dbglog!("[scoring] saved {} scores to {}", scores.len(), path.display()); + } +} + /// Which pane streaming text should go to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StreamTarget { @@ -267,6 +310,11 @@ impl Mind { // Restore conversation let mut ag = self.agent.lock().await; ag.restore_from_log(); + + // Restore persisted memory scores + let scores_path = self.config.session_dir.join("memory-scores.json"); + load_memory_scores(&mut ag.context.entries, &scores_path); + ag.changed.notify_one(); drop(ag); @@ -335,6 +383,7 @@ impl Mind { pub fn start_memory_scoring(&self) { let agent = self.agent.clone(); let bg_tx = self.bg_tx.clone(); + let scores_path = self.config.session_dir.join("memory-scores.json"); let cfg = crate::config::get(); let max_age = cfg.scoring_interval_secs; let response_window = cfg.scoring_response_window; @@ -362,6 +411,8 @@ impl Mind { } } } + // Persist all scores to disk + save_memory_scores(&ag.context.entries, &scores_path); } } let _ = bg_tx.send(BgEvent::ScoringDone);