// 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 { 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 { 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 } }