// hippocampus — memory storage, retrieval, and consolidation // // The graph-structured memory system: nodes, relations, queries, // similarity scoring, spectral analysis, and neuroscience-inspired // consolidation (spaced repetition, interference detection, schema // assimilation). // // Tool implementations are typed functions that take &Store or &mut Store. // The tools/memory.rs layer handles JSON parsing and RPC routing. pub mod memory; pub mod store; pub mod graph; pub mod local; pub mod lookups; pub mod query; pub mod spectral; pub mod neuro; pub mod counters; pub mod transcript; use std::cell::RefCell; use std::path::PathBuf; use std::sync::{Arc, OnceLock}; use anyhow::Result; use crate::hippocampus::store::Store; pub use local::{LinkInfo, JournalEntry}; // ── Store access ─────────────────────────────────────────────── /// Daemon's store (eager init) or client's fallback local store. static STORE_ACCESS: OnceLock>>> = OnceLock::new(); // Client's socket connection (thread-local for lock-free access). thread_local! { static SOCKET_CONN: RefCell> = const { RefCell::new(None) }; } /// How we access the memory store. pub enum StoreAccess { Daemon(Arc>), // Direct store access Client, // Socket to daemon (in thread-local) None(String), // Error: couldn't get access } /// Set the global store handle. Call once at daemon startup (eager init). pub fn set_store(store: Arc>) { STORE_ACCESS.set(Some(store)).ok(); } /// Get store access: daemon's store, socket, or local fallback. pub fn access() -> StoreAccess { // Daemon: already set via set_store() if let Some(Some(store)) = STORE_ACCESS.get() { return StoreAccess::Daemon(store.clone()); } // Client: check if socket already cached in thread-local let have_socket = SOCKET_CONN.with(|cell| cell.borrow().is_some()); if have_socket { return StoreAccess::Client; } // No socket cached, try connecting if let Ok(conn) = SocketConn::connect() { SOCKET_CONN.with(|cell| *cell.borrow_mut() = Some(conn)); return StoreAccess::Client; } // Socket failed - try local store as fallback (cached in STORE_ACCESS) let store_opt = STORE_ACCESS.get_or_init(|| { Store::load().ok().map(|s| Arc::new(crate::Mutex::new(s))) }); match store_opt { Some(store) => StoreAccess::Daemon(store.clone()), None => StoreAccess::None("could not connect to daemon or open store locally".into()), } } /// Get local store access. Returns error if only RPC available. pub fn access_local() -> Result>> { match access() { StoreAccess::Daemon(arc) => Ok(arc), StoreAccess::Client => anyhow::bail!("direct store access not available via RPC"), StoreAccess::None(err) => anyhow::bail!("{}", err), } } pub fn socket_path() -> PathBuf { dirs::home_dir() .unwrap_or_default() .join(".consciousness/mcp.sock") } struct SocketConn { reader: std::io::BufReader, writer: std::io::BufWriter, next_id: u64, } impl SocketConn { fn connect() -> Result { use std::os::unix::net::UnixStream; use std::io::{BufRead, BufReader, BufWriter, Write}; let path = socket_path(); let stream = UnixStream::connect(&path)?; let mut reader = BufReader::new(stream.try_clone()?); let mut writer = BufWriter::new(stream); // Initialize MCP connection let init = serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "forward", "version": "0.1"}}}); writeln!(writer, "{}", init)?; writer.flush()?; let mut buf = String::new(); reader.read_line(&mut buf)?; Ok(Self { reader, writer, next_id: 1 }) } fn call(&mut self, tool_name: &str, args: &serde_json::Value) -> Result { use std::io::{BufRead, Write}; self.next_id += 1; let call = serde_json::json!({"jsonrpc": "2.0", "id": self.next_id, "method": "tools/call", "params": {"name": tool_name, "arguments": args}}); writeln!(self.writer, "{}", call)?; self.writer.flush()?; let mut buf = String::new(); self.reader.read_line(&mut buf)?; let resp: serde_json::Value = serde_json::from_str(&buf)?; if let Some(err) = resp.get("error") { anyhow::bail!("daemon error: {}", err); } let result = resp.get("result").cloned().unwrap_or(serde_json::json!({})); let text = result.get("content") .and_then(|c| c.as_array()) .and_then(|arr| arr.first()) .and_then(|c| c.get("text")) .and_then(|t| t.as_str()) .unwrap_or(""); Ok(text.to_string()) } } /// Forward a tool call to the daemon via socket. /// Only valid when access() returns Client. pub fn memory_rpc(tool_name: &str, args: serde_json::Value) -> Result { SOCKET_CONN.with(|cell| { let mut conn = cell.borrow_mut(); let conn = conn.as_mut().expect("access() returned Client but SOCKET_CONN is None"); conn.call(tool_name, &args) }) } // ── Macro for generating tool wrappers ───────────────────────── // // memory_tool!(name, mut, arg1: [str], arg2: [Option]) // - mut/ref for store mutability // - generates jsonargs_* (internal, JSON args) and public typed API macro_rules! memory_tool { // ── Helper rules (must come first) ───────────────────────────── // Extract from JSON (@extract $args:ident, $name:ident, str) => { get_str($args, stringify!($name))? }; (@extract $args:ident, $name:ident, f32) => { get_f64($args, stringify!($name))? as f32 }; (@extract $args:ident, $name:ident, Vec) => { $args.get(stringify!($name)) .and_then(|v| v.as_array()) .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect::>()) .unwrap_or_default() }; (@extract $args:ident, $name:ident, Option<&str>) => { $args.get(stringify!($name)).and_then(|v| v.as_str()) }; (@extract $args:ident, $name:ident, Option) => { $args.get(stringify!($name)).and_then(|v| v.as_bool()) }; (@extract $args:ident, $name:ident, Option) => { $args.get(stringify!($name)).and_then(|v| v.as_u64()) }; (@extract $args:ident, $name:ident, Option) => { $args.get(stringify!($name)).and_then(|v| v.as_i64()) }; (@extract $args:ident, $name:ident, Option) => { $args.get(stringify!($name)).and_then(|v| v.as_u64()).map(|v| v as usize) }; (@extract $args:ident, $name:ident, Option) => { $args.get(stringify!($name)).and_then(|v| v.as_u64()).map(|v| v as u32) }; (@extract $args:ident, $name:ident, Option) => { $args.get(stringify!($name)).and_then(|v| v.as_f64()) }; // Parameter types for function signatures (@param_type str) => { &str }; (@param_type f32) => { f32 }; (@param_type Vec) => { Vec }; (@param_type Option<&str>) => { Option<&str> }; (@param_type Option) => { Option }; (@param_type Option) => { Option }; (@param_type Option) => { Option }; (@param_type Option) => { Option }; (@param_type Option) => { Option }; (@param_type Option) => { Option }; // Serialize result for jsonargs (@serialize $t:ty, $result:expr) => { serde_json::to_string(&$result)? }; // Deserialize RPC response (@deserialize $t:ty, $json:expr) => { serde_json::from_str(&$json).map_err(|e| anyhow::anyhow!("{}", e)) }; // Serialize to JSON for RPC (@insert_json $map:ident, $name:ident, str) => { $map.insert(stringify!($name).into(), serde_json::json!($name)); }; (@insert_json $map:ident, $name:ident, f32) => { $map.insert(stringify!($name).into(), serde_json::json!($name)); }; (@insert_json $map:ident, $name:ident, Vec) => { $map.insert(stringify!($name).into(), serde_json::json!($name)); }; (@insert_json $map:ident, $name:ident, Option<&str>) => { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; (@insert_json $map:ident, $name:ident, Option) => { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; (@insert_json $map:ident, $name:ident, Option) => { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; (@insert_json $map:ident, $name:ident, Option) => { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; (@insert_json $map:ident, $name:ident, Option) => { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; (@insert_json $map:ident, $name:ident, Option) => { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; (@insert_json $map:ident, $name:ident, Option) => { if let Some(v) = $name { $map.insert(stringify!($name).into(), serde_json::json!(v)); } }; // Call hippocampus with appropriate mutability (@call mut, $name:ident, $store:ident, $prov:expr $(, $arg:expr)*) => { local::$name(&mut $store, $prov $(, $arg)*) }; (@call ref, $name:ident, $store:ident, $prov:expr $(, $arg:expr)*) => { local::$name(&$store, $prov $(, $arg)*) }; // ── Main rules ───────────────────────────────────────────────── // Shorthand: mut/ref without return type defaults to String ($name:ident, $m:ident $(, $($arg:ident : [$($typ:tt)+]),* $(,)?)?) => { memory_tool!($name, $m -> String $(, $($arg : [$($typ)+]),*)?); }; // Full form with return type ($name:ident, $m:ident -> $ret:ty $(, $($arg:ident : [$($typ:tt)+]),* $(,)?)?) => { paste::paste! { pub async fn $name(agent: Option<&crate::agent::Agent> $($(, $arg: memory_tool!(@param_type $($typ)+))*)?) -> Result<$ret> { let prov = match agent { Some(a) => a.state.lock().await.provenance.clone(), None => "manual".to_string(), }; match access() { StoreAccess::Daemon(arc) => { #[allow(unused_mut)] let mut store = arc.lock().await; memory_tool!(@call $m, $name, store, &prov $($(, $arg)*)?) } StoreAccess::Client => { #[allow(unused_mut)] let mut map = serde_json::Map::new(); $($(memory_tool!(@insert_json map, $arg, $($typ)+);)*)? let json = memory_rpc(stringify!($name), serde_json::Value::Object(map))?; memory_tool!(@deserialize $ret, json) } StoreAccess::None(err) => anyhow::bail!("{}", err), } } } }; } // ── Memory tools ─────────────────────────────────────────────── memory_tool!(memory_render, ref, key: [str], raw: [Option]); memory_tool!(memory_write, mut, key: [str], content: [str]); memory_tool!(memory_search, ref, keys: [Vec], max_hops: [Option], edge_decay: [Option], min_activation: [Option], limit: [Option]); memory_tool!(memory_link_set, mut, source: [str], target: [str], strength: [f32]); memory_tool!(memory_link_add, mut, source: [str], target: [str]); memory_tool!(memory_delete, mut, key: [str]); memory_tool!(memory_history, ref, key: [str], full: [Option]); memory_tool!(memory_weight_set, mut, key: [str], weight: [f32]); memory_tool!(memory_rename, mut, old_key: [str], new_key: [str]); memory_tool!(memory_supersede, mut, old_key: [str], new_key: [str], reason: [Option<&str>]); memory_tool!(memory_query, ref, query: [str], format: [Option<&str>]); memory_tool!(memory_links, ref -> Vec, key: [str]); // ── Journal tools ────────────────────────────────────────────── memory_tool!(journal_tail, ref -> Vec, count: [Option], level: [Option], after: [Option<&str>]); memory_tool!(journal_new, mut, name: [str], title: [str], body: [str], level: [Option]); memory_tool!(journal_update, mut, body: [str], level: [Option]); // ── Graph tools ─────────────────────────────────────────────── memory_tool!(graph_topology, ref); memory_tool!(graph_health, ref); memory_tool!(graph_communities, ref, top_n: [Option], min_size: [Option]); memory_tool!(graph_normalize_strengths, mut, apply: [Option]); memory_tool!(graph_link_impact, ref, source: [str], target: [str]); memory_tool!(graph_hubs, ref, count: [Option]); memory_tool!(graph_trace, ref, key: [str]);