use std::path::{Path, PathBuf};
use mentedb_cognitive::EntityResolver;
use mentedb_cognitive::interference::{InterferenceDetector, InterferencePair};
use mentedb_cognitive::llm::EntityMergeGroup;
use mentedb_cognitive::pain::{PainRegistry, PainSignal};
use mentedb_cognitive::phantom::{PhantomConfig, PhantomMemory, PhantomTracker};
use mentedb_cognitive::speculative::{CacheEntry, CacheStats, SpeculativeCache};
use mentedb_cognitive::stream::{CognitionStream, StreamAlert, StreamConfig};
use mentedb_cognitive::trajectory::{TrajectoryNode, TrajectoryTracker};
use mentedb_cognitive::write_inference::{
InferredAction, WriteInferenceConfig, WriteInferenceEngine,
};
use mentedb_consolidation::archival::{ArchivalConfig, ArchivalDecision, ArchivalPipeline};
use mentedb_consolidation::compression::{CompressedMemory, MemoryCompressor};
use mentedb_consolidation::consolidation::{ConsolidationCandidate, ConsolidationEngine};
use mentedb_consolidation::decay::{DecayConfig, DecayEngine};
use mentedb_context::{AssemblyConfig, ContextAssembler, ContextWindow, ScoredMemory};
use mentedb_core::edge::EdgeType;
use mentedb_core::error::MenteResult;
use mentedb_core::memory::MemoryType;
use mentedb_core::types::{MemoryId, Timestamp};
use mentedb_core::{MemoryEdge, MemoryNode, MenteError};
use mentedb_embedding::provider::EmbeddingProvider;
use mentedb_graph::GraphManager;
use mentedb_index::IndexManager;
use mentedb_query::{Mql, QueryPlan};
use mentedb_storage::StorageEngine;
use parking_lot::RwLock;
use tracing::{debug, info, warn};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub use mentedb_cognitive as cognitive;
pub use mentedb_consolidation as consolidation;
pub use mentedb_context as context;
pub use mentedb_core as core;
pub use mentedb_graph as graph;
pub use mentedb_index as index;
pub use mentedb_query as query;
pub use mentedb_storage as storage;
pub mod process_turn;
pub mod prelude {
pub use mentedb_core::edge::EdgeType;
pub use mentedb_core::error::MenteResult;
pub use mentedb_core::memory::MemoryType;
pub use mentedb_core::types::*;
pub use mentedb_core::{MemoryEdge, MemoryNode, MemoryTier, MenteError};
pub use crate::MenteDb;
}
use mentedb_storage::PageId;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct EnrichmentConfig {
pub enabled: bool,
pub trigger_interval: u64,
pub min_confidence: f32,
pub max_enrichment_confidence: f32,
pub enable_user_model: bool,
pub entity_merge_threshold: f32,
pub entity_separate_threshold: f32,
}
impl Default for EnrichmentConfig {
fn default() -> Self {
Self {
enabled: false,
trigger_interval: 50,
min_confidence: 0.6,
max_enrichment_confidence: 0.7,
enable_user_model: false,
entity_merge_threshold: 0.7,
entity_separate_threshold: 0.4,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct EnrichmentResult {
pub memories_stored: usize,
pub entities_processed: usize,
pub edges_created: usize,
pub duplicates_skipped: usize,
pub contradictions_found: usize,
pub completed_at_turn: u64,
pub entities_linked: usize,
pub entities_ambiguous: usize,
}
#[derive(Debug, Clone, Default)]
pub struct EntityLinkResult {
pub linked: usize,
pub ambiguous: usize,
pub edges_created: usize,
}
#[derive(Debug, Clone)]
pub struct EntityLinkResolution {
pub canonical: String,
pub aliases: Vec<String>,
pub confidence: f32,
}
#[derive(Debug, Clone)]
pub struct EntitySeparation {
pub name_a: String,
pub name_b: String,
}
#[derive(Debug, Clone)]
pub struct CognitiveConfig {
pub write_inference: bool,
pub decay_on_recall: bool,
pub pain_tracking: bool,
pub interference_detection: bool,
pub phantom_tracking: bool,
pub speculative_cache: bool,
pub archival_evaluation: bool,
pub inference_config: WriteInferenceConfig,
pub decay_config: DecayConfig,
pub phantom_config: PhantomConfig,
pub archival_config: ArchivalConfig,
pub stream_config: StreamConfig,
pub enrichment_config: EnrichmentConfig,
pub interference_threshold: f32,
pub trajectory_max_turns: usize,
pub speculative_cache_size: usize,
pub pain_max_warnings: usize,
}
impl Default for CognitiveConfig {
fn default() -> Self {
Self {
write_inference: true,
decay_on_recall: true,
pain_tracking: true,
interference_detection: true,
phantom_tracking: true,
speculative_cache: true,
archival_evaluation: true,
inference_config: WriteInferenceConfig::default(),
decay_config: DecayConfig::default(),
phantom_config: PhantomConfig::default(),
archival_config: ArchivalConfig::default(),
stream_config: StreamConfig::default(),
enrichment_config: EnrichmentConfig::default(),
interference_threshold: 0.8,
trajectory_max_turns: 100,
speculative_cache_size: 10,
pain_max_warnings: 5,
}
}
}
pub struct MenteDb {
storage: StorageEngine,
index: IndexManager,
graph: GraphManager,
page_map: RwLock<HashMap<MemoryId, PageId>>,
embedding_dim: usize,
path: PathBuf,
embedder: Option<Box<dyn EmbeddingProvider>>,
cognitive_config: CognitiveConfig,
write_inference: WriteInferenceEngine,
decay: DecayEngine,
consolidation: ConsolidationEngine,
pain: RwLock<PainRegistry>,
trajectory: RwLock<TrajectoryTracker>,
stream: CognitionStream,
phantom: RwLock<PhantomTracker>,
speculative: RwLock<SpeculativeCache>,
interference: InterferenceDetector,
entity_resolver: RwLock<EntityResolver>,
compressor: MemoryCompressor,
archival: ArchivalPipeline,
last_enrichment_turn: RwLock<u64>,
enrichment_pending: RwLock<bool>,
}
impl MenteDb {
pub fn open(path: &Path) -> MenteResult<Self> {
Self::open_with_config(path, CognitiveConfig::default())
}
pub fn open_with_config(path: &Path, cognitive_config: CognitiveConfig) -> MenteResult<Self> {
info!("Opening MenteDB at {}", path.display());
let storage = StorageEngine::open(path)?;
let index_dir = path.join("indexes");
let graph_dir = path.join("graph");
let index = if index_dir.join("hnsw.bin").exists() || index_dir.join("hnsw.json").exists() {
debug!("Loading indexes from {}", index_dir.display());
IndexManager::load(&index_dir)?
} else {
IndexManager::default()
};
let graph = if graph_dir.join("graph.json").exists() {
debug!("Loading graph from {}", graph_dir.display());
GraphManager::load(&graph_dir)?
} else {
GraphManager::new()
};
let entries = storage.scan_all_memories();
let mut page_map = HashMap::new();
for (memory_id, page_id) in &entries {
page_map.insert(*memory_id, *page_id);
}
if !page_map.is_empty() {
info!(memories = page_map.len(), "rebuilt page map from storage");
}
let write_inference =
WriteInferenceEngine::with_config(cognitive_config.inference_config.clone());
let decay = DecayEngine::new(cognitive_config.decay_config.clone());
let consolidation = ConsolidationEngine::new();
let pain = RwLock::new(PainRegistry::new(cognitive_config.pain_max_warnings));
let trajectory = RwLock::new(TrajectoryTracker::new(
cognitive_config.trajectory_max_turns,
));
let stream = CognitionStream::with_config(cognitive_config.stream_config.clone());
let phantom = RwLock::new(PhantomTracker::new(cognitive_config.phantom_config.clone()));
let speculative = RwLock::new(SpeculativeCache::new(
cognitive_config.speculative_cache_size,
0.5,
0.4,
));
let interference = InterferenceDetector::new(cognitive_config.interference_threshold);
let entity_resolver = RwLock::new(EntityResolver::new());
let compressor = MemoryCompressor::new();
let archival = ArchivalPipeline::new(cognitive_config.archival_config.clone());
let cognitive_dir = path.join("cognitive");
if cognitive_dir.exists() {
let _ = trajectory
.write()
.transitions
.load(&cognitive_dir.join("transitions.json"));
let _ = speculative
.write()
.load(&cognitive_dir.join("speculative.json"));
let _ = entity_resolver
.write()
.load(&cognitive_dir.join("entities.json"));
}
Ok(Self {
storage,
index,
graph,
page_map: RwLock::new(page_map),
embedding_dim: 0,
path: path.to_path_buf(),
embedder: None,
cognitive_config,
write_inference,
decay,
consolidation,
pain,
trajectory,
stream,
phantom,
speculative,
interference,
entity_resolver,
compressor,
archival,
last_enrichment_turn: RwLock::new(0),
enrichment_pending: RwLock::new(false),
})
}
pub fn open_with_embedder(
path: &Path,
embedder: Box<dyn EmbeddingProvider>,
) -> MenteResult<Self> {
let mut db = Self::open(path)?;
db.embedding_dim = embedder.dimensions();
db.embedder = Some(embedder);
Ok(db)
}
pub fn open_with_embedder_and_config(
path: &Path,
embedder: Box<dyn EmbeddingProvider>,
cognitive_config: CognitiveConfig,
) -> MenteResult<Self> {
let mut db = Self::open_with_config(path, cognitive_config)?;
db.embedding_dim = embedder.dimensions();
db.embedder = Some(embedder);
Ok(db)
}
pub fn set_embedder(&mut self, embedder: Box<dyn EmbeddingProvider>) {
self.embedding_dim = embedder.dimensions();
self.embedder = Some(embedder);
}
pub fn embed_text(&self, text: &str) -> MenteResult<Option<Vec<f32>>> {
match &self.embedder {
Some(e) => Ok(Some(e.embed(text)?)),
None => Ok(None),
}
}
pub fn store(&self, node: MemoryNode) -> MenteResult<()> {
let id = node.id;
debug!("Storing memory {}", id);
if self.embedding_dim > 0
&& !node.embedding.is_empty()
&& node.embedding.len() != self.embedding_dim
{
return Err(MenteError::EmbeddingDimensionMismatch {
got: node.embedding.len(),
expected: self.embedding_dim,
});
}
let page_id = self.storage.store_memory(&node)?;
self.page_map.write().insert(id, page_id);
self.index.index_memory(&node);
self.graph.add_memory(id);
if self.cognitive_config.write_inference {
self.run_write_inference(&node);
}
Ok(())
}
pub fn recall(&self, query: &str) -> MenteResult<ContextWindow> {
debug!("Recalling with query: {}", query);
let plan = Mql::parse(query)?;
let scored = self.execute_plan(&plan)?;
let config = AssemblyConfig::default();
let window = ContextAssembler::assemble(scored, vec![], &config);
Ok(window)
}
pub fn recall_similar(&self, embedding: &[f32], k: usize) -> MenteResult<Vec<(MemoryId, f32)>> {
self.recall_similar_filtered(embedding, k, None, None)
}
pub fn recall_similar_filtered(
&self,
embedding: &[f32],
k: usize,
tags: Option<&[&str]>,
time_range: Option<(Timestamp, Timestamp)>,
) -> MenteResult<Vec<(MemoryId, f32)>> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.recall_similar_filtered_at(embedding, k, now, tags, time_range)
}
pub fn recall_similar_at(
&self,
embedding: &[f32],
k: usize,
at: Timestamp,
) -> MenteResult<Vec<(MemoryId, f32)>> {
self.recall_similar_filtered_at(embedding, k, at, None, None)
}
pub fn recall_similar_filtered_at(
&self,
embedding: &[f32],
k: usize,
at: Timestamp,
tags: Option<&[&str]>,
time_range: Option<(Timestamp, Timestamp)>,
) -> MenteResult<Vec<(MemoryId, f32)>> {
self.recall_hybrid_at(embedding, None, k, at, tags, time_range)
}
pub fn recall_hybrid_at(
&self,
embedding: &[f32],
query_text: Option<&str>,
k: usize,
at: Timestamp,
tags: Option<&[&str]>,
time_range: Option<(Timestamp, Timestamp)>,
) -> MenteResult<Vec<(MemoryId, f32)>> {
debug!(
"Recall hybrid, k={}, at={}, bm25={}",
k,
at,
query_text.is_some()
);
let results =
self.index
.hybrid_search_with_query(embedding, query_text, tags, time_range, k * 3);
let graph = self.graph.graph();
let pm = self.page_map.read();
let filtered: Vec<(MemoryId, f32)> = results
.into_iter()
.filter(|(id, _)| {
let incoming = graph.incoming(*id);
let has_active_supersede = incoming.iter().any(|(_, e)| {
(e.edge_type == EdgeType::Supersedes || e.edge_type == EdgeType::Contradicts)
&& e.is_valid_at(at)
});
!has_active_supersede
})
.filter(|(id, _)| {
if let Some(&page_id) = pm.get(id)
&& let Ok(node) = self.storage.load_memory(page_id)
{
node.is_valid_at(at)
} else {
true
}
})
.take(k)
.collect();
Ok(filtered)
}
pub fn recall_similar_multi(
&self,
embeddings: &[Vec<f32>],
k: usize,
tags: Option<&[&str]>,
time_range: Option<(Timestamp, Timestamp)>,
) -> MenteResult<Vec<(MemoryId, f32)>> {
self.recall_hybrid_multi(embeddings, None, k, tags, time_range)
}
pub fn recall_hybrid_multi(
&self,
embeddings: &[Vec<f32>],
query_texts: Option<&[String]>,
k: usize,
tags: Option<&[&str]>,
time_range: Option<(Timestamp, Timestamp)>,
) -> MenteResult<Vec<(MemoryId, f32)>> {
use std::collections::HashMap;
let rrf_k: f32 = 60.0;
let mut rrf_scores: HashMap<MemoryId, f32> = HashMap::new();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
for (i, emb) in embeddings.iter().enumerate() {
let qt = query_texts.and_then(|texts| texts.get(i).map(|s| s.as_str()));
let results = self.recall_hybrid_at(emb, qt, k, now, tags, time_range)?;
for (rank, (id, _score)) in results.iter().enumerate() {
*rrf_scores.entry(*id).or_insert(0.0) += 1.0 / (rrf_k + rank as f32);
}
}
let mut merged: Vec<(MemoryId, f32)> = rrf_scores.into_iter().collect();
merged.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
merged.truncate(k);
Ok(merged)
}
pub fn invalidate_memory(&self, id: MemoryId, at: Timestamp) -> MenteResult<()> {
debug!("Invalidating memory {} at {}", id, at);
let page_id = self
.page_map
.read()
.get(&id)
.copied()
.ok_or(MenteError::MemoryNotFound(id))?;
let mut node = self.storage.load_memory(page_id)?;
node.invalidate(at);
let new_page_id = self.storage.store_memory(&node)?;
self.page_map.write().insert(id, new_page_id);
Ok(())
}
pub fn relate(&self, edge: MemoryEdge) -> MenteResult<()> {
debug!("Relating {} -> {}", edge.source, edge.target);
self.graph.add_relationship(&edge)?;
Ok(())
}
pub fn get_memory(&self, id: MemoryId) -> MenteResult<MemoryNode> {
let page_id = self
.page_map
.read()
.get(&id)
.copied()
.ok_or(MenteError::MemoryNotFound(id))?;
self.storage.load_memory(page_id)
}
pub fn memory_ids(&self) -> Vec<MemoryId> {
self.page_map.read().keys().copied().collect()
}
pub fn memory_count(&self) -> usize {
self.page_map.read().len()
}
pub fn forget(&self, id: MemoryId) -> MenteResult<()> {
debug!("Forgetting memory {}", id);
if let Some(&page_id) = self.page_map.read().get(&id)
&& let Ok(node) = self.storage.load_memory(page_id)
{
self.index.remove_memory(id, &node);
}
self.graph.remove_memory(id);
self.page_map.write().remove(&id);
Ok(())
}
pub fn graph(&self) -> &GraphManager {
&self.graph
}
#[deprecated(note = "GraphManager now uses interior mutability; use graph() instead")]
pub fn graph_mut(&mut self) -> &mut GraphManager {
&mut self.graph
}
pub fn cognitive_config(&self) -> &CognitiveConfig {
&self.cognitive_config
}
fn run_write_inference(&self, new_memory: &MemoryNode) {
let candidates = if !new_memory.embedding.is_empty() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.recall_hybrid_at(&new_memory.embedding, None, 20, now, None, None)
.unwrap_or_default()
} else {
vec![]
};
if candidates.is_empty() {
return;
}
let pm = self.page_map.read();
let existing: Vec<MemoryNode> = candidates
.iter()
.filter(|(id, _)| *id != new_memory.id)
.filter_map(|(id, _)| {
pm.get(id)
.and_then(|&pid| self.storage.load_memory(pid).ok())
})
.collect();
drop(pm);
if existing.is_empty() {
return;
}
let actions = self
.write_inference
.infer_on_write(new_memory, &existing, &[]);
let action_count = actions.len();
for action in actions {
if let Err(e) = self.apply_inferred_action(action) {
warn!("Failed to apply inferred action: {}", e);
}
}
if action_count > 0 {
debug!(
"Write inference for {} produced {} actions",
new_memory.id, action_count
);
}
}
fn apply_inferred_action(&self, action: InferredAction) -> MenteResult<()> {
match action {
InferredAction::CreateEdge {
source,
target,
edge_type,
weight,
} => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let edge = MemoryEdge {
source,
target,
edge_type,
weight,
created_at: now,
valid_from: None,
valid_until: None,
label: None,
};
debug!(
"Auto-creating {:?} edge {} -> {}",
edge_type, source, target
);
self.graph.add_relationship(&edge)?;
}
InferredAction::InvalidateMemory {
memory,
superseded_by,
valid_until,
} => {
debug!(
"Invalidating memory {} (superseded by {})",
memory, superseded_by
);
self.invalidate_memory(memory, valid_until)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let edge = MemoryEdge {
source: superseded_by,
target: memory,
edge_type: EdgeType::Supersedes,
weight: 1.0,
created_at: now,
valid_from: None,
valid_until: None,
label: None,
};
self.graph.add_relationship(&edge)?;
}
InferredAction::MarkObsolete {
memory,
superseded_by,
} => {
debug!(
"Marking {} obsolete (superseded by {})",
memory, superseded_by
);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.invalidate_memory(memory, now)?;
let edge = MemoryEdge {
source: superseded_by,
target: memory,
edge_type: EdgeType::Supersedes,
weight: 1.0,
created_at: now,
valid_from: None,
valid_until: None,
label: None,
};
self.graph.add_relationship(&edge)?;
}
InferredAction::FlagContradiction {
existing,
new,
reason,
} => {
debug!(
"Contradiction detected: {} vs {} — {}",
existing, new, reason
);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let edge = MemoryEdge {
source: new,
target: existing,
edge_type: EdgeType::Contradicts,
weight: 1.0,
created_at: now,
valid_from: None,
valid_until: None,
label: Some(reason),
};
self.graph.add_relationship(&edge)?;
}
InferredAction::UpdateConfidence {
memory,
new_confidence,
} => {
debug!("Updating confidence for {} to {}", memory, new_confidence);
if let Ok(mut node) = self.get_memory(memory) {
node.confidence = new_confidence;
let new_page_id = self.storage.store_memory(&node)?;
self.page_map.write().insert(memory, new_page_id);
}
}
InferredAction::PropagateBeliefChange { root, delta } => {
debug!("Propagating belief change from {} (delta={})", root, delta);
if let Ok(node) = self.get_memory(root) {
let new_confidence = (node.confidence + delta).clamp(0.0, 1.0);
let affected = self.graph.propagate_belief_change(root, new_confidence);
for (affected_id, new_conf) in affected {
if let Ok(mut affected_node) = self.get_memory(affected_id) {
affected_node.confidence = new_conf;
if let Ok(pid) = self.storage.store_memory(&affected_node) {
self.page_map.write().insert(affected_id, pid);
}
}
}
}
}
InferredAction::UpdateContent {
memory,
new_content,
reason,
} => {
debug!("Updating content of {}: {}", memory, reason);
if let Ok(mut node) = self.get_memory(memory) {
node.content = new_content;
let new_page_id = self.storage.store_memory(&node)?;
self.page_map.write().insert(memory, new_page_id);
self.index.remove_memory(memory, &node);
self.index.index_memory(&node);
}
}
}
Ok(())
}
pub fn apply_decay(&self, memories: &mut [MemoryNode]) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.decay.apply_decay_batch(memories, now);
}
pub fn compute_decayed_salience(&self, memory: &MemoryNode) -> f32 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.decay.compute_decay(
memory.salience,
memory.created_at,
memory.accessed_at,
memory.access_count,
now,
)
}
pub fn apply_decay_global(&self) -> MenteResult<usize> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let ids: Vec<(MemoryId, PageId)> = self
.page_map
.read()
.iter()
.map(|(mid, pid)| (*mid, *pid))
.collect();
let mut updated = 0;
for (mid, pid) in &ids {
if let Ok(mut node) = self.storage.load_memory(*pid) {
let new_salience = self.decay.compute_decay(
node.salience,
node.created_at,
node.accessed_at,
node.access_count,
now,
);
if (new_salience - node.salience).abs() > 0.001 {
node.salience = new_salience;
let new_pid = self.storage.store_memory(&node)?;
self.page_map.write().insert(*mid, new_pid);
updated += 1;
}
}
}
if updated > 0 {
info!("Decay pass updated {} memories", updated);
}
Ok(updated)
}
pub fn find_consolidation_candidates(
&self,
min_cluster_size: usize,
similarity_threshold: f32,
) -> MenteResult<Vec<ConsolidationCandidate>> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let pm = self.page_map.read();
let eligible: Vec<MemoryNode> = pm
.values()
.filter_map(|pid| self.storage.load_memory(*pid).ok())
.filter(|node| ConsolidationEngine::should_consolidate(node, now))
.collect();
drop(pm);
if eligible.is_empty() {
return Ok(vec![]);
}
Ok(self
.consolidation
.find_candidates(&eligible, min_cluster_size, similarity_threshold))
}
pub fn consolidate_cluster(&self, memory_ids: &[MemoryId]) -> MenteResult<MemoryId> {
let pm = self.page_map.read();
let cluster: Vec<MemoryNode> = memory_ids
.iter()
.filter_map(|id| {
pm.get(id)
.and_then(|&pid| self.storage.load_memory(pid).ok())
})
.collect();
drop(pm);
if cluster.len() < 2 {
return Err(MenteError::Query(
"consolidation requires at least 2 memories".into(),
));
}
let result = self.consolidation.consolidate(&cluster);
let agent_id = cluster[0].agent_id;
let mut consolidated = MemoryNode::new(
agent_id,
result.new_type,
result.summary,
result.combined_embedding,
);
consolidated.confidence = result.combined_confidence;
let consolidated_id = consolidated.id;
self.store(consolidated)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
for source_id in &result.source_memories {
let _ = self.invalidate_memory(*source_id, now);
let edge = MemoryEdge {
source: consolidated_id,
target: *source_id,
edge_type: EdgeType::Derived,
weight: 1.0,
created_at: now,
valid_from: None,
valid_until: None,
label: None,
};
let _ = self.graph.add_relationship(&edge);
}
info!(
"Consolidated {} memories into {}",
result.source_memories.len(),
consolidated_id
);
Ok(consolidated_id)
}
pub fn close(&self) -> MenteResult<()> {
info!("Closing MenteDB");
self.flush()?;
self.storage.close()?;
Ok(())
}
pub fn flush(&self) -> MenteResult<()> {
debug!("Flushing MenteDB to disk");
self.index.save(&self.path.join("indexes"))?;
self.graph.save(&self.path.join("graph"))?;
self.storage.checkpoint()?;
let cognitive_dir = self.path.join("cognitive");
if std::fs::create_dir_all(&cognitive_dir).is_ok() {
let _ = self
.trajectory
.read()
.transitions
.save(&cognitive_dir.join("transitions.json"), 1);
let _ = self
.speculative
.read()
.save(&cognitive_dir.join("speculative.json"), 0);
let _ = self
.entity_resolver
.read()
.save(&cognitive_dir.join("entities.json"));
}
Ok(())
}
fn execute_plan(&self, plan: &QueryPlan) -> MenteResult<Vec<ScoredMemory>> {
match plan {
QueryPlan::VectorSearch { query, k, .. } => {
let hits = self.index.hybrid_search(query, None, None, *k);
self.load_scored_memories(&hits)
}
QueryPlan::TagScan { tags, limit, .. } => {
let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
let k = limit.unwrap_or(10);
let hits = self.index.hybrid_search(&[], Some(&tag_refs), None, k);
self.load_scored_memories(&hits)
}
QueryPlan::TemporalScan { start, end, .. } => {
let hits = self
.index
.hybrid_search(&[], None, Some((*start, *end)), 100);
self.load_scored_memories(&hits)
}
QueryPlan::GraphTraversal { start, depth, .. } => {
let (ids, _edges) = self.graph.get_context_subgraph(*start, *depth);
let pm = self.page_map.read();
let scored: Vec<ScoredMemory> = ids
.iter()
.filter_map(|id| {
pm.get(id).and_then(|&pid| {
self.storage.load_memory(pid).ok().map(|node| ScoredMemory {
memory: node,
score: 1.0,
})
})
})
.collect();
Ok(scored)
}
QueryPlan::PointLookup { id } => {
let page_id = self
.page_map
.read()
.get(id)
.copied()
.ok_or(MenteError::MemoryNotFound(*id))?;
let node = self.storage.load_memory(page_id)?;
Ok(vec![ScoredMemory {
memory: node,
score: 1.0,
}])
}
_ => Ok(vec![]),
}
}
fn load_scored_memories(&self, hits: &[(MemoryId, f32)]) -> MenteResult<Vec<ScoredMemory>> {
let pm = self.page_map.read();
let now = if self.cognitive_config.decay_on_recall {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64
} else {
0
};
let mut scored = Vec::with_capacity(hits.len());
for &(id, score) in hits {
if let Some(&page_id) = pm.get(&id)
&& let Ok(node) = self.storage.load_memory(page_id)
{
let final_score = if self.cognitive_config.decay_on_recall {
let decayed_salience = self.decay.compute_decay(
node.salience,
node.created_at,
node.accessed_at,
node.access_count,
now,
);
score * 0.7 + decayed_salience * 0.3
} else {
score
};
scored.push(ScoredMemory {
memory: node,
score: final_score,
});
}
}
if self.cognitive_config.decay_on_recall {
scored.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
Ok(scored)
}
pub fn record_pain(&self, signal: PainSignal) {
if self.cognitive_config.pain_tracking {
self.pain.write().record_pain(signal);
}
}
pub fn get_pain_warnings(&self, context_keywords: &[String]) -> Vec<PainSignal> {
if !self.cognitive_config.pain_tracking {
return vec![];
}
let registry = self.pain.read();
registry
.get_pain_for_context(context_keywords)
.into_iter()
.cloned()
.collect()
}
pub fn format_pain_warnings(&self, signals: &[&PainSignal]) -> String {
self.pain.read().format_pain_warnings(signals)
}
pub fn decay_pain(&self) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.pain.write().decay_all(now);
}
pub fn all_pain_signals(&self) -> Vec<PainSignal> {
self.pain.read().all_signals().to_vec()
}
pub fn record_trajectory_turn(&self, turn: TrajectoryNode) {
self.trajectory.write().record_turn(turn);
}
pub fn get_resume_context(&self) -> Option<String> {
self.trajectory.read().get_resume_context()
}
pub fn predict_next_topics(&self) -> Vec<String> {
self.trajectory.read().predict_next_topics()
}
pub fn get_trajectory(&self) -> Vec<TrajectoryNode> {
self.trajectory.read().get_trajectory().to_vec()
}
pub fn reinforce_transition(&self, hit_topic: &str) {
self.trajectory.write().reinforce_transition(hit_topic);
}
pub fn feed_stream_token(&self, token: &str) {
self.stream.feed_token(token);
}
pub fn check_stream_alerts(&self, known_facts: &[(MemoryId, String)]) -> Vec<StreamAlert> {
self.stream.check_alerts(known_facts)
}
pub fn drain_stream_buffer(&self) -> String {
self.stream.drain_buffer()
}
pub fn detect_phantoms(
&self,
content: &str,
known_entities: &[String],
turn_id: u64,
) -> Vec<PhantomMemory> {
if !self.cognitive_config.phantom_tracking {
return vec![];
}
self.phantom
.write()
.detect_gaps(content, known_entities, turn_id)
}
pub fn resolve_phantom(&self, phantom_id: MemoryId) {
self.phantom.write().resolve(phantom_id.into());
}
pub fn get_active_phantoms(&self) -> Vec<PhantomMemory> {
self.phantom
.read()
.get_active_phantoms()
.into_iter()
.cloned()
.collect()
}
pub fn format_phantom_warnings(&self) -> String {
self.phantom.read().format_phantom_warnings()
}
pub fn register_entity(&self, entity: &str) {
self.phantom.write().register_entity(entity);
}
pub fn register_entities(&self, entities: &[&str]) {
self.phantom.write().register_entities(entities);
}
pub fn try_speculative_hit(
&self,
query: &str,
query_embedding: Option<&[f32]>,
) -> Option<CacheEntry> {
if !self.cognitive_config.speculative_cache {
return None;
}
self.speculative.write().try_hit(query, query_embedding)
}
pub fn pre_assemble_speculative<F>(&self, predictions: Vec<String>, builder: F)
where
F: Fn(&str) -> Option<(String, Vec<MemoryId>, Option<Vec<f32>>)>,
{
if self.cognitive_config.speculative_cache {
self.speculative.write().pre_assemble(predictions, builder);
}
}
pub fn evict_stale_speculative(&self, max_age_us: u64) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.speculative.write().evict_stale(max_age_us, now);
}
pub fn speculative_cache_stats(&self) -> CacheStats {
self.speculative.read().stats()
}
pub fn detect_interference(&self, memories: &[MemoryNode]) -> Vec<InterferencePair> {
if !self.cognitive_config.interference_detection {
return vec![];
}
self.interference.detect_interference(memories)
}
pub fn generate_disambiguation(&self, a: &MemoryNode, b: &MemoryNode) -> String {
self.interference.generate_disambiguation(a, b)
}
pub fn arrange_with_separation(
memories: Vec<MemoryId>,
pairs: &[InterferencePair],
) -> Vec<MemoryId> {
InterferenceDetector::arrange_with_separation(memories, pairs)
}
pub fn resolve_entity(&self, name: &str) -> mentedb_cognitive::ResolvedEntity {
self.entity_resolver.read().resolve(name)
}
pub fn add_entity_alias(&self, alias: &str, canonical: &str, confidence: f32) {
self.entity_resolver
.write()
.add_alias(alias, canonical, confidence);
}
pub fn get_canonical_entity(&self, name: &str) -> Option<String> {
self.entity_resolver.read().get_canonical(name).cloned()
}
pub fn known_entities(&self) -> Vec<String> {
self.entity_resolver.read().known_entities()
}
pub fn compress_memory(&self, memory: &MemoryNode) -> CompressedMemory {
self.compressor.compress(memory)
}
pub fn compress_memories(&self, memories: &[MemoryNode]) -> Vec<CompressedMemory> {
self.compressor.compress_batch(memories)
}
pub fn estimate_tokens(text: &str) -> usize {
MemoryCompressor::estimate_tokens(text)
}
pub fn evaluate_archival(&self, memory: &MemoryNode) -> ArchivalDecision {
if !self.cognitive_config.archival_evaluation {
return ArchivalDecision::Keep;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.archival.evaluate(memory, now)
}
pub fn evaluate_archival_batch(
&self,
memories: &[MemoryNode],
) -> Vec<(MemoryId, ArchivalDecision)> {
if !self.cognitive_config.archival_evaluation {
return memories
.iter()
.map(|m| (m.id, ArchivalDecision::Keep))
.collect();
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
self.archival.evaluate_batch(memories, now)
}
pub fn evaluate_archival_global(&self) -> MenteResult<Vec<(MemoryId, ArchivalDecision)>> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let pm = self.page_map.read();
let memories: Vec<MemoryNode> = pm
.values()
.filter_map(|pid| self.storage.load_memory(*pid).ok())
.collect();
drop(pm);
Ok(self.archival.evaluate_batch(&memories, now))
}
pub fn needs_enrichment(&self) -> bool {
if !self.cognitive_config.enrichment_config.enabled {
return false;
}
*self.enrichment_pending.read()
}
pub fn last_enrichment_turn(&self) -> u64 {
*self.last_enrichment_turn.read()
}
pub fn request_enrichment(&self) {
*self.enrichment_pending.write() = true;
}
pub fn enrichment_candidates(&self) -> Vec<MemoryNode> {
let last_turn = *self.last_enrichment_turn.read();
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
let mut candidates: Vec<MemoryNode> = page_ids
.iter()
.filter_map(|pid| self.storage.load_memory(*pid).ok())
.filter(|m| {
m.memory_type == mentedb_core::memory::MemoryType::Episodic
&& !m.tags.contains(&"source:enrichment".to_string())
&& m.created_at > last_turn
})
.collect();
candidates.sort_by_key(|m| m.created_at);
candidates
}
pub fn store_enrichment_memories(
&self,
memories: Vec<MemoryNode>,
source_ids: &[MemoryId],
) -> MenteResult<(usize, usize)> {
let max_conf = self
.cognitive_config
.enrichment_config
.max_enrichment_confidence;
let mut stored = 0usize;
let mut edges = 0usize;
for mut mem in memories {
if !mem.tags.contains(&"source:enrichment".to_string()) {
mem.tags.push("source:enrichment".to_string());
}
if mem.confidence > max_conf {
mem.confidence = max_conf;
}
let mem_id = mem.id;
self.store(mem)?;
stored += 1;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
for src_id in source_ids {
let edge = MemoryEdge {
source: mem_id,
target: *src_id,
edge_type: EdgeType::Derived,
weight: 0.8,
created_at: now,
valid_from: None,
valid_until: None,
label: Some("enrichment".to_string()),
};
if self.relate(edge).is_ok() {
edges += 1;
}
}
}
debug!(stored, edges, "enrichment memories stored");
Ok((stored, edges))
}
pub fn mark_enrichment_complete(&self, turn_id: u64) {
*self.last_enrichment_turn.write() = turn_id;
*self.enrichment_pending.write() = false;
debug!(turn_id, "enrichment cycle complete");
}
pub fn enrichment_config(&self) -> &EnrichmentConfig {
&self.cognitive_config.enrichment_config
}
pub fn all_entity_names(&self) -> Vec<String> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
let mut names = std::collections::HashSet::new();
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid) {
for tag in &mem.tags {
if let Some(name) = tag.strip_prefix("entity:") {
names.insert(name.to_lowercase().trim().to_string());
}
}
}
}
let mut sorted: Vec<String> = names.into_iter().collect();
sorted.sort();
sorted
}
pub fn unresolved_entity_names(&self) -> Vec<String> {
let all_names = self.all_entity_names();
self.entity_resolver.read().unresolved_names(&all_names)
}
pub fn entity_names_with_context(&self) -> Vec<(String, Option<String>)> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
let mut entity_contexts: HashMap<String, String> = HashMap::new();
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid) {
for tag in &mem.tags {
if let Some(name) = tag.strip_prefix("entity:") {
let normalized = name.to_lowercase().trim().to_string();
entity_contexts
.entry(normalized)
.and_modify(|existing| {
if existing.len() < 300 {
existing.push_str(" | ");
let remaining = 500usize.saturating_sub(existing.len());
existing
.push_str(&mem.content[..mem.content.len().min(remaining)]);
}
})
.or_insert_with(|| {
mem.content[..mem.content.len().min(300)].to_string()
});
break;
}
}
}
}
entity_contexts
.into_iter()
.map(|(name, ctx)| (name, Some(ctx)))
.collect()
}
pub fn apply_entity_link_resolutions(
&self,
merge_groups: &[EntityLinkResolution],
separations: &[EntitySeparation],
) -> MenteResult<EntityLinkResult> {
let mut result = EntityLinkResult::default();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let entity_memory_map = self.build_entity_memory_map();
let mut resolver = self.entity_resolver.write();
for group in merge_groups {
let mut aliases: Vec<String> = group.aliases.clone();
aliases.retain(|a| a.to_lowercase() != group.canonical.to_lowercase());
resolver.learn_group(&EntityMergeGroup {
canonical: group.canonical.clone(),
aliases,
confidence: group.confidence,
});
let mut group_memory_ids: Vec<MemoryId> = Vec::new();
let canonical_norm = group.canonical.to_lowercase();
if let Some(ids) = entity_memory_map.get(&canonical_norm) {
group_memory_ids.extend(ids);
}
for alias in &group.aliases {
let alias_norm = alias.to_lowercase();
if let Some(ids) = entity_memory_map.get(&alias_norm) {
group_memory_ids.extend(ids);
}
}
group_memory_ids.sort();
group_memory_ids.dedup();
let label = format!("entity_link:{}", canonical_norm);
for i in 0..group_memory_ids.len() {
for j in (i + 1)..group_memory_ids.len() {
let a_id = group_memory_ids[i];
let b_id = group_memory_ids[j];
let graph = self.graph.read_graph();
let already_linked = graph.outgoing(a_id).iter().any(|(tid, e)| {
*tid == b_id
&& e.edge_type == EdgeType::Related
&& e.label
.as_ref()
.is_some_and(|l| l.starts_with("entity_link:"))
});
drop(graph);
if already_linked {
continue;
}
let edge = MemoryEdge {
source: a_id,
target: b_id,
edge_type: EdgeType::Related,
weight: group.confidence,
created_at: now,
valid_from: None,
valid_until: None,
label: Some(label.clone()),
};
if self.relate(edge).is_ok() {
result.edges_created += 1;
}
result.linked += 1;
}
}
debug!(
canonical = group.canonical,
aliases = ?group.aliases,
memories = group_memory_ids.len(),
"entity resolution: merged group"
);
}
for sep in separations {
resolver.mark_different(&sep.name_a, &sep.name_b);
debug!(
a = sep.name_a,
b = sep.name_b,
"entity resolution: confirmed different"
);
}
let cognitive_dir = self.path.join("cognitive");
if cognitive_dir.exists() || std::fs::create_dir_all(&cognitive_dir).is_ok() {
let _ = resolver.save(&cognitive_dir.join("entities.json"));
}
debug!(
linked = result.linked,
edges = result.edges_created,
groups = merge_groups.len(),
separations = separations.len(),
"entity link resolutions applied"
);
Ok(result)
}
pub fn link_entities(&self) -> MenteResult<EntityLinkResult> {
let entity_memory_map = self.build_entity_memory_map();
let resolver = self.entity_resolver.read();
let mut canonical_groups: HashMap<String, Vec<String>> = HashMap::new();
for entity_name in entity_memory_map.keys() {
let resolved = resolver.resolve(entity_name);
if resolved.source != mentedb_cognitive::ResolutionSource::Identity {
canonical_groups
.entry(resolved.canonical.clone())
.or_default()
.push(entity_name.clone());
}
}
drop(resolver);
let mut result = EntityLinkResult::default();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
for (canonical, names) in &canonical_groups {
let mut group_memory_ids: Vec<MemoryId> = Vec::new();
for name in names {
if let Some(ids) = entity_memory_map.get(name) {
group_memory_ids.extend(ids);
}
}
if let Some(ids) = entity_memory_map.get(canonical) {
group_memory_ids.extend(ids);
}
group_memory_ids.sort();
group_memory_ids.dedup();
if group_memory_ids.len() < 2 {
continue;
}
let label = format!("entity_link:{}", canonical);
for i in 0..group_memory_ids.len() {
for j in (i + 1)..group_memory_ids.len() {
let a_id = group_memory_ids[i];
let b_id = group_memory_ids[j];
let graph = self.graph.read_graph();
let already_linked = graph.outgoing(a_id).iter().any(|(tid, e)| {
*tid == b_id
&& e.edge_type == EdgeType::Related
&& e.label
.as_ref()
.is_some_and(|l| l.starts_with("entity_link:"))
});
drop(graph);
if already_linked {
continue;
}
let edge = MemoryEdge {
source: a_id,
target: b_id,
edge_type: EdgeType::Related,
weight: 1.0,
created_at: now,
valid_from: None,
valid_until: None,
label: Some(label.clone()),
};
if self.relate(edge).is_ok() {
result.edges_created += 1;
}
result.linked += 1;
}
}
}
debug!(
linked = result.linked,
edges = result.edges_created,
groups = canonical_groups.len(),
"sync entity linking complete"
);
Ok(result)
}
fn build_entity_memory_map(&self) -> HashMap<String, Vec<MemoryId>> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
let mut map: HashMap<String, Vec<MemoryId>> = HashMap::new();
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid) {
for tag in &mem.tags {
if let Some(name) = tag.strip_prefix("entity:") {
let normalized = name.to_lowercase().trim().to_string();
map.entry(normalized).or_default().push(mem.id);
break;
}
}
}
}
map
}
pub fn entity_memories(&self) -> Vec<MemoryNode> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
page_ids
.iter()
.filter_map(|pid| self.storage.load_memory(*pid).ok())
.filter(|m| m.tags.iter().any(|t| t.starts_with("entity:")))
.collect()
}
pub fn entity_communities(&self) -> HashMap<String, Vec<(String, String)>> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
let mut categories: HashMap<String, Vec<(String, String)>> = HashMap::new();
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid) {
if mem.tags.iter().any(|t| t == "community_summary") {
continue;
}
let entity_name = mem
.tags
.iter()
.find_map(|t| t.strip_prefix("entity:"))
.map(|n| n.to_string());
if let Some(name) = entity_name {
let entity_type = mem
.tags
.iter()
.find_map(|t| t.strip_prefix("entity_type:"))
.unwrap_or("general")
.to_lowercase();
let context: String = mem.content.chars().take(200).collect();
categories
.entry(entity_type)
.or_default()
.push((name, context));
}
}
}
categories.retain(|_, members| members.len() >= 2);
categories
}
pub fn store_community_summary(
&self,
category: &str,
summary: &str,
member_names: &[String],
) -> MenteResult<MemoryId> {
if category.is_empty() {
return Err(MenteError::Storage(
"community category cannot be empty".into(),
));
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64;
let community_tag = format!("community:{}", category);
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
let mut existing_id = None;
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid)
&& mem.tags.iter().any(|t| t == &community_tag)
{
let mut updated = mem.clone();
updated.content = summary.to_string();
if let Some(ref embedder) = self.embedder {
updated.embedding = embedder
.embed(summary)
.unwrap_or_else(|_| updated.embedding.clone());
}
self.storage.store_memory(&updated)?;
existing_id = Some(updated.id);
break;
}
}
let node_id = if let Some(id) = existing_id {
id
} else {
let embedding = self
.embedder
.as_ref()
.and_then(|e| e.embed(summary).ok())
.unwrap_or_default();
let mut node = MemoryNode::new(
mentedb_core::types::AgentId::new(),
MemoryType::Semantic,
summary.to_string(),
embedding,
);
node.tags = vec![
"community_summary".to_string(),
community_tag,
"source:enrichment".to_string(),
];
node.confidence = 0.7;
let id = node.id;
self.store(node)?;
id
};
let entity_map = self.build_entity_memory_map();
for name in member_names {
let normalized = name.to_lowercase();
if let Some(member_ids) = entity_map.get(&normalized) {
for member_id in member_ids {
self.relate(MemoryEdge {
source: node_id,
target: *member_id,
edge_type: EdgeType::Derived,
weight: 0.8,
created_at: now,
valid_from: None,
valid_until: None,
label: Some(format!("community_member:{}", category)),
})?;
}
}
}
Ok(node_id)
}
pub fn community_summaries(&self) -> Vec<MemoryNode> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
page_ids
.iter()
.filter_map(|pid| self.storage.load_memory(*pid).ok())
.filter(|m| m.tags.iter().any(|t| t == "community_summary"))
.collect()
}
pub fn profile_facts(&self) -> Vec<String> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
let mut facts = Vec::new();
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid) {
if mem.confidence < 0.5 {
continue;
}
match mem.memory_type {
MemoryType::Semantic | MemoryType::Procedural => {
if mem
.tags
.iter()
.any(|t| t == "community_summary" || t.starts_with("entity:"))
{
continue;
}
facts.push(mem.content.chars().take(300).collect());
}
_ => {}
}
}
}
facts.truncate(100);
facts
}
pub fn store_user_profile(&self, profile: &str) -> MenteResult<MemoryId> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid)
&& mem.tags.iter().any(|t| t == "user_profile")
{
let mut updated = mem.clone();
updated.content = profile.to_string();
if let Some(ref embedder) = self.embedder {
updated.embedding = embedder
.embed(profile)
.unwrap_or_else(|_| updated.embedding.clone());
}
self.storage.store_memory(&updated)?;
return Ok(updated.id);
}
}
let embedding = self
.embedder
.as_ref()
.and_then(|e| e.embed(profile).ok())
.unwrap_or_default();
let mut node = MemoryNode::new(
mentedb_core::types::AgentId::new(),
MemoryType::Semantic,
profile.to_string(),
embedding,
);
node.tags = vec![
"user_profile".to_string(),
"scope:always".to_string(),
"source:enrichment".to_string(),
];
node.confidence = 0.8;
let node_id = node.id;
self.store(node)?;
Ok(node_id)
}
pub fn user_profile(&self) -> Option<MemoryNode> {
let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
for pid in &page_ids {
if let Ok(mem) = self.storage.load_memory(*pid)
&& mem.tags.iter().any(|t| t == "user_profile")
{
return Some(mem);
}
}
None
}
}