1use lmdb::{Cursor, Database, DatabaseFlags, Environment, Transaction, WriteFlags};
7use std::collections::HashMap;
8use std::path::Path;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::{Arc, RwLock};
11use wm_core::{CoreError, Galaxy, Result};
12
13#[cfg(unix)]
14use std::os::unix::fs::PermissionsExt;
15
16use crate::episodic::EpisodicStore;
17use crate::indexes::IndexDbs;
18use crate::memory::{Memory, MemoryId, decode_embedding, encode_embedding};
19use crate::semantic::SemanticEncoder;
20
21#[derive(Debug, Clone, Default)]
23pub struct MemoryQuery {
24 pub tags: Vec<String>,
26 pub min_importance: Option<f32>,
28 pub max_importance: Option<f32>,
30 pub created_after: Option<chrono::DateTime<chrono::Utc>>,
32 pub created_before: Option<chrono::DateTime<chrono::Utc>>,
34 pub content_substring: Option<String>,
37 pub limit: usize,
39}
40
41impl MemoryQuery {
42 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 limit: 100,
47 ..Default::default()
48 }
49 }
50
51 #[must_use]
53 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
54 self.tags = tags;
55 self
56 }
57
58 #[must_use]
60 pub const fn with_importance_range(mut self, min: f32, max: f32) -> Self {
61 self.min_importance = Some(min);
62 self.max_importance = Some(max);
63 self
64 }
65
66 #[must_use]
68 pub const fn with_time_range(
69 mut self,
70 after: chrono::DateTime<chrono::Utc>,
71 before: chrono::DateTime<chrono::Utc>,
72 ) -> Self {
73 self.created_after = Some(after);
74 self.created_before = Some(before);
75 self
76 }
77
78 #[must_use]
81 pub const fn with_created_after(mut self, after: chrono::DateTime<chrono::Utc>) -> Self {
82 self.created_after = Some(after);
83 self
84 }
85
86 #[must_use]
89 pub const fn with_created_before(mut self, before: chrono::DateTime<chrono::Utc>) -> Self {
90 self.created_before = Some(before);
91 self
92 }
93
94 #[must_use]
96 pub const fn with_limit(mut self, limit: usize) -> Self {
97 self.limit = limit;
98 self
99 }
100
101 #[must_use]
103 pub fn with_content_substring(mut self, substring: impl Into<String>) -> Self {
104 self.content_substring = Some(substring.into().to_lowercase());
105 self
106 }
107
108 #[must_use]
110 pub fn matches(&self, mem: &Memory) -> bool {
111 if !self.tags.is_empty() {
113 for tag in &self.tags {
114 if !mem.metadata.tags.iter().any(|t| t == tag) {
115 return false;
116 }
117 }
118 }
119
120 if let Some(min) = self.min_importance {
122 if mem.metadata.importance < min {
123 return false;
124 }
125 }
126 if let Some(max) = self.max_importance {
127 if mem.metadata.importance > max {
128 return false;
129 }
130 }
131
132 if let Some(after) = self.created_after {
134 if mem.metadata.created_at < after {
135 return false;
136 }
137 }
138 if let Some(before) = self.created_before {
139 if mem.metadata.created_at > before {
140 return false;
141 }
142 }
143
144 if let Some(sub) = &self.content_substring {
146 if !mem.content.to_lowercase().contains(sub) {
147 return false;
148 }
149 }
150
151 true
152 }
153}
154
155pub struct MemoryStore {
157 path: std::path::PathBuf,
159 env: Environment,
161 index_dbs: IndexDbs,
163 semantic_encoder: SemanticEncoder,
165 max_entries_per_galaxy: Option<usize>,
167 mutation_count: AtomicU64,
171 episodic_db: Database,
173 episodic_terms_v2_db: Database,
175 embedding_cache_db: Database,
180 revisions_db: Database,
184 attestations_db: Database,
188 episodic_term_cache: std::sync::Arc<RwLock<HashMap<String, Vec<uuid::Uuid>>>>,
190 episodic_embedder:
192 std::sync::OnceLock<Option<Arc<dyn crate::embedder::Embedder + Send + Sync>>>,
193 episodic_sidecar_ensured: std::sync::OnceLock<()>,
195 episodic_aliases: std::sync::OnceLock<Option<crate::episodic_keys::AdaptiveAliases>>,
197 episodic_enrichment: std::sync::OnceLock<Option<crate::enrichment::VocabularyEnrichment>>,
199}
200
201impl MemoryStore {
202 pub fn open(path: impl AsRef<Path>, map_size: usize) -> Result<Self> {
208 let path = path.as_ref().to_path_buf();
209
210 std::fs::create_dir_all(&path)
212 .map_err(|e| CoreError::Memory(format!("Cannot create store dir: {e}")))?;
213 #[cfg(unix)]
214 {
215 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))
216 .map_err(|e| CoreError::Memory(format!("Cannot set store dir permissions: {e}")))?;
217 }
218
219 let env = Environment::new()
220 .set_map_size(map_size)
221 .set_max_dbs(32)
222 .open(&path)
223 .map_err(|e| CoreError::Memory(format!("LMDB open failed: {e}")))?;
224
225 for galaxy in Galaxy::all() {
227 let db = env
228 .create_db(Some(galaxy.db_name()), DatabaseFlags::default())
229 .map_err(|e| {
230 CoreError::Memory(format!(
231 "LMDB create_db failed for {}: {e}",
232 galaxy.db_name()
233 ))
234 })?;
235 let _ = db;
236 }
237
238 for (name, flags) in crate::indexes::INDEX_DBS {
240 let db = env
241 .create_db(Some(name), *flags)
242 .map_err(|e| CoreError::Memory(format!("LMDB create_db failed for {name}: {e}")))?;
243 let _ = db;
244 }
245
246 let index_dbs = IndexDbs::open(&env)?;
247 let episodic_db = env
248 .create_db(Some("episodic_records"), DatabaseFlags::default())
249 .map_err(|e| {
250 CoreError::Memory(format!("LMDB create_db failed for episodic_records: {e}"))
251 })?;
252 let episodic_terms_v2_db = env
259 .create_db(Some("episodic_terms_v2"), DatabaseFlags::DUP_SORT)
260 .map_err(|e| {
261 CoreError::Memory(format!("LMDB create_db failed for episodic_terms_v2: {e}"))
262 })?;
263 let embedding_cache_db = env
264 .create_db(Some("embedding_cache"), DatabaseFlags::default())
265 .map_err(|e| {
266 CoreError::Memory(format!("LMDB create_db failed for embedding_cache: {e}"))
267 })?;
268 let revisions_db = env
269 .create_db(Some("revisions"), DatabaseFlags::default())
270 .map_err(|e| CoreError::Memory(format!("LMDB create_db failed for revisions: {e}")))?;
271 let attestations_db = env
272 .create_db(
273 Some(crate::attestation::ATTESTATIONS_DB),
274 DatabaseFlags::default(),
275 )
276 .map_err(|e| {
277 CoreError::Memory(format!("LMDB create_db failed for attestations: {e}"))
278 })?;
279 Ok(Self {
280 path,
281 env,
282 index_dbs,
283 semantic_encoder: SemanticEncoder::new(),
284 max_entries_per_galaxy: None,
285 mutation_count: AtomicU64::new(0),
286 episodic_db,
287 episodic_terms_v2_db,
288 embedding_cache_db,
289 revisions_db,
290 attestations_db,
291 episodic_term_cache: std::sync::Arc::new(RwLock::new(HashMap::new())),
292 episodic_embedder: std::sync::OnceLock::new(),
293 episodic_sidecar_ensured: std::sync::OnceLock::new(),
294 episodic_aliases: std::sync::OnceLock::new(),
295 episodic_enrichment: std::sync::OnceLock::new(),
296 })
297 }
298
299 pub fn open_default(path: impl AsRef<Path>) -> Result<Self> {
308 let platform_default = if cfg!(windows) {
313 256 * 1024 * 1024
314 } else {
315 4 * 1024 * 1024 * 1024
316 };
317 let size = std::env::var("WM_DEFAULT_MAP_SIZE")
318 .ok()
319 .and_then(|v| v.parse::<usize>().ok())
320 .filter(|&v| v > 0)
321 .unwrap_or(platform_default);
322 Self::open(path, size)
323 }
324
325 #[must_use]
330 pub const fn with_entry_limit(mut self, limit: usize) -> Self {
331 self.max_entries_per_galaxy = Some(limit);
332 self
333 }
334
335 pub fn path(&self) -> &Path {
337 &self.path
338 }
339
340 pub const fn env(&self) -> &Environment {
342 &self.env
343 }
344
345 pub fn mutation_count(&self) -> u64 {
349 self.mutation_count.load(Ordering::Relaxed)
350 }
351
352 pub const fn index_dbs(&self) -> &IndexDbs {
354 &self.index_dbs
355 }
356
357 pub const fn semantic_encoder(&self) -> &SemanticEncoder {
359 &self.semantic_encoder
360 }
361
362 fn ensure_episodic_sidecar(&self) {
367 if self.episodic_sidecar_ensured.get().is_some() {
368 return;
369 }
370 let _ = self.episodic_sidecar_ensured.set(());
371 let view = EpisodicStore::new(
372 &self.env,
373 self.episodic_db,
374 self.episodic_terms_v2_db,
375 self.episodic_term_cache.clone(),
376 &self.mutation_count,
377 );
378 let needs_rebuild = matches!(
379 (view.sidecar_is_empty(), view.record_count()),
380 (Ok(true), Ok(n)) if n > 0
381 );
382 if needs_rebuild {
383 match view.rebuild_sidecar() {
384 Ok(n) => tracing::info!("episodic sidecar rebuilt from {n} records"),
385 Err(e) => {
386 tracing::warn!("episodic sidecar rebuild failed: {e}");
387 }
388 }
389 }
390 }
391
392 #[must_use]
394 pub fn episodic(&self) -> EpisodicStore<'_> {
395 self.ensure_episodic_sidecar();
396 let mut store = EpisodicStore::new(
397 &self.env,
398 self.episodic_db,
399 self.episodic_terms_v2_db,
400 self.episodic_term_cache.clone(),
401 &self.mutation_count,
402 );
403 if let Some(Some(embedder)) = self.episodic_embedder.get() {
404 store = store.with_embedder(embedder.clone());
405 }
406 if let Some(Some(aliases)) = self.episodic_aliases.get() {
407 store = store.with_adaptive_aliases(aliases.clone());
408 }
409 if let Some(Some(enrichment)) = self.episodic_enrichment.get() {
410 store = store.with_enrichment(enrichment.clone());
411 }
412 store
413 }
414
415 pub fn set_episodic_embedder(
417 &self,
418 embedder: Arc<dyn crate::embedder::Embedder + Send + Sync>,
419 ) {
420 let _ = self.episodic_embedder.set(Some(embedder));
421 }
422
423 pub fn set_episodic_aliases(&self, aliases: crate::episodic_keys::AdaptiveAliases) {
425 let _ = self.episodic_aliases.set(Some(aliases));
426 }
427
428 pub fn set_episodic_enrichment(&self, enrichment: crate::enrichment::VocabularyEnrichment) {
430 let _ = self.episodic_enrichment.set(Some(enrichment));
431 }
432
433 pub fn galaxy_db(&self, galaxy: Galaxy) -> Result<Database> {
435 self.env.open_db(Some(galaxy.db_name())).map_err(|e| {
436 CoreError::Memory(format!("LMDB open_db failed for {}: {e}", galaxy.db_name()))
437 })
438 }
439
440 pub fn put(&self, galaxy: Galaxy, memory: &Memory) -> Result<()> {
448 if let Some(limit) = self.max_entries_per_galaxy {
450 let current = self.count(galaxy)?;
451 if current >= limit {
452 return Err(CoreError::Memory(format!(
453 "galaxy {} entry limit reached ({current}/{limit}), write rejected",
454 galaxy.db_name()
455 )));
456 }
457 }
458
459 let db = self.galaxy_db(galaxy)?;
460 let key = memory.metadata.id.as_bytes();
461 let val = rmp_serde::to_vec_named(memory)
462 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
463
464 let mut tx = self
465 .env
466 .begin_rw_txn()
467 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
468
469 let existing = tx
474 .get(db, key)
475 .ok()
476 .and_then(|bytes| crate::codec::decode(bytes).ok());
477
478 match tx.put(db, key, &val, lmdb::WriteFlags::default()) {
479 Ok(()) => {}
480 Err(lmdb::Error::MapFull) => {
481 tx.abort();
482 return Err(CoreError::Memory(format!(
483 "LMDB map full: galaxy {}, consider growing map size or pruning old memories",
484 galaxy.db_name()
485 )));
486 }
487 Err(e) => {
488 tx.abort();
489 return Err(CoreError::Memory(format!("LMDB put failed: {e}")));
490 }
491 }
492 if let Some(existing) = existing {
493 self.index_dbs.remove(&mut tx, galaxy, &existing)?;
494 }
495 self.index_dbs.add(&mut tx, galaxy, memory)?;
496 tx.commit()
497 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
498 self.mutation_count.fetch_add(1, Ordering::Relaxed);
499 Ok(())
500 }
501
502 pub fn get(&self, galaxy: Galaxy, id: uuid::Uuid) -> Result<Option<Memory>> {
504 let db = self.galaxy_db(galaxy)?;
505 let key = id.as_bytes();
506
507 let tx = self
508 .env
509 .begin_ro_txn()
510 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
511 let result = tx.get(db, key);
512 match result {
513 Ok(bytes) => {
514 let memory: Memory = crate::codec::decode(bytes)
515 .map_err(|e| CoreError::Memory(format!("deserialize failed: {e}")))?;
516 tx.commit()
517 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
518 Ok(Some(memory))
519 }
520 Err(lmdb::Error::NotFound) => {
521 tx.commit()
523 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
524 Ok(None)
525 }
526 Err(e) => Err(CoreError::Memory(format!("LMDB get failed: {e}"))),
527 }
528 }
529
530 pub fn find_across_galaxies(&self, id: uuid::Uuid) -> Result<Option<(Galaxy, Memory)>> {
536 for galaxy in Galaxy::memory_galaxies() {
537 if let Some(mem) = self.get(galaxy, id)? {
538 return Ok(Some((galaxy, mem)));
539 }
540 }
541 Ok(None)
542 }
543
544 pub fn delete(&self, galaxy: Galaxy, id: uuid::Uuid) -> Result<bool> {
547 let db = self.galaxy_db(galaxy)?;
548 let key = id.as_bytes();
549
550 let mut tx = self
551 .env
552 .begin_rw_txn()
553 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
554
555 let exists = tx.get(db, key).is_ok();
557 if exists {
558 if let Ok(bytes) = tx.get(db, key) {
560 if let Ok(memory) = crate::codec::decode(bytes) {
561 let _ = self.index_dbs.remove(&mut tx, galaxy, &memory);
562 }
563 }
564 tx.del(db, key, None)
565 .map_err(|e| CoreError::Memory(format!("LMDB del failed: {e}")))?;
566 }
567 tx.commit()
568 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
569 if exists {
570 self.mutation_count.fetch_add(1, Ordering::Relaxed);
571 }
572 Ok(exists)
573 }
574
575 pub fn scan(&self, galaxy: Galaxy, limit: usize) -> Result<Vec<Memory>> {
577 let db = self.galaxy_db(galaxy)?;
578 let tx = self
579 .env
580 .begin_ro_txn()
581 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
582
583 let mut cursor = tx
584 .open_ro_cursor(db)
585 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
586
587 let mut memories = Vec::with_capacity(limit.min(256));
588 for (i, (_key, val)) in cursor.iter().enumerate() {
589 if memories.len() >= limit {
590 break;
591 }
592 match crate::codec::decode(val) {
593 Ok(memory) => memories.push(memory),
594 Err(e) => {
595 tracing::warn!(
596 "Skipping corrupted entry at index {i} in galaxy {:?}: {e}",
597 galaxy
598 );
599 }
600 }
601 }
602
603 drop(cursor);
604 tx.commit()
605 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
606 Ok(memories)
607 }
608
609 pub fn scan_all(&self, galaxy: Galaxy) -> Result<Vec<Memory>> {
614 self.scan_all_impl(galaxy, false)
615 }
616
617 pub fn scan_all_strict(&self, galaxy: Galaxy) -> Result<Vec<Memory>> {
621 self.scan_all_impl(galaxy, true)
622 }
623
624 fn scan_all_impl(&self, galaxy: Galaxy, strict: bool) -> Result<Vec<Memory>> {
625 let db = self.galaxy_db(galaxy)?;
626 let tx = self
627 .env
628 .begin_ro_txn()
629 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
630
631 let mut cursor = tx
632 .open_ro_cursor(db)
633 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
634
635 let mut memories = Vec::new();
636 for (i, (_key, val)) in cursor.iter().enumerate() {
637 match crate::codec::decode(val) {
638 Ok(memory) => memories.push(memory),
639 Err(e) => {
640 if strict {
641 return Err(CoreError::Memory(format!(
642 "refusing incomplete scan of {}: record {i} cannot be decoded: {e}",
643 galaxy.db_name()
644 )));
645 }
646 tracing::warn!(
647 "Skipping corrupted entry at index {i} in galaxy {:?}: {e}",
648 galaxy
649 );
650 }
651 }
652 }
653
654 drop(cursor);
655 tx.commit()
656 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
657 Ok(memories)
658 }
659
660 pub fn count(&self, galaxy: Galaxy) -> Result<usize> {
662 let db = self.galaxy_db(galaxy)?;
663 let tx = self
664 .env
665 .begin_ro_txn()
666 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
667 let mut cursor = tx
668 .open_ro_cursor(db)
669 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
670 let count = cursor.iter().count();
671 drop(cursor);
672 tx.commit()
673 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
674 Ok(count)
675 }
676
677 pub fn clear_galaxy(&self, galaxy: Galaxy) -> Result<usize> {
681 let db = self.galaxy_db(galaxy)?;
682
683 let mut tx = self
684 .env
685 .begin_rw_txn()
686 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
687
688 let mut cursor = tx
689 .open_ro_cursor(db)
690 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
691
692 let mut count = 0usize;
693 let keys_to_delete: Vec<(Vec<u8>, Memory)> = cursor
694 .iter()
695 .filter_map(|(key, val)| {
696 if let Ok(memory) = crate::codec::decode(val) {
697 Some((key.to_vec(), memory))
698 } else {
699 None
700 }
701 })
702 .collect();
703
704 drop(cursor);
705
706 for (key, memory) in &keys_to_delete {
707 let _ = self.index_dbs.remove(&mut tx, galaxy, memory);
708 tx.del(db, &key, None)
709 .map_err(|e| CoreError::Memory(format!("LMDB del failed: {e}")))?;
710 count += 1;
711 }
712
713 tx.commit()
714 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
715 self.mutation_count
716 .fetch_add(count as u64, Ordering::Relaxed);
717 Ok(count)
718 }
719
720 pub fn batch_put(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<usize> {
723 if memories.is_empty() {
724 return Ok(0);
725 }
726
727 let db = self.galaxy_db(galaxy)?;
728
729 let mut tx = self
730 .env
731 .begin_rw_txn()
732 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
733
734 let mut count = 0usize;
735 for memory in memories {
736 let key = memory.metadata.id.as_bytes();
737 let val = rmp_serde::to_vec_named(memory)
738 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
739 match tx.put(db, key, &val, WriteFlags::default()) {
740 Ok(()) => {}
741 Err(lmdb::Error::MapFull) => {
742 tx.abort();
743 return Err(CoreError::Memory(format!(
744 "LMDB map full: galaxy {}, consider growing map size",
745 galaxy.db_name()
746 )));
747 }
748 Err(e) => {
749 tx.abort();
750 return Err(CoreError::Memory(format!("LMDB put failed: {e}")));
751 }
752 }
753 self.index_dbs.add(&mut tx, galaxy, memory)?;
754 count += 1;
755 }
756
757 tx.commit()
758 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
759 self.mutation_count
760 .fetch_add(count as u64, Ordering::Relaxed);
761 Ok(count)
762 }
763
764 pub fn get_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<Option<Vec<u8>>> {
766 let db = self.galaxy_db(galaxy)?;
767 let tx = self
768 .env
769 .begin_ro_txn()
770 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
771 match tx.get(db, &key) {
772 Ok(bytes) => {
773 let data = bytes.to_vec();
774 tx.commit()
775 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
776 Ok(Some(data))
777 }
778 Err(lmdb::Error::NotFound) => {
779 tx.commit()
780 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
781 Ok(None)
782 }
783 Err(e) => Err(CoreError::Memory(format!("LMDB get_raw failed: {e}"))),
784 }
785 }
786
787 pub fn put_raw(&self, galaxy: Galaxy, key: &[u8], val: &[u8]) -> Result<()> {
789 let db = self.galaxy_db(galaxy)?;
790 let mut tx = self
791 .env
792 .begin_rw_txn()
793 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
794 tx.put(db, &key, &val, lmdb::WriteFlags::default())
795 .map_err(|e| CoreError::Memory(format!("LMDB put_raw failed: {e}")))?;
796 tx.commit()
797 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
798 self.mutation_count.fetch_add(1, Ordering::Relaxed);
799 Ok(())
800 }
801
802 pub fn delete_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<bool> {
805 let db = self.galaxy_db(galaxy)?;
806 let mut tx = self
807 .env
808 .begin_rw_txn()
809 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
810 let deleted = tx.del(db, &key, None).is_ok();
811 tx.commit()
812 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
813 if deleted {
814 self.mutation_count.fetch_add(1, Ordering::Relaxed);
815 }
816 Ok(deleted)
817 }
818
819 pub fn put_raw_batch(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
822 self.put_raw_batch_impl(galaxy, entries)?;
823 self.mutation_count
824 .fetch_add(entries.len() as u64, Ordering::Relaxed);
825 Ok(())
826 }
827
828 pub fn put_raw_batch_untracked(
838 &self,
839 galaxy: Galaxy,
840 entries: &[(&[u8], &[u8])],
841 ) -> Result<()> {
842 self.put_raw_batch_impl(galaxy, entries)
843 }
844
845 fn put_raw_batch_impl(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
846 let db = self.galaxy_db(galaxy)?;
847 let mut tx = self
848 .env
849 .begin_rw_txn()
850 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
851 for (key, val) in entries {
852 tx.put(db, key, val, WriteFlags::default())
853 .map_err(|e| CoreError::Memory(format!("LMDB put_raw_batch failed: {e}")))?;
854 }
855 tx.commit()
856 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
857 Ok(())
858 }
859
860 pub fn find_by_content_hash(&self, galaxy: Galaxy, hash: &str) -> Result<Option<uuid::Uuid>> {
866 let tx = self
867 .env
868 .begin_ro_txn()
869 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
870 let result = self.index_dbs.find_by_content_hash(&tx, galaxy, hash)?;
871 tx.commit()
872 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
873 Ok(result)
874 }
875
876 pub fn find_by_content_hash_scan(
878 &self,
879 galaxy: Galaxy,
880 hash: &str,
881 ) -> Result<Option<uuid::Uuid>> {
882 let memories = self.scan(galaxy, 10_000)?;
883 for mem in memories {
884 if mem.metadata.content_hash == hash {
885 return Ok(Some(mem.metadata.id));
886 }
887 }
888 Ok(None)
889 }
890
891 pub fn put_dedup(&self, galaxy: Galaxy, memory: &Memory) -> Result<uuid::Uuid> {
895 if let Some(existing_id) =
896 self.find_by_content_hash(galaxy, &memory.metadata.content_hash)?
897 {
898 return Ok(existing_id);
899 }
900 let id = memory.metadata.id;
901 self.put(galaxy, memory)?;
902 Ok(id)
903 }
904
905 pub fn put_batch(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<()> {
910 let db = self.galaxy_db(galaxy)?;
911 let mut tx = self
912 .env
913 .begin_rw_txn()
914 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
915
916 for memory in memories {
917 let key = memory.metadata.id.as_bytes();
918 let val = rmp_serde::to_vec_named(memory)
919 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
920 tx.put(db, key, &val, WriteFlags::default())
921 .map_err(|e| CoreError::Memory(format!("LMDB put_batch failed: {e}")))?;
922 self.index_dbs.add(&mut tx, galaxy, memory)?;
923 }
924
925 tx.commit()
926 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
927 self.mutation_count
928 .fetch_add(memories.len() as u64, Ordering::Relaxed);
929 Ok(())
930 }
931
932 pub fn query(&self, galaxy: Galaxy, query: &MemoryQuery) -> Result<Vec<Memory>> {
939 if query.content_substring.is_none()
942 && query.tags.len() == 1
943 && query.min_importance.is_none()
944 && query.max_importance.is_none()
945 && query.created_after.is_none()
946 && query.created_before.is_none()
947 {
948 return self.query_by_tag_indexed(galaxy, &query.tags[0], query.limit);
949 }
950
951 if query.content_substring.is_none()
952 && query.tags.is_empty()
953 && let Some(min) = query.min_importance
954 && let Some(max) = query.max_importance
955 && query.created_after.is_none()
956 && query.created_before.is_none()
957 {
958 return self.query_by_importance_indexed(galaxy, min, max, query.limit);
959 }
960
961 if query.content_substring.is_none()
962 && query.tags.is_empty()
963 && query.min_importance.is_none()
964 && query.max_importance.is_none()
965 && let Some(after) = query.created_after
966 && let Some(before) = query.created_before
967 {
968 return self.query_by_time_indexed(galaxy, after, before, query.limit);
969 }
970
971 let memories = self.scan(galaxy, 10_000)?;
973 let mut results = Vec::new();
974 for mem in memories {
975 if query.matches(&mem) {
976 results.push(mem);
977 if results.len() >= query.limit {
978 break;
979 }
980 }
981 }
982 Ok(results)
983 }
984
985 fn query_by_tag_indexed(&self, galaxy: Galaxy, tag: &str, limit: usize) -> Result<Vec<Memory>> {
987 let db = self.galaxy_db(galaxy)?;
988 let tx = self
989 .env
990 .begin_ro_txn()
991 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
992 let ids = self.index_dbs.find_by_tag(&tx, galaxy, tag)?;
993 let mut results = Vec::new();
994 for id in &ids {
995 if results.len() >= limit {
996 break;
997 }
998 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
999 if let Ok(mem) = crate::codec::decode(bytes) {
1000 results.push(mem);
1001 }
1002 }
1003 }
1004 tx.commit()
1005 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1006 Ok(results)
1007 }
1008
1009 fn query_by_importance_indexed(
1011 &self,
1012 galaxy: Galaxy,
1013 min: f32,
1014 max: f32,
1015 limit: usize,
1016 ) -> Result<Vec<Memory>> {
1017 let db = self.galaxy_db(galaxy)?;
1018 let tx = self
1019 .env
1020 .begin_ro_txn()
1021 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1022 let ids = self
1023 .index_dbs
1024 .find_by_importance_range(&tx, galaxy, min, max)?;
1025 let mut results = Vec::new();
1026 for id in &ids {
1027 if results.len() >= limit {
1028 break;
1029 }
1030 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1031 if let Ok(mem) = crate::codec::decode(bytes) {
1032 results.push(mem);
1033 }
1034 }
1035 }
1036 tx.commit()
1037 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1038 Ok(results)
1039 }
1040
1041 fn query_by_time_indexed(
1043 &self,
1044 galaxy: Galaxy,
1045 after: chrono::DateTime<chrono::Utc>,
1046 before: chrono::DateTime<chrono::Utc>,
1047 limit: usize,
1048 ) -> Result<Vec<Memory>> {
1049 let db = self.galaxy_db(galaxy)?;
1050 let tx = self
1051 .env
1052 .begin_ro_txn()
1053 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1054 let ids = self
1055 .index_dbs
1056 .find_by_time_range(&tx, galaxy, after, before)?;
1057 let mut results = Vec::new();
1058 for id in &ids {
1059 if results.len() >= limit {
1060 break;
1061 }
1062 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1063 if let Ok(mem) = crate::codec::decode(bytes) {
1064 results.push(mem);
1065 }
1066 }
1067 }
1068 tx.commit()
1069 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1070 Ok(results)
1071 }
1072
1073 pub fn put_semantic(&self, galaxy: Galaxy, memory: &mut Memory) -> Result<()> {
1081 let temporal_weight = memory.metadata.coord5d.w;
1082 let importance = memory.metadata.importance;
1083 memory.metadata.coord5d =
1084 self.semantic_encoder
1085 .encode_coordinate(&memory.content, temporal_weight, importance);
1086 self.put(galaxy, memory)
1087 }
1088
1089 pub fn find_similar(
1094 &self,
1095 galaxy: Galaxy,
1096 query_text: &str,
1097 limit: usize,
1098 ) -> Result<Vec<(Memory, f32)>> {
1099 let query_coord = self
1100 .semantic_encoder
1101 .encode_coordinate(query_text, 0.5, 0.5);
1102 let memories = self.scan(galaxy, 10_000)?;
1103 let mut results: Vec<(Memory, f32)> = memories
1104 .into_iter()
1105 .map(|m| {
1106 let dist = query_coord.semantic_distance_to(&m.metadata.coord5d);
1107 (m, dist)
1108 })
1109 .collect();
1110 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1111 results.truncate(limit);
1112 Ok(results)
1113 }
1114
1115 pub fn put_embedding(&self, memory_id: uuid::Uuid, embedding: &[f32]) -> Result<()> {
1120 let db = self.galaxy_db(Galaxy::Embeddings)?;
1121 let key = memory_id.as_bytes();
1122 let val = encode_embedding(embedding);
1123
1124 let mut tx = self
1125 .env
1126 .begin_rw_txn()
1127 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1128 tx.put(db, key, &val, WriteFlags::default())
1129 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding failed: {e}")))?;
1130 tx.commit()
1131 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1132 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1133 Ok(())
1134 }
1135
1136 pub fn get_embedding(&self, memory_id: uuid::Uuid) -> Result<Option<Vec<f32>>> {
1138 let db = self.galaxy_db(Galaxy::Embeddings)?;
1139 let key = memory_id.as_bytes();
1140
1141 let tx = self
1142 .env
1143 .begin_ro_txn()
1144 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1145 match tx.get(db, key) {
1146 Ok(bytes) => {
1147 let embedding = decode_embedding(bytes);
1148 tx.commit()
1149 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1150 Ok(Some(embedding))
1151 }
1152 Err(lmdb::Error::NotFound) => {
1153 tx.commit()
1154 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1155 Ok(None)
1156 }
1157 Err(e) => Err(CoreError::Memory(format!("LMDB get_embedding failed: {e}"))),
1158 }
1159 }
1160
1161 pub fn delete_embedding(&self, memory_id: uuid::Uuid) -> Result<bool> {
1163 let db = self.galaxy_db(Galaxy::Embeddings)?;
1164 let key = memory_id.as_bytes();
1165
1166 let mut tx = self
1167 .env
1168 .begin_rw_txn()
1169 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1170 let exists = tx.get(db, key).is_ok();
1171 if exists {
1172 tx.del(db, key, None)
1173 .map_err(|e| CoreError::Memory(format!("LMDB del_embedding failed: {e}")))?;
1174 }
1175 tx.commit()
1176 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1177 if exists {
1178 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1179 }
1180 Ok(exists)
1181 }
1182
1183 pub fn put_embedding_cache(&self, cache_key: &str, embedding: &[f32]) -> Result<()> {
1189 let mut tx = self
1190 .env
1191 .begin_rw_txn()
1192 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1193 tx.put(
1194 self.embedding_cache_db,
1195 &cache_key.as_bytes().to_vec(),
1196 &encode_embedding(embedding),
1197 WriteFlags::default(),
1198 )
1199 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1200 tx.commit()
1201 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1202 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1203 Ok(())
1204 }
1205
1206 pub fn put_embedding_cache_batch(&self, entries: &[(String, Vec<f32>)]) -> Result<()> {
1208 if entries.is_empty() {
1209 return Ok(());
1210 }
1211 let mut tx = self
1212 .env
1213 .begin_rw_txn()
1214 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1215 for (key, embedding) in entries {
1216 tx.put(
1217 self.embedding_cache_db,
1218 &key.as_bytes().to_vec(),
1219 &encode_embedding(embedding),
1220 WriteFlags::default(),
1221 )
1222 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1223 }
1224 tx.commit()
1225 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1226 self.mutation_count
1227 .fetch_add(entries.len() as u64, Ordering::Relaxed);
1228 Ok(())
1229 }
1230
1231 pub fn get_embedding_cache(&self, cache_key: &str) -> Result<Option<Vec<f32>>> {
1233 let tx = self
1234 .env
1235 .begin_ro_txn()
1236 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1237 match tx.get(self.embedding_cache_db, &cache_key.as_bytes().to_vec()) {
1238 Ok(bytes) => {
1239 let embedding = decode_embedding(bytes);
1240 tx.commit()
1241 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1242 Ok(Some(embedding))
1243 }
1244 Err(lmdb::Error::NotFound) => {
1245 tx.commit()
1246 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1247 Ok(None)
1248 }
1249 Err(e) => Err(CoreError::Memory(format!(
1250 "LMDB get_embedding_cache failed: {e}"
1251 ))),
1252 }
1253 }
1254
1255 pub fn get_embedding_cache_batch(&self, keys: &[String]) -> Result<Vec<Option<Vec<f32>>>> {
1258 let tx = self
1259 .env
1260 .begin_ro_txn()
1261 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1262 let mut out = Vec::with_capacity(keys.len());
1263 for key in keys {
1264 out.push(
1265 tx.get(self.embedding_cache_db, &key.as_bytes().to_vec())
1266 .ok()
1267 .map(decode_embedding),
1268 );
1269 }
1270 tx.commit()
1271 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1272 Ok(out)
1273 }
1274
1275 pub fn embedding_cache_count(&self) -> Result<u64> {
1277 let tx = self
1278 .env
1279 .begin_ro_txn()
1280 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1281 let mut cursor = tx
1282 .open_ro_cursor(self.embedding_cache_db)
1283 .map_err(|e| CoreError::Memory(format!("LMDB cursor embedding_cache failed: {e}")))?;
1284 let mut count = 0u64;
1285 for _ in cursor.iter() {
1286 count += 1;
1287 }
1288 Ok(count)
1289 }
1290
1291 pub fn record_revision(
1297 &self,
1298 galaxy: Galaxy,
1299 id: MemoryId,
1300 old_hash: &str,
1301 new_hash: &str,
1302 actor: crate::revision::RevisionActor,
1303 ) -> Result<crate::revision::MemoryRevision> {
1304 let seq = self.revisions(galaxy, id)?.len() as u32;
1305 let entry = crate::revision::MemoryRevision {
1306 seq,
1307 timestamp: wm_core::time::now_unix_secs(),
1308 old_hash: old_hash.to_string(),
1309 new_hash: new_hash.to_string(),
1310 actor_session: actor.session,
1311 actor_user: actor.user,
1312 actor_compartment: actor.compartment,
1313 };
1314 let key = crate::revision::revision_key(galaxy, id, seq);
1315 let val = serde_json::to_vec(&entry)
1316 .map_err(|e| CoreError::Memory(format!("revision serialize failed: {e}")))?;
1317 let mut tx = self
1318 .env
1319 .begin_rw_txn()
1320 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1321 tx.put(self.revisions_db, &key, &val, WriteFlags::default())
1322 .map_err(|e| CoreError::Memory(format!("LMDB put revision failed: {e}")))?;
1323 tx.commit()
1324 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1325 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1326 Ok(entry)
1327 }
1328
1329 pub fn revisions(
1332 &self,
1333 galaxy: Galaxy,
1334 id: MemoryId,
1335 ) -> Result<Vec<crate::revision::MemoryRevision>> {
1336 const MDB_GET_CURRENT: u32 = 4;
1341 const MDB_NEXT: u32 = 8;
1342 const MDB_SET_RANGE: u32 = 17;
1343 let prefix = crate::revision::revision_prefix(galaxy, id);
1344 let tx = self
1345 .env
1346 .begin_ro_txn()
1347 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1348 let cursor = tx
1349 .open_ro_cursor(self.revisions_db)
1350 .map_err(|e| CoreError::Memory(format!("LMDB cursor revisions failed: {e}")))?;
1351 let mut out = Vec::new();
1352 if cursor.get(Some(&prefix), None, MDB_SET_RANGE).is_ok() {
1353 while let Ok((key, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1354 if !key.is_some_and(|k| k.starts_with(&prefix)) {
1357 break;
1358 }
1359 let entry: crate::revision::MemoryRevision = serde_json::from_slice(val)
1360 .map_err(|e| CoreError::Memory(format!("revision deserialize failed: {e}")))?;
1361 out.push(entry);
1362 if out.len() >= 10_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1364 break;
1365 }
1366 }
1367 }
1368 drop(cursor);
1369 tx.commit()
1370 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1371 Ok(out)
1372 }
1373
1374 pub fn verify_revision_chain(
1377 &self,
1378 galaxy: Galaxy,
1379 id: MemoryId,
1380 current_hash: &str,
1381 ) -> Result<crate::revision::RevisionChainReport> {
1382 let entries = self.revisions(galaxy, id)?;
1383 Ok(crate::revision::verify_chain(&entries, current_hash))
1384 }
1385
1386 pub fn record_attestation(
1394 &self,
1395 galaxy: Galaxy,
1396 id: MemoryId,
1397 entry: &crate::attestation::RecordAttestation,
1398 ) -> Result<()> {
1399 let key = crate::attestation::attestation_key(galaxy, id);
1400 let val = serde_json::to_vec(entry)
1401 .map_err(|e| CoreError::Memory(format!("attestation serialize failed: {e}")))?;
1402 let mut tx = self
1403 .env
1404 .begin_rw_txn()
1405 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1406 tx.put(self.attestations_db, &key, &val, WriteFlags::default())
1407 .map_err(|e| CoreError::Memory(format!("LMDB put attestation failed: {e}")))?;
1408 tx.commit()
1409 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1410 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1411 Ok(())
1412 }
1413
1414 pub fn attestation(
1417 &self,
1418 galaxy: Galaxy,
1419 id: MemoryId,
1420 ) -> Result<Option<crate::attestation::RecordAttestation>> {
1421 let key = crate::attestation::attestation_key(galaxy, id);
1422 let tx = self
1423 .env
1424 .begin_ro_txn()
1425 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1426 let out =
1427 match tx.get(self.attestations_db, &key) {
1428 Ok(val) => Some(serde_json::from_slice(val).map_err(|e| {
1429 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1430 })?),
1431 Err(lmdb::Error::NotFound) => None,
1432 Err(e) => {
1433 return Err(CoreError::Memory(format!(
1434 "LMDB get attestation failed: {e}"
1435 )));
1436 }
1437 };
1438 drop(tx);
1439 Ok(out)
1440 }
1441
1442 pub fn scan_attestations(&self) -> Result<Vec<crate::attestation::RecordAttestation>> {
1445 const MDB_GET_CURRENT: u32 = 4;
1446 const MDB_NEXT: u32 = 8;
1447 const MDB_FIRST: u32 = 9;
1448 let tx = self
1449 .env
1450 .begin_ro_txn()
1451 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1452 let cursor = tx
1453 .open_ro_cursor(self.attestations_db)
1454 .map_err(|e| CoreError::Memory(format!("LMDB cursor attestations failed: {e}")))?;
1455 let mut out = Vec::new();
1456 if cursor.get(None, None, MDB_FIRST).is_ok() {
1457 while let Ok((_, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1458 let entry: crate::attestation::RecordAttestation = serde_json::from_slice(val)
1459 .map_err(|e| {
1460 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1461 })?;
1462 out.push(entry);
1463 if out.len() >= 1_000_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1465 break;
1466 }
1467 }
1468 }
1469 drop(cursor);
1470 tx.commit()
1471 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1472 Ok(out)
1473 }
1474
1475 pub fn verify_attestation(
1481 &self,
1482 galaxy: Galaxy,
1483 id: MemoryId,
1484 ) -> Result<crate::attestation::AttestationReport> {
1485 use crate::attestation::AttestationReport;
1486 let Some(att) = self.attestation(galaxy, id)? else {
1487 return Ok(AttestationReport {
1488 attested: false,
1489 signature_valid: false,
1490 matches_head: false,
1491 memory_present: self.get(galaxy, id)?.is_some(),
1492 breaks: vec!["no attestation recorded for this memory".to_string()],
1493 });
1494 };
1495 let mut breaks = Vec::new();
1496 let signature_valid = crate::attestation::verify_attestation(&att);
1497 if !signature_valid {
1498 breaks.push("signature does not verify against recorded pubkey".to_string());
1499 }
1500 let (matches_head, memory_present) = if let Some(memory) = self.get(galaxy, id)? {
1501 let matches = memory.metadata.content_hash == att.record_hash;
1502 if !matches {
1503 breaks.push(
1504 "attested record_hash != live content_hash (memory updated after attestation)"
1505 .to_string(),
1506 );
1507 }
1508 (matches, true)
1509 } else {
1510 breaks.push("attested memory id not present in galaxy".to_string());
1511 (false, false)
1512 };
1513 Ok(AttestationReport {
1514 attested: true,
1515 signature_valid,
1516 matches_head,
1517 memory_present,
1518 breaks,
1519 })
1520 }
1521
1522 pub fn attestation_sweep(
1526 &self,
1527 ) -> Result<
1528 Vec<(
1529 crate::attestation::RecordAttestation,
1530 crate::attestation::AttestationReport,
1531 )>,
1532 > {
1533 use crate::attestation::AttestationReport;
1534 let mut out = Vec::new();
1535 for att in self.scan_attestations()? {
1536 let parsed = match (
1537 Galaxy::from_db_name(&att.galaxy),
1538 uuid::Uuid::parse_str(&att.memory_id),
1539 ) {
1540 (Some(galaxy), Ok(id)) => Some((galaxy, id)),
1541 _ => None,
1542 };
1543 match parsed {
1544 Some((galaxy, id)) => out.push((att, self.verify_attestation(galaxy, id)?)),
1545 None => out.push((
1546 att,
1547 AttestationReport {
1548 attested: true,
1549 signature_valid: false,
1550 matches_head: false,
1551 memory_present: false,
1552 breaks: vec![
1553 "attestation row has unparseable galaxy or memory id".to_string(),
1554 ],
1555 },
1556 )),
1557 }
1558 }
1559 Ok(out)
1560 }
1561}
1562
1563#[cfg(test)]
1564mod tests {
1565 use super::*;
1566 use crate::content_hash;
1567
1568 #[test]
1569 fn open_and_create_galaxies() {
1570 let tmp = tempfile::tempdir().unwrap();
1571 let store = MemoryStore::open_default(tmp.path()).unwrap();
1572 for galaxy in Galaxy::all() {
1573 let _db = store.galaxy_db(galaxy).unwrap();
1574 }
1575 }
1576
1577 #[test]
1581 fn query_substring_filters_galaxy_wide() {
1582 let tmp = tempfile::tempdir().unwrap();
1583 let store = MemoryStore::open_default(tmp.path()).unwrap();
1584
1585 for (i, content) in [
1586 "the mesh joins at dawn",
1587 "unrelated content entirely",
1588 "MESH joins at dusk",
1589 ]
1590 .iter()
1591 .enumerate()
1592 {
1593 let mut m = Memory::new(Galaxy::Codex, content.to_string());
1594 m.metadata.importance = 0.5 + i as f32 / 10.0;
1595 store.put(Galaxy::Codex, &m).unwrap();
1596 }
1597
1598 let hits = store
1599 .query(
1600 Galaxy::Codex,
1601 &MemoryQuery::new().with_content_substring("mesh joins"),
1602 )
1603 .unwrap();
1604 assert_eq!(hits.len(), 2, "CI substring must match both: {hits:?}");
1605 assert!(
1606 hits.iter()
1607 .all(|m| m.content.to_lowercase().contains("mesh joins"))
1608 );
1609
1610 let none = store
1611 .query(
1612 Galaxy::Codex,
1613 &MemoryQuery::new().with_content_substring("quantum calendar"),
1614 )
1615 .unwrap();
1616 assert!(none.is_empty(), "no match must be an honest empty set");
1617
1618 let mut tagged = Memory::new(Galaxy::Codex, "mesh joins again".to_string());
1620 tagged.metadata.tags = vec!["mesh".into()];
1621 store.put(Galaxy::Codex, &tagged).unwrap();
1622 let combined = store
1623 .query(
1624 Galaxy::Codex,
1625 &MemoryQuery::new()
1626 .with_tags(vec!["mesh".into()])
1627 .with_content_substring("again"),
1628 )
1629 .unwrap();
1630 assert_eq!(combined.len(), 1);
1631 assert_eq!(combined[0].content, "mesh joins again");
1632 }
1633
1634 #[cfg(unix)]
1635 #[test]
1636 fn store_dir_has_restrictive_permissions() {
1637 let tmp = tempfile::tempdir().unwrap();
1638 let store_path = tmp.path().join("lmdb");
1639 let _store = MemoryStore::open_default(&store_path).unwrap();
1640 let perms = std::fs::metadata(&store_path).unwrap().permissions().mode();
1641 assert_eq!(
1642 perms & 0o777,
1643 0o700,
1644 "store directory should have 0700 permissions, got {:o}",
1645 perms & 0o777
1646 );
1647 }
1648
1649 #[test]
1650 fn put_get_delete_memory() {
1651 let tmp = tempfile::tempdir().unwrap();
1652 let store = MemoryStore::open_default(tmp.path()).unwrap();
1653
1654 let mem = Memory::new(Galaxy::Codex, "Hello world".to_string());
1655 let id = mem.metadata.id;
1656
1657 store.put(Galaxy::Codex, &mem).unwrap();
1658 let retrieved = store.get(Galaxy::Codex, id).unwrap();
1659 assert!(retrieved.is_some());
1660 assert_eq!(retrieved.unwrap().content, "Hello world");
1661
1662 let deleted = store.delete(Galaxy::Codex, id).unwrap();
1663 assert!(deleted);
1664
1665 let gone = store.get(Galaxy::Codex, id).unwrap();
1666 assert!(gone.is_none());
1667 }
1668
1669 #[test]
1670 fn scan_memories() {
1671 let tmp = tempfile::tempdir().unwrap();
1672 let store = MemoryStore::open_default(tmp.path()).unwrap();
1673
1674 for i in 0..5 {
1675 let mem = Memory::new(Galaxy::Codex, format!("memory-{i}"));
1676 store.put(Galaxy::Codex, &mem).unwrap();
1677 }
1678
1679 let all = store.scan(Galaxy::Codex, 100).unwrap();
1680 assert_eq!(all.len(), 5);
1681
1682 let limited = store.scan(Galaxy::Codex, 3).unwrap();
1683 assert_eq!(limited.len(), 3);
1684 }
1685
1686 #[test]
1687 fn overwrite_removes_stale_index_entries() {
1688 let tmp = tempfile::tempdir().unwrap();
1689 let store = MemoryStore::open_default(tmp.path()).unwrap();
1690
1691 let mut mem = Memory::new(Galaxy::Codex, "overwrite target".to_string());
1693 mem.metadata.tags = vec!["alpha".to_string()];
1694 mem.metadata.importance = 0.9;
1695 let id = mem.metadata.id;
1696 store.put(Galaxy::Codex, &mem).unwrap();
1697
1698 let mut updated = Memory::new(Galaxy::Codex, "overwritten content".to_string());
1700 updated.metadata.id = id;
1701 updated.metadata.tags = vec!["beta".to_string()];
1702 updated.metadata.importance = 0.1;
1703 store.put(Galaxy::Codex, &updated).unwrap();
1704
1705 let tx = store.env().begin_ro_txn().unwrap();
1707 let by_alpha = store
1708 .index_dbs()
1709 .find_by_tag(&tx, Galaxy::Codex, "alpha")
1710 .unwrap();
1711 let by_beta = store
1712 .index_dbs()
1713 .find_by_tag(&tx, Galaxy::Codex, "beta")
1714 .unwrap();
1715 assert!(
1716 by_alpha.is_empty(),
1717 "stale tag index entries must be removed on overwrite"
1718 );
1719 assert_eq!(by_beta, vec![id]);
1720
1721 let by_importance = store
1722 .index_dbs()
1723 .find_by_importance_range(&tx, Galaxy::Codex, 0.0, 0.2)
1724 .unwrap();
1725 assert!(
1726 by_importance.contains(&id),
1727 "new importance must be indexed"
1728 );
1729 let by_high = store
1730 .index_dbs()
1731 .find_by_importance_range(&tx, Galaxy::Codex, 0.8, 1.0)
1732 .unwrap();
1733 assert!(
1734 !by_high.contains(&id),
1735 "stale importance index entries must be removed on overwrite"
1736 );
1737
1738 let old_hash = content_hash("overwrite target");
1739 let new_hash = content_hash("overwritten content");
1740 assert_eq!(
1741 store
1742 .index_dbs()
1743 .find_by_content_hash(&tx, Galaxy::Codex, &old_hash)
1744 .unwrap(),
1745 None,
1746 "stale content-hash index entry must be removed"
1747 );
1748 assert_eq!(
1749 store
1750 .index_dbs()
1751 .find_by_content_hash(&tx, Galaxy::Codex, &new_hash)
1752 .unwrap(),
1753 Some(id)
1754 );
1755 }
1756
1757 #[test]
1758 fn count_memories() {
1759 let tmp = tempfile::tempdir().unwrap();
1760 let store = MemoryStore::open_default(tmp.path()).unwrap();
1761
1762 assert_eq!(store.count(Galaxy::Codex).unwrap(), 0);
1763
1764 for i in 0..3 {
1765 let mem = Memory::new(Galaxy::Codex, format!("count-{i}"));
1766 store.put(Galaxy::Codex, &mem).unwrap();
1767 }
1768
1769 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
1770 }
1771
1772 #[test]
1773 fn get_nonexistent_returns_none() {
1774 let tmp = tempfile::tempdir().unwrap();
1775 let store = MemoryStore::open_default(tmp.path()).unwrap();
1776 let result = store.get(Galaxy::Codex, uuid::Uuid::new_v4()).unwrap();
1777 assert!(result.is_none());
1778 }
1779
1780 #[test]
1781 fn raw_put_get() {
1782 let tmp = tempfile::tempdir().unwrap();
1783 let store = MemoryStore::open_default(tmp.path()).unwrap();
1784
1785 store
1786 .put_raw(Galaxy::Substrate, b"config:key", b"value123")
1787 .unwrap();
1788 let val = store.get_raw(Galaxy::Substrate, b"config:key").unwrap();
1789 assert_eq!(val, Some(b"value123".to_vec()));
1790 }
1791
1792 #[test]
1793 fn put_dedup_prevents_duplicates() {
1794 let tmp = tempfile::tempdir().unwrap();
1795 let store = MemoryStore::open_default(tmp.path()).unwrap();
1796
1797 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
1798 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
1799
1800 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
1801 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
1802
1803 assert_eq!(id1, id2, "dedup should return same ID for same content");
1804 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
1805 }
1806
1807 #[test]
1808 fn put_dedup_allows_different_content() {
1809 let tmp = tempfile::tempdir().unwrap();
1810 let store = MemoryStore::open_default(tmp.path()).unwrap();
1811
1812 let mem1 = Memory::new(Galaxy::Codex, "content A".into());
1813 store.put_dedup(Galaxy::Codex, &mem1).unwrap();
1814
1815 let mem2 = Memory::new(Galaxy::Codex, "content B".into());
1816 store.put_dedup(Galaxy::Codex, &mem2).unwrap();
1817
1818 assert_eq!(store.count(Galaxy::Codex).unwrap(), 2);
1819 }
1820
1821 #[test]
1822 fn put_batch_atomic_write() {
1823 let tmp = tempfile::tempdir().unwrap();
1824 let store = MemoryStore::open_default(tmp.path()).unwrap();
1825
1826 let memories: Vec<Memory> = (0..10)
1827 .map(|i| Memory::new(Galaxy::Codex, format!("batch-{i}")))
1828 .collect();
1829
1830 store.put_batch(Galaxy::Codex, &memories).unwrap();
1831 assert_eq!(store.count(Galaxy::Codex).unwrap(), 10);
1832 }
1833
1834 #[test]
1835 fn query_by_tags() {
1836 let tmp = tempfile::tempdir().unwrap();
1837 let store = MemoryStore::open_default(tmp.path()).unwrap();
1838
1839 let mem1 = Memory::new(Galaxy::Codex, "tagged memory".into())
1840 .with_tags(vec!["rust".into(), "memory".into()]);
1841 let mem2 =
1842 Memory::new(Galaxy::Codex, "other memory".into()).with_tags(vec!["python".into()]);
1843 store.put(Galaxy::Codex, &mem1).unwrap();
1844 store.put(Galaxy::Codex, &mem2).unwrap();
1845
1846 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
1847 let results = store.query(Galaxy::Codex, &query).unwrap();
1848 assert_eq!(results.len(), 1);
1849 assert_eq!(results[0].content, "tagged memory");
1850 }
1851
1852 #[test]
1853 fn query_by_importance_range() {
1854 let tmp = tempfile::tempdir().unwrap();
1855 let store = MemoryStore::open_default(tmp.path()).unwrap();
1856
1857 store
1858 .put(
1859 Galaxy::Codex,
1860 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
1861 )
1862 .unwrap();
1863 store
1864 .put(
1865 Galaxy::Codex,
1866 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
1867 )
1868 .unwrap();
1869 store
1870 .put(
1871 Galaxy::Codex,
1872 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
1873 )
1874 .unwrap();
1875
1876 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
1877 let results = store.query(Galaxy::Codex, &query).unwrap();
1878 assert_eq!(results.len(), 1);
1879 assert_eq!(results[0].content, "mid");
1880 }
1881
1882 #[test]
1883 fn memory_query_one_sided_time_bounds() {
1884 let old = Memory::new(Galaxy::Codex, "old".into());
1887 let mut recent = Memory::new(Galaxy::Codex, "recent".into());
1888 recent.metadata.created_at = old.metadata.created_at + chrono::Duration::days(30);
1889
1890 let cutoff = old.metadata.created_at + chrono::Duration::days(10);
1891 let after = MemoryQuery::new().with_created_after(cutoff);
1892 assert!(!after.matches(&old), "pre-cutoff memory must not match");
1893 assert!(after.matches(&recent), "post-cutoff memory must match");
1894
1895 let before = MemoryQuery::new().with_created_before(cutoff);
1896 assert!(before.matches(&old), "pre-cutoff memory must match");
1897 assert!(
1898 !before.matches(&recent),
1899 "post-cutoff memory must not match"
1900 );
1901
1902 let edge = MemoryQuery::new().with_created_after(cutoff);
1904 let mut at = Memory::new(Galaxy::Codex, "at cutoff".into());
1905 at.metadata.created_at = cutoff;
1906 assert!(edge.matches(&at), "created_at == after bound is inclusive");
1907 }
1908
1909 #[test]
1910 fn embedding_put_get_delete() {
1911 let tmp = tempfile::tempdir().unwrap();
1912 let store = MemoryStore::open_default(tmp.path()).unwrap();
1913
1914 let id = uuid::Uuid::new_v4();
1915 let embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5];
1916
1917 store.put_embedding(id, &embedding).unwrap();
1918 let retrieved = store.get_embedding(id).unwrap();
1919 assert!(retrieved.is_some());
1920 let retrieved = retrieved.unwrap();
1921 assert_eq!(retrieved.len(), 5);
1922 assert!((retrieved[0] - 0.1).abs() < f32::EPSILON);
1923
1924 assert!(store.delete_embedding(id).unwrap());
1925 assert!(store.get_embedding(id).unwrap().is_none());
1926 }
1927
1928 #[test]
1929 fn embedding_cache_roundtrip_batch_and_count() {
1930 let tmp = tempfile::tempdir().unwrap();
1931 let store = MemoryStore::open_default(tmp.path()).unwrap();
1932
1933 let entries: Vec<(String, Vec<f32>)> = (0..5)
1934 .map(|i| (format!("ns:model:{i:016x}"), vec![i as f32; 8]))
1935 .collect();
1936 store.put_embedding_cache_batch(&entries).unwrap();
1937 assert_eq!(store.embedding_cache_count().unwrap(), 5);
1938
1939 let hit = store
1941 .get_embedding_cache("ns:model:0000000000000003")
1942 .unwrap();
1943 assert_eq!(hit.unwrap(), vec![3.0f32; 8]);
1944 assert!(
1945 store
1946 .get_embedding_cache("ns:model:absent")
1947 .unwrap()
1948 .is_none()
1949 );
1950
1951 let keys: Vec<String> = (0..6).map(|i| format!("ns:model:{i:016x}")).collect();
1953 let batch = store.get_embedding_cache_batch(&keys).unwrap();
1954 assert_eq!(batch.len(), 6);
1955 assert!(batch[0..5].iter().all(Option::is_some));
1956 assert!(batch[5].is_none());
1957
1958 store
1960 .put_embedding_cache("ns:model:0000000000000001", &[9.0; 8])
1961 .unwrap();
1962 assert_eq!(store.embedding_cache_count().unwrap(), 5);
1963 assert_eq!(
1964 store
1965 .get_embedding_cache("ns:model:0000000000000001")
1966 .unwrap()
1967 .unwrap(),
1968 vec![9.0f32; 8]
1969 );
1970 }
1971
1972 #[test]
1973 fn embedding_cache_survives_store_reopen() {
1974 let tmp = tempfile::tempdir().unwrap();
1976 {
1977 let store = MemoryStore::open_default(tmp.path()).unwrap();
1978 store
1979 .put_embedding_cache("onnx:bge-small:abc", &[0.5; 384])
1980 .unwrap();
1981 }
1982 let reopened = MemoryStore::open_default(tmp.path()).unwrap();
1983 let cached = reopened.get_embedding_cache("onnx:bge-small:abc").unwrap();
1984 assert_eq!(cached.unwrap(), vec![0.5f32; 384]);
1985 }
1986
1987 #[test]
1988 fn content_hash_is_sha256() {
1989 let hash1 = content_hash("test content");
1990 let hash2 = content_hash("test content");
1991 let hash3 = content_hash("different content");
1992
1993 assert_eq!(hash1, hash2, "same content should produce same hash");
1994 assert_ne!(
1995 hash1, hash3,
1996 "different content should produce different hash"
1997 );
1998 assert_eq!(hash1.len(), 64, "SHA-256 hex should be 64 chars");
1999 }
2000
2001 #[test]
2002 fn query_by_tag_uses_index() {
2003 let tmp = tempfile::tempdir().unwrap();
2004 let store = MemoryStore::open_default(tmp.path()).unwrap();
2005
2006 let mem1 = Memory::new(Galaxy::Codex, "tagged".into())
2007 .with_tags(vec!["rust".into(), "memory".into()]);
2008 let mem2 = Memory::new(Galaxy::Codex, "other".into()).with_tags(vec!["python".into()]);
2009 store.put(Galaxy::Codex, &mem1).unwrap();
2010 store.put(Galaxy::Codex, &mem2).unwrap();
2011
2012 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
2013 let results = store.query(Galaxy::Codex, &query).unwrap();
2014 assert_eq!(results.len(), 1);
2015 assert_eq!(results[0].content, "tagged");
2016 }
2017
2018 #[test]
2019 fn query_by_importance_uses_index() {
2020 let tmp = tempfile::tempdir().unwrap();
2021 let store = MemoryStore::open_default(tmp.path()).unwrap();
2022
2023 store
2024 .put(
2025 Galaxy::Codex,
2026 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
2027 )
2028 .unwrap();
2029 store
2030 .put(
2031 Galaxy::Codex,
2032 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
2033 )
2034 .unwrap();
2035 store
2036 .put(
2037 Galaxy::Codex,
2038 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
2039 )
2040 .unwrap();
2041
2042 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
2043 let results = store.query(Galaxy::Codex, &query).unwrap();
2044 assert_eq!(results.len(), 1);
2045 assert_eq!(results[0].content, "mid");
2046 }
2047
2048 #[test]
2049 fn query_by_time_uses_index() {
2050 let tmp = tempfile::tempdir().unwrap();
2051 let store = MemoryStore::open_default(tmp.path()).unwrap();
2052
2053 let t0 = chrono::Utc::now();
2054 std::thread::sleep(std::time::Duration::from_millis(10));
2055 let mem = Memory::new(Galaxy::Codex, "timed".into());
2056 store.put(Galaxy::Codex, &mem).unwrap();
2057 std::thread::sleep(std::time::Duration::from_millis(10));
2058 let t2 = chrono::Utc::now();
2059
2060 let query = MemoryQuery::new().with_time_range(t0, t2);
2061 let results = store.query(Galaxy::Codex, &query).unwrap();
2062 assert_eq!(results.len(), 1);
2063 assert_eq!(results[0].content, "timed");
2064 }
2065
2066 #[test]
2067 fn delete_removes_index_entries() {
2068 let tmp = tempfile::tempdir().unwrap();
2069 let store = MemoryStore::open_default(tmp.path()).unwrap();
2070
2071 let mem = Memory::new(Galaxy::Codex, "test".into())
2072 .with_tags(vec!["tag1".into()])
2073 .with_importance(0.7);
2074 let id = mem.metadata.id;
2075 let hash = mem.metadata.content_hash.clone();
2076 store.put(Galaxy::Codex, &mem).unwrap();
2077
2078 assert!(
2080 store
2081 .find_by_content_hash(Galaxy::Codex, &hash)
2082 .unwrap()
2083 .is_some()
2084 );
2085
2086 store.delete(Galaxy::Codex, id).unwrap();
2088
2089 assert!(
2091 store
2092 .find_by_content_hash(Galaxy::Codex, &hash)
2093 .unwrap()
2094 .is_none()
2095 );
2096
2097 let query = MemoryQuery::new().with_tags(vec!["tag1".into()]);
2099 let results = store.query(Galaxy::Codex, &query).unwrap();
2100 assert!(results.is_empty());
2101 }
2102
2103 #[test]
2104 fn put_batch_updates_indexes() {
2105 let tmp = tempfile::tempdir().unwrap();
2106 let store = MemoryStore::open_default(tmp.path()).unwrap();
2107
2108 let memories: Vec<Memory> = (0..5)
2109 .map(|i| {
2110 Memory::new(Galaxy::Codex, format!("batch-{i}"))
2111 .with_tags(vec![format!("tag{i}")])
2112 .with_importance(i as f32 * 0.2)
2113 })
2114 .collect();
2115 store.put_batch(Galaxy::Codex, &memories).unwrap();
2116
2117 for i in 0..5 {
2118 let query = MemoryQuery::new().with_tags(vec![format!("tag{i}")]);
2119 let results = store.query(Galaxy::Codex, &query).unwrap();
2120 assert_eq!(results.len(), 1, "tag{i} should have 1 result");
2121 }
2122 }
2123
2124 #[test]
2125 fn find_by_content_hash_indexed_matches_scan() {
2126 let tmp = tempfile::tempdir().unwrap();
2127 let store = MemoryStore::open_default(tmp.path()).unwrap();
2128
2129 let mem = Memory::new(Galaxy::Codex, "dedup test".into());
2130 let id = mem.metadata.id;
2131 let hash = mem.metadata.content_hash.clone();
2132 store.put(Galaxy::Codex, &mem).unwrap();
2133
2134 let indexed = store.find_by_content_hash(Galaxy::Codex, &hash).unwrap();
2135 let scanned = store
2136 .find_by_content_hash_scan(Galaxy::Codex, &hash)
2137 .unwrap();
2138
2139 assert_eq!(indexed, scanned);
2140 assert_eq!(indexed, Some(id));
2141 }
2142
2143 #[test]
2144 fn put_dedup_uses_index() {
2145 let tmp = tempfile::tempdir().unwrap();
2146 let store = MemoryStore::open_default(tmp.path()).unwrap();
2147
2148 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
2149 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
2150
2151 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
2152 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
2153
2154 assert_eq!(id1, id2, "dedup should return same ID for same content");
2155 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
2156 }
2157
2158 #[test]
2159 fn put_semantic_updates_coord5d() {
2160 let tmp = tempfile::tempdir().unwrap();
2161 let store = MemoryStore::open_default(tmp.path()).unwrap();
2162
2163 let mut mem = Memory::new(
2164 Galaxy::Codex,
2165 "The algorithm computes data using a systematic method".to_string(),
2166 );
2167 let original_coord = mem.metadata.coord5d.clone();
2168 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2169
2170 assert_ne!(
2172 mem.metadata.coord5d.x, original_coord.x,
2173 "semantic encoding should change x"
2174 );
2175 assert_ne!(
2176 mem.metadata.coord5d.y, original_coord.y,
2177 "semantic encoding should change y"
2178 );
2179
2180 let retrieved = store.get(Galaxy::Codex, mem.metadata.id).unwrap().unwrap();
2182 assert_eq!(retrieved.metadata.coord5d.x, mem.metadata.coord5d.x);
2183 }
2184
2185 #[test]
2186 fn put_semantic_preserves_temporal_and_importance() {
2187 let tmp = tempfile::tempdir().unwrap();
2188 let store = MemoryStore::open_default(tmp.path()).unwrap();
2189
2190 let mut mem = Memory::new(Galaxy::Codex, "test content".into()).with_importance(0.8);
2191 mem.metadata.coord5d.w = 0.6;
2192 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2193
2194 assert!((mem.metadata.coord5d.w - 0.6).abs() < f32::EPSILON);
2195 assert!((mem.metadata.coord5d.v - 0.8).abs() < f32::EPSILON);
2196 }
2197
2198 #[test]
2199 fn find_similar_returns_nearest_first() {
2200 let tmp = tempfile::tempdir().unwrap();
2201 let store = MemoryStore::open_default(tmp.path()).unwrap();
2202
2203 let mut logic_mem = Memory::new(
2204 Galaxy::Codex,
2205 "The algorithm computes data using systematic logic and analysis".to_string(),
2206 );
2207 store.put_semantic(Galaxy::Codex, &mut logic_mem).unwrap();
2208
2209 let mut emotion_mem = Memory::new(
2210 Galaxy::Codex,
2211 "I feel love and joy with deep passion and empathy in my heart".to_string(),
2212 );
2213 store.put_semantic(Galaxy::Codex, &mut emotion_mem).unwrap();
2214
2215 let results = store
2217 .find_similar(Galaxy::Codex, "algorithm data systematic method", 10)
2218 .unwrap();
2219 assert!(!results.is_empty());
2220 assert_eq!(results[0].0.metadata.id, logic_mem.metadata.id);
2221
2222 let results = store
2224 .find_similar(Galaxy::Codex, "love joy passion heart feeling", 10)
2225 .unwrap();
2226 assert!(!results.is_empty());
2227 assert_eq!(results[0].0.metadata.id, emotion_mem.metadata.id);
2228 }
2229
2230 #[test]
2231 fn find_similar_empty_galaxy() {
2232 let tmp = tempfile::tempdir().unwrap();
2233 let store = MemoryStore::open_default(tmp.path()).unwrap();
2234
2235 let results = store.find_similar(Galaxy::Codex, "anything", 10).unwrap();
2236 assert!(results.is_empty());
2237 }
2238
2239 #[test]
2240 fn find_similar_respects_limit() {
2241 let tmp = tempfile::tempdir().unwrap();
2242 let store = MemoryStore::open_default(tmp.path()).unwrap();
2243
2244 for i in 0..5 {
2245 let mut mem = Memory::new(Galaxy::Codex, format!("algorithm data method {i}"));
2246 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2247 }
2248
2249 let results = store
2250 .find_similar(Galaxy::Codex, "algorithm data", 3)
2251 .unwrap();
2252 assert_eq!(results.len(), 3);
2253 }
2254
2255 #[test]
2256 fn semantic_encoder_accessible() {
2257 let tmp = tempfile::tempdir().unwrap();
2258 let store = MemoryStore::open_default(tmp.path()).unwrap();
2259
2260 let scores = store.semantic_encoder().encode("algorithm data logic");
2261 assert!(scores.x < 0.5);
2263 }
2264
2265 #[test]
2266 fn put_raw_batch_writes_atomically() {
2267 let tmp = tempfile::tempdir().unwrap();
2268 let store = MemoryStore::open_default(tmp.path()).unwrap();
2269
2270 let entries: &[(&[u8], &[u8])] =
2271 &[(b"key1", b"val1"), (b"key2", b"val2"), (b"key3", b"val3")];
2272 store.put_raw_batch(Galaxy::Karma, entries).unwrap();
2273
2274 assert_eq!(
2275 store.get_raw(Galaxy::Karma, b"key1").unwrap().unwrap(),
2276 b"val1"
2277 );
2278 assert_eq!(
2279 store.get_raw(Galaxy::Karma, b"key2").unwrap().unwrap(),
2280 b"val2"
2281 );
2282 assert_eq!(
2283 store.get_raw(Galaxy::Karma, b"key3").unwrap().unwrap(),
2284 b"val3"
2285 );
2286 }
2287
2288 #[test]
2289 fn put_raw_batch_empty_is_noop() {
2290 let tmp = tempfile::tempdir().unwrap();
2291 let store = MemoryStore::open_default(tmp.path()).unwrap();
2292
2293 store.put_raw_batch(Galaxy::Karma, &[]).unwrap();
2294 assert_eq!(store.count(Galaxy::Karma).unwrap(), 0);
2295 }
2296
2297 #[test]
2298 fn entry_limit_rejects_excess_writes() {
2299 let tmp = tempfile::tempdir().unwrap();
2300 let store = MemoryStore::open_default(tmp.path())
2301 .unwrap()
2302 .with_entry_limit(3);
2303
2304 for i in 0..3 {
2305 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
2306 store.put(Galaxy::Codex, &mem).unwrap();
2307 }
2308
2309 let mem = Memory::new(Galaxy::Codex, "overflow memory".to_string());
2311 let result = store.put(Galaxy::Codex, &mem);
2312 assert!(result.is_err(), "write beyond limit should be rejected");
2313 let err_msg = result.unwrap_err().to_string();
2314 assert!(
2315 err_msg.contains("entry limit reached"),
2316 "error should mention entry limit: {err_msg}"
2317 );
2318 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
2319 }
2320
2321 #[test]
2322 fn entry_limit_per_galaxy_independent() {
2323 let tmp = tempfile::tempdir().unwrap();
2324 let store = MemoryStore::open_default(tmp.path())
2325 .unwrap()
2326 .with_entry_limit(2);
2327
2328 for i in 0..2 {
2330 let mem = Memory::new(Galaxy::Codex, format!("codex {i}"));
2331 store.put(Galaxy::Codex, &mem).unwrap();
2332 }
2333
2334 let mem = Memory::new(Galaxy::Research, "science memory".to_string());
2336 let result = store.put(Galaxy::Research, &mem);
2337 assert!(
2338 result.is_ok(),
2339 "different galaxy should not be affected by limit"
2340 );
2341 }
2342
2343 #[test]
2344 fn entry_limit_none_allows_unlimited() {
2345 let tmp = tempfile::tempdir().unwrap();
2346 let store = MemoryStore::open_default(tmp.path()).unwrap();
2347
2348 for i in 0..50 {
2350 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
2351 store.put(Galaxy::Codex, &mem).unwrap();
2352 }
2353 assert_eq!(store.count(Galaxy::Codex).unwrap(), 50);
2354 }
2355
2356 #[test]
2357 fn map_full_error_is_graceful() {
2358 let tmp = tempfile::tempdir().unwrap();
2362 let store = MemoryStore::open(tmp.path(), 512 * 1024).unwrap();
2363
2364 let mut written = 0;
2366 let mut got_map_full = false;
2367 for i in 0..1000 {
2368 let mem = Memory::new(
2369 Galaxy::Codex,
2370 format!("memory content {i} {}", "with padding ".repeat(50)),
2371 );
2372 match store.put(Galaxy::Codex, &mem) {
2373 Ok(()) => written += 1,
2374 Err(e) => {
2375 let msg = e.to_string();
2376 if msg.contains("map full") {
2377 got_map_full = true;
2378 break;
2379 }
2380 break;
2382 }
2383 }
2384 }
2385
2386 assert!(
2387 got_map_full || written < 1000,
2388 "should eventually hit map full or error"
2389 );
2390 assert!(written > 0, "should have written at least some memories");
2391 }
2392
2393 #[test]
2394 fn test_find_across_galaxies() {
2395 let tmp = tempfile::tempdir().unwrap();
2396 let store = MemoryStore::open_default(tmp.path()).unwrap();
2397
2398 let mem = Memory::new(Galaxy::Research, "Cross-galaxy research memo".into());
2399 let id = mem.metadata.id;
2400 store.put(Galaxy::Research, &mem).unwrap();
2401
2402 let found = store.find_across_galaxies(id).unwrap();
2403 assert!(found.is_some());
2404 let (galaxy, retrieved) = found.unwrap();
2405 assert_eq!(galaxy, Galaxy::Research);
2406 assert_eq!(retrieved.metadata.id, id);
2407 assert_eq!(retrieved.content, "Cross-galaxy research memo");
2408
2409 assert!(
2411 store
2412 .find_across_galaxies(uuid::Uuid::new_v4())
2413 .unwrap()
2414 .is_none()
2415 );
2416 }
2417}