From fd58386951ca48aebcb6a3a7d8bc288ea75d41b4 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Tue, 7 Apr 2026 21:10:09 -0400 Subject: [PATCH] Incremental memory scoring with per-score persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/mind/mod.rs | 32 +++++++++++++++++--------------- src/subconscious/learn.rs | 16 +++++++++++----- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 90c711a..9bf9677 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -396,26 +396,28 @@ impl Mind { ag.memory_scoring_in_flight = true; (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, + |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; { let mut ag = agent.lock().await; 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); }); diff --git a/src/subconscious/learn.rs b/src/subconscious/learn.rs index b6e293e..cc4d614 100644 --- a/src/subconscious/learn.rs +++ b/src/subconscious/learn.rs @@ -299,13 +299,18 @@ pub async fn score_memory( /// Updates the graph weight (EWMA) and last_scored after each. /// /// Returns the number of nodes scored and their (key, score) pairs. -pub async fn score_memories_incremental( +pub async fn score_memories_incremental( context: &ContextState, max_age_secs: i64, response_window: usize, client: &ApiClient, agent: &std::sync::Arc>, -) -> anyhow::Result> { + mut on_score: F, +) -> anyhow::Result +where + F: FnMut(String, f64) -> Fut, + Fut: std::future::Future, +{ let now = chrono::Utc::now().timestamp(); // Collect unique memory keys with their first position @@ -330,7 +335,7 @@ pub async fn score_memories_incremental( candidates.sort_by_key(|&(_, _, last)| last); let http = http_client(); - let mut results = Vec::new(); + let mut scored = 0; let total_entries = context.conversation.entries().len(); let first_quarter = total_entries / 4; @@ -355,7 +360,8 @@ pub async fn score_memories_incremental( dbglog!( "[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) => { dbglog!( @@ -365,7 +371,7 @@ pub async fn score_memories_incremental( } } - Ok(results) + Ok(scored) } // ── Fine-tuning scoring ─────────────────────────────────────────