Fix bounds check panic and batched lock in collect_results
- subconscious.rs: use .get(fork_point..) instead of direct slice to avoid panic when fork_point > entries.len() - dmn.rs: batch all output injections (surface, reflection, thalamus) under a single agent lock acquisition instead of three separate ones - dmn.rs: use Store::cached() instead of Store::load() when rendering surfaced memories - Add scoring persistence analysis notes Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
03d2d070f9
commit
0df5ec11d1
3 changed files with 87 additions and 31 deletions
46
.claude/scoring-persistence-analysis.md
Normal file
46
.claude/scoring-persistence-analysis.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -416,42 +416,52 @@ impl Subconscious {
|
||||||
.collect();
|
.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;
|
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(
|
if let Some(surface_str) = outputs.get("surface") {
|
||||||
&crate::store::Store::load().unwrap_or_default(), key,
|
let store = crate::store::Store::cached().await.ok();
|
||||||
) {
|
let store_guard = match &store {
|
||||||
let mut msg = crate::agent::api::types::Message::user(format!(
|
Some(s) => Some(s.lock().await),
|
||||||
"<system-reminder>\n--- {} (surfaced) ---\n{}\n</system-reminder>",
|
None => None,
|
||||||
key, rendered,
|
};
|
||||||
));
|
for key in surface_str.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
||||||
msg.stamp();
|
let rendered = store_guard.as_ref()
|
||||||
ag.push_entry(ConversationEntry::Memory {
|
.and_then(|s| crate::cli::node::render_node(s, key));
|
||||||
key: key.to_string(), message: msg, score: None,
|
if let Some(rendered) = rendered {
|
||||||
});
|
let mut msg = crate::agent::api::types::Message::user(format!(
|
||||||
|
"<system-reminder>\n--- {} (surfaced) ---\n{}\n</system-reminder>",
|
||||||
|
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 let Some(reflection) = outputs.get("reflection") {
|
||||||
if !reflection.trim().is_empty() {
|
if !reflection.trim().is_empty() {
|
||||||
let mut ag = agent.lock().await;
|
ag.push_message(crate::agent::api::types::Message::user(format!(
|
||||||
ag.push_message(crate::agent::api::types::Message::user(format!(
|
"<system-reminder>\n--- subconscious reflection ---\n{}\n</system-reminder>",
|
||||||
"<system-reminder>\n--- subconscious reflection ---\n{}\n</system-reminder>",
|
reflection.trim(),
|
||||||
reflection.trim(),
|
)));
|
||||||
)));
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(nudge) = outputs.get("thalamus") {
|
if let Some(nudge) = outputs.get("thalamus") {
|
||||||
let nudge = nudge.trim();
|
let nudge = nudge.trim();
|
||||||
if !nudge.is_empty() && nudge != "ok" {
|
if !nudge.is_empty() && nudge != "ok" {
|
||||||
let mut ag = agent.lock().await;
|
ag.push_message(crate::agent::api::types::Message::user(format!(
|
||||||
ag.push_message(crate::agent::api::types::Message::user(format!(
|
"<system-reminder>\n--- thalamus ---\n{}\n</system-reminder>",
|
||||||
"<system-reminder>\n--- thalamus ---\n{}\n</system-reminder>",
|
nudge,
|
||||||
nudge,
|
)));
|
||||||
)));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ impl SubconsciousScreen {
|
||||||
// Read entries from the forked agent (from fork point onward)
|
// Read entries from the forked agent (from fork point onward)
|
||||||
let entries: Vec<ConversationEntry> = snap.forked_agent.as_ref()
|
let entries: Vec<ConversationEntry> = snap.forked_agent.as_ref()
|
||||||
.and_then(|agent| agent.try_lock().ok())
|
.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();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue