use std::path::{Path, PathBuf};
use std::sync::Arc;
use chrono::Utc;
use trusty_common::memory_core::palace::{Palace, PalaceId, RoomType};
use trusty_common::memory_core::registry::PalaceRegistry;
use trusty_common::memory_core::retrieval::{
PalaceHandle, RecallResult, RememberOptions, recall_deep_with_default_embedder,
recall_with_default_embedder,
};
use super::config::SmMemoryConfig;
const SM_DEFAULT_IMPORTANCE: f32 = 0.7;
#[derive(Debug, thiserror::Error)]
pub enum SmMemoryError {
#[error("session-manager palace '{palace}' unavailable: {source}")]
Palace {
palace: String,
source: anyhow::Error,
},
#[error("session-manager memory operation '{op}' failed: {source}")]
Operation {
op: &'static str,
source: anyhow::Error,
},
}
pub type SmMemoryResult<T> = std::result::Result<T, SmMemoryError>;
#[derive(Clone)]
pub struct SmMemory {
registry: PalaceRegistry,
data_root: PathBuf,
palace_id: PalaceId,
recall_top_k: usize,
}
impl SmMemory {
pub fn open(data_root: impl Into<PathBuf>, cfg: &SmMemoryConfig) -> SmMemoryResult<Self> {
let data_root = data_root.into();
let palace_id = PalaceId::new(cfg.palace.clone());
let recall_top_k = cfg.recall_top_k as usize;
let registry =
PalaceRegistry::open(&data_root).map_err(|source| SmMemoryError::Palace {
palace: palace_id.to_string(),
source,
})?;
let me = Self {
registry,
data_root,
palace_id,
recall_top_k,
};
me.ensure_palace()?;
Ok(me)
}
pub fn palace_id(&self) -> &PalaceId {
&self.palace_id
}
fn ensure_palace(&self) -> SmMemoryResult<Arc<PalaceHandle>> {
if let Some(handle) = self.registry.get(&self.palace_id) {
return Ok(handle);
}
if let Ok(handle) = self.registry.open_palace(&self.data_root, &self.palace_id) {
return Ok(handle);
}
let palace = Palace {
id: self.palace_id.clone(),
name: "Session Manager".to_string(),
description: Some(
"Dedicated Session-Manager memory palace (DOC-14 §8): goals, \
outcomes, and decisions across sessions."
.to_string(),
),
created_at: Utc::now(),
data_dir: self.data_root.join(self.palace_id.as_str()),
};
self.registry
.create_palace(&self.data_root, palace)
.map_err(|source| SmMemoryError::Palace {
palace: self.palace_id.to_string(),
source,
})
}
pub async fn recall(&self, query: &str) -> SmMemoryResult<Vec<RecallResult>> {
let handle = self.ensure_palace()?;
recall_with_default_embedder(&handle, query, self.recall_top_k)
.await
.map_err(|source| SmMemoryError::Operation {
op: "recall",
source,
})
}
pub async fn recall_deep(&self, query: &str) -> SmMemoryResult<Vec<RecallResult>> {
let handle = self.ensure_palace()?;
recall_deep_with_default_embedder(&handle, query, self.recall_top_k)
.await
.map_err(|source| SmMemoryError::Operation {
op: "recall_deep",
source,
})
}
pub async fn remember(&self, text: impl Into<String>) -> SmMemoryResult<String> {
let handle = self.ensure_palace()?;
handle
.remember(
text.into(),
RoomType::General,
Vec::new(),
SM_DEFAULT_IMPORTANCE,
)
.await
.map(|id| id.to_string())
.map_err(|source| SmMemoryError::Operation {
op: "remember",
source,
})
}
pub async fn note(&self, text: impl Into<String>) -> SmMemoryResult<String> {
let handle = self.ensure_palace()?;
handle
.remember_with_options(
text.into(),
RoomType::General,
Vec::new(),
SM_DEFAULT_IMPORTANCE,
RememberOptions::note(),
)
.await
.map(|id| id.to_string())
.map_err(|source| SmMemoryError::Operation { op: "note", source })
}
pub async fn remember_tagged(
&self,
text: impl Into<String>,
tags: Vec<String>,
) -> SmMemoryResult<String> {
let handle = self.ensure_palace()?;
handle
.remember_with_options(
text.into(),
RoomType::General,
tags,
SM_DEFAULT_IMPORTANCE,
RememberOptions::note(),
)
.await
.map(|id| id.to_string())
.map_err(|source| SmMemoryError::Operation {
op: "remember_tagged",
source,
})
}
pub fn list_tagged(&self, tag: &str) -> SmMemoryResult<Vec<String>> {
let handle = self.ensure_palace()?;
const LIST_CAP: usize = 100_000;
const LIST_WARN_THRESHOLD: usize = LIST_CAP / 10 * 9; let drawers = handle.list_drawers(None, Some(tag.to_string()), LIST_CAP);
if drawers.len() >= LIST_WARN_THRESHOLD {
tracing::warn!(
tag,
returned = drawers.len(),
cap = LIST_CAP,
"SM palace enumeration is approaching LIST_CAP; goal entries may be \
truncated on rebuild — revisit the cap"
);
}
Ok(drawers.into_iter().map(|d| d.content).collect())
}
pub fn persisted_palace_count(&self) -> SmMemoryResult<usize> {
Self::count_persisted(&self.data_root, &self.palace_id)
}
fn count_persisted(data_root: &Path, palace_id: &PalaceId) -> SmMemoryResult<usize> {
PalaceRegistry::list_palaces(data_root)
.map(|palaces| palaces.len())
.map_err(|source| SmMemoryError::Palace {
palace: palace_id.to_string(),
source,
})
}
}
#[cfg(test)]
#[path = "memory_tests.rs"]
mod tests;