diff --git a/.claude/scoring-persistence-analysis.md b/.claude/scoring-persistence-analysis.md new file mode 100644 index 0000000..5cc3580 --- /dev/null +++ b/.claude/scoring-persistence-analysis.md @@ -0,0 +1,46 @@ +# Memory Scoring Persistence — Analysis (2026-04-07) + +## Problem + +Scores computed by `score_memories_incremental` are written to +`ConversationEntry::Memory::score` (in-memory, serialized to +conversation.log) but never written back to the Store. This means: + +- `Node.last_scored` stays at 0 — every restart re-scores everything +- `score_weight()` in `ops.rs:304-313` exists but is never called +- Scoring is wasted work on every session start + +## Fix + +In `mind/mod.rs` scoring completion handler (currently ~line 341-352), +after writing scores to entries, also persist to Store: + +```rust +if let Ok(ref scores) = result { + let mut ag = agent.lock().await; + // Write to entries (already done) + for (key, weight) in scores { ... } + + // NEW: persist to Store + let store_arc = Store::cached().await.ok(); + if let Some(arc) = store_arc { + let mut store = arc.lock().await; + for (key, weight) in scores { + store.score_weight(key, *weight as f32); + } + store.save().ok(); + } +} +``` + +This calls `score_weight()` which updates `node.weight` and sets +`node.last_scored = now()`. The staleness check in +`score_memories_incremental` (learn.rs:325) then skips recently-scored +nodes on subsequent runs. + +## Files + +- `src/mind/mod.rs:341-352` — scoring completion handler (add Store write) +- `src/hippocampus/store/ops.rs:304-313` — `score_weight()` (exists, unused) +- `src/subconscious/learn.rs:322-326` — staleness check (already correct) +- `src/hippocampus/store/types.rs:219` — `Node.last_scored` field diff --git a/src/mind/dmn.rs b/src/mind/dmn.rs index cfe408e..e465a16 100644 --- a/src/mind/dmn.rs +++ b/src/mind/dmn.rs @@ -416,42 +416,52 @@ impl Subconscious { .collect(); } - if let Some(surface_str) = outputs.get("surface") { + // Inject all outputs into the conscious agent under one lock + let has_outputs = outputs.contains_key("surface") + || outputs.contains_key("reflection") + || outputs.contains_key("thalamus"); + if has_outputs { let mut ag = agent.lock().await; - for key in surface_str.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) { - if let Some(rendered) = crate::cli::node::render_node( - &crate::store::Store::load().unwrap_or_default(), key, - ) { - let mut msg = crate::agent::api::types::Message::user(format!( - "\n--- {} (surfaced) ---\n{}\n", - key, rendered, - )); - msg.stamp(); - ag.push_entry(ConversationEntry::Memory { - key: key.to_string(), message: msg, score: None, - }); + + if let Some(surface_str) = outputs.get("surface") { + let store = crate::store::Store::cached().await.ok(); + let store_guard = match &store { + Some(s) => Some(s.lock().await), + None => None, + }; + for key in surface_str.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) { + let rendered = store_guard.as_ref() + .and_then(|s| crate::cli::node::render_node(s, key)); + if let Some(rendered) = rendered { + let mut msg = crate::agent::api::types::Message::user(format!( + "\n--- {} (surfaced) ---\n{}\n", + key, rendered, + )); + msg.stamp(); + ag.push_entry(ConversationEntry::Memory { + key: key.to_string(), message: msg, score: None, + }); + } } } - } - if let Some(reflection) = outputs.get("reflection") { - if !reflection.trim().is_empty() { - let mut ag = agent.lock().await; - ag.push_message(crate::agent::api::types::Message::user(format!( - "\n--- subconscious reflection ---\n{}\n", - reflection.trim(), - ))); + if let Some(reflection) = outputs.get("reflection") { + if !reflection.trim().is_empty() { + ag.push_message(crate::agent::api::types::Message::user(format!( + "\n--- subconscious reflection ---\n{}\n", + reflection.trim(), + ))); + } } - } - if let Some(nudge) = outputs.get("thalamus") { - let nudge = nudge.trim(); - if !nudge.is_empty() && nudge != "ok" { - let mut ag = agent.lock().await; - ag.push_message(crate::agent::api::types::Message::user(format!( - "\n--- thalamus ---\n{}\n", - nudge, - ))); + if let Some(nudge) = outputs.get("thalamus") { + let nudge = nudge.trim(); + if !nudge.is_empty() && nudge != "ok" { + ag.push_message(crate::agent::api::types::Message::user(format!( + "\n--- thalamus ---\n{}\n", + nudge, + ))); + } } } diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index e688ad4..24e88cb 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -166,7 +166,7 @@ impl SubconsciousScreen { // Read entries from the forked agent (from fork point onward) let entries: Vec = snap.forked_agent.as_ref() .and_then(|agent| agent.try_lock().ok()) - .map(|ag| ag.context.entries[snap.fork_point..].to_vec()) + .map(|ag| ag.context.entries.get(snap.fork_point..).unwrap_or(&[]).to_vec()) .unwrap_or_default(); if entries.is_empty() {