Incremental memory scoring with per-score persistence

score_memories_incremental now takes an async callback that fires
after each memory is scored. The callback:
- Writes the score to the conversation entry via set_score()
- Persists to memory-scores.json immediately
- Notifies the UI so the context screen updates live

Scoring no longer batches — each score is visible and persisted
as it completes. Does not touch the memory store.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 21:10:09 -04:00
parent e213644514
commit fd58386951
2 changed files with 28 additions and 20 deletions

View file

@ -396,26 +396,28 @@ impl Mind {
ag.memory_scoring_in_flight = true; ag.memory_scoring_in_flight = true;
(ag.context.clone(), ag.client_clone()) (ag.context.clone(), ag.client_clone())
}; };
let result = learn::score_memories_incremental( let _result = learn::score_memories_incremental(
&context, max_age as i64, response_window, &client, &agent, &context, max_age as i64, response_window, &client, &agent,
|key: String, score: f64| {
let agent = agent.clone();
let path = scores_path.clone();
async move {
let mut ag = agent.lock().await;
for i in 0..ag.context.conversation.len() {
if let ConversationEntry::Memory { key: k, .. } = &ag.context.conversation.entries()[i].entry {
if *k == key {
ag.context.conversation.set_score(i, Some(score));
}
}
}
save_memory_scores(&ag.context.conversation, &path);
ag.changed.notify_one();
}
},
).await; ).await;
{ {
let mut ag = agent.lock().await; let mut ag = agent.lock().await;
ag.memory_scoring_in_flight = false; ag.memory_scoring_in_flight = false;
if let Ok(ref scores) = result {
// Write scores onto Memory entries
for (key, weight) in scores {
for i in 0..ag.context.conversation.len() {
if let ConversationEntry::Memory { key: k, .. } = &ag.context.conversation.entries()[i].entry {
if k == key {
ag.context.conversation.set_score(i, Some(*weight));
}
}
}
}
// Persist all scores to disk
save_memory_scores(&ag.context.conversation, &scores_path);
}
} }
let _ = bg_tx.send(BgEvent::ScoringDone); let _ = bg_tx.send(BgEvent::ScoringDone);
}); });

View file

@ -299,13 +299,18 @@ pub async fn score_memory(
/// Updates the graph weight (EWMA) and last_scored after each. /// Updates the graph weight (EWMA) and last_scored after each.
/// ///
/// Returns the number of nodes scored and their (key, score) pairs. /// Returns the number of nodes scored and their (key, score) pairs.
pub async fn score_memories_incremental( pub async fn score_memories_incremental<F, Fut>(
context: &ContextState, context: &ContextState,
max_age_secs: i64, max_age_secs: i64,
response_window: usize, response_window: usize,
client: &ApiClient, client: &ApiClient,
agent: &std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>, agent: &std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>,
) -> anyhow::Result<Vec<(String, f64)>> { mut on_score: F,
) -> anyhow::Result<usize>
where
F: FnMut(String, f64) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let now = chrono::Utc::now().timestamp(); let now = chrono::Utc::now().timestamp();
// Collect unique memory keys with their first position // Collect unique memory keys with their first position
@ -330,7 +335,7 @@ pub async fn score_memories_incremental(
candidates.sort_by_key(|&(_, _, last)| last); candidates.sort_by_key(|&(_, _, last)| last);
let http = http_client(); let http = http_client();
let mut results = Vec::new(); let mut scored = 0;
let total_entries = context.conversation.entries().len(); let total_entries = context.conversation.entries().len();
let first_quarter = total_entries / 4; let first_quarter = total_entries / 4;
@ -355,7 +360,8 @@ pub async fn score_memories_incremental(
dbglog!( dbglog!(
"[scoring] {} max:{:.3} ({} responses)", key, max_div, n_responses, "[scoring] {} max:{:.3} ({} responses)", key, max_div, n_responses,
); );
results.push((key.clone(), max_div)); on_score(key.clone(), max_div).await;
scored += 1;
} }
Err(e) => { Err(e) => {
dbglog!( dbglog!(
@ -365,7 +371,7 @@ pub async fn score_memories_incremental(
} }
} }
Ok(results) Ok(scored)
} }
// ── Fine-tuning scoring ───────────────────────────────────────── // ── Fine-tuning scoring ─────────────────────────────────────────