training: per-node scoring with graph weight updates

Memory scoring now uses the graph as source of truth:
- last_scored timestamp on each node (new capnp field @22)
- Nodes scored when older than scoring_interval_secs (default 1hr)
- Oldest-scored-first ordering
- Window: scoring_response_window assistant responses (default 100)
- First-quarter memories scored even without full window
- Per-response normalization (raw divergence / response count)
- Asymmetric weight update: alpha=0.5 up, alpha=0.1 down
  (responds fast to importance, decays slowly — memories stay
  surfaced even if only useful 1/4 of the time)

Graph writes disabled pending normalization calibration.

Also: configurable scoring_interval_secs and scoring_response_window.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-04 05:01:49 -04:00 committed by Kent Overstreet
parent b0603fd1ef
commit fcd77fb79e
8 changed files with 109 additions and 64 deletions

View file

@ -219,41 +219,31 @@ impl Session {
fn start_memory_scoring(&self) {
let agent = self.agent.clone();
let ui_tx = self.ui_tx.clone();
let cfg = crate::config::get();
let max_age = cfg.scoring_interval_secs;
let response_window = cfg.scoring_response_window;
tokio::spawn(async move {
// Check + snapshot under one brief lock
let (context, client, cursor) = {
let (context, client) = {
let mut agent = agent.lock().await;
if agent.agent_cycles.memory_scoring_in_flight {
return;
}
let cursor = agent.agent_cycles.memory_score_cursor;
agent.agent_cycles.memory_scoring_in_flight = true;
// Count total unique memories
let mut seen = std::collections::HashSet::new();
for entry in &agent.context.entries {
if let crate::agent::context::ConversationEntry::Memory { key, .. } = entry {
seen.insert(key.clone());
}
}
agent.agent_cycles.memory_total = seen.len();
let _ = ui_tx.send(UiMessage::AgentUpdate(agent.agent_cycles.snapshots()));
(agent.context.clone(), agent.client_clone(), cursor)
(agent.context.clone(), agent.client_clone())
};
// Lock released — event loop is free
let result = crate::agent::training::score_memories_incremental(
&context, cursor, &client, &ui_tx,
&context, max_age as i64, response_window, &client, &ui_tx,
).await;
// Brief lock — just update fields, no heavy work
{
let mut agent = agent.lock().await;
agent.agent_cycles.memory_scoring_in_flight = false;
if let Ok((new_cursor, ref scores)) = result {
agent.agent_cycles.memory_score_cursor = new_cursor;
agent.agent_cycles.memory_scores.extend(scores.clone());
if let Ok(ref scores) = result {
agent.agent_cycles.memory_scores = scores.clone();
}
}
// Snapshot and log outside the lock
match result {
Ok(_) => {
let agent = agent.lock().await;