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:
parent
bef1bfbb33
commit
df62b7ceaa
2 changed files with 68 additions and 5 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue