use crate::memory_core::analytics::{RecallEvent, RecallLog, query_hash};
const RECALL_LOG_FILENAME: &str = "recall.db";
use crate::memory_core::decay::DecayConfig;
use crate::memory_core::dream::extract_keywords;
use crate::memory_core::embed::{Embedder, FastEmbedder};
use crate::memory_core::palace::{Drawer, Palace, PalaceId, RoomType};
use crate::memory_core::store::kg::KnowledgeGraph;
use crate::memory_core::store::l1_cache::L1Cache;
use crate::memory_core::store::palace_store::PalaceStore;
use crate::memory_core::store::vector::{UsearchStore, VectorStore};
use anyhow::{Context, Result};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::OnceCell;
use uuid::Uuid;
static SHARED_EMBEDDER: OnceCell<Arc<FastEmbedder>> = OnceCell::const_new();
pub async fn shared_embedder() -> Result<Arc<FastEmbedder>> {
SHARED_EMBEDDER
.get_or_try_init(|| async {
let e = FastEmbedder::new()
.await
.context("init shared FastEmbedder")?;
Ok::<Arc<FastEmbedder>, anyhow::Error>(Arc::new(e))
})
.await
.cloned()
}
pub struct L0Identity {
pub content: String,
}
pub struct L1Essential {
pub drawers: Vec<Drawer>,
}
#[derive(Debug, Clone)]
pub struct RecallResult {
pub drawer: Drawer,
pub score: f32,
pub layer: u8,
}
const L1_CAP: usize = 15;
pub struct PalaceHandle {
pub id: PalaceId,
pub identity: String,
pub l1_drawers: Vec<Drawer>,
pub vector_store: Arc<UsearchStore>,
pub kg: Arc<KnowledgeGraph>,
pub drawers: Arc<RwLock<Vec<Drawer>>>,
pub data_dir: Option<std::path::PathBuf>,
pub decay_config: DecayConfig,
pub recall_log: Option<Arc<RecallLog>>,
pub closets: Arc<RwLock<HashMap<String, Vec<Uuid>>>>,
}
impl PalaceHandle {
pub fn new(
id: PalaceId,
identity: String,
vector_store: UsearchStore,
kg: KnowledgeGraph,
) -> Self {
Self {
id,
identity,
l1_drawers: Vec::new(),
vector_store: Arc::new(vector_store),
kg: Arc::new(kg),
drawers: Arc::new(RwLock::new(Vec::new())),
data_dir: None,
decay_config: DecayConfig::default(),
recall_log: None,
closets: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn open(palace: &Palace) -> Result<Arc<PalaceHandle>> {
let data_dir = &palace.data_dir;
std::fs::create_dir_all(data_dir)
.with_context(|| format!("create palace data dir {}", data_dir.display()))?;
let identity = PalaceStore::load_identity(data_dir)
.with_context(|| format!("load identity for {}", palace.id))?
.unwrap_or_default();
let l1_drawers = L1Cache::load_l1_cache(data_dir)
.with_context(|| format!("load L1 cache for {}", palace.id))?;
let vector_path = data_dir.join("index.usearch");
let vector_store = UsearchStore::new(vector_path, 384)
.with_context(|| format!("open vector store for {}", palace.id))?;
let kg_path = data_dir.join("kg.db");
let kg =
KnowledgeGraph::open(&kg_path).with_context(|| format!("open KG for {}", palace.id))?;
let persisted_drawers = match kg.load_drawers() {
Ok(d) => d,
Err(e) => {
tracing::warn!(palace = %palace.id, "load_drawers failed, falling back to L1 only: {e:#}");
Vec::new()
}
};
let mut all_drawers = persisted_drawers;
for l1 in &l1_drawers {
if !all_drawers.iter().any(|d| d.id == l1.id) {
all_drawers.push(l1.clone());
}
}
let index_count = vector_store.index_size();
let drawer_count = all_drawers.len();
if index_count > drawer_count + 5 {
tracing::warn!(
palace = %palace.id,
index_vectors = index_count,
drawer_records = drawer_count,
"vector index has orphaned entries — consider re-ingesting"
);
}
let drawers = Arc::new(RwLock::new(all_drawers));
let recall_log = match RecallLog::open(&data_dir.join(RECALL_LOG_FILENAME)) {
Ok(log) => Some(Arc::new(log)),
Err(e) => {
tracing::warn!(palace = %palace.id, "open recall log failed, analytics disabled: {e:#}");
None
}
};
let handle = PalaceHandle {
id: palace.id.clone(),
identity,
l1_drawers,
vector_store: Arc::new(vector_store),
kg: Arc::new(kg),
drawers,
data_dir: Some(data_dir.clone()),
decay_config: DecayConfig::default(),
recall_log,
closets: Arc::new(RwLock::new(HashMap::new())),
};
Ok(Arc::new(handle))
}
pub fn flush(&self) -> Result<()> {
let Some(data_dir) = self.data_dir.as_ref() else {
return Ok(());
};
let drawers = self.drawers.read().clone();
L1Cache::save_l1_cache(&drawers, data_dir)
.with_context(|| format!("save L1 cache for {}", self.id))?;
PalaceStore::save_identity(&self.id, &self.identity, data_dir)
.with_context(|| format!("save identity for {}", self.id))?;
Ok(())
}
pub fn with_recall_log(mut self, log: Arc<RecallLog>) -> Self {
self.recall_log = Some(log);
self
}
pub fn with_decay_config(mut self, config: DecayConfig) -> Self {
self.decay_config = config;
self
}
pub fn add_drawer(&self, drawer: Drawer) {
let mut drawers = self.drawers.write();
drawers.push(drawer);
}
pub fn refresh_l1(&mut self) {
let drawers = self.drawers.read();
let mut sorted: Vec<Drawer> = drawers.clone();
sorted.sort_by(|a, b| {
b.importance
.partial_cmp(&a.importance)
.unwrap_or(std::cmp::Ordering::Equal)
});
self.l1_drawers = sorted.into_iter().take(L1_CAP).collect();
}
pub async fn remember(
&self,
content: String,
room: RoomType,
tags: Vec<String>,
importance: f32,
) -> Result<Uuid> {
let room_id = room_to_uuid(&room);
let mut drawer = Drawer::new(room_id, content.clone());
drawer.tags = tags;
drawer.importance = importance.clamp(0.0, 1.0);
let id = drawer.id;
let embedder = shared_embedder()
.await
.context("acquire shared embedder for remember")?;
let vecs = embedder
.embed_batch(&[content])
.await
.context("embed drawer content")?;
if let Some(v) = vecs.into_iter().next() {
self.vector_store
.upsert(id, v)
.await
.context("upsert drawer vector")?;
}
self.kg
.upsert_drawer(&drawer)
.context("persist drawer metadata")?;
{
let mut drawers = self.drawers.write();
drawers.push(drawer);
}
if let Some(data_dir) = self.data_dir.as_ref() {
let snap = self.drawers.read().clone();
L1Cache::save_l1_cache(&snap, data_dir).context("save L1 snapshot")?;
}
self.rebuild_closets();
Ok(id)
}
pub fn rebuild_closets(&self) {
let snapshot: Vec<Drawer> = self.drawers.read().clone();
let mut new_index: HashMap<String, Vec<Uuid>> = HashMap::new();
for drawer in snapshot.iter() {
for kw in extract_keywords(&drawer.content) {
new_index.entry(kw).or_default().push(drawer.id);
}
}
let mut closets = self.closets.write();
*closets = new_index;
}
pub async fn forget(&self, id: Uuid) -> Result<()> {
if let Err(e) = self.vector_store.remove(id).await {
tracing::warn!(?id, "vector remove failed: {e:#}");
}
if let Err(e) = self.kg.delete_drawer(id) {
tracing::warn!(?id, "drawer metadata delete failed: {e:#}");
}
{
let mut drawers = self.drawers.write();
drawers.retain(|d| d.id != id);
}
if let Some(data_dir) = self.data_dir.as_ref() {
let snap = self.drawers.read().clone();
L1Cache::save_l1_cache(&snap, data_dir).context("save L1 snapshot after forget")?;
}
Ok(())
}
pub fn list_drawers(
&self,
room: Option<RoomType>,
tag: Option<String>,
limit: usize,
) -> Vec<Drawer> {
let drawers = self.drawers.read();
let target_room_id = room.as_ref().map(room_to_uuid);
let mut filtered: Vec<Drawer> = drawers
.iter()
.filter(|d| match &target_room_id {
Some(rid) => d.room_id == *rid,
None => true,
})
.filter(|d| match &tag {
Some(t) => d.tags.iter().any(|x| x == t),
None => true,
})
.cloned()
.collect();
drop(drawers);
filtered.sort_by(|a, b| {
b.importance
.partial_cmp(&a.importance)
.unwrap_or(std::cmp::Ordering::Equal)
});
filtered.truncate(limit);
filtered
}
}
pub async fn recall_with_default_embedder(
handle: &PalaceHandle,
query: &str,
top_k: usize,
) -> Result<Vec<RecallResult>> {
let embedder = shared_embedder()
.await
.context("acquire shared embedder for recall")?;
recall(handle, embedder.as_ref(), query, top_k).await
}
pub async fn recall_deep_with_default_embedder(
handle: &PalaceHandle,
query: &str,
top_k: usize,
) -> Result<Vec<RecallResult>> {
let embedder = shared_embedder()
.await
.context("acquire shared embedder for recall_deep")?;
recall_deep(handle, embedder.as_ref(), query, top_k).await
}
#[derive(Debug, Clone)]
pub struct CrossPalaceResult {
pub palace_id: String,
pub result: RecallResult,
}
pub async fn recall_across_palaces(
handles: &[Arc<PalaceHandle>],
embedder: &Arc<dyn Embedder + Send + Sync>,
query: &str,
top_k: usize,
deep: bool,
) -> Result<Vec<CrossPalaceResult>> {
if handles.is_empty() || top_k == 0 {
return Ok(Vec::new());
}
let mut futures = Vec::with_capacity(handles.len());
for handle in handles {
let palace_id = handle.id.as_str().to_string();
let handle = handle.clone();
let embedder = embedder.clone();
let query = query.to_string();
futures.push(async move {
let result = if deep {
recall_deep(&handle, embedder.as_ref(), &query, top_k).await
} else {
recall(&handle, embedder.as_ref(), &query, top_k).await
};
(palace_id, result)
});
}
let outcomes = futures::future::join_all(futures).await;
let mut merged: Vec<CrossPalaceResult> = Vec::new();
let mut by_drawer: HashMap<Uuid, usize> = HashMap::new();
for (palace_id, outcome) in outcomes {
match outcome {
Ok(hits) => {
for r in hits {
let drawer_id = r.drawer.id;
let candidate = CrossPalaceResult {
palace_id: palace_id.clone(),
result: r,
};
match by_drawer.get(&drawer_id).copied() {
Some(idx) if merged[idx].result.score >= candidate.result.score => {
}
Some(idx) => {
merged[idx] = candidate;
}
None => {
by_drawer.insert(drawer_id, merged.len());
merged.push(candidate);
}
}
}
}
Err(e) => {
tracing::warn!(palace = %palace_id, "recall_across_palaces: skipping palace: {e:#}");
}
}
}
merged.sort_by(|a, b| {
b.result
.score
.partial_cmp(&a.result.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
merged.truncate(top_k);
Ok(merged)
}
pub async fn recall_across_palaces_with_default_embedder(
handles: &[Arc<PalaceHandle>],
query: &str,
top_k: usize,
deep: bool,
) -> Result<Vec<CrossPalaceResult>> {
let embedder = shared_embedder()
.await
.context("acquire shared embedder for recall_across_palaces")?;
let erased: Arc<dyn Embedder + Send + Sync> = embedder;
recall_across_palaces(handles, &erased, query, top_k, deep).await
}
fn room_to_uuid(room: &RoomType) -> Uuid {
let label = format!("{room:?}");
let mut bytes = [0u8; 16];
for (i, b) in label.bytes().enumerate() {
bytes[i % 16] ^= b.wrapping_add(i as u8);
}
Uuid::from_bytes(bytes)
}
fn uuid_prefix_eq(a: Uuid, b: Uuid) -> bool {
a.as_bytes()[..8] == b.as_bytes()[..8]
}
pub fn retrieve_l0_l1(handle: &PalaceHandle) -> Vec<RecallResult> {
let mut out: Vec<RecallResult> = Vec::with_capacity(1 + handle.l1_drawers.len());
if !handle.identity.is_empty() {
let identity_drawer = Drawer {
id: Uuid::nil(),
room_id: Uuid::nil(),
content: handle.identity.clone(),
importance: 1.0,
source_file: None,
created_at: chrono::Utc::now(),
tags: Vec::new(),
last_accessed_at: None,
access_count: 0,
};
out.push(RecallResult {
drawer: identity_drawer,
score: 1.0,
layer: 0,
});
}
for d in &handle.l1_drawers {
out.push(RecallResult {
drawer: d.clone(),
score: d.importance,
layer: 1,
});
}
out
}
pub async fn retrieve_l2(
handle: &PalaceHandle,
embedder: &dyn Embedder,
query: &str,
room_filter: Option<RoomType>,
top_k: usize,
) -> Result<Vec<RecallResult>> {
if top_k == 0 {
return Ok(Vec::new());
}
let embeddings = embedder.embed_batch(&[query.to_string()]).await?;
let Some(query_vec) = embeddings.into_iter().next() else {
return Ok(Vec::new());
};
let overfetch = top_k.saturating_mul(3).max(top_k);
let hits = handle.vector_store.search(&query_vec, overfetch).await?;
let drawers = handle.drawers.read();
let closets = handle.closets.read();
let query_tokens: Vec<String> = extract_keywords(query);
let mut results: Vec<RecallResult> = Vec::with_capacity(hits.len());
for hit in hits {
let Some(drawer) = drawers.iter().find(|d| uuid_prefix_eq(d.id, hit.drawer_id)) else {
continue;
};
if room_filter.is_some() {
}
let age_days = DecayConfig::age_days(drawer.created_at);
let boost = drawer.accumulated_boost(&handle.decay_config);
let eff_importance =
handle
.decay_config
.effective_importance(drawer.importance, age_days, boost);
let effective_score = eff_importance * hit.score;
let drawer_id = drawer.id;
let in_closet = query_tokens
.iter()
.any(|tok| closets.get(tok).is_some_and(|ids| ids.contains(&drawer_id)));
let tag_boost = if in_closet { 0.15_f32 } else { 0.0 };
let final_score = (effective_score + tag_boost).min(1.0);
results.push(RecallResult {
drawer: drawer.clone(),
score: final_score,
layer: 2,
});
}
drop(closets);
drop(drawers);
results.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
results.truncate(top_k);
Ok(results)
}
pub async fn retrieve_l3(
handle: &PalaceHandle,
embedder: &dyn Embedder,
query: &str,
top_k: usize,
) -> Result<Vec<RecallResult>> {
if top_k == 0 {
return Ok(Vec::new());
}
let embeddings = embedder.embed_batch(&[query.to_string()]).await?;
let Some(query_vec) = embeddings.into_iter().next() else {
return Ok(Vec::new());
};
let hits = handle.vector_store.search(&query_vec, top_k).await?;
let drawers = handle.drawers.read();
let closets = handle.closets.read();
let query_tokens: Vec<String> = extract_keywords(query);
let mut results: Vec<RecallResult> = Vec::with_capacity(hits.len());
for hit in hits {
let Some(drawer) = drawers.iter().find(|d| uuid_prefix_eq(d.id, hit.drawer_id)) else {
continue;
};
let age_days = DecayConfig::age_days(drawer.created_at);
let boost = drawer.accumulated_boost(&handle.decay_config);
let eff_importance =
handle
.decay_config
.effective_importance(drawer.importance, age_days, boost);
let effective_score = eff_importance * hit.score;
let drawer_id = drawer.id;
let in_closet = query_tokens
.iter()
.any(|tok| closets.get(tok).is_some_and(|ids| ids.contains(&drawer_id)));
let tag_boost = if in_closet { 0.15_f32 } else { 0.0 };
let final_score = (effective_score + tag_boost).min(1.0);
results.push(RecallResult {
drawer: drawer.clone(),
score: final_score,
layer: 3,
});
}
drop(closets);
drop(drawers);
results.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
results.truncate(top_k);
Ok(results)
}
pub fn expand_query(query: &str) -> String {
let q = query.to_lowercase();
let mut extra: Vec<&str> = Vec::new();
if q.contains("fast")
|| q.contains("speed")
|| q.contains("latency")
|| q.contains("performance")
{
extra.push("latency performance speed throughput");
}
if q.contains("vector search")
|| q.contains("semantic search")
|| q.contains("nearest neighbor")
{
extra.push("HNSW ANN approximate nearest neighbor usearch vector index");
}
if q.contains("memory safe") || q.contains("borrow") || q.contains("ownership") {
extra.push("borrow checker lifetime ownership Rust memory safety");
}
if q.contains("concurren") || q.contains("thread") || q.contains("parallel") {
extra.push("concurrent async tokio DashMap RwLock mutex thread-safe");
}
if extra.is_empty() {
query.to_string()
} else {
format!("{} {}", query, extra.join(" "))
}
}
pub async fn recall(
handle: &PalaceHandle,
embedder: &dyn Embedder,
query: &str,
top_k: usize,
) -> Result<Vec<RecallResult>> {
let expanded = expand_query(query);
let mut combined = retrieve_l0_l1(handle);
let l2 = retrieve_l2(handle, embedder, &expanded, None, top_k).await?;
dedup_extend(&mut combined, l2);
log_recall(handle, query, &combined);
Ok(combined)
}
pub async fn recall_deep(
handle: &PalaceHandle,
embedder: &dyn Embedder,
query: &str,
top_k: usize,
) -> Result<Vec<RecallResult>> {
let expanded = expand_query(query);
let mut combined = retrieve_l0_l1(handle);
let l3 = retrieve_l3(handle, embedder, &expanded, top_k).await?;
dedup_extend(&mut combined, l3);
log_recall(handle, query, &combined);
Ok(combined)
}
fn log_recall(handle: &PalaceHandle, query: &str, results: &[RecallResult]) {
let Some(log) = handle.recall_log.clone() else {
return;
};
let palace_id = handle.id.as_str().to_string();
let q_hash = query_hash(query);
let logged: Vec<RecallResult> = results.iter().filter(|r| r.layer > 0).cloned().collect();
tokio::spawn(async move {
let now = chrono::Utc::now();
if logged.is_empty() {
let _ = log
.record(RecallEvent {
palace_id,
query_hash: q_hash,
layer: 3,
drawer_id: None,
score: 0.0,
occurred_at: now,
})
.await;
} else {
for r in &logged {
let _ = log
.record(RecallEvent {
palace_id: palace_id.clone(),
query_hash: q_hash,
layer: r.layer,
drawer_id: Some(r.drawer.id),
score: r.score,
occurred_at: now,
})
.await;
}
}
});
}
fn dedup_extend(base: &mut Vec<RecallResult>, extra: Vec<RecallResult>) {
for r in extra {
if !base.iter().any(|b| b.drawer.id == r.drawer.id) {
base.push(r);
}
}
}
pub struct RetrievalLayers;
impl RetrievalLayers {
pub async fn load_l0(_palace_data_dir: &Path) -> Result<L0Identity> {
Ok(L0Identity {
content: String::new(),
})
}
pub async fn load_l1(_palace_id: &PalaceId) -> Result<L1Essential> {
Ok(L1Essential {
drawers: Vec::new(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_core::store::{kg::KnowledgeGraph, vector::UsearchStore};
use tempfile::tempdir;
fn make_handle(dir: &std::path::Path) -> PalaceHandle {
let vs = UsearchStore::new(dir.join("idx.usearch"), 384).unwrap();
let kg = KnowledgeGraph::open(&dir.join("kg.db")).unwrap();
PalaceHandle::new(PalaceId::new("test"), "Test palace".to_string(), vs, kg)
}
#[test]
fn l0_l1_always_present() {
let dir = tempdir().unwrap();
let mut handle = make_handle(dir.path());
let room_id = uuid::Uuid::new_v4();
let mut d = Drawer::new(room_id, "important fact");
d.importance = 0.9;
handle.add_drawer(d);
handle.refresh_l1();
let results = retrieve_l0_l1(&handle);
assert!(results.iter().any(|r| r.layer == 0), "L0 identity missing");
assert!(results.iter().any(|r| r.layer == 1), "L1 drawer missing");
}
#[tokio::test]
async fn l2_returns_relevant_drawer() {
let dir = tempdir().unwrap();
let handle = make_handle(dir.path());
let embedder = crate::memory_core::embed::FastEmbedder::new()
.await
.unwrap();
let room_id = uuid::Uuid::new_v4();
let drawer = Drawer::new(room_id, "Rust is a systems programming language");
let drawer_id = drawer.id;
let vecs = embedder
.embed_batch(std::slice::from_ref(&drawer.content))
.await
.unwrap();
handle
.vector_store
.upsert(drawer_id, vecs[0].clone())
.await
.unwrap();
handle.add_drawer(drawer);
let results = retrieve_l2(&handle, &embedder, "systems programming Rust", None, 5)
.await
.unwrap();
assert!(!results.is_empty(), "L2 should return results");
assert!(
uuid_prefix_eq(results[0].drawer.id, drawer_id),
"Top L2 result should match the upserted drawer (got {:?}, want {:?})",
results[0].drawer.id,
drawer_id
);
assert_eq!(results[0].layer, 2);
}
#[tokio::test]
async fn cli_remember_and_recall() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace = Palace {
id: PalaceId::new("test"),
name: "Test".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("test"),
};
std::fs::create_dir_all(&palace.data_dir).unwrap();
let handle = PalaceHandle::open(&palace).unwrap();
let _id = handle
.remember(
"Rust async runtime is tokio".into(),
RoomType::Backend,
vec!["rust".into()],
0.7,
)
.await
.unwrap();
handle
.remember(
"React uses a virtual DOM".into(),
RoomType::Frontend,
vec![],
0.5,
)
.await
.unwrap();
let results = recall_with_default_embedder(&handle, "tokio rust async", 5)
.await
.unwrap();
assert!(
results.iter().any(|r| r.drawer.content.contains("tokio")),
"expected to recall the tokio drawer; got {results:?}"
);
}
#[tokio::test]
async fn cli_forget_removes_drawer() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace = Palace {
id: PalaceId::new("forget-test"),
name: "Forget".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("forget-test"),
};
std::fs::create_dir_all(&palace.data_dir).unwrap();
let handle = PalaceHandle::open(&palace).unwrap();
let id = handle
.remember(
"ephemeral fact about Quokkas".into(),
RoomType::General,
vec![],
0.5,
)
.await
.unwrap();
handle.forget(id).await.unwrap();
let results = recall_with_default_embedder(&handle, "Quokkas ephemeral", 5)
.await
.unwrap();
assert!(
!results.iter().any(|r| r.drawer.id == id),
"forgotten drawer should not appear in recall results"
);
}
#[tokio::test]
async fn cli_list_filters_by_room() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace = Palace {
id: PalaceId::new("list-test"),
name: "List".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("list-test"),
};
std::fs::create_dir_all(&palace.data_dir).unwrap();
let handle = PalaceHandle::open(&palace).unwrap();
handle
.remember("backend fact".into(), RoomType::Backend, vec![], 0.5)
.await
.unwrap();
handle
.remember("frontend fact".into(), RoomType::Frontend, vec![], 0.5)
.await
.unwrap();
handle
.remember("docs fact".into(), RoomType::Documentation, vec![], 0.5)
.await
.unwrap();
let backend_only = handle.list_drawers(Some(RoomType::Backend), None, 10);
assert_eq!(
backend_only.len(),
1,
"expected exactly 1 backend drawer, got {backend_only:?}"
);
assert!(backend_only[0].content.contains("backend"));
}
#[tokio::test]
async fn recall_logs_events_when_log_present() {
let dir = tempdir().unwrap();
let log = Arc::new(RecallLog::open(&dir.path().join("recall.db")).unwrap());
let mut handle = make_handle(dir.path()).with_recall_log(log.clone());
let embedder = crate::memory_core::embed::FastEmbedder::new()
.await
.unwrap();
let room_id = uuid::Uuid::new_v4();
let drawer = Drawer::new(room_id, "Rust is a systems programming language");
let drawer_id = drawer.id;
let vecs = embedder
.embed_batch(std::slice::from_ref(&drawer.content))
.await
.unwrap();
handle
.vector_store
.upsert(drawer_id, vecs[0].clone())
.await
.unwrap();
handle.add_drawer(drawer);
handle.refresh_l1();
let _ = recall(&handle, &embedder, "systems programming Rust", 5)
.await
.unwrap();
let mut hits = 0u64;
for _ in 0..20 {
hits = log.hit_count(drawer_id).await.unwrap();
if hits >= 1 {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(hits >= 1, "expected at least one logged hit, got {hits}");
}
#[tokio::test]
async fn open_attaches_recall_log_automatically() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace = Palace {
id: PalaceId::new("analytics-auto"),
name: "AnalyticsAuto".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("analytics-auto"),
};
std::fs::create_dir_all(&palace.data_dir).unwrap();
let handle = PalaceHandle::open(&palace).unwrap();
assert!(
handle.recall_log.is_some(),
"PalaceHandle::open must auto-attach a RecallLog (issue #53)"
);
assert!(
palace.data_dir.join("recall.db").exists(),
"recall.db must exist on disk after open"
);
let drawer_id = handle
.remember(
"the platypus is a monotreme native to eastern Australia".into(),
RoomType::Research,
vec!["wildlife".into()],
0.7,
)
.await
.unwrap();
let embedder = crate::memory_core::embed::FastEmbedder::new()
.await
.unwrap();
let _ = recall(&handle, &embedder, "platypus monotreme", 5)
.await
.unwrap();
let log = handle.recall_log.as_ref().unwrap().clone();
let mut hits = 0u64;
for _ in 0..20 {
hits = log.hit_count(drawer_id).await.unwrap();
if hits >= 1 {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(
hits >= 1,
"auto-attached recall log must record events; got {hits}"
);
}
#[tokio::test]
async fn closet_updated_after_remember() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace = Palace {
id: PalaceId::new("closet-test"),
name: "Closet".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("closet-test"),
};
std::fs::create_dir_all(&palace.data_dir).unwrap();
let handle = PalaceHandle::open(&palace).unwrap();
let id = handle
.remember(
"Quokkas are happy marsupials".into(),
RoomType::General,
vec![],
0.5,
)
.await
.unwrap();
let closets = handle.closets.read();
let entry = closets
.get("quokkas")
.expect("expected `quokkas` keyword in closet index");
assert!(
entry.contains(&id),
"closet entry for `quokkas` should contain the new drawer id"
);
}
#[test]
fn expand_query_adds_synonyms() {
let out = expand_query("how fast is vector search?");
assert!(out.contains("HNSW"), "expected HNSW synonym, got: {out}");
assert!(
out.contains("latency"),
"expected latency synonym, got: {out}"
);
}
#[test]
fn expand_query_noop_for_unmatched() {
let out = expand_query("what is a borrow checker?");
assert!(
out.contains("borrow checker"),
"expected original query preserved, got: {out}"
);
assert!(
out.contains("ownership") || out.contains("lifetime"),
"expected ownership/lifetime synonyms, got: {out}"
);
let untouched = expand_query("what colour is the sky on Tuesday");
assert_eq!(
untouched, "what colour is the sky on Tuesday",
"queries with no triggers must pass through unchanged"
);
}
#[tokio::test]
async fn cold_restart_recalls_beyond_l1_snapshot() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace = Palace {
id: PalaceId::new("cold-restart"),
name: "Cold".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("cold-restart"),
};
std::fs::create_dir_all(&palace.data_dir).unwrap();
let needle_id = {
let handle = PalaceHandle::open(&palace).unwrap();
for i in 0..19 {
handle
.remember(
format!("filler drawer number {i} about generic topics"),
RoomType::General,
vec![],
0.9,
)
.await
.unwrap();
}
handle
.remember(
"the pangolin is a scaly nocturnal mammal".into(),
RoomType::Research,
vec![],
0.1,
)
.await
.unwrap()
};
let handle2 = PalaceHandle::open(&palace).unwrap();
let count = handle2.drawers.read().len();
assert!(
count >= 20,
"expected >=20 drawers after cold reopen, got {count}"
);
let results = recall_with_default_embedder(&handle2, "pangolin scaly mammal", 10)
.await
.unwrap();
assert!(
results.iter().any(|r| r.drawer.id == needle_id),
"low-importance drawer beyond L1 must still be recallable after cold restart; got {results:?}"
);
}
#[tokio::test]
async fn shared_embedder_is_singleton() {
let a = shared_embedder().await.unwrap();
let b = shared_embedder().await.unwrap();
assert!(
Arc::ptr_eq(&a, &b),
"shared_embedder must return the same Arc on every call"
);
}
#[tokio::test]
async fn retrieve_l2_tag_boost_raises_rank() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace = Palace {
id: PalaceId::new("boost-test"),
name: "Boost".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("boost-test"),
};
std::fs::create_dir_all(&palace.data_dir).unwrap();
let handle = PalaceHandle::open(&palace).unwrap();
let id_tagged = handle
.remember(
"Vector search performance benchmarks show low latency".into(),
RoomType::Backend,
vec!["vector-search".into()],
0.5,
)
.await
.unwrap();
let _id_other = handle
.remember(
"React components render through a virtual DOM".into(),
RoomType::Frontend,
vec![],
0.5,
)
.await
.unwrap();
let embedder = crate::memory_core::embed::FastEmbedder::new()
.await
.unwrap();
let results = retrieve_l2(&handle, &embedder, "vector search performance", None, 5)
.await
.unwrap();
assert!(!results.is_empty(), "L2 should return results");
assert!(
uuid_prefix_eq(results[0].drawer.id, id_tagged),
"tagged drawer should rank first; got {:?}",
results[0].drawer.content
);
}
#[tokio::test]
async fn recall_across_palaces_merges_results() {
use crate::memory_core::palace::Palace;
let dir = tempdir().unwrap();
let palace_a = Palace {
id: PalaceId::new("alpha"),
name: "Alpha".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("alpha"),
};
std::fs::create_dir_all(&palace_a.data_dir).unwrap();
let handle_a = PalaceHandle::open(&palace_a).unwrap();
handle_a
.remember(
"the pangolin is a scaly nocturnal mammal".into(),
RoomType::Research,
vec![],
0.6,
)
.await
.unwrap();
let palace_b = Palace {
id: PalaceId::new("beta"),
name: "Beta".into(),
description: None,
created_at: chrono::Utc::now(),
data_dir: dir.path().join("beta"),
};
std::fs::create_dir_all(&palace_b.data_dir).unwrap();
let handle_b = PalaceHandle::open(&palace_b).unwrap();
handle_b
.remember(
"the platypus is a venomous monotreme".into(),
RoomType::Research,
vec![],
0.6,
)
.await
.unwrap();
let handles = vec![handle_a, handle_b];
let results = recall_across_palaces_with_default_embedder(
&handles,
"pangolin platypus mammal",
10,
false,
)
.await
.unwrap();
assert!(!results.is_empty(), "expected merged results, got none");
assert!(
results.iter().any(|r| r.palace_id == "alpha"),
"expected at least one alpha result; got {:?}",
results.iter().map(|r| &r.palace_id).collect::<Vec<_>>()
);
assert!(
results.iter().any(|r| r.palace_id == "beta"),
"expected at least one beta result; got {:?}",
results.iter().map(|r| &r.palace_id).collect::<Vec<_>>()
);
for w in results.windows(2) {
assert!(
w[0].result.score >= w[1].result.score,
"results not sorted: {} < {}",
w[0].result.score,
w[1].result.score
);
}
}
}