Persist memory scores, use them for eviction in trim_entries

Scores are saved to memory-scores.json alongside the conversation log
after each scoring run, and loaded on startup — no more re-scoring
on restart.

trim_entries now evicts lowest-scored memories first (instead of
oldest-first) when memories exceed 50% of context. The 50% threshold
stays as a heuristic for memory-vs-conversation balance until we have
a scoring signal for conversation entries too. Unscored memories get
0.0, so they're evicted before scored ones.

save_memory_scores rebuilds from current entries, so evicted memories
are automatically expired from the scores file.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 19:35:46 -04:00
parent bef1bfbb33
commit df62b7ceaa
2 changed files with 68 additions and 5 deletions

View file

@ -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<usize> = (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; }

View file

@ -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<String, f64> = 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<String, f64> = 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);