consciousness/src/hippocampus/memory.rs
ProofOfConcept 27861a44e5 surface: tag recent nodes as (new) instead of hiding them
Links to nodes created after the conversation window start are
tagged with (new) in memory_render output. The surface prompt
tells the agent not to surface these — they're its own recent
output, not prior memories. Observe can still see and update them.

POC_MEMORIES_OLDER_THAN env var set from the oldest message
timestamp in the conversation window.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-03-26 21:19:19 -04:00

93 lines
3.1 KiB
Rust

// hippocampus/memory.rs — In-memory view of a graph node
//
// MemoryNode is a lightweight representation of a loaded node:
// key, content, links, version, weight. Used by the agent for
// context tracking and by the CLI for rendering.
use super::store::Store;
/// A memory node loaded into the agent's working memory.
#[derive(Debug, Clone)]
pub struct MemoryNode {
pub key: String,
pub content: String,
pub links: Vec<(String, f32, bool)>, // (target_key, strength, is_new)
pub version: u32,
pub weight: f32,
}
impl MemoryNode {
/// Load a node from the store by key.
pub fn load(key: &str) -> Option<Self> {
let store = Store::load().ok()?;
Self::from_store(&store, key)
}
/// Load from an already-open store.
pub fn from_store(store: &Store, key: &str) -> Option<Self> {
let node = store.nodes.get(key)?;
// If set, tag links to nodes created after this timestamp as (new)
let older_than: i64 = std::env::var("POC_MEMORIES_OLDER_THAN")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let mut neighbors: std::collections::HashMap<&str, (f32, bool)> = std::collections::HashMap::new();
for r in &store.relations {
if r.deleted { continue; }
let neighbor_key = if r.source_key == key {
&r.target_key
} else if r.target_key == key {
&r.source_key
} else {
continue;
};
let is_new = older_than > 0 && store.nodes.get(neighbor_key.as_str())
.map(|n| n.created_at > older_than)
.unwrap_or(false);
let e = neighbors.entry(neighbor_key.as_str()).or_insert((0.0, false));
e.0 = e.0.max(r.strength);
e.1 = e.1 || is_new;
}
let mut links: Vec<(String, f32, bool)> = neighbors.into_iter()
.map(|(k, (s, new))| (k.to_string(), s, new))
.collect();
links.sort_by(|a, b| b.1.total_cmp(&a.1));
Some(MemoryNode {
key: key.to_string(),
content: node.content.clone(),
links,
version: node.version,
weight: node.weight,
})
}
/// Render for inclusion in the context window.
pub fn render(&self) -> String {
let mut out = self.content.clone();
// Footer: links not already referenced inline
let footer: Vec<&(String, f32, bool)> = self.links.iter()
.filter(|(target, _, _)| !self.content.contains(target.as_str()))
.collect();
if !footer.is_empty() {
let total = footer.len();
out.push_str("\n\n---\nLinks:");
for (target, strength, is_new) in footer.iter().take(15) {
let tag = if *is_new { " (new)" } else { "" };
out.push_str(&format!("\n ({:.2}) `{}`{}", strength, target, tag));
}
if total > 15 {
out.push_str(&format!("\n ... and {} more (memory_links({{\"{}\"}}))",
total - 15, self.key));
}
}
out
}
}