1use std::path::{Path, PathBuf};
46
47use mentedb_cognitive::EntityResolver;
48use mentedb_cognitive::interference::{InterferenceDetector, InterferencePair};
49use mentedb_cognitive::llm::EntityMergeGroup;
50use mentedb_cognitive::pain::{PainRegistry, PainSignal};
51use mentedb_cognitive::phantom::{PhantomConfig, PhantomMemory, PhantomTracker};
52use mentedb_cognitive::speculative::{CacheEntry, CacheStats, SpeculativeCache};
53use mentedb_cognitive::stream::{CognitionStream, StreamAlert, StreamConfig};
54use mentedb_cognitive::trajectory::{TrajectoryNode, TrajectoryTracker};
55use mentedb_cognitive::write_inference::{
56 InferredAction, WriteInferenceConfig, WriteInferenceEngine,
57};
58use mentedb_consolidation::archival::{ArchivalConfig, ArchivalDecision, ArchivalPipeline};
59use mentedb_consolidation::compression::{CompressedMemory, MemoryCompressor};
60use mentedb_consolidation::consolidation::{ConsolidationCandidate, ConsolidationEngine};
61use mentedb_consolidation::decay::{DecayConfig, DecayEngine};
62use mentedb_context::{AssemblyConfig, ContextAssembler, ContextWindow, ScoredMemory};
63use mentedb_core::edge::EdgeType;
64use mentedb_core::error::MenteResult;
65use mentedb_core::memory::MemoryType;
66use mentedb_core::types::{AgentId, MemoryId, Timestamp, UserId};
67use mentedb_core::{MemoryEdge, MemoryNode, MenteError};
68use mentedb_embedding::provider::EmbeddingProvider;
69use mentedb_graph::GraphManager;
70use mentedb_index::IndexManager;
71use mentedb_query::{Condition, Field, Filter, Mql, Operator, QueryPlan, Value};
72use mentedb_storage::StorageEngine;
73use parking_lot::RwLock;
74use tracing::{debug, info, warn};
75
76pub const VERSION: &str = env!("CARGO_PKG_VERSION");
80
81pub use mentedb_cognitive as cognitive;
83pub use mentedb_consolidation as consolidation;
85pub use mentedb_context as context;
87pub use mentedb_core as core;
89pub use mentedb_graph as graph;
91pub use mentedb_index as index;
93pub use mentedb_query as query;
95pub use mentedb_storage as storage;
97
98pub mod process_turn;
100
101pub mod injection;
103
104pub mod llm_consolidation;
106pub use llm_consolidation::ConsolidationParams;
107
108#[cfg(feature = "enrichment")]
110pub mod enrichment;
111
112pub mod reranker;
114
115pub mod export;
117
118pub mod sharding;
123
124pub mod prelude {
126 pub use mentedb_core::edge::EdgeType;
127 pub use mentedb_core::error::MenteResult;
128 pub use mentedb_core::memory::MemoryType;
129 pub use mentedb_core::types::*;
130 pub use mentedb_core::{MemoryEdge, MemoryNode, MemoryTier, MenteError};
131
132 pub use crate::MenteDb;
133}
134
135use mentedb_storage::PageId;
136use std::collections::HashMap;
138
139#[derive(Debug, Clone)]
145pub struct EnrichmentConfig {
146 pub enabled: bool,
148 pub trigger_interval: u64,
150 pub min_confidence: f32,
152 pub max_enrichment_confidence: f32,
154 pub enable_user_model: bool,
156 pub entity_merge_threshold: f32,
158 pub entity_separate_threshold: f32,
160}
161
162impl Default for EnrichmentConfig {
163 fn default() -> Self {
164 Self {
165 enabled: false,
166 trigger_interval: 50,
167 min_confidence: 0.6,
168 max_enrichment_confidence: 0.7,
169 enable_user_model: false,
170 entity_merge_threshold: 0.7,
171 entity_separate_threshold: 0.4,
172 }
173 }
174}
175
176#[derive(Debug, Clone, Default)]
178pub struct EnrichmentResult {
179 pub memories_stored: usize,
181 pub entities_processed: usize,
183 pub edges_created: usize,
185 pub duplicates_skipped: usize,
187 pub contradictions_found: usize,
189 pub completed_at_turn: u64,
191 pub entities_linked: usize,
193 pub entities_ambiguous: usize,
195}
196
197#[derive(Debug, Clone, Default)]
199pub struct EntityLinkResult {
200 pub linked: usize,
202 pub ambiguous: usize,
204 pub edges_created: usize,
206}
207
208#[derive(Debug, Clone, Default)]
210pub struct PruneReport {
211 pub total_always: usize,
213 pub duplicate_groups: usize,
215 pub pruned: Vec<MemoryId>,
217 pub unpinned: Vec<MemoryId>,
220}
221
222#[derive(Debug, Clone)]
227pub struct EntityLinkResolution {
228 pub canonical: String,
230 pub aliases: Vec<String>,
232 pub confidence: f32,
234}
235
236#[derive(Debug, Clone)]
238pub struct EntitySeparation {
239 pub name_a: String,
240 pub name_b: String,
241}
242
243#[derive(Debug, Clone)]
245pub struct CognitiveConfig {
246 pub write_inference: bool,
248 pub decay_on_recall: bool,
250 pub pain_tracking: bool,
252 pub interference_detection: bool,
254 pub phantom_tracking: bool,
256 pub speculative_cache: bool,
258 pub archival_evaluation: bool,
260 pub inference_config: WriteInferenceConfig,
262 pub decay_config: DecayConfig,
264 pub phantom_config: PhantomConfig,
266 pub archival_config: ArchivalConfig,
268 pub stream_config: StreamConfig,
270 pub enrichment_config: EnrichmentConfig,
272 pub interference_threshold: f32,
274 pub trajectory_max_turns: usize,
276 pub speculative_cache_size: usize,
278 pub pain_max_warnings: usize,
280 pub injection_config: injection::InjectionConfig,
282 pub flush_snapshot_interval: u32,
287 pub entity_boost_enabled: bool,
292 pub entity_boost_weight: f32,
295}
296
297impl Default for CognitiveConfig {
298 fn default() -> Self {
299 Self {
300 write_inference: true,
301 decay_on_recall: true,
302 pain_tracking: true,
303 interference_detection: true,
304 phantom_tracking: true,
305 speculative_cache: true,
306 archival_evaluation: true,
307 inference_config: WriteInferenceConfig::default(),
308 decay_config: DecayConfig::default(),
309 phantom_config: PhantomConfig::default(),
310 archival_config: ArchivalConfig::default(),
311 stream_config: StreamConfig::default(),
312 enrichment_config: EnrichmentConfig::default(),
313 interference_threshold: 0.8,
314 trajectory_max_turns: 100,
315 speculative_cache_size: 10,
316 pain_max_warnings: 5,
317 injection_config: injection::InjectionConfig::default(),
318 flush_snapshot_interval: 8,
319 entity_boost_enabled: false,
320 entity_boost_weight: 0.15,
321 }
322 }
323}
324
325pub struct MenteDb {
334 storage: StorageEngine,
335 index: IndexManager,
336 graph: GraphManager,
337 page_map: RwLock<HashMap<MemoryId, PageId>>,
339 flushes_since_snapshot: std::sync::atomic::AtomicU32,
341 embedding_dim: usize,
343 path: PathBuf,
345 embedder: Option<Box<dyn EmbeddingProvider>>,
347 cognitive_config: CognitiveConfig,
349 write_inference: WriteInferenceEngine,
351 decay: DecayEngine,
353 consolidation: ConsolidationEngine,
355 pain: RwLock<PainRegistry>,
357 trajectory: RwLock<TrajectoryTracker>,
359 stream: CognitionStream,
361 phantom: RwLock<PhantomTracker>,
363 speculative: RwLock<SpeculativeCache>,
365 interference: InterferenceDetector,
367 entity_resolver: RwLock<EntityResolver>,
369 compressor: MemoryCompressor,
371 archival: ArchivalPipeline,
373 last_enrichment_turn: RwLock<u64>,
375 enrichment_pending: RwLock<bool>,
377}
378
379pub(crate) fn agent_visible(owner: AgentId, scope: Option<AgentId>) -> bool {
383 match scope {
384 None => true,
385 Some(a) => owner == a || owner.is_nil(),
386 }
387}
388
389pub(crate) fn user_visible(owner: UserId, scope: Option<UserId>) -> bool {
395 match scope {
396 None => true,
397 Some(u) => owner == u || owner.is_nil(),
398 }
399}
400
401impl MenteDb {
402 pub fn open(path: &Path) -> MenteResult<Self> {
404 Self::open_with_config(path, CognitiveConfig::default())
405 }
406
407 pub fn open_with_config(path: &Path, cognitive_config: CognitiveConfig) -> MenteResult<Self> {
409 info!("Opening MenteDB at {}", path.display());
410 let storage = StorageEngine::open(path)?;
411
412 let index_dir = path.join("indexes");
413 let graph_dir = path.join("graph");
414
415 let index = if index_dir.join("hnsw.bin").exists() || index_dir.join("hnsw.json").exists() {
416 debug!("Loading indexes from {}", index_dir.display());
417 IndexManager::load(&index_dir)?
418 } else {
419 IndexManager::default()
420 };
421
422 let graph = GraphManager::open(&graph_dir)?;
425
426 let entries = storage.scan_all_memories();
428 let mut page_map = HashMap::new();
429 for (memory_id, page_id) in &entries {
430 page_map.insert(*memory_id, *page_id);
431 }
432 if !page_map.is_empty() {
433 info!(memories = page_map.len(), "rebuilt page map from storage");
434 }
435
436 for memory_id in page_map.keys() {
440 if !graph.read_graph().contains_node(*memory_id) {
441 graph.add_memory(*memory_id);
442 }
443 }
444
445 let mut reindexed = 0usize;
450 for (memory_id, page_id) in &page_map {
451 if !index.contains_vector(*memory_id)
452 && let Ok(node) = storage.load_memory(*page_id)
453 && !node.embedding.is_empty()
454 {
455 index.index_memory(&node);
456 reindexed += 1;
457 }
458 }
459 let mut retired = 0usize;
460 for id in index.vector_ids() {
461 if !page_map.contains_key(&id) {
462 index.remove_vector_only(id);
463 retired += 1;
464 }
465 }
466 if reindexed > 0 || retired > 0 {
467 info!(reindexed, retired, "reconciled index with storage");
468 }
469
470 let write_inference =
471 WriteInferenceEngine::with_config(cognitive_config.inference_config.clone());
472 let decay = DecayEngine::new(cognitive_config.decay_config.clone());
473 let consolidation = ConsolidationEngine::new();
474 let pain = RwLock::new(PainRegistry::new(cognitive_config.pain_max_warnings));
475 let trajectory = RwLock::new(TrajectoryTracker::new(
476 cognitive_config.trajectory_max_turns,
477 ));
478 let stream = CognitionStream::with_config(cognitive_config.stream_config.clone());
479 let phantom = RwLock::new(PhantomTracker::new(cognitive_config.phantom_config.clone()));
480 let speculative = RwLock::new(SpeculativeCache::new(
481 cognitive_config.speculative_cache_size,
482 0.5,
483 0.4,
484 ));
485 let interference = InterferenceDetector::new(cognitive_config.interference_threshold);
486 let entity_resolver = RwLock::new(EntityResolver::new());
487 let compressor = MemoryCompressor::new();
488 let archival = ArchivalPipeline::new(cognitive_config.archival_config.clone());
489
490 let cognitive_dir = path.join("cognitive");
492 if cognitive_dir.exists() {
493 let _ = trajectory
494 .write()
495 .transitions
496 .load(&cognitive_dir.join("transitions.json"));
497 let _ = speculative
498 .write()
499 .load(&cognitive_dir.join("speculative.json"));
500 let _ = entity_resolver
501 .write()
502 .load(&cognitive_dir.join("entities.json"));
503 }
504
505 Ok(Self {
506 storage,
507 index,
508 graph,
509 page_map: RwLock::new(page_map),
510 flushes_since_snapshot: std::sync::atomic::AtomicU32::new(0),
511 embedding_dim: 0,
512 path: path.to_path_buf(),
513 embedder: None,
514 cognitive_config,
515 write_inference,
516 decay,
517 consolidation,
518 pain,
519 trajectory,
520 stream,
521 phantom,
522 speculative,
523 interference,
524 entity_resolver,
525 compressor,
526 archival,
527 last_enrichment_turn: RwLock::new(0),
528 enrichment_pending: RwLock::new(false),
529 })
530 }
531
532 pub fn open_with_embedder(
534 path: &Path,
535 embedder: Box<dyn EmbeddingProvider>,
536 ) -> MenteResult<Self> {
537 let mut db = Self::open(path)?;
538 db.embedding_dim = embedder.dimensions();
539 db.embedder = Some(embedder);
540 Ok(db)
541 }
542
543 pub fn open_with_embedder_and_config(
545 path: &Path,
546 embedder: Box<dyn EmbeddingProvider>,
547 cognitive_config: CognitiveConfig,
548 ) -> MenteResult<Self> {
549 let mut db = Self::open_with_config(path, cognitive_config)?;
550 db.embedding_dim = embedder.dimensions();
551 db.embedder = Some(embedder);
552 Ok(db)
553 }
554
555 pub fn set_embedder(&mut self, embedder: Box<dyn EmbeddingProvider>) {
557 self.embedding_dim = embedder.dimensions();
558 self.embedder = Some(embedder);
559 }
560
561 pub fn embed_text(&self, text: &str) -> MenteResult<Option<Vec<f32>>> {
564 match &self.embedder {
565 Some(e) => Ok(Some(e.embed(text)?)),
566 None => Ok(None),
567 }
568 }
569
570 pub fn store(&self, node: MemoryNode) -> MenteResult<()> {
582 let id = node.id;
583 debug!("Storing memory {}", id);
584
585 if self.embedding_dim > 0
587 && !node.embedding.is_empty()
588 && node.embedding.len() != self.embedding_dim
589 {
590 return Err(MenteError::EmbeddingDimensionMismatch {
591 got: node.embedding.len(),
592 expected: self.embedding_dim,
593 });
594 }
595
596 let page_id = self.storage.store_memory(&node)?;
597 self.page_map.write().insert(id, page_id);
598 self.index.index_memory(&node);
599 self.graph.add_memory(id);
600
601 if self.cognitive_config.write_inference {
603 self.run_write_inference(&node);
604 }
605
606 Ok(())
607 }
608
609 pub fn store_batch(&self, nodes: Vec<MemoryNode>) -> MenteResult<Vec<MemoryId>> {
615 for node in &nodes {
617 if self.embedding_dim > 0
618 && !node.embedding.is_empty()
619 && node.embedding.len() != self.embedding_dim
620 {
621 return Err(MenteError::EmbeddingDimensionMismatch {
622 got: node.embedding.len(),
623 expected: self.embedding_dim,
624 });
625 }
626 }
627
628 let page_ids = self.storage.store_memory_batch(&nodes)?;
629
630 let mut ids = Vec::with_capacity(nodes.len());
631 let mut page_map = self.page_map.write();
632 for (node, page_id) in nodes.iter().zip(page_ids.iter()) {
633 page_map.insert(node.id, *page_id);
634 self.index.index_memory(node);
635 self.graph.add_memory(node.id);
636 ids.push(node.id);
637 }
638 drop(page_map);
639
640 if self.cognitive_config.write_inference {
643 for node in &nodes {
644 self.run_write_inference(node);
645 }
646 }
647
648 Ok(ids)
649 }
650
651 pub fn recall(&self, query: &str) -> MenteResult<ContextWindow> {
657 debug!("Recalling with query: {}", query);
658 let plan = Mql::parse(query)?;
659
660 let scored = self.execute_plan(&plan)?;
661 let config = AssemblyConfig::default();
662 let window = ContextAssembler::assemble(scored, vec![], &config);
663 Ok(window)
664 }
665
666 pub fn query(&self, mql: &str) -> MenteResult<Vec<ScoredMemory>> {
671 let plan = Mql::parse(mql)?;
672 self.execute_plan(&plan)
673 }
674
675 pub fn recall_reranked(
682 &self,
683 query: &str,
684 query_text: &str,
685 reranker: &dyn crate::reranker::Reranker,
686 ) -> MenteResult<ContextWindow> {
687 use crate::reranker::RerankCandidate;
688 let plan = Mql::parse(query)?;
689 let mut scored = self.execute_plan(&plan)?;
690
691 let candidates: Vec<RerankCandidate<'_>> = scored
692 .iter()
693 .map(|s| RerankCandidate {
694 id: s.memory.id,
695 content: &s.memory.content,
696 score: s.score,
697 })
698 .collect();
699 let new_scores: std::collections::HashMap<MemoryId, f32> = reranker
700 .rerank(query_text, &candidates)
701 .into_iter()
702 .collect();
703 for s in &mut scored {
704 if let Some(&ns) = new_scores.get(&s.memory.id) {
705 s.score = ns;
706 }
707 }
708 scored.sort_by(|a, b| b.score.total_cmp(&a.score));
709
710 let config = AssemblyConfig::default();
711 Ok(ContextAssembler::assemble(scored, vec![], &config))
712 }
713
714 pub fn recall_similar(&self, embedding: &[f32], k: usize) -> MenteResult<Vec<(MemoryId, f32)>> {
720 self.recall_similar_filtered(embedding, k, None, None)
721 }
722
723 pub fn recall_similar_filtered(
725 &self,
726 embedding: &[f32],
727 k: usize,
728 tags: Option<&[&str]>,
729 time_range: Option<(Timestamp, Timestamp)>,
730 ) -> MenteResult<Vec<(MemoryId, f32)>> {
731 let now = std::time::SystemTime::now()
732 .duration_since(std::time::UNIX_EPOCH)
733 .unwrap_or_default()
734 .as_micros() as u64;
735 self.recall_similar_filtered_at(embedding, k, now, tags, time_range)
736 }
737
738 pub fn recall_similar_at(
744 &self,
745 embedding: &[f32],
746 k: usize,
747 at: Timestamp,
748 ) -> MenteResult<Vec<(MemoryId, f32)>> {
749 self.recall_similar_filtered_at(embedding, k, at, None, None)
750 }
751
752 pub fn recall_similar_filtered_at(
758 &self,
759 embedding: &[f32],
760 k: usize,
761 at: Timestamp,
762 tags: Option<&[&str]>,
763 time_range: Option<(Timestamp, Timestamp)>,
764 ) -> MenteResult<Vec<(MemoryId, f32)>> {
765 self.recall_hybrid_at(embedding, None, k, at, tags, time_range)
766 }
767
768 pub fn recall_hybrid_at(
774 &self,
775 embedding: &[f32],
776 query_text: Option<&str>,
777 k: usize,
778 at: Timestamp,
779 tags: Option<&[&str]>,
780 time_range: Option<(Timestamp, Timestamp)>,
781 ) -> MenteResult<Vec<(MemoryId, f32)>> {
782 self.recall_hybrid_at_mode(embedding, query_text, k, at, tags, false, time_range)
783 }
784
785 #[allow(clippy::too_many_arguments)]
787 pub fn recall_hybrid_at_mode(
788 &self,
789 embedding: &[f32],
790 query_text: Option<&str>,
791 k: usize,
792 at: Timestamp,
793 tags: Option<&[&str]>,
794 tags_or: bool,
795 time_range: Option<(Timestamp, Timestamp)>,
796 ) -> MenteResult<Vec<(MemoryId, f32)>> {
797 self.recall_hybrid_scoped_at_mode(
798 embedding, query_text, k, at, tags, tags_or, time_range, None, None,
799 )
800 }
801
802 #[allow(clippy::too_many_arguments)]
809 pub fn recall_hybrid_scoped_at_mode(
810 &self,
811 embedding: &[f32],
812 query_text: Option<&str>,
813 k: usize,
814 at: Timestamp,
815 tags: Option<&[&str]>,
816 tags_or: bool,
817 time_range: Option<(Timestamp, Timestamp)>,
818 agent: Option<AgentId>,
819 user: Option<UserId>,
820 ) -> MenteResult<Vec<(MemoryId, f32)>> {
821 debug!(
822 "Recall hybrid, k={}, at={}, bm25={}, tags_or={}",
823 k,
824 at,
825 query_text.is_some(),
826 tags_or
827 );
828 let results = self.index.hybrid_search_with_query_mode(
830 embedding,
831 query_text,
832 tags,
833 tags_or,
834 time_range,
835 k * 3,
836 );
837 let graph = self.graph.graph();
838 let pm = self.page_map.read();
839 let mut scored: Vec<(MemoryId, f32)> = results
840 .into_iter()
841 .filter_map(|(id, raw_score)| {
842 let incoming = graph.incoming(id);
845 let has_active_supersede = incoming.iter().any(|(_, e)| {
846 (e.edge_type == EdgeType::Supersedes || e.edge_type == EdgeType::Contradicts)
847 && e.is_valid_at(at)
848 });
849 if has_active_supersede {
850 return None;
851 }
852 let Some(&page_id) = pm.get(&id) else {
855 return Some((id, raw_score));
856 };
857 let Ok(node) = self.storage.load_memory(page_id) else {
858 return Some((id, raw_score));
859 };
860 let visible =
863 agent_visible(node.agent_id, agent) && user_visible(node.user_id, user);
864 if !node.is_valid_at(at) || !visible {
865 return None;
866 }
867 let score = if self.cognitive_config.decay_on_recall {
873 let decayed = self.decay.compute_decay(
874 node.salience,
875 node.created_at,
876 node.accessed_at,
877 node.access_count,
878 at,
879 );
880 raw_score * 0.7 + decayed * 0.3
881 } else {
882 raw_score
883 };
884 Some((id, score))
885 })
886 .collect();
887 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
890 scored.truncate(k);
891 Ok(scored)
892 }
893
894 pub fn recall_similar_multi(
901 &self,
902 embeddings: &[Vec<f32>],
903 k: usize,
904 tags: Option<&[&str]>,
905 time_range: Option<(Timestamp, Timestamp)>,
906 ) -> MenteResult<Vec<(MemoryId, f32)>> {
907 self.recall_hybrid_multi(embeddings, None, k, tags, time_range)
908 }
909
910 pub fn recall_hybrid_multi(
915 &self,
916 embeddings: &[Vec<f32>],
917 query_texts: Option<&[String]>,
918 k: usize,
919 tags: Option<&[&str]>,
920 time_range: Option<(Timestamp, Timestamp)>,
921 ) -> MenteResult<Vec<(MemoryId, f32)>> {
922 self.recall_hybrid_multi_mode(embeddings, query_texts, k, tags, false, time_range)
923 }
924
925 pub fn recall_hybrid_multi_mode(
927 &self,
928 embeddings: &[Vec<f32>],
929 query_texts: Option<&[String]>,
930 k: usize,
931 tags: Option<&[&str]>,
932 tags_or: bool,
933 time_range: Option<(Timestamp, Timestamp)>,
934 ) -> MenteResult<Vec<(MemoryId, f32)>> {
935 use std::collections::HashMap;
936
937 let rrf_k: f32 = 60.0;
938 let mut rrf_scores: HashMap<MemoryId, f32> = HashMap::new();
939
940 let now = std::time::SystemTime::now()
941 .duration_since(std::time::UNIX_EPOCH)
942 .unwrap_or_default()
943 .as_micros() as u64;
944
945 for (i, emb) in embeddings.iter().enumerate() {
946 let qt = query_texts.and_then(|texts| texts.get(i).map(|s| s.as_str()));
947 let results = self.recall_hybrid_at_mode(emb, qt, k, now, tags, tags_or, time_range)?;
948 for (rank, (id, _score)) in results.iter().enumerate() {
949 *rrf_scores.entry(*id).or_insert(0.0) += 1.0 / (rrf_k + rank as f32);
950 }
951 }
952
953 let mut merged: Vec<(MemoryId, f32)> = rrf_scores.into_iter().collect();
954 merged.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
955 merged.truncate(k);
956 Ok(merged)
957 }
958
959 pub fn invalidate_memory(&self, id: MemoryId, at: Timestamp) -> MenteResult<()> {
964 debug!("Invalidating memory {} at {}", id, at);
965 let page_id = self
966 .page_map
967 .read()
968 .get(&id)
969 .copied()
970 .ok_or(MenteError::MemoryNotFound(id))?;
971 let mut node = self.storage.load_memory(page_id)?;
972 node.invalidate(at);
973 self.storage.update_memory(page_id, &node)?;
974 Ok(())
975 }
976
977 pub fn relate(&self, edge: MemoryEdge) -> MenteResult<()> {
979 debug!("Relating {} -> {}", edge.source, edge.target);
980 self.graph.add_relationship(&edge)?;
981 Ok(())
982 }
983
984 pub fn get_memory(&self, id: MemoryId) -> MenteResult<MemoryNode> {
986 let page_id = self
987 .page_map
988 .read()
989 .get(&id)
990 .copied()
991 .ok_or(MenteError::MemoryNotFound(id))?;
992 self.storage.load_memory(page_id)
993 }
994
995 pub fn memory_ids(&self) -> Vec<MemoryId> {
997 self.page_map.read().keys().copied().collect()
998 }
999
1000 pub fn memory_count(&self) -> usize {
1002 self.page_map.read().len()
1003 }
1004
1005 pub fn list_memories(
1012 &self,
1013 limit: usize,
1014 offset: usize,
1015 agent: Option<AgentId>,
1016 memory_type: Option<MemoryType>,
1017 content_query: Option<&str>,
1018 ) -> MenteResult<(usize, Vec<MemoryNode>)> {
1019 let pm = self.page_map.read();
1020 let total = pm.len();
1021 let mut ids: Vec<MemoryId> = pm.keys().copied().collect();
1022 ids.sort();
1023 let needle = content_query.map(|q| q.to_lowercase());
1024 let mut out = Vec::new();
1025 for id in ids.into_iter().skip(offset).take(limit) {
1026 if let Some(&page_id) = pm.get(&id)
1027 && let Ok(node) = self.storage.load_memory(page_id)
1028 {
1029 if agent.is_some_and(|a| node.agent_id != a) {
1030 continue;
1031 }
1032 if memory_type.is_some_and(|t| node.memory_type != t) {
1033 continue;
1034 }
1035 if let Some(n) = &needle
1036 && !node.content.to_lowercase().contains(n)
1037 {
1038 continue;
1039 }
1040 out.push(node);
1041 }
1042 }
1043 Ok((total, out))
1044 }
1045
1046 pub fn forget(&self, id: MemoryId) -> MenteResult<()> {
1048 debug!("Forgetting memory {}", id);
1049
1050 if let Some(&page_id) = self.page_map.read().get(&id) {
1051 if let Ok(node) = self.storage.load_memory(page_id) {
1052 self.index.remove_memory(id, &node);
1053 }
1054 self.storage.delete_memory(page_id)?;
1057 }
1058
1059 self.graph.remove_memory(id);
1060 self.page_map.write().remove(&id);
1061 Ok(())
1062 }
1063
1064 pub fn prune_standing_rules(&self) -> MenteResult<PruneReport> {
1076 use std::collections::{HashMap, HashSet};
1077
1078 let always: Vec<MemoryNode> = self
1079 .memory_ids()
1080 .into_iter()
1081 .filter_map(|id| self.get_memory(id).ok())
1082 .filter(|n| n.tags.iter().any(|t| t == "scope:always"))
1083 .collect();
1084
1085 let mut report = PruneReport {
1086 total_always: always.len(),
1087 ..Default::default()
1088 };
1089
1090 let mut by_content: HashMap<&str, Vec<&MemoryNode>> = HashMap::new();
1093 for n in &always {
1094 by_content.entry(n.content.as_str()).or_default().push(n);
1095 }
1096 let mut prune_ids: HashSet<MemoryId> = HashSet::new();
1097 for (_content, mut group) in by_content {
1098 if group.len() > 1 {
1099 report.duplicate_groups += 1;
1100 group.sort_by(|a, b| {
1101 b.salience
1102 .partial_cmp(&a.salience)
1103 .unwrap_or(std::cmp::Ordering::Equal)
1104 });
1105 for n in group.into_iter().skip(1) {
1106 prune_ids.insert(n.id);
1107 }
1108 }
1109 }
1110
1111 let mut to_unpin: Vec<MemoryNode> = Vec::new();
1114 for n in &always {
1115 if prune_ids.contains(&n.id) {
1116 continue;
1117 }
1118 let is_manual = n.tags.iter().any(|t| t == "source:manual");
1119 let is_profile = n.tags.iter().any(|t| t == "user_profile");
1120 if !is_manual && !is_profile {
1121 to_unpin.push(n.clone());
1122 }
1123 }
1124
1125 for id in prune_ids {
1126 self.forget(id)?;
1127 report.pruned.push(id);
1128 }
1129 for mut n in to_unpin {
1130 n.tags.retain(|t| t != "scope:always");
1131 let id = n.id;
1132 self.store(n)?;
1133 report.unpinned.push(id);
1134 }
1135
1136 Ok(report)
1137 }
1138
1139 pub fn graph(&self) -> &GraphManager {
1141 &self.graph
1142 }
1143
1144 #[deprecated(note = "GraphManager now uses interior mutability; use graph() instead")]
1146 pub fn graph_mut(&mut self) -> &mut GraphManager {
1147 &mut self.graph
1148 }
1149
1150 pub fn cognitive_config(&self) -> &CognitiveConfig {
1152 &self.cognitive_config
1153 }
1154
1155 fn run_write_inference(&self, new_memory: &MemoryNode) {
1165 let candidates = if !new_memory.embedding.is_empty() {
1168 let now = std::time::SystemTime::now()
1169 .duration_since(std::time::UNIX_EPOCH)
1170 .unwrap_or_default()
1171 .as_micros() as u64;
1172 self.recall_hybrid_scoped_at_mode(
1177 &new_memory.embedding,
1178 None,
1179 20,
1180 now,
1181 None,
1182 false,
1183 None,
1184 Some(new_memory.agent_id),
1185 Some(new_memory.user_id),
1186 )
1187 .unwrap_or_default()
1188 } else {
1189 vec![]
1190 };
1191
1192 if candidates.is_empty() {
1193 return;
1194 }
1195
1196 let pm = self.page_map.read();
1198 let existing: Vec<MemoryNode> = candidates
1199 .iter()
1200 .filter(|(id, _)| *id != new_memory.id)
1201 .filter_map(|(id, _)| {
1202 pm.get(id)
1203 .and_then(|&pid| self.storage.load_memory(pid).ok())
1204 })
1205 .collect();
1206 drop(pm);
1207
1208 if existing.is_empty() {
1209 return;
1210 }
1211
1212 let actions = self
1213 .write_inference
1214 .infer_on_write(new_memory, &existing, &[]);
1215
1216 let action_count = actions.len();
1217 for action in actions {
1218 if let Err(e) = self.apply_inferred_action(action) {
1219 warn!("Failed to apply inferred action: {}", e);
1220 }
1221 }
1222 if action_count > 0 {
1223 debug!(
1224 "Write inference for {} produced {} actions",
1225 new_memory.id, action_count
1226 );
1227 }
1228 }
1229
1230 fn apply_inferred_action(&self, action: InferredAction) -> MenteResult<()> {
1232 match action {
1233 InferredAction::CreateEdge {
1234 source,
1235 target,
1236 edge_type,
1237 weight,
1238 } => {
1239 let now = std::time::SystemTime::now()
1240 .duration_since(std::time::UNIX_EPOCH)
1241 .unwrap_or_default()
1242 .as_micros() as u64;
1243 let edge = MemoryEdge {
1244 source,
1245 target,
1246 edge_type,
1247 weight,
1248 created_at: now,
1249 valid_from: None,
1250 valid_until: None,
1251 label: None,
1252 };
1253 debug!(
1254 "Auto-creating {:?} edge {} -> {}",
1255 edge_type, source, target
1256 );
1257 self.graph.add_relationship(&edge)?;
1258 }
1259 InferredAction::InvalidateMemory {
1260 memory,
1261 superseded_by,
1262 valid_until,
1263 } => {
1264 debug!(
1265 "Invalidating memory {} (superseded by {})",
1266 memory, superseded_by
1267 );
1268 self.invalidate_memory(memory, valid_until)?;
1269 let now = std::time::SystemTime::now()
1271 .duration_since(std::time::UNIX_EPOCH)
1272 .unwrap_or_default()
1273 .as_micros() as u64;
1274 let edge = MemoryEdge {
1275 source: superseded_by,
1276 target: memory,
1277 edge_type: EdgeType::Supersedes,
1278 weight: 1.0,
1279 created_at: now,
1280 valid_from: None,
1281 valid_until: None,
1282 label: None,
1283 };
1284 self.graph.add_relationship(&edge)?;
1285 }
1286 InferredAction::DeduplicateExact { duplicate, keeper } => {
1287 debug!("Deduplicating exact copy {duplicate} into {keeper}");
1288 let now = std::time::SystemTime::now()
1289 .duration_since(std::time::UNIX_EPOCH)
1290 .unwrap_or_default()
1291 .as_micros() as u64;
1292 self.invalidate_memory(duplicate, now)?;
1293 let edge = MemoryEdge {
1297 source: keeper,
1298 target: duplicate,
1299 edge_type: EdgeType::Derived,
1300 weight: 1.0,
1301 created_at: now,
1302 valid_from: None,
1303 valid_until: None,
1304 label: None,
1305 };
1306 self.graph.add_relationship(&edge)?;
1307 }
1308 InferredAction::MarkObsolete {
1309 memory,
1310 superseded_by,
1311 } => {
1312 debug!(
1313 "Marking {} obsolete (superseded by {})",
1314 memory, superseded_by
1315 );
1316 let now = std::time::SystemTime::now()
1317 .duration_since(std::time::UNIX_EPOCH)
1318 .unwrap_or_default()
1319 .as_micros() as u64;
1320 self.invalidate_memory(memory, now)?;
1321 let edge = MemoryEdge {
1322 source: superseded_by,
1323 target: memory,
1324 edge_type: EdgeType::Supersedes,
1325 weight: 1.0,
1326 created_at: now,
1327 valid_from: None,
1328 valid_until: None,
1329 label: None,
1330 };
1331 self.graph.add_relationship(&edge)?;
1332 }
1333 InferredAction::FlagContradiction {
1334 existing,
1335 new,
1336 reason,
1337 } => {
1338 debug!(
1339 "Contradiction detected: {} vs {} — {}",
1340 existing, new, reason
1341 );
1342 let now = std::time::SystemTime::now()
1343 .duration_since(std::time::UNIX_EPOCH)
1344 .unwrap_or_default()
1345 .as_micros() as u64;
1346 let edge = MemoryEdge {
1347 source: new,
1348 target: existing,
1349 edge_type: EdgeType::Contradicts,
1350 weight: 1.0,
1351 created_at: now,
1352 valid_from: None,
1353 valid_until: None,
1354 label: Some(reason),
1355 };
1356 self.graph.add_relationship(&edge)?;
1357 }
1358 InferredAction::UpdateConfidence {
1359 memory,
1360 new_confidence,
1361 } => {
1362 debug!("Updating confidence for {} to {}", memory, new_confidence);
1363 if let Ok(mut node) = self.get_memory(memory) {
1364 node.confidence = new_confidence;
1365 if let Some(&pid) = self.page_map.read().get(&memory) {
1366 self.storage.update_memory(pid, &node)?;
1367 }
1368 }
1369 }
1370 InferredAction::PropagateBeliefChange { root, delta } => {
1371 debug!("Propagating belief change from {} (delta={})", root, delta);
1372 if let Ok(node) = self.get_memory(root) {
1373 let new_confidence = (node.confidence + delta).clamp(0.0, 1.0);
1374 let affected = self.graph.propagate_belief_change(root, new_confidence);
1375 for (affected_id, new_conf) in affected {
1376 if let Ok(mut affected_node) = self.get_memory(affected_id) {
1377 affected_node.confidence = new_conf;
1378 if let Some(&pid) = self.page_map.read().get(&affected_id)
1379 && let Err(e) = self.storage.update_memory(pid, &affected_node)
1380 {
1381 warn!("Failed to persist belief update for {affected_id}: {e}");
1382 }
1383 }
1384 }
1385 }
1386 }
1387 InferredAction::UpdateContent {
1388 memory,
1389 new_content,
1390 reason,
1391 } => {
1392 debug!("Updating content of {}: {}", memory, reason);
1393 if let Ok(mut node) = self.get_memory(memory) {
1394 node.content = new_content;
1395 if let Some(&pid) = self.page_map.read().get(&memory) {
1396 self.storage.update_memory(pid, &node)?;
1397 }
1398 self.index.remove_memory(memory, &node);
1399 self.index.index_memory(&node);
1400 }
1401 }
1402 }
1403 Ok(())
1404 }
1405
1406 pub fn apply_decay(&self, memories: &mut [MemoryNode]) {
1415 let now = std::time::SystemTime::now()
1416 .duration_since(std::time::UNIX_EPOCH)
1417 .unwrap_or_default()
1418 .as_micros() as u64;
1419 self.decay.apply_decay_batch(memories, now);
1420 }
1421
1422 pub fn compute_decayed_salience(&self, memory: &MemoryNode) -> f32 {
1424 let now = std::time::SystemTime::now()
1425 .duration_since(std::time::UNIX_EPOCH)
1426 .unwrap_or_default()
1427 .as_micros() as u64;
1428 self.decay.compute_decay(
1429 memory.salience,
1430 memory.created_at,
1431 memory.accessed_at,
1432 memory.access_count,
1433 now,
1434 )
1435 }
1436
1437 pub fn apply_decay_global(&self) -> MenteResult<usize> {
1451 Ok(0)
1452 }
1453
1454 pub fn reset_decay_state(&self) -> MenteResult<usize> {
1465 let now = std::time::SystemTime::now()
1466 .duration_since(std::time::UNIX_EPOCH)
1467 .unwrap_or_default()
1468 .as_micros() as u64;
1469 let max_salience = self.decay.config.max_salience;
1470 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
1471
1472 let mut reset = 0;
1473 for pid in &page_ids {
1474 if let Ok(mut node) = self.storage.load_memory(*pid) {
1475 node.salience = max_salience;
1476 node.accessed_at = now;
1477 self.storage.update_memory(*pid, &node)?;
1478 reset += 1;
1479 }
1480 }
1481 if reset > 0 {
1482 info!("Reset decay state for {} memories", reset);
1483 }
1484 Ok(reset)
1485 }
1486
1487 pub fn reembed_all(&self) -> MenteResult<usize> {
1502 let target = self.embedding_dim;
1503 if target == 0 {
1504 return Ok(0);
1505 }
1506 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
1507
1508 let mut reembedded = 0usize;
1509 for pid in &page_ids {
1510 let Ok(mut node) = self.storage.load_memory(*pid) else {
1511 continue;
1512 };
1513 if node.embedding.len() == target || node.content.is_empty() {
1515 continue;
1516 }
1517 let Some(embedding) = self.embed_text(&node.content)? else {
1518 continue;
1519 };
1520 if embedding.len() != target {
1523 continue;
1524 }
1525 self.index.remove_vector_only(node.id);
1526 node.embedding = embedding;
1527 self.storage.update_memory(*pid, &node)?;
1528 self.index.index_memory(&node);
1529 reembedded += 1;
1530 }
1531 if reembedded > 0 {
1532 info!("Re-embedded {reembedded} memories at dimension {target}");
1533 }
1534 Ok(reembedded)
1535 }
1536
1537 pub fn find_consolidation_candidates(
1546 &self,
1547 min_cluster_size: usize,
1548 similarity_threshold: f32,
1549 ) -> MenteResult<Vec<ConsolidationCandidate>> {
1550 let now = std::time::SystemTime::now()
1551 .duration_since(std::time::UNIX_EPOCH)
1552 .unwrap_or_default()
1553 .as_micros() as u64;
1554
1555 let pm = self.page_map.read();
1557 let eligible: Vec<MemoryNode> = pm
1558 .values()
1559 .filter_map(|pid| self.storage.load_memory(*pid).ok())
1560 .filter(|node| ConsolidationEngine::should_consolidate(node, now))
1561 .collect();
1562 drop(pm);
1563
1564 if eligible.is_empty() {
1565 return Ok(vec![]);
1566 }
1567
1568 Ok(self
1569 .consolidation
1570 .find_candidates(&eligible, min_cluster_size, similarity_threshold))
1571 }
1572
1573 pub fn consolidate_cluster(&self, memory_ids: &[MemoryId]) -> MenteResult<MemoryId> {
1578 let pm = self.page_map.read();
1579 let cluster: Vec<MemoryNode> = memory_ids
1580 .iter()
1581 .filter_map(|id| {
1582 pm.get(id)
1583 .and_then(|&pid| self.storage.load_memory(pid).ok())
1584 })
1585 .collect();
1586 drop(pm);
1587
1588 if cluster.len() < 2 {
1589 return Err(MenteError::Query(
1590 "consolidation requires at least 2 memories".into(),
1591 ));
1592 }
1593
1594 let owner_agent = cluster[0].agent_id;
1599 if cluster.iter().any(|m| m.agent_id != owner_agent) {
1600 return Err(MenteError::Query(
1601 "consolidation cluster mixes agents; refusing to merge across owners".into(),
1602 ));
1603 }
1604 let owner_user = cluster[0].user_id;
1605 if cluster.iter().any(|m| m.user_id != owner_user) {
1606 return Err(MenteError::Query(
1607 "consolidation cluster mixes users; refusing to merge across owners".into(),
1608 ));
1609 }
1610
1611 let result = self.consolidation.consolidate(&cluster);
1612
1613 let agent_id = cluster[0].agent_id;
1616 let user_id = cluster[0].user_id;
1617 let mut consolidated = MemoryNode::new(
1618 agent_id,
1619 result.new_type,
1620 result.summary,
1621 result.combined_embedding,
1622 )
1623 .with_user_id(user_id);
1624 consolidated.confidence = result.combined_confidence;
1625
1626 let consolidated_id = consolidated.id;
1627 self.store(consolidated)?;
1628
1629 let now = std::time::SystemTime::now()
1631 .duration_since(std::time::UNIX_EPOCH)
1632 .unwrap_or_default()
1633 .as_micros() as u64;
1634 for source_id in &result.source_memories {
1635 let _ = self.invalidate_memory(*source_id, now);
1636 let edge = MemoryEdge {
1637 source: consolidated_id,
1638 target: *source_id,
1639 edge_type: EdgeType::Derived,
1640 weight: 1.0,
1641 created_at: now,
1642 valid_from: None,
1643 valid_until: None,
1644 label: None,
1645 };
1646 let _ = self.graph.add_relationship(&edge);
1647 }
1648
1649 info!(
1650 "Consolidated {} memories into {}",
1651 result.source_memories.len(),
1652 consolidated_id
1653 );
1654 Ok(consolidated_id)
1655 }
1656
1657 pub fn close(&self) -> MenteResult<()> {
1659 info!("Closing MenteDB");
1660 self.flush_full()?;
1661 self.storage.close()?;
1662 Ok(())
1663 }
1664
1665 pub fn close_quick(&self) -> MenteResult<()> {
1670 info!("Quick closing MenteDB");
1671 self.storage.checkpoint()?;
1672 self.storage.close()?;
1673 Ok(())
1674 }
1675
1676 #[doc(hidden)]
1680 pub fn simulate_crash(self) {
1681 self.storage.release_process_lock();
1682 std::mem::forget(self);
1683 }
1684
1685 pub fn rebuild_indexes(&self) -> MenteResult<usize> {
1690 info!("Rebuilding indexes from storage...");
1691 let ids: Vec<MemoryId> = self.page_map.read().keys().copied().collect();
1692 let total = ids.len();
1693 let mut indexed = 0usize;
1694 for id in ids {
1695 if let Ok(node) = self.get_memory(id) {
1696 self.index.index_memory(&node);
1697 indexed += 1;
1698 }
1699 }
1700 self.index.save(&self.path.join("indexes"))?;
1701 info!(indexed, total, "index rebuild complete");
1702 Ok(indexed)
1703 }
1704
1705 pub fn flush(&self) -> MenteResult<()> {
1710 debug!("Flushing MenteDB to disk");
1711 self.storage.checkpoint()?;
1717
1718 let n = self
1719 .flushes_since_snapshot
1720 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
1721 + 1;
1722 if n >= self.cognitive_config.flush_snapshot_interval {
1723 self.write_snapshots()?;
1724 }
1725 Ok(())
1726 }
1727
1728 pub fn flush_full(&self) -> MenteResult<()> {
1732 debug!("Full flush of MenteDB to disk");
1733 self.storage.checkpoint()?;
1734 self.write_snapshots()
1735 }
1736
1737 fn write_snapshots(&self) -> MenteResult<()> {
1738 self.index.save(&self.path.join("indexes"))?;
1739 self.graph.save(&self.path.join("graph"))?;
1740
1741 let cognitive_dir = self.path.join("cognitive");
1743 if std::fs::create_dir_all(&cognitive_dir).is_ok() {
1744 let _ = self
1745 .trajectory
1746 .read()
1747 .transitions
1748 .save(&cognitive_dir.join("transitions.json"), 1);
1749 let _ = self
1750 .speculative
1751 .read()
1752 .save(&cognitive_dir.join("speculative.json"), 0);
1753 let _ = self
1754 .entity_resolver
1755 .read()
1756 .save(&cognitive_dir.join("entities.json"));
1757 }
1758 self.flushes_since_snapshot
1759 .store(0, std::sync::atomic::Ordering::Relaxed);
1760 Ok(())
1761 }
1762
1763 fn execute_plan(&self, plan: &QueryPlan) -> MenteResult<Vec<ScoredMemory>> {
1765 match plan {
1766 QueryPlan::VectorSearch {
1767 query,
1768 k,
1769 filters,
1770 condition,
1771 } => {
1772 let embedded;
1776 let qvec: &[f32] = if query.is_empty() {
1777 match filters
1778 .iter()
1779 .find(|f| f.op == Operator::SimilarTo)
1780 .map(|f| &f.value)
1781 {
1782 Some(Value::Text(text)) => {
1783 match self.embedder.as_ref().and_then(|e| e.embed(text).ok()) {
1784 Some(v) => {
1785 embedded = v;
1786 &embedded
1787 }
1788 None => return Ok(vec![]),
1790 }
1791 }
1792 _ => query,
1793 }
1794 } else {
1795 query
1796 };
1797 let hits = self.index.hybrid_search(qvec, None, None, *k);
1798 let mut scored = self.load_scored_memories(&hits)?;
1799 match condition {
1803 Some(cond) => {
1804 scored.retain(|sm| Self::condition_matches(cond, &sm.memory));
1805 }
1806 None => {
1807 scored.retain(|sm| {
1808 filters
1809 .iter()
1810 .filter(|f| f.op != Operator::SimilarTo)
1811 .all(|f| Self::filter_matches(f, &sm.memory))
1812 });
1813 }
1814 }
1815 if self.cognitive_config.entity_boost_enabled
1818 && let Some(qtext) = filters
1819 .iter()
1820 .find(|f| f.op == Operator::SimilarTo)
1821 .and_then(|f| match &f.value {
1822 Value::Text(t) => Some(t.clone()),
1823 _ => None,
1824 })
1825 {
1826 self.apply_entity_boost(&mut scored, &qtext);
1827 }
1828 Ok(scored)
1829 }
1830 QueryPlan::TagScan {
1831 tags,
1832 filters,
1833 limit,
1834 condition,
1835 } => {
1836 let k = limit.unwrap_or(20);
1837 let mut scored = if tags.is_empty() {
1838 let pm = self.page_map.read();
1842 pm.values()
1843 .filter_map(|&pid| self.storage.load_memory(pid).ok())
1844 .map(|memory| ScoredMemory { memory, score: 1.0 })
1845 .collect::<Vec<_>>()
1846 } else {
1847 let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect();
1848 let hits = self.index.hybrid_search(&[], Some(&tag_refs), None, k);
1850 self.load_scored_memories(&hits)?
1851 };
1852 match condition {
1855 Some(cond) => {
1856 scored.retain(|sm| Self::condition_matches(cond, &sm.memory));
1857 }
1858 None => {
1859 scored.retain(|sm| {
1860 filters.iter().all(|f| Self::filter_matches(f, &sm.memory))
1861 });
1862 }
1863 }
1864 scored.truncate(k);
1865 Ok(scored)
1866 }
1867 QueryPlan::TemporalScan { start, end, .. } => {
1868 let hits = self
1869 .index
1870 .hybrid_search(&[], None, Some((*start, *end)), 100);
1871 self.load_scored_memories(&hits)
1872 }
1873 QueryPlan::GraphTraversal { start, depth, .. } => {
1874 let (ids, _edges) = self.graph.get_context_subgraph(*start, *depth);
1875 let pm = self.page_map.read();
1876 let scored: Vec<ScoredMemory> = ids
1877 .iter()
1878 .filter_map(|id| {
1879 pm.get(id).and_then(|&pid| {
1880 self.storage.load_memory(pid).ok().map(|node| ScoredMemory {
1881 memory: node,
1882 score: 1.0,
1883 })
1884 })
1885 })
1886 .collect();
1887 Ok(scored)
1888 }
1889 QueryPlan::PointLookup { id } => {
1890 let page_id = self
1891 .page_map
1892 .read()
1893 .get(id)
1894 .copied()
1895 .ok_or(MenteError::MemoryNotFound(*id))?;
1896 let node = self.storage.load_memory(page_id)?;
1897 Ok(vec![ScoredMemory {
1898 memory: node,
1899 score: 1.0,
1900 }])
1901 }
1902 _ => Ok(vec![]),
1903 }
1904 }
1905
1906 fn entity_boost_ids(&self, query_text: &str) -> std::collections::HashSet<MemoryId> {
1913 let mut ids = std::collections::HashSet::new();
1914 let graph = self.graph.graph();
1915 let mut seen_tokens = std::collections::HashSet::new();
1916 for token in query_text
1917 .split(|c: char| !c.is_alphanumeric())
1918 .map(|w| w.to_lowercase())
1919 .filter(|w| w.len() >= 2)
1920 {
1921 if !seen_tokens.insert(token.clone()) {
1922 continue;
1923 }
1924 for entity_id in self.index.bitmap.query_tag(&format!("entity:{token}")) {
1925 for (mid, edge) in graph.outgoing(entity_id) {
1926 if edge.edge_type == EdgeType::Derived {
1927 ids.insert(mid);
1928 }
1929 }
1930 }
1931 }
1932 ids
1933 }
1934
1935 fn apply_entity_boost(&self, scored: &mut [ScoredMemory], query_text: &str) {
1938 let ids = self.entity_boost_ids(query_text);
1939 if ids.is_empty() {
1940 return;
1941 }
1942 let w = self.cognitive_config.entity_boost_weight;
1943 for sm in scored.iter_mut() {
1944 if ids.contains(&sm.memory.id) {
1945 sm.score += w;
1946 }
1947 }
1948 scored.sort_by(|a, b| {
1949 b.score
1950 .partial_cmp(&a.score)
1951 .unwrap_or(std::cmp::Ordering::Equal)
1952 });
1953 }
1954
1955 fn condition_matches(c: &Condition, node: &MemoryNode) -> bool {
1959 match c {
1960 Condition::Leaf(f) => Self::filter_matches(f, node),
1961 Condition::And(children) => children.iter().all(|ch| Self::condition_matches(ch, node)),
1962 Condition::Or(children) => children.iter().any(|ch| Self::condition_matches(ch, node)),
1963 Condition::Not(inner) => !Self::condition_matches(inner, node),
1964 }
1965 }
1966
1967 fn filter_matches(f: &Filter, node: &MemoryNode) -> bool {
1974 if let Value::List(items) = &f.value {
1977 return items.iter().any(|v| {
1978 Self::filter_matches(
1979 &Filter {
1980 field: f.field,
1981 op: Operator::Eq,
1982 value: v.clone(),
1983 },
1984 node,
1985 )
1986 });
1987 }
1988 match (&f.field, &f.value) {
1989 (Field::Type, Value::MemoryType(t)) => {
1990 if f.op == Operator::Neq {
1991 node.memory_type != *t
1992 } else {
1993 node.memory_type == *t
1994 }
1995 }
1996 (Field::Tag, Value::Text(tag)) => {
1997 let has = node.tags.iter().any(|x| x == tag);
1998 if f.op == Operator::Neq { !has } else { has }
1999 }
2000 (Field::Content, Value::Text(s)) => {
2001 let hit = node.content.to_lowercase().contains(&s.to_lowercase());
2002 if f.op == Operator::Neq { !hit } else { hit }
2003 }
2004 (Field::Created, v) => Self::num_cmp(node.created_at as f64, f.op, Self::value_f64(v)),
2005 (Field::Accessed, v) => {
2006 Self::num_cmp(node.accessed_at as f64, f.op, Self::value_f64(v))
2007 }
2008 (Field::ValidAt, v) => Self::value_f64(v)
2010 .map(|t| node.is_valid_at(t as u64))
2011 .unwrap_or(true),
2012 _ => true,
2013 }
2014 }
2015
2016 fn value_f64(v: &Value) -> Option<f64> {
2017 match v {
2018 Value::Number(n) => Some(*n),
2019 Value::Integer(i) => Some(*i as f64),
2020 _ => None,
2021 }
2022 }
2023
2024 fn num_cmp(a: f64, op: Operator, b: Option<f64>) -> bool {
2025 let Some(b) = b else { return true };
2026 match op {
2027 Operator::Gt => a > b,
2028 Operator::Gte => a >= b,
2029 Operator::Lt => a < b,
2030 Operator::Lte => a <= b,
2031 Operator::Eq => (a - b).abs() < f64::EPSILON,
2032 Operator::Neq => (a - b).abs() >= f64::EPSILON,
2033 Operator::SimilarTo => true,
2034 Operator::In | Operator::Contains => false,
2037 }
2038 }
2039
2040 fn load_scored_memories(&self, hits: &[(MemoryId, f32)]) -> MenteResult<Vec<ScoredMemory>> {
2045 let pm = self.page_map.read();
2046 let now = if self.cognitive_config.decay_on_recall {
2047 std::time::SystemTime::now()
2048 .duration_since(std::time::UNIX_EPOCH)
2049 .unwrap_or_default()
2050 .as_micros() as u64
2051 } else {
2052 0
2053 };
2054
2055 let mut scored = Vec::with_capacity(hits.len());
2056 for &(id, score) in hits {
2057 if let Some(&page_id) = pm.get(&id)
2058 && let Ok(node) = self.storage.load_memory(page_id)
2059 {
2060 let final_score = if self.cognitive_config.decay_on_recall {
2061 let decayed_salience = self.decay.compute_decay(
2062 node.salience,
2063 node.created_at,
2064 node.accessed_at,
2065 node.access_count,
2066 now,
2067 );
2068 score * 0.7 + decayed_salience * 0.3
2072 } else {
2073 score
2074 };
2075 scored.push(ScoredMemory {
2076 memory: node,
2077 score: final_score,
2078 });
2079 }
2080 }
2081 if self.cognitive_config.decay_on_recall {
2083 scored.sort_unstable_by(|a, b| {
2084 b.score
2085 .partial_cmp(&a.score)
2086 .unwrap_or(std::cmp::Ordering::Equal)
2087 });
2088 }
2089 Ok(scored)
2090 }
2091
2092 pub fn record_pain(&self, signal: PainSignal) {
2101 if self.cognitive_config.pain_tracking {
2102 self.pain.write().record_pain(signal);
2103 }
2104 }
2105
2106 pub fn get_pain_warnings(&self, context_keywords: &[String]) -> Vec<PainSignal> {
2111 if !self.cognitive_config.pain_tracking {
2112 return vec![];
2113 }
2114 let registry = self.pain.read();
2115 registry
2116 .get_pain_for_context(context_keywords)
2117 .into_iter()
2118 .cloned()
2119 .collect()
2120 }
2121
2122 pub fn format_pain_warnings(&self, signals: &[&PainSignal]) -> String {
2124 self.pain.read().format_pain_warnings(signals)
2125 }
2126
2127 pub fn decay_pain(&self) {
2129 let now = std::time::SystemTime::now()
2130 .duration_since(std::time::UNIX_EPOCH)
2131 .unwrap_or_default()
2132 .as_micros() as u64;
2133 self.pain.write().decay_all(now);
2134 }
2135
2136 pub fn all_pain_signals(&self) -> Vec<PainSignal> {
2138 self.pain.read().all_signals().to_vec()
2139 }
2140
2141 pub fn record_trajectory_turn(&self, turn: TrajectoryNode) {
2150 self.trajectory.write().record_turn(turn);
2151 }
2152
2153 pub async fn canonicalize_trajectory_topics<J: mentedb_cognitive::llm::LlmJudge>(
2165 &self,
2166 judge: J,
2167 limit: usize,
2168 ) -> usize {
2169 let (pending, mut known) = {
2171 let traj = self.trajectory.read();
2172 (traj.pending_canonicalization(limit), traj.known_topics())
2173 };
2174 if pending.is_empty() {
2175 return 0;
2176 }
2177 let svc = mentedb_cognitive::llm::CognitiveLlmService::new(judge);
2179 let mut learned = Vec::new();
2180 for raw in pending {
2181 if let Ok(label) = svc.canonicalize_topic(&raw, &known).await {
2182 known.push(label.topic.clone());
2183 learned.push((raw, label.topic));
2184 }
2185 }
2186 let n = learned.len();
2188 if n > 0 {
2189 let mut traj = self.trajectory.write();
2190 for (raw, canonical) in learned {
2191 traj.learn_canonical(&raw, &canonical);
2192 }
2193 }
2194 n
2195 }
2196
2197 pub fn get_resume_context(&self) -> Option<String> {
2201 self.trajectory.read().get_resume_context()
2202 }
2203
2204 pub fn predict_next_topics(&self) -> Vec<String> {
2208 self.trajectory.read().predict_next_topics()
2209 }
2210
2211 pub fn get_trajectory(&self) -> Vec<TrajectoryNode> {
2213 self.trajectory.read().get_trajectory().to_vec()
2214 }
2215
2216 pub fn reinforce_transition(&self, hit_topic: &str) {
2218 self.trajectory.write().reinforce_transition(hit_topic);
2219 }
2220
2221 pub fn feed_stream_token(&self, token: &str) {
2230 self.stream.feed_token(token);
2231 }
2232
2233 pub fn check_stream_alerts(&self, known_facts: &[(MemoryId, String)]) -> Vec<StreamAlert> {
2238 self.stream.check_alerts(known_facts)
2239 }
2240
2241 pub fn drain_stream_buffer(&self) -> String {
2243 self.stream.drain_buffer()
2244 }
2245
2246 pub fn detect_phantoms(
2255 &self,
2256 content: &str,
2257 known_entities: &[String],
2258 turn_id: u64,
2259 ) -> Vec<PhantomMemory> {
2260 if !self.cognitive_config.phantom_tracking {
2261 return vec![];
2262 }
2263 self.phantom
2264 .write()
2265 .detect_gaps(content, known_entities, turn_id)
2266 }
2267
2268 pub fn resolve_phantom(&self, phantom_id: MemoryId) {
2270 self.phantom.write().resolve(phantom_id.into());
2271 }
2272
2273 pub fn get_active_phantoms(&self) -> Vec<PhantomMemory> {
2275 self.phantom
2276 .read()
2277 .get_active_phantoms()
2278 .into_iter()
2279 .cloned()
2280 .collect()
2281 }
2282
2283 pub fn format_phantom_warnings(&self) -> String {
2285 self.phantom.read().format_phantom_warnings()
2286 }
2287
2288 pub fn register_entity(&self, entity: &str) {
2290 self.phantom.write().register_entity(entity);
2291 }
2292
2293 pub fn register_entities(&self, entities: &[&str]) {
2295 self.phantom.write().register_entities(entities);
2296 }
2297
2298 pub fn try_speculative_hit(
2307 &self,
2308 query: &str,
2309 query_embedding: Option<&[f32]>,
2310 ) -> Option<CacheEntry> {
2311 if !self.cognitive_config.speculative_cache {
2312 return None;
2313 }
2314 self.speculative.write().try_hit(query, query_embedding)
2315 }
2316
2317 pub fn pre_assemble_speculative<F>(&self, predictions: Vec<String>, builder: F)
2322 where
2323 F: Fn(&str) -> Option<(String, Vec<MemoryId>, Option<Vec<f32>>)>,
2324 {
2325 if self.cognitive_config.speculative_cache {
2326 self.speculative.write().pre_assemble(predictions, builder);
2327 }
2328 }
2329
2330 pub fn evict_stale_speculative(&self, max_age_us: u64) {
2332 let now = std::time::SystemTime::now()
2333 .duration_since(std::time::UNIX_EPOCH)
2334 .unwrap_or_default()
2335 .as_micros() as u64;
2336 self.speculative.write().evict_stale(max_age_us, now);
2337 }
2338
2339 pub fn speculative_cache_stats(&self) -> CacheStats {
2341 self.speculative.read().stats()
2342 }
2343
2344 pub fn speculative_cache_entries(&self) -> Vec<CacheEntry> {
2348 self.speculative.read().entries().to_vec()
2349 }
2350
2351 pub fn detect_interference(&self, memories: &[MemoryNode]) -> Vec<InterferencePair> {
2361 if !self.cognitive_config.interference_detection {
2362 return vec![];
2363 }
2364 self.interference.detect_interference(memories)
2365 }
2366
2367 pub fn generate_disambiguation(&self, a: &MemoryNode, b: &MemoryNode) -> String {
2369 self.interference.generate_disambiguation(a, b)
2370 }
2371
2372 pub fn arrange_with_separation(
2374 memories: Vec<MemoryId>,
2375 pairs: &[InterferencePair],
2376 ) -> Vec<MemoryId> {
2377 InterferenceDetector::arrange_with_separation(memories, pairs)
2378 }
2379
2380 pub fn resolve_entity(&self, name: &str) -> mentedb_cognitive::ResolvedEntity {
2388 self.entity_resolver.read().resolve(name)
2389 }
2390
2391 pub fn add_entity_alias(&self, alias: &str, canonical: &str, confidence: f32) {
2393 self.entity_resolver
2394 .write()
2395 .add_alias(alias, canonical, confidence);
2396 }
2397
2398 pub fn get_canonical_entity(&self, name: &str) -> Option<String> {
2400 self.entity_resolver.read().get_canonical(name).cloned()
2401 }
2402
2403 pub fn known_entities(&self) -> Vec<String> {
2405 self.entity_resolver.read().known_entities()
2406 }
2407
2408 pub fn compress_memory(&self, memory: &MemoryNode) -> CompressedMemory {
2417 self.compressor.compress(memory)
2418 }
2419
2420 pub fn compress_memories(&self, memories: &[MemoryNode]) -> Vec<CompressedMemory> {
2422 self.compressor.compress_batch(memories)
2423 }
2424
2425 pub fn estimate_tokens(text: &str) -> usize {
2427 MemoryCompressor::estimate_tokens(text)
2428 }
2429
2430 pub fn evaluate_archival(&self, memory: &MemoryNode) -> ArchivalDecision {
2438 if !self.cognitive_config.archival_evaluation {
2439 return ArchivalDecision::Keep;
2440 }
2441 let now = std::time::SystemTime::now()
2442 .duration_since(std::time::UNIX_EPOCH)
2443 .unwrap_or_default()
2444 .as_micros() as u64;
2445 let effective = self.decay.compute_decay(
2449 memory.salience,
2450 memory.created_at,
2451 memory.accessed_at,
2452 memory.access_count,
2453 now,
2454 );
2455 self.archival.evaluate_effective(
2456 effective,
2457 memory.memory_type,
2458 memory.created_at,
2459 memory.access_count,
2460 now,
2461 )
2462 }
2463
2464 pub fn evaluate_archival_batch(
2466 &self,
2467 memories: &[MemoryNode],
2468 ) -> Vec<(MemoryId, ArchivalDecision)> {
2469 if !self.cognitive_config.archival_evaluation {
2470 return memories
2471 .iter()
2472 .map(|m| (m.id, ArchivalDecision::Keep))
2473 .collect();
2474 }
2475 let now = std::time::SystemTime::now()
2476 .duration_since(std::time::UNIX_EPOCH)
2477 .unwrap_or_default()
2478 .as_micros() as u64;
2479 memories
2480 .iter()
2481 .map(|m| {
2482 let effective = self.decay.compute_decay(
2483 m.salience,
2484 m.created_at,
2485 m.accessed_at,
2486 m.access_count,
2487 now,
2488 );
2489 (
2490 m.id,
2491 self.archival.evaluate_effective(
2492 effective,
2493 m.memory_type,
2494 m.created_at,
2495 m.access_count,
2496 now,
2497 ),
2498 )
2499 })
2500 .collect()
2501 }
2502
2503 pub fn evaluate_archival_global(&self) -> MenteResult<Vec<(MemoryId, ArchivalDecision)>> {
2508 let pm = self.page_map.read();
2509 let memories: Vec<MemoryNode> = pm
2510 .values()
2511 .filter_map(|pid| self.storage.load_memory(*pid).ok())
2512 .collect();
2513 drop(pm);
2514 Ok(self.evaluate_archival_batch(&memories))
2517 }
2518
2519 pub fn needs_enrichment(&self) -> bool {
2525 if !self.cognitive_config.enrichment_config.enabled {
2526 return false;
2527 }
2528 *self.enrichment_pending.read()
2529 }
2530
2531 pub fn last_enrichment_turn(&self) -> u64 {
2533 *self.last_enrichment_turn.read()
2534 }
2535
2536 pub fn request_enrichment(&self) {
2538 *self.enrichment_pending.write() = true;
2539 }
2540
2541 fn distinct_owner_pairs(&self) -> Vec<(UserId, AgentId)> {
2548 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
2549 let mut pairs: Vec<(UserId, AgentId)> = Vec::new();
2550 for pid in &page_ids {
2551 if let Ok(mem) = self.storage.load_memory(*pid) {
2552 let pair = (mem.user_id, mem.agent_id);
2553 if !pairs.contains(&pair) {
2554 pairs.push(pair);
2555 }
2556 }
2557 }
2558 pairs
2559 }
2560
2561 pub fn enrichment_candidates(&self, user: UserId, agent: AgentId) -> Vec<MemoryNode> {
2569 let last_turn = *self.last_enrichment_turn.read();
2570 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
2571 let mut candidates: Vec<MemoryNode> = page_ids
2572 .iter()
2573 .filter_map(|pid| self.storage.load_memory(*pid).ok())
2574 .filter(|m| {
2575 m.agent_id == agent
2576 && m.user_id == user
2577 && m.memory_type == mentedb_core::memory::MemoryType::Episodic
2578 && !m.tags.contains(&"source:enrichment".to_string())
2579 && m.created_at > last_turn
2580 })
2581 .collect();
2582 candidates.sort_by_key(|m| m.created_at);
2583 candidates
2584 }
2585
2586 pub fn all_enrichment_candidates(&self) -> Vec<MemoryNode> {
2591 let mut candidates: Vec<MemoryNode> = self
2592 .distinct_owner_pairs()
2593 .into_iter()
2594 .flat_map(|(user, agent)| self.enrichment_candidates(user, agent))
2595 .collect();
2596 candidates.sort_by_key(|m| m.created_at);
2597 candidates
2598 }
2599
2600 pub fn store_enrichment_memories(
2609 &self,
2610 memories: Vec<MemoryNode>,
2611 source_ids: &[MemoryId],
2612 ) -> MenteResult<(usize, usize)> {
2613 let max_conf = self
2614 .cognitive_config
2615 .enrichment_config
2616 .max_enrichment_confidence;
2617 let mut stored = 0usize;
2618 let mut edges = 0usize;
2619
2620 for mut mem in memories {
2621 if !mem.tags.contains(&"source:enrichment".to_string()) {
2623 mem.tags.push("source:enrichment".to_string());
2624 }
2625 if mem.confidence > max_conf {
2627 mem.confidence = max_conf;
2628 }
2629
2630 let mem_id = mem.id;
2631 self.store(mem)?;
2632 stored += 1;
2633
2634 let now = std::time::SystemTime::now()
2636 .duration_since(std::time::UNIX_EPOCH)
2637 .unwrap_or_default()
2638 .as_micros() as u64;
2639 for src_id in source_ids {
2640 let edge = MemoryEdge {
2641 source: mem_id,
2642 target: *src_id,
2643 edge_type: EdgeType::Derived,
2644 weight: 0.8,
2645 created_at: now,
2646 valid_from: None,
2647 valid_until: None,
2648 label: Some("enrichment".to_string()),
2649 };
2650 if self.relate(edge).is_ok() {
2651 edges += 1;
2652 }
2653 }
2654 }
2655
2656 debug!(stored, edges, "enrichment memories stored");
2657 Ok((stored, edges))
2658 }
2659
2660 pub fn mark_enrichment_complete(&self, turn_id: u64) {
2662 *self.last_enrichment_turn.write() = turn_id;
2663 *self.enrichment_pending.write() = false;
2664 debug!(turn_id, "enrichment cycle complete");
2665 }
2666
2667 pub fn enrichment_config(&self) -> &EnrichmentConfig {
2669 &self.cognitive_config.enrichment_config
2670 }
2671
2672 pub fn all_entity_names(&self, user: UserId, agent: AgentId) -> Vec<String> {
2678 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
2679 let mut names = std::collections::HashSet::new();
2680 for pid in &page_ids {
2681 if let Ok(mem) = self.storage.load_memory(*pid) {
2682 if mem.agent_id != agent || mem.user_id != user {
2683 continue;
2684 }
2685 for tag in &mem.tags {
2686 if let Some(name) = tag.strip_prefix("entity:") {
2687 names.insert(name.to_lowercase().trim().to_string());
2688 }
2689 }
2690 }
2691 }
2692 let mut sorted: Vec<String> = names.into_iter().collect();
2693 sorted.sort();
2694 sorted
2695 }
2696
2697 pub fn unresolved_entity_names(&self, user: UserId, agent: AgentId) -> Vec<String> {
2702 let all_names = self.all_entity_names(user, agent);
2703 self.entity_resolver.read().unresolved_names(&all_names)
2704 }
2705
2706 pub fn entity_names_with_context(
2712 &self,
2713 user: UserId,
2714 agent: AgentId,
2715 ) -> Vec<(String, Option<String>)> {
2716 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
2717 let mut entity_contexts: HashMap<String, String> = HashMap::new();
2718
2719 for pid in &page_ids {
2720 if let Ok(mem) = self.storage.load_memory(*pid) {
2721 if mem.agent_id != agent || mem.user_id != user {
2722 continue;
2723 }
2724 for tag in &mem.tags {
2725 if let Some(name) = tag.strip_prefix("entity:") {
2726 let normalized = name.to_lowercase().trim().to_string();
2727 entity_contexts
2729 .entry(normalized)
2730 .and_modify(|existing| {
2731 if existing.len() < 300 {
2733 existing.push_str(" | ");
2734 let remaining = 500usize.saturating_sub(existing.len());
2735 existing.push_str(
2736 mentedb_core::text::truncate_on_char_boundary(
2737 &mem.content,
2738 remaining,
2739 ),
2740 );
2741 }
2742 })
2743 .or_insert_with(|| {
2744 mentedb_core::text::truncate_on_char_boundary(&mem.content, 300)
2745 .to_string()
2746 });
2747 break;
2748 }
2749 }
2750 }
2751 }
2752
2753 entity_contexts
2754 .into_iter()
2755 .map(|(name, ctx)| (name, Some(ctx)))
2756 .collect()
2757 }
2758
2759 pub fn apply_entity_link_resolutions(
2766 &self,
2767 merge_groups: &[EntityLinkResolution],
2768 separations: &[EntitySeparation],
2769 user: UserId,
2770 agent: AgentId,
2771 ) -> MenteResult<EntityLinkResult> {
2772 let mut result = EntityLinkResult::default();
2773 let now = std::time::SystemTime::now()
2774 .duration_since(std::time::UNIX_EPOCH)
2775 .unwrap_or_default()
2776 .as_micros() as u64;
2777
2778 let entity_memory_map = self.build_entity_memory_map(user, agent);
2782
2783 let mut resolver = self.entity_resolver.write();
2784
2785 for group in merge_groups {
2786 let mut aliases: Vec<String> = group.aliases.clone();
2788 aliases.retain(|a| a.to_lowercase() != group.canonical.to_lowercase());
2789 resolver.learn_group(&EntityMergeGroup {
2790 canonical: group.canonical.clone(),
2791 aliases,
2792 confidence: group.confidence,
2793 });
2794
2795 let mut group_memory_ids: Vec<MemoryId> = Vec::new();
2797
2798 let canonical_norm = group.canonical.to_lowercase();
2800 if let Some(ids) = entity_memory_map.get(&canonical_norm) {
2801 group_memory_ids.extend(ids);
2802 }
2803
2804 for alias in &group.aliases {
2806 let alias_norm = alias.to_lowercase();
2807 if let Some(ids) = entity_memory_map.get(&alias_norm) {
2808 group_memory_ids.extend(ids);
2809 }
2810 }
2811
2812 group_memory_ids.sort();
2813 group_memory_ids.dedup();
2814
2815 let label = format!("entity_link:{}", canonical_norm);
2817 for i in 0..group_memory_ids.len() {
2818 for j in (i + 1)..group_memory_ids.len() {
2819 let a_id = group_memory_ids[i];
2820 let b_id = group_memory_ids[j];
2821
2822 let graph = self.graph.read_graph();
2824 let already_linked = graph.outgoing(a_id).iter().any(|(tid, e)| {
2825 *tid == b_id
2826 && e.edge_type == EdgeType::Related
2827 && e.label
2828 .as_ref()
2829 .is_some_and(|l| l.starts_with("entity_link:"))
2830 });
2831 drop(graph);
2832
2833 if already_linked {
2834 continue;
2835 }
2836
2837 let edge = MemoryEdge {
2838 source: a_id,
2839 target: b_id,
2840 edge_type: EdgeType::Related,
2841 weight: group.confidence,
2842 created_at: now,
2843 valid_from: None,
2844 valid_until: None,
2845 label: Some(label.clone()),
2846 };
2847 if self.relate(edge).is_ok() {
2848 result.edges_created += 1;
2849 }
2850 result.linked += 1;
2851 }
2852 }
2853
2854 debug!(
2855 canonical = group.canonical,
2856 aliases = ?group.aliases,
2857 memories = group_memory_ids.len(),
2858 "entity resolution: merged group"
2859 );
2860 }
2861
2862 for sep in separations {
2864 resolver.mark_different(&sep.name_a, &sep.name_b);
2865 debug!(
2866 a = sep.name_a,
2867 b = sep.name_b,
2868 "entity resolution: confirmed different"
2869 );
2870 }
2871
2872 let cognitive_dir = self.path.join("cognitive");
2874 if cognitive_dir.exists() || std::fs::create_dir_all(&cognitive_dir).is_ok() {
2875 let _ = resolver.save(&cognitive_dir.join("entities.json"));
2876 }
2877
2878 debug!(
2879 linked = result.linked,
2880 edges = result.edges_created,
2881 groups = merge_groups.len(),
2882 separations = separations.len(),
2883 "entity link resolutions applied"
2884 );
2885 Ok(result)
2886 }
2887
2888 pub fn link_entities(&self) -> MenteResult<EntityLinkResult> {
2895 let mut total = EntityLinkResult::default();
2896 for (user, agent) in self.distinct_owner_pairs() {
2897 let r = self.link_entities_for(user, agent)?;
2898 total.linked += r.linked;
2899 total.edges_created += r.edges_created;
2900 }
2901 Ok(total)
2902 }
2903
2904 pub(crate) fn link_entities_for(
2907 &self,
2908 user: UserId,
2909 agent: AgentId,
2910 ) -> MenteResult<EntityLinkResult> {
2911 let entity_memory_map = self.build_entity_memory_map(user, agent);
2912 let resolver = self.entity_resolver.read();
2913
2914 let mut canonical_groups: HashMap<String, Vec<String>> = HashMap::new();
2916 for entity_name in entity_memory_map.keys() {
2917 let resolved = resolver.resolve(entity_name);
2918 if resolved.source != mentedb_cognitive::ResolutionSource::Identity {
2919 canonical_groups
2920 .entry(resolved.canonical.clone())
2921 .or_default()
2922 .push(entity_name.clone());
2923 }
2924 }
2925
2926 drop(resolver);
2927
2928 let mut result = EntityLinkResult::default();
2929 let now = std::time::SystemTime::now()
2930 .duration_since(std::time::UNIX_EPOCH)
2931 .unwrap_or_default()
2932 .as_micros() as u64;
2933
2934 for (canonical, names) in &canonical_groups {
2935 let mut group_memory_ids: Vec<MemoryId> = Vec::new();
2937 for name in names {
2938 if let Some(ids) = entity_memory_map.get(name) {
2939 group_memory_ids.extend(ids);
2940 }
2941 }
2942 if let Some(ids) = entity_memory_map.get(canonical) {
2944 group_memory_ids.extend(ids);
2945 }
2946 group_memory_ids.sort();
2947 group_memory_ids.dedup();
2948
2949 if group_memory_ids.len() < 2 {
2950 continue;
2951 }
2952
2953 let label = format!("entity_link:{}", canonical);
2954 for i in 0..group_memory_ids.len() {
2955 for j in (i + 1)..group_memory_ids.len() {
2956 let a_id = group_memory_ids[i];
2957 let b_id = group_memory_ids[j];
2958
2959 let graph = self.graph.read_graph();
2960 let already_linked = graph.outgoing(a_id).iter().any(|(tid, e)| {
2961 *tid == b_id
2962 && e.edge_type == EdgeType::Related
2963 && e.label
2964 .as_ref()
2965 .is_some_and(|l| l.starts_with("entity_link:"))
2966 });
2967 drop(graph);
2968
2969 if already_linked {
2970 continue;
2971 }
2972
2973 let edge = MemoryEdge {
2974 source: a_id,
2975 target: b_id,
2976 edge_type: EdgeType::Related,
2977 weight: 1.0,
2978 created_at: now,
2979 valid_from: None,
2980 valid_until: None,
2981 label: Some(label.clone()),
2982 };
2983 if self.relate(edge).is_ok() {
2984 result.edges_created += 1;
2985 }
2986 result.linked += 1;
2987 }
2988 }
2989 }
2990
2991 debug!(
2992 linked = result.linked,
2993 edges = result.edges_created,
2994 groups = canonical_groups.len(),
2995 "sync entity linking complete"
2996 );
2997 Ok(result)
2998 }
2999
3000 fn build_entity_memory_map(
3004 &self,
3005 user: UserId,
3006 agent: AgentId,
3007 ) -> HashMap<String, Vec<MemoryId>> {
3008 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3009 let mut map: HashMap<String, Vec<MemoryId>> = HashMap::new();
3010 for pid in &page_ids {
3011 if let Ok(mem) = self.storage.load_memory(*pid) {
3012 if mem.agent_id != agent || mem.user_id != user {
3013 continue;
3014 }
3015 for tag in &mem.tags {
3016 if let Some(name) = tag.strip_prefix("entity:") {
3017 let normalized = name.to_lowercase().trim().to_string();
3018 map.entry(normalized).or_default().push(mem.id);
3020 break;
3021 }
3022 }
3023 }
3024 }
3025 map
3026 }
3027
3028 pub fn entity_memories(&self) -> Vec<MemoryNode> {
3030 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3031 page_ids
3032 .iter()
3033 .filter_map(|pid| self.storage.load_memory(*pid).ok())
3034 .filter(|m| m.tags.iter().any(|t| t.starts_with("entity:")))
3035 .collect()
3036 }
3037
3038 pub fn entity_communities(
3043 &self,
3044 user: UserId,
3045 agent: AgentId,
3046 ) -> HashMap<String, Vec<(String, String)>> {
3047 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3048 let mut categories: HashMap<String, Vec<(String, String)>> = HashMap::new();
3049
3050 for pid in &page_ids {
3051 if let Ok(mem) = self.storage.load_memory(*pid) {
3052 if mem.agent_id != agent || mem.user_id != user {
3055 continue;
3056 }
3057 if mem.tags.iter().any(|t| t == "community_summary") {
3059 continue;
3060 }
3061
3062 let entity_name = mem
3063 .tags
3064 .iter()
3065 .find_map(|t| t.strip_prefix("entity:"))
3066 .map(|n| n.to_string());
3067
3068 if let Some(name) = entity_name {
3069 let entity_type = mem
3070 .tags
3071 .iter()
3072 .find_map(|t| t.strip_prefix("entity_type:"))
3073 .unwrap_or("general")
3074 .to_lowercase();
3075
3076 let context: String = mem.content.chars().take(200).collect();
3077 categories
3078 .entry(entity_type)
3079 .or_default()
3080 .push((name, context));
3081 }
3082 }
3083 }
3084
3085 categories.retain(|_, members| members.len() >= 2);
3087 categories
3088 }
3089
3090 pub fn store_community_summary(
3095 &self,
3096 category: &str,
3097 summary: &str,
3098 member_names: &[String],
3099 user: UserId,
3100 agent: AgentId,
3101 ) -> MenteResult<MemoryId> {
3102 if category.is_empty() {
3103 return Err(MenteError::Storage(
3104 "community category cannot be empty".into(),
3105 ));
3106 }
3107
3108 let now = std::time::SystemTime::now()
3109 .duration_since(std::time::UNIX_EPOCH)
3110 .unwrap_or_default()
3111 .as_micros() as u64;
3112
3113 let community_tag = format!("community:{}", category);
3116 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3117 let mut existing_id = None;
3118 for pid in &page_ids {
3119 if let Ok(mem) = self.storage.load_memory(*pid)
3120 && mem.agent_id == agent
3121 && mem.user_id == user
3122 && mem.tags.iter().any(|t| t == &community_tag)
3123 {
3124 let mut updated = mem.clone();
3126 updated.content = summary.to_string();
3127 if let Some(ref embedder) = self.embedder {
3128 updated.embedding = embedder
3129 .embed(summary)
3130 .unwrap_or_else(|_| updated.embedding.clone());
3131 }
3132 self.storage.update_memory(*pid, &updated)?;
3133 existing_id = Some(updated.id);
3134 break;
3135 }
3136 }
3137
3138 let node_id = if let Some(id) = existing_id {
3139 id
3140 } else {
3141 let embedding = self
3143 .embedder
3144 .as_ref()
3145 .and_then(|e| e.embed(summary).ok())
3146 .unwrap_or_default();
3147
3148 let mut node =
3149 MemoryNode::new(agent, MemoryType::Semantic, summary.to_string(), embedding)
3150 .with_user_id(user);
3151 node.tags = vec![
3152 "community_summary".to_string(),
3153 community_tag,
3154 "source:enrichment".to_string(),
3155 ];
3156 node.confidence = 0.7;
3157 let id = node.id;
3158 self.store(node)?;
3159 id
3160 };
3161
3162 let entity_map = self.build_entity_memory_map(user, agent);
3165 for name in member_names {
3166 let normalized = name.to_lowercase();
3167 if let Some(member_ids) = entity_map.get(&normalized) {
3168 for member_id in member_ids {
3169 self.relate(MemoryEdge {
3170 source: node_id,
3171 target: *member_id,
3172 edge_type: EdgeType::Derived,
3173 weight: 0.8,
3174 created_at: now,
3175 valid_from: None,
3176 valid_until: None,
3177 label: Some(format!("community_member:{}", category)),
3178 })?;
3179 }
3180 }
3181 }
3182
3183 Ok(node_id)
3184 }
3185
3186 pub fn community_summaries(&self, user: UserId, agent: AgentId) -> Vec<MemoryNode> {
3189 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3190 page_ids
3191 .iter()
3192 .filter_map(|pid| self.storage.load_memory(*pid).ok())
3193 .filter(|m| {
3194 m.agent_id == agent
3195 && m.user_id == user
3196 && m.tags.iter().any(|t| t == "community_summary")
3197 })
3198 .collect()
3199 }
3200
3201 pub fn profile_facts(&self, user: UserId, agent: AgentId) -> Vec<String> {
3205 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3206 let mut facts = Vec::new();
3207
3208 for pid in &page_ids {
3209 if let Ok(mem) = self.storage.load_memory(*pid) {
3210 if mem.agent_id != agent || mem.user_id != user {
3214 continue;
3215 }
3216 if mem.confidence < 0.5 {
3218 continue;
3219 }
3220 match mem.memory_type {
3221 MemoryType::Semantic | MemoryType::Procedural => {
3222 if mem
3224 .tags
3225 .iter()
3226 .any(|t| t == "community_summary" || t.starts_with("entity:"))
3227 {
3228 continue;
3229 }
3230 facts.push(mem.content.chars().take(300).collect());
3231 }
3232 _ => {}
3233 }
3234 }
3235 }
3236
3237 facts.truncate(100);
3239 facts
3240 }
3241
3242 pub fn store_user_profile(
3249 &self,
3250 profile: &str,
3251 user: UserId,
3252 agent: AgentId,
3253 ) -> MenteResult<MemoryId> {
3254 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3256 for pid in &page_ids {
3257 if let Ok(mem) = self.storage.load_memory(*pid)
3258 && mem.agent_id == agent
3259 && mem.user_id == user
3260 && mem.tags.iter().any(|t| t == "user_profile")
3261 {
3262 let mut updated = mem.clone();
3267 updated.content = profile.to_string();
3268 updated.created_at = std::time::SystemTime::now()
3269 .duration_since(std::time::UNIX_EPOCH)
3270 .unwrap_or_default()
3271 .as_micros() as u64;
3272 if let Some(ref embedder) = self.embedder {
3273 updated.embedding = embedder
3274 .embed(profile)
3275 .unwrap_or_else(|_| updated.embedding.clone());
3276 }
3277 self.storage.update_memory(*pid, &updated)?;
3278 return Ok(updated.id);
3279 }
3280 }
3281
3282 let embedding = self
3284 .embedder
3285 .as_ref()
3286 .and_then(|e| e.embed(profile).ok())
3287 .unwrap_or_default();
3288
3289 let mut node = MemoryNode::new(agent, MemoryType::Semantic, profile.to_string(), embedding)
3290 .with_user_id(user);
3291 node.tags = vec![
3292 "user_profile".to_string(),
3293 "scope:always".to_string(),
3294 "source:enrichment".to_string(),
3295 ];
3296 node.confidence = 0.8;
3297 let node_id = node.id;
3298 self.store(node)?;
3299
3300 Ok(node_id)
3301 }
3302
3303 pub fn user_profile(&self) -> Option<MemoryNode> {
3312 self.user_profile_for(UserId::nil(), AgentId::nil())
3313 }
3314
3315 pub fn user_profile_for(&self, user: UserId, agent: AgentId) -> Option<MemoryNode> {
3319 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3320 for pid in &page_ids {
3321 if let Ok(mem) = self.storage.load_memory(*pid)
3322 && mem.user_id == user
3323 && mem.agent_id == agent
3324 && mem.tags.iter().any(|t| t == "user_profile")
3325 {
3326 return Some(mem);
3327 }
3328 }
3329 None
3330 }
3331
3332 pub fn profile_owners(&self) -> Vec<(UserId, AgentId)> {
3336 let page_ids: Vec<PageId> = self.page_map.read().values().copied().collect();
3337 let mut owners: Vec<(UserId, AgentId, u64)> = Vec::new();
3338 for pid in &page_ids {
3339 if let Ok(mem) = self.storage.load_memory(*pid)
3340 && mem.tags.iter().any(|t| t == "user_profile")
3341 {
3342 owners.push((mem.user_id, mem.agent_id, mem.created_at));
3343 }
3344 }
3345 owners.sort_by_key(|(_, _, ts)| std::cmp::Reverse(*ts));
3346 owners.into_iter().map(|(u, a, _)| (u, a)).collect()
3347 }
3348}
3349
3350#[cfg(test)]
3351mod mql_execution_tests {
3352 use super::*;
3353
3354 #[test]
3358 fn recall_bare_and_by_type_return_matches() {
3359 let dir = tempfile::tempdir().unwrap();
3360 let db = MenteDb::open(dir.path()).unwrap();
3361 db.store(MemoryNode::new(
3362 AgentId::nil(),
3363 MemoryType::Semantic,
3364 "the sky is blue".into(),
3365 vec![0.1_f32; 8],
3366 ))
3367 .unwrap();
3368 db.store(MemoryNode::new(
3369 AgentId::nil(),
3370 MemoryType::Semantic,
3371 "water is wet".into(),
3372 vec![0.2_f32; 8],
3373 ))
3374 .unwrap();
3375 db.store(MemoryNode::new(
3376 AgentId::nil(),
3377 MemoryType::Procedural,
3378 "how to tie a knot".into(),
3379 vec![0.3_f32; 8],
3380 ))
3381 .unwrap();
3382
3383 let all = db.recall("RECALL memories LIMIT 50").unwrap();
3385 let all_n: usize = all.blocks.iter().map(|b| b.memories.len()).sum();
3386 assert_eq!(all_n, 3, "bare RECALL should return all three memories");
3387
3388 let sem = db
3390 .recall("RECALL memories WHERE type = semantic LIMIT 50")
3391 .unwrap();
3392 let sem_mems: Vec<&ScoredMemory> = sem.blocks.iter().flat_map(|b| &b.memories).collect();
3393 assert_eq!(
3394 sem_mems.len(),
3395 2,
3396 "type = semantic should return two memories"
3397 );
3398 assert!(
3399 sem_mems
3400 .iter()
3401 .all(|sm| sm.memory.memory_type == MemoryType::Semantic),
3402 "only semantic memories should come back"
3403 );
3404 }
3405
3406 fn boolean_fixture() -> (tempfile::TempDir, MenteDb) {
3409 let dir = tempfile::tempdir().unwrap();
3410 let db = MenteDb::open(dir.path()).unwrap();
3411 let mut sem_keep = MemoryNode::new(
3412 AgentId::nil(),
3413 MemoryType::Semantic,
3414 "semantic kept".into(),
3415 vec![0.1_f32; 8],
3416 );
3417 sem_keep.tags = vec!["keep".into()];
3418 let sem_plain = MemoryNode::new(
3419 AgentId::nil(),
3420 MemoryType::Semantic,
3421 "semantic plain".into(),
3422 vec![0.2_f32; 8],
3423 );
3424 let mut proc_keep = MemoryNode::new(
3425 AgentId::nil(),
3426 MemoryType::Procedural,
3427 "procedural kept".into(),
3428 vec![0.3_f32; 8],
3429 );
3430 proc_keep.tags = vec!["keep".into()];
3431 let anti = MemoryNode::new(
3432 AgentId::nil(),
3433 MemoryType::AntiPattern,
3434 "antipattern plain".into(),
3435 vec![0.4_f32; 8],
3436 );
3437 for n in [sem_keep, sem_plain, proc_keep, anti] {
3438 db.store(n).unwrap();
3439 }
3440 (dir, db)
3441 }
3442
3443 fn contents(db: &MenteDb, mql: &str) -> Vec<String> {
3444 let mut v: Vec<String> = db
3445 .query(mql)
3446 .unwrap()
3447 .into_iter()
3448 .map(|s| s.memory.content)
3449 .collect();
3450 v.sort();
3451 v
3452 }
3453
3454 #[test]
3457 fn or_unions_branches() {
3458 let (_d, db) = boolean_fixture();
3459 let got = contents(
3460 &db,
3461 "RECALL WHERE type = semantic OR type = procedural LIMIT 50",
3462 );
3463 assert_eq!(
3464 got,
3465 vec![
3466 "procedural kept".to_string(),
3467 "semantic kept".to_string(),
3468 "semantic plain".to_string()
3469 ],
3470 "OR should return semantic and procedural, excluding antipattern"
3471 );
3472 }
3473
3474 #[test]
3477 fn not_excludes() {
3478 let (_d, db) = boolean_fixture();
3479 let got = contents(&db, "RECALL WHERE NOT type = semantic LIMIT 50");
3480 assert_eq!(
3481 got,
3482 vec![
3483 "antipattern plain".to_string(),
3484 "procedural kept".to_string()
3485 ],
3486 "NOT type = semantic should drop both semantic rows"
3487 );
3488 }
3489
3490 #[test]
3493 fn and_not_composes() {
3494 let (_d, db) = boolean_fixture();
3495 let got = contents(
3496 &db,
3497 r#"RECALL WHERE type = semantic AND NOT tag = "keep" LIMIT 50"#,
3498 );
3499 assert_eq!(
3500 got,
3501 vec!["semantic plain".to_string()],
3502 "only the untagged semantic row should remain"
3503 );
3504 }
3505
3506 #[test]
3510 fn grouping_binds_before_and() {
3511 let (_d, db) = boolean_fixture();
3512 let got = contents(
3513 &db,
3514 r#"RECALL WHERE (type = semantic OR type = procedural) AND tag = "keep" LIMIT 50"#,
3515 );
3516 assert_eq!(
3517 got,
3518 vec!["procedural kept".to_string(), "semantic kept".to_string()],
3519 "grouping must apply the tag filter to both OR branches"
3520 );
3521 }
3522
3523 #[test]
3526 fn pure_and_still_intersects() {
3527 let (_d, db) = boolean_fixture();
3528 let got = contents(
3529 &db,
3530 r#"RECALL WHERE type = semantic AND tag = "keep" LIMIT 50"#,
3531 );
3532 assert_eq!(got, vec!["semantic kept".to_string()]);
3533 }
3534}
3535
3536#[cfg(test)]
3537mod entity_boost_tests {
3538 use super::*;
3539
3540 #[test]
3545 fn boost_lifts_entity_linked_memory() {
3546 let dir = tempfile::tempdir().unwrap();
3547 let db = MenteDb::open(dir.path()).unwrap();
3548
3549 let kafka_mem = MemoryNode::new(
3550 AgentId::nil(),
3551 MemoryType::Episodic,
3552 "we chose kafka for the pipeline".into(),
3553 vec![0.1_f32; 8],
3554 );
3555 let kafka_id = kafka_mem.id;
3556 let other_mem = MemoryNode::new(
3557 AgentId::nil(),
3558 MemoryType::Episodic,
3559 "the sky is blue".into(),
3560 vec![0.2_f32; 8],
3561 );
3562 let other_id = other_mem.id;
3563 let mut entity = MemoryNode::new(
3564 AgentId::nil(),
3565 MemoryType::Semantic,
3566 "Kafka, a message broker".into(),
3567 vec![0.3_f32; 8],
3568 );
3569 entity.tags.push("entity:kafka".into());
3570 let entity_id = entity.id;
3571
3572 db.store(kafka_mem.clone()).unwrap();
3573 db.store(other_mem.clone()).unwrap();
3574 db.store(entity).unwrap();
3575 db.relate(MemoryEdge {
3578 source: entity_id,
3579 target: kafka_id,
3580 edge_type: EdgeType::Derived,
3581 weight: 1.0,
3582 created_at: 0,
3583 valid_from: None,
3584 valid_until: None,
3585 label: Some("enrichment".into()),
3586 })
3587 .unwrap();
3588
3589 let mut scored = vec![
3591 ScoredMemory {
3592 memory: other_mem,
3593 score: 0.50,
3594 },
3595 ScoredMemory {
3596 memory: kafka_mem,
3597 score: 0.40,
3598 },
3599 ];
3600 db.apply_entity_boost(&mut scored, "did we pick kafka");
3601
3602 assert_eq!(
3603 scored[0].memory.id, kafka_id,
3604 "the kafka-linked memory should rank first after the entity boost"
3605 );
3606 let boosted = scored.iter().find(|s| s.memory.id == kafka_id).unwrap();
3607 assert!(
3608 (boosted.score - 0.55).abs() < 1e-6,
3609 "the linked memory should gain exactly the boost weight"
3610 );
3611 assert!(
3612 scored.iter().any(|s| s.memory.id == other_id),
3613 "the unrelated memory is kept, just reordered"
3614 );
3615 }
3616
3617 #[test]
3619 fn no_entity_in_query_is_a_noop() {
3620 let dir = tempfile::tempdir().unwrap();
3621 let db = MenteDb::open(dir.path()).unwrap();
3622 let a = MemoryNode::new(
3623 AgentId::nil(),
3624 MemoryType::Episodic,
3625 "alpha".into(),
3626 vec![0.1_f32; 8],
3627 );
3628 let b = MemoryNode::new(
3629 AgentId::nil(),
3630 MemoryType::Episodic,
3631 "beta".into(),
3632 vec![0.2_f32; 8],
3633 );
3634 let (aid, bid) = (a.id, b.id);
3635 let mut scored = vec![
3636 ScoredMemory {
3637 memory: a,
3638 score: 0.50,
3639 },
3640 ScoredMemory {
3641 memory: b,
3642 score: 0.40,
3643 },
3644 ];
3645 db.apply_entity_boost(&mut scored, "nothing here resolves to an entity");
3646 assert_eq!(scored[0].memory.id, aid);
3647 assert_eq!(scored[1].memory.id, bid);
3648 }
3649}
3650
3651#[cfg(test)]
3652mod distinct_owner_tests {
3653 use super::*;
3654
3655 #[test]
3660 fn distinct_owner_pairs_returns_all_owner_agents() {
3661 let dir = tempfile::tempdir().unwrap();
3662 let db = MenteDb::open(dir.path()).unwrap();
3663 let alice = AgentId::new();
3664 let bob = AgentId::new();
3665
3666 db.store(MemoryNode::new(
3667 alice,
3668 MemoryType::Semantic,
3669 "alice likes falcons".into(),
3670 vec![0.1_f32; 8],
3671 ))
3672 .unwrap();
3673 db.store(MemoryNode::new(
3674 bob,
3675 MemoryType::Semantic,
3676 "bob likes turtles".into(),
3677 vec![0.2_f32; 8],
3678 ))
3679 .unwrap();
3680
3681 let pairs = db.distinct_owner_pairs();
3682 let agents: Vec<AgentId> = pairs.iter().map(|(_, a)| *a).collect();
3683 assert!(
3684 agents.contains(&alice),
3685 "distinct owners must include alice"
3686 );
3687 assert!(agents.contains(&bob), "distinct owners must include bob");
3688 }
3689
3690 #[test]
3694 fn distinct_owner_pairs_splits_by_user_under_same_agent() {
3695 let dir = tempfile::tempdir().unwrap();
3696 let db = MenteDb::open(dir.path()).unwrap();
3697 let agent = AgentId::new();
3698 let ua = UserId::new();
3699 let ub = UserId::new();
3700
3701 db.store(
3702 MemoryNode::new(
3703 agent,
3704 MemoryType::Semantic,
3705 "user a fact".into(),
3706 vec![0.1_f32; 8],
3707 )
3708 .with_user_id(ua),
3709 )
3710 .unwrap();
3711 db.store(
3712 MemoryNode::new(
3713 agent,
3714 MemoryType::Semantic,
3715 "user b fact".into(),
3716 vec![0.2_f32; 8],
3717 )
3718 .with_user_id(ub),
3719 )
3720 .unwrap();
3721
3722 let pairs = db.distinct_owner_pairs();
3723 assert!(
3724 pairs.contains(&(ua, agent)),
3725 "must include (user a, agent) pair"
3726 );
3727 assert!(
3728 pairs.contains(&(ub, agent)),
3729 "must include (user b, agent) pair"
3730 );
3731 assert_ne!(
3732 (ua, agent),
3733 (ub, agent),
3734 "same agent, different users are distinct owner pairs"
3735 );
3736 }
3737
3738 #[test]
3745 fn profile_is_owner_scoped_and_enumerable() {
3746 let dir = tempfile::tempdir().unwrap();
3747 let db = MenteDb::open(dir.path()).unwrap();
3748 let agent = AgentId::new();
3749 let alice = UserId::new();
3750 let bob = UserId::new();
3751
3752 db.store_user_profile("alice's profile", alice, agent)
3753 .unwrap();
3754 db.store_user_profile("bob's profile", bob, agent).unwrap();
3755 db.store_user_profile("the shared profile", UserId::nil(), AgentId::nil())
3756 .unwrap();
3757
3758 assert_eq!(
3760 db.user_profile_for(alice, agent).unwrap().content,
3761 "alice's profile"
3762 );
3763 assert_eq!(
3764 db.user_profile_for(bob, agent).unwrap().content,
3765 "bob's profile"
3766 );
3767
3768 assert_eq!(db.user_profile().unwrap().content, "the shared profile");
3771
3772 assert!(db.user_profile_for(UserId::new(), agent).is_none());
3774
3775 let owners = db.profile_owners();
3777 assert_eq!(owners.len(), 3);
3778 assert!(owners.contains(&(alice, agent)));
3779 assert!(owners.contains(&(bob, agent)));
3780 assert!(owners.contains(&(UserId::nil(), AgentId::nil())));
3781 }
3782}