// 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::{Context, 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()),
}
}
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)
})
}
// ── Helpers ────────────────────────────────────────────────────
fn get_str<'a>(args: &'a serde_json::Value, name: &'a str) -> Result<&'a str> {
args.get(name).and_then(|v| v.as_str()).context(format!("{} is required", name))
}
fn get_f64(args: &serde_json::Value, name: &str) -> Result {
args.get(name).and_then(|v| v.as_f64()).context(format!("{} is required", name))
}
/// Get provenance from agent state, or "manual".
async fn get_provenance(agent: &Option>) -> String {
match agent {
Some(a) => a.state.lock().await.provenance.clone(),
None => "manual".to_string(),
}
}
// ── 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]);