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(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| rmp_serde::from_slice::<Memory>(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 = rmp_serde::from_slice(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) = rmp_serde::from_slice::<Memory>(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 rmp_serde::from_slice::<Memory>(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 let db = self.galaxy_db(galaxy)?;
615 let tx = self
616 .env
617 .begin_ro_txn()
618 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
619
620 let mut cursor = tx
621 .open_ro_cursor(db)
622 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
623
624 let mut memories = Vec::new();
625 for (i, (_key, val)) in cursor.iter().enumerate() {
626 match rmp_serde::from_slice::<Memory>(val) {
627 Ok(memory) => memories.push(memory),
628 Err(e) => {
629 tracing::warn!(
630 "Skipping corrupted entry at index {i} in galaxy {:?}: {e}",
631 galaxy
632 );
633 }
634 }
635 }
636
637 drop(cursor);
638 tx.commit()
639 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
640 Ok(memories)
641 }
642
643 pub fn count(&self, galaxy: Galaxy) -> Result<usize> {
645 let db = self.galaxy_db(galaxy)?;
646 let tx = self
647 .env
648 .begin_ro_txn()
649 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
650 let mut cursor = tx
651 .open_ro_cursor(db)
652 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
653 let count = cursor.iter().count();
654 drop(cursor);
655 tx.commit()
656 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
657 Ok(count)
658 }
659
660 pub fn clear_galaxy(&self, galaxy: Galaxy) -> Result<usize> {
664 let db = self.galaxy_db(galaxy)?;
665
666 let mut tx = self
667 .env
668 .begin_rw_txn()
669 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
670
671 let mut cursor = tx
672 .open_ro_cursor(db)
673 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
674
675 let mut count = 0usize;
676 let keys_to_delete: Vec<(Vec<u8>, Memory)> = cursor
677 .iter()
678 .filter_map(|(key, val)| {
679 if let Ok(memory) = rmp_serde::from_slice::<Memory>(val) {
680 Some((key.to_vec(), memory))
681 } else {
682 None
683 }
684 })
685 .collect();
686
687 drop(cursor);
688
689 for (key, memory) in &keys_to_delete {
690 let _ = self.index_dbs.remove(&mut tx, galaxy, memory);
691 tx.del(db, &key, None)
692 .map_err(|e| CoreError::Memory(format!("LMDB del failed: {e}")))?;
693 count += 1;
694 }
695
696 tx.commit()
697 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
698 self.mutation_count
699 .fetch_add(count as u64, Ordering::Relaxed);
700 Ok(count)
701 }
702
703 pub fn batch_put(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<usize> {
706 if memories.is_empty() {
707 return Ok(0);
708 }
709
710 let db = self.galaxy_db(galaxy)?;
711
712 let mut tx = self
713 .env
714 .begin_rw_txn()
715 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
716
717 let mut count = 0usize;
718 for memory in memories {
719 let key = memory.metadata.id.as_bytes();
720 let val = rmp_serde::to_vec(memory)
721 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
722 match tx.put(db, key, &val, WriteFlags::default()) {
723 Ok(()) => {}
724 Err(lmdb::Error::MapFull) => {
725 tx.abort();
726 return Err(CoreError::Memory(format!(
727 "LMDB map full: galaxy {}, consider growing map size",
728 galaxy.db_name()
729 )));
730 }
731 Err(e) => {
732 tx.abort();
733 return Err(CoreError::Memory(format!("LMDB put failed: {e}")));
734 }
735 }
736 self.index_dbs.add(&mut tx, galaxy, memory)?;
737 count += 1;
738 }
739
740 tx.commit()
741 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
742 self.mutation_count
743 .fetch_add(count as u64, Ordering::Relaxed);
744 Ok(count)
745 }
746
747 pub fn get_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<Option<Vec<u8>>> {
749 let db = self.galaxy_db(galaxy)?;
750 let tx = self
751 .env
752 .begin_ro_txn()
753 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
754 match tx.get(db, &key) {
755 Ok(bytes) => {
756 let data = bytes.to_vec();
757 tx.commit()
758 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
759 Ok(Some(data))
760 }
761 Err(lmdb::Error::NotFound) => {
762 tx.commit()
763 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
764 Ok(None)
765 }
766 Err(e) => Err(CoreError::Memory(format!("LMDB get_raw failed: {e}"))),
767 }
768 }
769
770 pub fn put_raw(&self, galaxy: Galaxy, key: &[u8], val: &[u8]) -> Result<()> {
772 let db = self.galaxy_db(galaxy)?;
773 let mut tx = self
774 .env
775 .begin_rw_txn()
776 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
777 tx.put(db, &key, &val, lmdb::WriteFlags::default())
778 .map_err(|e| CoreError::Memory(format!("LMDB put_raw failed: {e}")))?;
779 tx.commit()
780 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
781 self.mutation_count.fetch_add(1, Ordering::Relaxed);
782 Ok(())
783 }
784
785 pub fn delete_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<bool> {
788 let db = self.galaxy_db(galaxy)?;
789 let mut tx = self
790 .env
791 .begin_rw_txn()
792 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
793 let deleted = tx.del(db, &key, None).is_ok();
794 tx.commit()
795 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
796 if deleted {
797 self.mutation_count.fetch_add(1, Ordering::Relaxed);
798 }
799 Ok(deleted)
800 }
801
802 pub fn put_raw_batch(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
805 self.put_raw_batch_impl(galaxy, entries)?;
806 self.mutation_count
807 .fetch_add(entries.len() as u64, Ordering::Relaxed);
808 Ok(())
809 }
810
811 pub fn put_raw_batch_untracked(
821 &self,
822 galaxy: Galaxy,
823 entries: &[(&[u8], &[u8])],
824 ) -> Result<()> {
825 self.put_raw_batch_impl(galaxy, entries)
826 }
827
828 fn put_raw_batch_impl(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
829 let db = self.galaxy_db(galaxy)?;
830 let mut tx = self
831 .env
832 .begin_rw_txn()
833 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
834 for (key, val) in entries {
835 tx.put(db, key, val, WriteFlags::default())
836 .map_err(|e| CoreError::Memory(format!("LMDB put_raw_batch failed: {e}")))?;
837 }
838 tx.commit()
839 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
840 Ok(())
841 }
842
843 pub fn find_by_content_hash(&self, galaxy: Galaxy, hash: &str) -> Result<Option<uuid::Uuid>> {
849 let tx = self
850 .env
851 .begin_ro_txn()
852 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
853 let result = self.index_dbs.find_by_content_hash(&tx, galaxy, hash)?;
854 tx.commit()
855 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
856 Ok(result)
857 }
858
859 pub fn find_by_content_hash_scan(
861 &self,
862 galaxy: Galaxy,
863 hash: &str,
864 ) -> Result<Option<uuid::Uuid>> {
865 let memories = self.scan(galaxy, 10_000)?;
866 for mem in memories {
867 if mem.metadata.content_hash == hash {
868 return Ok(Some(mem.metadata.id));
869 }
870 }
871 Ok(None)
872 }
873
874 pub fn put_dedup(&self, galaxy: Galaxy, memory: &Memory) -> Result<uuid::Uuid> {
878 if let Some(existing_id) =
879 self.find_by_content_hash(galaxy, &memory.metadata.content_hash)?
880 {
881 return Ok(existing_id);
882 }
883 let id = memory.metadata.id;
884 self.put(galaxy, memory)?;
885 Ok(id)
886 }
887
888 pub fn put_batch(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<()> {
893 let db = self.galaxy_db(galaxy)?;
894 let mut tx = self
895 .env
896 .begin_rw_txn()
897 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
898
899 for memory in memories {
900 let key = memory.metadata.id.as_bytes();
901 let val = rmp_serde::to_vec(memory)
902 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
903 tx.put(db, key, &val, WriteFlags::default())
904 .map_err(|e| CoreError::Memory(format!("LMDB put_batch failed: {e}")))?;
905 self.index_dbs.add(&mut tx, galaxy, memory)?;
906 }
907
908 tx.commit()
909 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
910 self.mutation_count
911 .fetch_add(memories.len() as u64, Ordering::Relaxed);
912 Ok(())
913 }
914
915 pub fn query(&self, galaxy: Galaxy, query: &MemoryQuery) -> Result<Vec<Memory>> {
922 if query.content_substring.is_none()
925 && query.tags.len() == 1
926 && query.min_importance.is_none()
927 && query.max_importance.is_none()
928 && query.created_after.is_none()
929 && query.created_before.is_none()
930 {
931 return self.query_by_tag_indexed(galaxy, &query.tags[0], query.limit);
932 }
933
934 if query.content_substring.is_none()
935 && query.tags.is_empty()
936 && let Some(min) = query.min_importance
937 && let Some(max) = query.max_importance
938 && query.created_after.is_none()
939 && query.created_before.is_none()
940 {
941 return self.query_by_importance_indexed(galaxy, min, max, query.limit);
942 }
943
944 if query.content_substring.is_none()
945 && query.tags.is_empty()
946 && query.min_importance.is_none()
947 && query.max_importance.is_none()
948 && let Some(after) = query.created_after
949 && let Some(before) = query.created_before
950 {
951 return self.query_by_time_indexed(galaxy, after, before, query.limit);
952 }
953
954 let memories = self.scan(galaxy, 10_000)?;
956 let mut results = Vec::new();
957 for mem in memories {
958 if query.matches(&mem) {
959 results.push(mem);
960 if results.len() >= query.limit {
961 break;
962 }
963 }
964 }
965 Ok(results)
966 }
967
968 fn query_by_tag_indexed(&self, galaxy: Galaxy, tag: &str, limit: usize) -> Result<Vec<Memory>> {
970 let db = self.galaxy_db(galaxy)?;
971 let tx = self
972 .env
973 .begin_ro_txn()
974 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
975 let ids = self.index_dbs.find_by_tag(&tx, galaxy, tag)?;
976 let mut results = Vec::new();
977 for id in &ids {
978 if results.len() >= limit {
979 break;
980 }
981 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
982 if let Ok(mem) = rmp_serde::from_slice::<Memory>(bytes) {
983 results.push(mem);
984 }
985 }
986 }
987 tx.commit()
988 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
989 Ok(results)
990 }
991
992 fn query_by_importance_indexed(
994 &self,
995 galaxy: Galaxy,
996 min: f32,
997 max: f32,
998 limit: usize,
999 ) -> Result<Vec<Memory>> {
1000 let db = self.galaxy_db(galaxy)?;
1001 let tx = self
1002 .env
1003 .begin_ro_txn()
1004 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1005 let ids = self
1006 .index_dbs
1007 .find_by_importance_range(&tx, galaxy, min, max)?;
1008 let mut results = Vec::new();
1009 for id in &ids {
1010 if results.len() >= limit {
1011 break;
1012 }
1013 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1014 if let Ok(mem) = rmp_serde::from_slice::<Memory>(bytes) {
1015 results.push(mem);
1016 }
1017 }
1018 }
1019 tx.commit()
1020 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1021 Ok(results)
1022 }
1023
1024 fn query_by_time_indexed(
1026 &self,
1027 galaxy: Galaxy,
1028 after: chrono::DateTime<chrono::Utc>,
1029 before: chrono::DateTime<chrono::Utc>,
1030 limit: usize,
1031 ) -> Result<Vec<Memory>> {
1032 let db = self.galaxy_db(galaxy)?;
1033 let tx = self
1034 .env
1035 .begin_ro_txn()
1036 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1037 let ids = self
1038 .index_dbs
1039 .find_by_time_range(&tx, galaxy, after, before)?;
1040 let mut results = Vec::new();
1041 for id in &ids {
1042 if results.len() >= limit {
1043 break;
1044 }
1045 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1046 if let Ok(mem) = rmp_serde::from_slice::<Memory>(bytes) {
1047 results.push(mem);
1048 }
1049 }
1050 }
1051 tx.commit()
1052 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1053 Ok(results)
1054 }
1055
1056 pub fn put_semantic(&self, galaxy: Galaxy, memory: &mut Memory) -> Result<()> {
1064 let temporal_weight = memory.metadata.coord5d.w;
1065 let importance = memory.metadata.importance;
1066 memory.metadata.coord5d =
1067 self.semantic_encoder
1068 .encode_coordinate(&memory.content, temporal_weight, importance);
1069 self.put(galaxy, memory)
1070 }
1071
1072 pub fn find_similar(
1077 &self,
1078 galaxy: Galaxy,
1079 query_text: &str,
1080 limit: usize,
1081 ) -> Result<Vec<(Memory, f32)>> {
1082 let query_coord = self
1083 .semantic_encoder
1084 .encode_coordinate(query_text, 0.5, 0.5);
1085 let memories = self.scan(galaxy, 10_000)?;
1086 let mut results: Vec<(Memory, f32)> = memories
1087 .into_iter()
1088 .map(|m| {
1089 let dist = query_coord.semantic_distance_to(&m.metadata.coord5d);
1090 (m, dist)
1091 })
1092 .collect();
1093 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1094 results.truncate(limit);
1095 Ok(results)
1096 }
1097
1098 pub fn put_embedding(&self, memory_id: uuid::Uuid, embedding: &[f32]) -> Result<()> {
1103 let db = self.galaxy_db(Galaxy::Embeddings)?;
1104 let key = memory_id.as_bytes();
1105 let val = encode_embedding(embedding);
1106
1107 let mut tx = self
1108 .env
1109 .begin_rw_txn()
1110 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1111 tx.put(db, key, &val, WriteFlags::default())
1112 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding failed: {e}")))?;
1113 tx.commit()
1114 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1115 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1116 Ok(())
1117 }
1118
1119 pub fn get_embedding(&self, memory_id: uuid::Uuid) -> Result<Option<Vec<f32>>> {
1121 let db = self.galaxy_db(Galaxy::Embeddings)?;
1122 let key = memory_id.as_bytes();
1123
1124 let tx = self
1125 .env
1126 .begin_ro_txn()
1127 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1128 match tx.get(db, key) {
1129 Ok(bytes) => {
1130 let embedding = decode_embedding(bytes);
1131 tx.commit()
1132 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1133 Ok(Some(embedding))
1134 }
1135 Err(lmdb::Error::NotFound) => {
1136 tx.commit()
1137 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1138 Ok(None)
1139 }
1140 Err(e) => Err(CoreError::Memory(format!("LMDB get_embedding failed: {e}"))),
1141 }
1142 }
1143
1144 pub fn delete_embedding(&self, memory_id: uuid::Uuid) -> Result<bool> {
1146 let db = self.galaxy_db(Galaxy::Embeddings)?;
1147 let key = memory_id.as_bytes();
1148
1149 let mut tx = self
1150 .env
1151 .begin_rw_txn()
1152 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1153 let exists = tx.get(db, key).is_ok();
1154 if exists {
1155 tx.del(db, key, None)
1156 .map_err(|e| CoreError::Memory(format!("LMDB del_embedding failed: {e}")))?;
1157 }
1158 tx.commit()
1159 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1160 if exists {
1161 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1162 }
1163 Ok(exists)
1164 }
1165
1166 pub fn put_embedding_cache(&self, cache_key: &str, embedding: &[f32]) -> Result<()> {
1172 let mut tx = self
1173 .env
1174 .begin_rw_txn()
1175 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1176 tx.put(
1177 self.embedding_cache_db,
1178 &cache_key.as_bytes().to_vec(),
1179 &encode_embedding(embedding),
1180 WriteFlags::default(),
1181 )
1182 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1183 tx.commit()
1184 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1185 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1186 Ok(())
1187 }
1188
1189 pub fn put_embedding_cache_batch(&self, entries: &[(String, Vec<f32>)]) -> Result<()> {
1191 if entries.is_empty() {
1192 return Ok(());
1193 }
1194 let mut tx = self
1195 .env
1196 .begin_rw_txn()
1197 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1198 for (key, embedding) in entries {
1199 tx.put(
1200 self.embedding_cache_db,
1201 &key.as_bytes().to_vec(),
1202 &encode_embedding(embedding),
1203 WriteFlags::default(),
1204 )
1205 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1206 }
1207 tx.commit()
1208 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1209 self.mutation_count
1210 .fetch_add(entries.len() as u64, Ordering::Relaxed);
1211 Ok(())
1212 }
1213
1214 pub fn get_embedding_cache(&self, cache_key: &str) -> Result<Option<Vec<f32>>> {
1216 let tx = self
1217 .env
1218 .begin_ro_txn()
1219 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1220 match tx.get(self.embedding_cache_db, &cache_key.as_bytes().to_vec()) {
1221 Ok(bytes) => {
1222 let embedding = decode_embedding(bytes);
1223 tx.commit()
1224 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1225 Ok(Some(embedding))
1226 }
1227 Err(lmdb::Error::NotFound) => {
1228 tx.commit()
1229 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1230 Ok(None)
1231 }
1232 Err(e) => Err(CoreError::Memory(format!(
1233 "LMDB get_embedding_cache failed: {e}"
1234 ))),
1235 }
1236 }
1237
1238 pub fn get_embedding_cache_batch(&self, keys: &[String]) -> Result<Vec<Option<Vec<f32>>>> {
1241 let tx = self
1242 .env
1243 .begin_ro_txn()
1244 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1245 let mut out = Vec::with_capacity(keys.len());
1246 for key in keys {
1247 out.push(
1248 tx.get(self.embedding_cache_db, &key.as_bytes().to_vec())
1249 .ok()
1250 .map(decode_embedding),
1251 );
1252 }
1253 tx.commit()
1254 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1255 Ok(out)
1256 }
1257
1258 pub fn embedding_cache_count(&self) -> Result<u64> {
1260 let tx = self
1261 .env
1262 .begin_ro_txn()
1263 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1264 let mut cursor = tx
1265 .open_ro_cursor(self.embedding_cache_db)
1266 .map_err(|e| CoreError::Memory(format!("LMDB cursor embedding_cache failed: {e}")))?;
1267 let mut count = 0u64;
1268 for _ in cursor.iter() {
1269 count += 1;
1270 }
1271 Ok(count)
1272 }
1273
1274 pub fn record_revision(
1280 &self,
1281 galaxy: Galaxy,
1282 id: MemoryId,
1283 old_hash: &str,
1284 new_hash: &str,
1285 actor: crate::revision::RevisionActor,
1286 ) -> Result<crate::revision::MemoryRevision> {
1287 let seq = self.revisions(galaxy, id)?.len() as u32;
1288 let entry = crate::revision::MemoryRevision {
1289 seq,
1290 timestamp: wm_core::time::now_unix_secs(),
1291 old_hash: old_hash.to_string(),
1292 new_hash: new_hash.to_string(),
1293 actor_session: actor.session,
1294 actor_user: actor.user,
1295 actor_compartment: actor.compartment,
1296 };
1297 let key = crate::revision::revision_key(galaxy, id, seq);
1298 let val = serde_json::to_vec(&entry)
1299 .map_err(|e| CoreError::Memory(format!("revision serialize failed: {e}")))?;
1300 let mut tx = self
1301 .env
1302 .begin_rw_txn()
1303 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1304 tx.put(self.revisions_db, &key, &val, WriteFlags::default())
1305 .map_err(|e| CoreError::Memory(format!("LMDB put revision failed: {e}")))?;
1306 tx.commit()
1307 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1308 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1309 Ok(entry)
1310 }
1311
1312 pub fn revisions(
1315 &self,
1316 galaxy: Galaxy,
1317 id: MemoryId,
1318 ) -> Result<Vec<crate::revision::MemoryRevision>> {
1319 const MDB_GET_CURRENT: u32 = 4;
1324 const MDB_NEXT: u32 = 8;
1325 const MDB_SET_RANGE: u32 = 17;
1326 let prefix = crate::revision::revision_prefix(galaxy, id);
1327 let tx = self
1328 .env
1329 .begin_ro_txn()
1330 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1331 let cursor = tx
1332 .open_ro_cursor(self.revisions_db)
1333 .map_err(|e| CoreError::Memory(format!("LMDB cursor revisions failed: {e}")))?;
1334 let mut out = Vec::new();
1335 if cursor.get(Some(&prefix), None, MDB_SET_RANGE).is_ok() {
1336 while let Ok((key, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1337 if !key.is_some_and(|k| k.starts_with(&prefix)) {
1340 break;
1341 }
1342 let entry: crate::revision::MemoryRevision = serde_json::from_slice(val)
1343 .map_err(|e| CoreError::Memory(format!("revision deserialize failed: {e}")))?;
1344 out.push(entry);
1345 if out.len() >= 10_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1347 break;
1348 }
1349 }
1350 }
1351 drop(cursor);
1352 tx.commit()
1353 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1354 Ok(out)
1355 }
1356
1357 pub fn verify_revision_chain(
1360 &self,
1361 galaxy: Galaxy,
1362 id: MemoryId,
1363 current_hash: &str,
1364 ) -> Result<crate::revision::RevisionChainReport> {
1365 let entries = self.revisions(galaxy, id)?;
1366 Ok(crate::revision::verify_chain(&entries, current_hash))
1367 }
1368
1369 pub fn record_attestation(
1377 &self,
1378 galaxy: Galaxy,
1379 id: MemoryId,
1380 entry: &crate::attestation::RecordAttestation,
1381 ) -> Result<()> {
1382 let key = crate::attestation::attestation_key(galaxy, id);
1383 let val = serde_json::to_vec(entry)
1384 .map_err(|e| CoreError::Memory(format!("attestation serialize failed: {e}")))?;
1385 let mut tx = self
1386 .env
1387 .begin_rw_txn()
1388 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1389 tx.put(self.attestations_db, &key, &val, WriteFlags::default())
1390 .map_err(|e| CoreError::Memory(format!("LMDB put attestation failed: {e}")))?;
1391 tx.commit()
1392 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1393 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1394 Ok(())
1395 }
1396
1397 pub fn attestation(
1400 &self,
1401 galaxy: Galaxy,
1402 id: MemoryId,
1403 ) -> Result<Option<crate::attestation::RecordAttestation>> {
1404 let key = crate::attestation::attestation_key(galaxy, id);
1405 let tx = self
1406 .env
1407 .begin_ro_txn()
1408 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1409 let out =
1410 match tx.get(self.attestations_db, &key) {
1411 Ok(val) => Some(serde_json::from_slice(val).map_err(|e| {
1412 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1413 })?),
1414 Err(lmdb::Error::NotFound) => None,
1415 Err(e) => {
1416 return Err(CoreError::Memory(format!(
1417 "LMDB get attestation failed: {e}"
1418 )));
1419 }
1420 };
1421 drop(tx);
1422 Ok(out)
1423 }
1424
1425 pub fn scan_attestations(&self) -> Result<Vec<crate::attestation::RecordAttestation>> {
1428 const MDB_GET_CURRENT: u32 = 4;
1429 const MDB_NEXT: u32 = 8;
1430 const MDB_FIRST: u32 = 9;
1431 let tx = self
1432 .env
1433 .begin_ro_txn()
1434 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1435 let cursor = tx
1436 .open_ro_cursor(self.attestations_db)
1437 .map_err(|e| CoreError::Memory(format!("LMDB cursor attestations failed: {e}")))?;
1438 let mut out = Vec::new();
1439 if cursor.get(None, None, MDB_FIRST).is_ok() {
1440 while let Ok((_, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1441 let entry: crate::attestation::RecordAttestation = serde_json::from_slice(val)
1442 .map_err(|e| {
1443 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1444 })?;
1445 out.push(entry);
1446 if out.len() >= 1_000_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1448 break;
1449 }
1450 }
1451 }
1452 drop(cursor);
1453 tx.commit()
1454 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1455 Ok(out)
1456 }
1457
1458 pub fn verify_attestation(
1464 &self,
1465 galaxy: Galaxy,
1466 id: MemoryId,
1467 ) -> Result<crate::attestation::AttestationReport> {
1468 use crate::attestation::AttestationReport;
1469 let Some(att) = self.attestation(galaxy, id)? else {
1470 return Ok(AttestationReport {
1471 attested: false,
1472 signature_valid: false,
1473 matches_head: false,
1474 memory_present: self.get(galaxy, id)?.is_some(),
1475 breaks: vec!["no attestation recorded for this memory".to_string()],
1476 });
1477 };
1478 let mut breaks = Vec::new();
1479 let signature_valid = crate::attestation::verify_attestation(&att);
1480 if !signature_valid {
1481 breaks.push("signature does not verify against recorded pubkey".to_string());
1482 }
1483 let (matches_head, memory_present) = if let Some(memory) = self.get(galaxy, id)? {
1484 let matches = memory.metadata.content_hash == att.record_hash;
1485 if !matches {
1486 breaks.push(
1487 "attested record_hash != live content_hash (memory updated after attestation)"
1488 .to_string(),
1489 );
1490 }
1491 (matches, true)
1492 } else {
1493 breaks.push("attested memory id not present in galaxy".to_string());
1494 (false, false)
1495 };
1496 Ok(AttestationReport {
1497 attested: true,
1498 signature_valid,
1499 matches_head,
1500 memory_present,
1501 breaks,
1502 })
1503 }
1504
1505 pub fn attestation_sweep(
1509 &self,
1510 ) -> Result<
1511 Vec<(
1512 crate::attestation::RecordAttestation,
1513 crate::attestation::AttestationReport,
1514 )>,
1515 > {
1516 use crate::attestation::AttestationReport;
1517 let mut out = Vec::new();
1518 for att in self.scan_attestations()? {
1519 let parsed = match (
1520 Galaxy::from_db_name(&att.galaxy),
1521 uuid::Uuid::parse_str(&att.memory_id),
1522 ) {
1523 (Some(galaxy), Ok(id)) => Some((galaxy, id)),
1524 _ => None,
1525 };
1526 match parsed {
1527 Some((galaxy, id)) => out.push((att, self.verify_attestation(galaxy, id)?)),
1528 None => out.push((
1529 att,
1530 AttestationReport {
1531 attested: true,
1532 signature_valid: false,
1533 matches_head: false,
1534 memory_present: false,
1535 breaks: vec![
1536 "attestation row has unparseable galaxy or memory id".to_string(),
1537 ],
1538 },
1539 )),
1540 }
1541 }
1542 Ok(out)
1543 }
1544}
1545
1546#[cfg(test)]
1547mod tests {
1548 use super::*;
1549 use crate::content_hash;
1550
1551 #[test]
1552 fn open_and_create_galaxies() {
1553 let tmp = tempfile::tempdir().unwrap();
1554 let store = MemoryStore::open_default(tmp.path()).unwrap();
1555 for galaxy in Galaxy::all() {
1556 let _db = store.galaxy_db(galaxy).unwrap();
1557 }
1558 }
1559
1560 #[test]
1564 fn query_substring_filters_galaxy_wide() {
1565 let tmp = tempfile::tempdir().unwrap();
1566 let store = MemoryStore::open_default(tmp.path()).unwrap();
1567
1568 for (i, content) in [
1569 "the mesh joins at dawn",
1570 "unrelated content entirely",
1571 "MESH joins at dusk",
1572 ]
1573 .iter()
1574 .enumerate()
1575 {
1576 let mut m = Memory::new(Galaxy::Codex, content.to_string());
1577 m.metadata.importance = 0.5 + i as f32 / 10.0;
1578 store.put(Galaxy::Codex, &m).unwrap();
1579 }
1580
1581 let hits = store
1582 .query(
1583 Galaxy::Codex,
1584 &MemoryQuery::new().with_content_substring("mesh joins"),
1585 )
1586 .unwrap();
1587 assert_eq!(hits.len(), 2, "CI substring must match both: {hits:?}");
1588 assert!(
1589 hits.iter()
1590 .all(|m| m.content.to_lowercase().contains("mesh joins"))
1591 );
1592
1593 let none = store
1594 .query(
1595 Galaxy::Codex,
1596 &MemoryQuery::new().with_content_substring("quantum calendar"),
1597 )
1598 .unwrap();
1599 assert!(none.is_empty(), "no match must be an honest empty set");
1600
1601 let mut tagged = Memory::new(Galaxy::Codex, "mesh joins again".to_string());
1603 tagged.metadata.tags = vec!["mesh".into()];
1604 store.put(Galaxy::Codex, &tagged).unwrap();
1605 let combined = store
1606 .query(
1607 Galaxy::Codex,
1608 &MemoryQuery::new()
1609 .with_tags(vec!["mesh".into()])
1610 .with_content_substring("again"),
1611 )
1612 .unwrap();
1613 assert_eq!(combined.len(), 1);
1614 assert_eq!(combined[0].content, "mesh joins again");
1615 }
1616
1617 #[cfg(unix)]
1618 #[test]
1619 fn store_dir_has_restrictive_permissions() {
1620 let tmp = tempfile::tempdir().unwrap();
1621 let store_path = tmp.path().join("lmdb");
1622 let _store = MemoryStore::open_default(&store_path).unwrap();
1623 let perms = std::fs::metadata(&store_path).unwrap().permissions().mode();
1624 assert_eq!(
1625 perms & 0o777,
1626 0o700,
1627 "store directory should have 0700 permissions, got {:o}",
1628 perms & 0o777
1629 );
1630 }
1631
1632 #[test]
1633 fn put_get_delete_memory() {
1634 let tmp = tempfile::tempdir().unwrap();
1635 let store = MemoryStore::open_default(tmp.path()).unwrap();
1636
1637 let mem = Memory::new(Galaxy::Codex, "Hello world".to_string());
1638 let id = mem.metadata.id;
1639
1640 store.put(Galaxy::Codex, &mem).unwrap();
1641 let retrieved = store.get(Galaxy::Codex, id).unwrap();
1642 assert!(retrieved.is_some());
1643 assert_eq!(retrieved.unwrap().content, "Hello world");
1644
1645 let deleted = store.delete(Galaxy::Codex, id).unwrap();
1646 assert!(deleted);
1647
1648 let gone = store.get(Galaxy::Codex, id).unwrap();
1649 assert!(gone.is_none());
1650 }
1651
1652 #[test]
1653 fn scan_memories() {
1654 let tmp = tempfile::tempdir().unwrap();
1655 let store = MemoryStore::open_default(tmp.path()).unwrap();
1656
1657 for i in 0..5 {
1658 let mem = Memory::new(Galaxy::Codex, format!("memory-{i}"));
1659 store.put(Galaxy::Codex, &mem).unwrap();
1660 }
1661
1662 let all = store.scan(Galaxy::Codex, 100).unwrap();
1663 assert_eq!(all.len(), 5);
1664
1665 let limited = store.scan(Galaxy::Codex, 3).unwrap();
1666 assert_eq!(limited.len(), 3);
1667 }
1668
1669 #[test]
1670 fn overwrite_removes_stale_index_entries() {
1671 let tmp = tempfile::tempdir().unwrap();
1672 let store = MemoryStore::open_default(tmp.path()).unwrap();
1673
1674 let mut mem = Memory::new(Galaxy::Codex, "overwrite target".to_string());
1676 mem.metadata.tags = vec!["alpha".to_string()];
1677 mem.metadata.importance = 0.9;
1678 let id = mem.metadata.id;
1679 store.put(Galaxy::Codex, &mem).unwrap();
1680
1681 let mut updated = Memory::new(Galaxy::Codex, "overwritten content".to_string());
1683 updated.metadata.id = id;
1684 updated.metadata.tags = vec!["beta".to_string()];
1685 updated.metadata.importance = 0.1;
1686 store.put(Galaxy::Codex, &updated).unwrap();
1687
1688 let tx = store.env().begin_ro_txn().unwrap();
1690 let by_alpha = store
1691 .index_dbs()
1692 .find_by_tag(&tx, Galaxy::Codex, "alpha")
1693 .unwrap();
1694 let by_beta = store
1695 .index_dbs()
1696 .find_by_tag(&tx, Galaxy::Codex, "beta")
1697 .unwrap();
1698 assert!(
1699 by_alpha.is_empty(),
1700 "stale tag index entries must be removed on overwrite"
1701 );
1702 assert_eq!(by_beta, vec![id]);
1703
1704 let by_importance = store
1705 .index_dbs()
1706 .find_by_importance_range(&tx, Galaxy::Codex, 0.0, 0.2)
1707 .unwrap();
1708 assert!(
1709 by_importance.contains(&id),
1710 "new importance must be indexed"
1711 );
1712 let by_high = store
1713 .index_dbs()
1714 .find_by_importance_range(&tx, Galaxy::Codex, 0.8, 1.0)
1715 .unwrap();
1716 assert!(
1717 !by_high.contains(&id),
1718 "stale importance index entries must be removed on overwrite"
1719 );
1720
1721 let old_hash = content_hash("overwrite target");
1722 let new_hash = content_hash("overwritten content");
1723 assert_eq!(
1724 store
1725 .index_dbs()
1726 .find_by_content_hash(&tx, Galaxy::Codex, &old_hash)
1727 .unwrap(),
1728 None,
1729 "stale content-hash index entry must be removed"
1730 );
1731 assert_eq!(
1732 store
1733 .index_dbs()
1734 .find_by_content_hash(&tx, Galaxy::Codex, &new_hash)
1735 .unwrap(),
1736 Some(id)
1737 );
1738 }
1739
1740 #[test]
1741 fn count_memories() {
1742 let tmp = tempfile::tempdir().unwrap();
1743 let store = MemoryStore::open_default(tmp.path()).unwrap();
1744
1745 assert_eq!(store.count(Galaxy::Codex).unwrap(), 0);
1746
1747 for i in 0..3 {
1748 let mem = Memory::new(Galaxy::Codex, format!("count-{i}"));
1749 store.put(Galaxy::Codex, &mem).unwrap();
1750 }
1751
1752 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
1753 }
1754
1755 #[test]
1756 fn get_nonexistent_returns_none() {
1757 let tmp = tempfile::tempdir().unwrap();
1758 let store = MemoryStore::open_default(tmp.path()).unwrap();
1759 let result = store.get(Galaxy::Codex, uuid::Uuid::new_v4()).unwrap();
1760 assert!(result.is_none());
1761 }
1762
1763 #[test]
1764 fn raw_put_get() {
1765 let tmp = tempfile::tempdir().unwrap();
1766 let store = MemoryStore::open_default(tmp.path()).unwrap();
1767
1768 store
1769 .put_raw(Galaxy::Substrate, b"config:key", b"value123")
1770 .unwrap();
1771 let val = store.get_raw(Galaxy::Substrate, b"config:key").unwrap();
1772 assert_eq!(val, Some(b"value123".to_vec()));
1773 }
1774
1775 #[test]
1776 fn put_dedup_prevents_duplicates() {
1777 let tmp = tempfile::tempdir().unwrap();
1778 let store = MemoryStore::open_default(tmp.path()).unwrap();
1779
1780 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
1781 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
1782
1783 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
1784 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
1785
1786 assert_eq!(id1, id2, "dedup should return same ID for same content");
1787 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
1788 }
1789
1790 #[test]
1791 fn put_dedup_allows_different_content() {
1792 let tmp = tempfile::tempdir().unwrap();
1793 let store = MemoryStore::open_default(tmp.path()).unwrap();
1794
1795 let mem1 = Memory::new(Galaxy::Codex, "content A".into());
1796 store.put_dedup(Galaxy::Codex, &mem1).unwrap();
1797
1798 let mem2 = Memory::new(Galaxy::Codex, "content B".into());
1799 store.put_dedup(Galaxy::Codex, &mem2).unwrap();
1800
1801 assert_eq!(store.count(Galaxy::Codex).unwrap(), 2);
1802 }
1803
1804 #[test]
1805 fn put_batch_atomic_write() {
1806 let tmp = tempfile::tempdir().unwrap();
1807 let store = MemoryStore::open_default(tmp.path()).unwrap();
1808
1809 let memories: Vec<Memory> = (0..10)
1810 .map(|i| Memory::new(Galaxy::Codex, format!("batch-{i}")))
1811 .collect();
1812
1813 store.put_batch(Galaxy::Codex, &memories).unwrap();
1814 assert_eq!(store.count(Galaxy::Codex).unwrap(), 10);
1815 }
1816
1817 #[test]
1818 fn query_by_tags() {
1819 let tmp = tempfile::tempdir().unwrap();
1820 let store = MemoryStore::open_default(tmp.path()).unwrap();
1821
1822 let mem1 = Memory::new(Galaxy::Codex, "tagged memory".into())
1823 .with_tags(vec!["rust".into(), "memory".into()]);
1824 let mem2 =
1825 Memory::new(Galaxy::Codex, "other memory".into()).with_tags(vec!["python".into()]);
1826 store.put(Galaxy::Codex, &mem1).unwrap();
1827 store.put(Galaxy::Codex, &mem2).unwrap();
1828
1829 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
1830 let results = store.query(Galaxy::Codex, &query).unwrap();
1831 assert_eq!(results.len(), 1);
1832 assert_eq!(results[0].content, "tagged memory");
1833 }
1834
1835 #[test]
1836 fn query_by_importance_range() {
1837 let tmp = tempfile::tempdir().unwrap();
1838 let store = MemoryStore::open_default(tmp.path()).unwrap();
1839
1840 store
1841 .put(
1842 Galaxy::Codex,
1843 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
1844 )
1845 .unwrap();
1846 store
1847 .put(
1848 Galaxy::Codex,
1849 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
1850 )
1851 .unwrap();
1852 store
1853 .put(
1854 Galaxy::Codex,
1855 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
1856 )
1857 .unwrap();
1858
1859 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
1860 let results = store.query(Galaxy::Codex, &query).unwrap();
1861 assert_eq!(results.len(), 1);
1862 assert_eq!(results[0].content, "mid");
1863 }
1864
1865 #[test]
1866 fn memory_query_one_sided_time_bounds() {
1867 let old = Memory::new(Galaxy::Codex, "old".into());
1870 let mut recent = Memory::new(Galaxy::Codex, "recent".into());
1871 recent.metadata.created_at = old.metadata.created_at + chrono::Duration::days(30);
1872
1873 let cutoff = old.metadata.created_at + chrono::Duration::days(10);
1874 let after = MemoryQuery::new().with_created_after(cutoff);
1875 assert!(!after.matches(&old), "pre-cutoff memory must not match");
1876 assert!(after.matches(&recent), "post-cutoff memory must match");
1877
1878 let before = MemoryQuery::new().with_created_before(cutoff);
1879 assert!(before.matches(&old), "pre-cutoff memory must match");
1880 assert!(
1881 !before.matches(&recent),
1882 "post-cutoff memory must not match"
1883 );
1884
1885 let edge = MemoryQuery::new().with_created_after(cutoff);
1887 let mut at = Memory::new(Galaxy::Codex, "at cutoff".into());
1888 at.metadata.created_at = cutoff;
1889 assert!(edge.matches(&at), "created_at == after bound is inclusive");
1890 }
1891
1892 #[test]
1893 fn embedding_put_get_delete() {
1894 let tmp = tempfile::tempdir().unwrap();
1895 let store = MemoryStore::open_default(tmp.path()).unwrap();
1896
1897 let id = uuid::Uuid::new_v4();
1898 let embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5];
1899
1900 store.put_embedding(id, &embedding).unwrap();
1901 let retrieved = store.get_embedding(id).unwrap();
1902 assert!(retrieved.is_some());
1903 let retrieved = retrieved.unwrap();
1904 assert_eq!(retrieved.len(), 5);
1905 assert!((retrieved[0] - 0.1).abs() < f32::EPSILON);
1906
1907 assert!(store.delete_embedding(id).unwrap());
1908 assert!(store.get_embedding(id).unwrap().is_none());
1909 }
1910
1911 #[test]
1912 fn embedding_cache_roundtrip_batch_and_count() {
1913 let tmp = tempfile::tempdir().unwrap();
1914 let store = MemoryStore::open_default(tmp.path()).unwrap();
1915
1916 let entries: Vec<(String, Vec<f32>)> = (0..5)
1917 .map(|i| (format!("ns:model:{i:016x}"), vec![i as f32; 8]))
1918 .collect();
1919 store.put_embedding_cache_batch(&entries).unwrap();
1920 assert_eq!(store.embedding_cache_count().unwrap(), 5);
1921
1922 let hit = store
1924 .get_embedding_cache("ns:model:0000000000000003")
1925 .unwrap();
1926 assert_eq!(hit.unwrap(), vec![3.0f32; 8]);
1927 assert!(
1928 store
1929 .get_embedding_cache("ns:model:absent")
1930 .unwrap()
1931 .is_none()
1932 );
1933
1934 let keys: Vec<String> = (0..6).map(|i| format!("ns:model:{i:016x}")).collect();
1936 let batch = store.get_embedding_cache_batch(&keys).unwrap();
1937 assert_eq!(batch.len(), 6);
1938 assert!(batch[0..5].iter().all(Option::is_some));
1939 assert!(batch[5].is_none());
1940
1941 store
1943 .put_embedding_cache("ns:model:0000000000000001", &[9.0; 8])
1944 .unwrap();
1945 assert_eq!(store.embedding_cache_count().unwrap(), 5);
1946 assert_eq!(
1947 store
1948 .get_embedding_cache("ns:model:0000000000000001")
1949 .unwrap()
1950 .unwrap(),
1951 vec![9.0f32; 8]
1952 );
1953 }
1954
1955 #[test]
1956 fn embedding_cache_survives_store_reopen() {
1957 let tmp = tempfile::tempdir().unwrap();
1959 {
1960 let store = MemoryStore::open_default(tmp.path()).unwrap();
1961 store
1962 .put_embedding_cache("onnx:bge-small:abc", &[0.5; 384])
1963 .unwrap();
1964 }
1965 let reopened = MemoryStore::open_default(tmp.path()).unwrap();
1966 let cached = reopened.get_embedding_cache("onnx:bge-small:abc").unwrap();
1967 assert_eq!(cached.unwrap(), vec![0.5f32; 384]);
1968 }
1969
1970 #[test]
1971 fn content_hash_is_sha256() {
1972 let hash1 = content_hash("test content");
1973 let hash2 = content_hash("test content");
1974 let hash3 = content_hash("different content");
1975
1976 assert_eq!(hash1, hash2, "same content should produce same hash");
1977 assert_ne!(
1978 hash1, hash3,
1979 "different content should produce different hash"
1980 );
1981 assert_eq!(hash1.len(), 64, "SHA-256 hex should be 64 chars");
1982 }
1983
1984 #[test]
1985 fn query_by_tag_uses_index() {
1986 let tmp = tempfile::tempdir().unwrap();
1987 let store = MemoryStore::open_default(tmp.path()).unwrap();
1988
1989 let mem1 = Memory::new(Galaxy::Codex, "tagged".into())
1990 .with_tags(vec!["rust".into(), "memory".into()]);
1991 let mem2 = Memory::new(Galaxy::Codex, "other".into()).with_tags(vec!["python".into()]);
1992 store.put(Galaxy::Codex, &mem1).unwrap();
1993 store.put(Galaxy::Codex, &mem2).unwrap();
1994
1995 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
1996 let results = store.query(Galaxy::Codex, &query).unwrap();
1997 assert_eq!(results.len(), 1);
1998 assert_eq!(results[0].content, "tagged");
1999 }
2000
2001 #[test]
2002 fn query_by_importance_uses_index() {
2003 let tmp = tempfile::tempdir().unwrap();
2004 let store = MemoryStore::open_default(tmp.path()).unwrap();
2005
2006 store
2007 .put(
2008 Galaxy::Codex,
2009 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
2010 )
2011 .unwrap();
2012 store
2013 .put(
2014 Galaxy::Codex,
2015 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
2016 )
2017 .unwrap();
2018 store
2019 .put(
2020 Galaxy::Codex,
2021 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
2022 )
2023 .unwrap();
2024
2025 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
2026 let results = store.query(Galaxy::Codex, &query).unwrap();
2027 assert_eq!(results.len(), 1);
2028 assert_eq!(results[0].content, "mid");
2029 }
2030
2031 #[test]
2032 fn query_by_time_uses_index() {
2033 let tmp = tempfile::tempdir().unwrap();
2034 let store = MemoryStore::open_default(tmp.path()).unwrap();
2035
2036 let t0 = chrono::Utc::now();
2037 std::thread::sleep(std::time::Duration::from_millis(10));
2038 let mem = Memory::new(Galaxy::Codex, "timed".into());
2039 store.put(Galaxy::Codex, &mem).unwrap();
2040 std::thread::sleep(std::time::Duration::from_millis(10));
2041 let t2 = chrono::Utc::now();
2042
2043 let query = MemoryQuery::new().with_time_range(t0, t2);
2044 let results = store.query(Galaxy::Codex, &query).unwrap();
2045 assert_eq!(results.len(), 1);
2046 assert_eq!(results[0].content, "timed");
2047 }
2048
2049 #[test]
2050 fn delete_removes_index_entries() {
2051 let tmp = tempfile::tempdir().unwrap();
2052 let store = MemoryStore::open_default(tmp.path()).unwrap();
2053
2054 let mem = Memory::new(Galaxy::Codex, "test".into())
2055 .with_tags(vec!["tag1".into()])
2056 .with_importance(0.7);
2057 let id = mem.metadata.id;
2058 let hash = mem.metadata.content_hash.clone();
2059 store.put(Galaxy::Codex, &mem).unwrap();
2060
2061 assert!(
2063 store
2064 .find_by_content_hash(Galaxy::Codex, &hash)
2065 .unwrap()
2066 .is_some()
2067 );
2068
2069 store.delete(Galaxy::Codex, id).unwrap();
2071
2072 assert!(
2074 store
2075 .find_by_content_hash(Galaxy::Codex, &hash)
2076 .unwrap()
2077 .is_none()
2078 );
2079
2080 let query = MemoryQuery::new().with_tags(vec!["tag1".into()]);
2082 let results = store.query(Galaxy::Codex, &query).unwrap();
2083 assert!(results.is_empty());
2084 }
2085
2086 #[test]
2087 fn put_batch_updates_indexes() {
2088 let tmp = tempfile::tempdir().unwrap();
2089 let store = MemoryStore::open_default(tmp.path()).unwrap();
2090
2091 let memories: Vec<Memory> = (0..5)
2092 .map(|i| {
2093 Memory::new(Galaxy::Codex, format!("batch-{i}"))
2094 .with_tags(vec![format!("tag{i}")])
2095 .with_importance(i as f32 * 0.2)
2096 })
2097 .collect();
2098 store.put_batch(Galaxy::Codex, &memories).unwrap();
2099
2100 for i in 0..5 {
2101 let query = MemoryQuery::new().with_tags(vec![format!("tag{i}")]);
2102 let results = store.query(Galaxy::Codex, &query).unwrap();
2103 assert_eq!(results.len(), 1, "tag{i} should have 1 result");
2104 }
2105 }
2106
2107 #[test]
2108 fn find_by_content_hash_indexed_matches_scan() {
2109 let tmp = tempfile::tempdir().unwrap();
2110 let store = MemoryStore::open_default(tmp.path()).unwrap();
2111
2112 let mem = Memory::new(Galaxy::Codex, "dedup test".into());
2113 let id = mem.metadata.id;
2114 let hash = mem.metadata.content_hash.clone();
2115 store.put(Galaxy::Codex, &mem).unwrap();
2116
2117 let indexed = store.find_by_content_hash(Galaxy::Codex, &hash).unwrap();
2118 let scanned = store
2119 .find_by_content_hash_scan(Galaxy::Codex, &hash)
2120 .unwrap();
2121
2122 assert_eq!(indexed, scanned);
2123 assert_eq!(indexed, Some(id));
2124 }
2125
2126 #[test]
2127 fn put_dedup_uses_index() {
2128 let tmp = tempfile::tempdir().unwrap();
2129 let store = MemoryStore::open_default(tmp.path()).unwrap();
2130
2131 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
2132 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
2133
2134 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
2135 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
2136
2137 assert_eq!(id1, id2, "dedup should return same ID for same content");
2138 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
2139 }
2140
2141 #[test]
2142 fn put_semantic_updates_coord5d() {
2143 let tmp = tempfile::tempdir().unwrap();
2144 let store = MemoryStore::open_default(tmp.path()).unwrap();
2145
2146 let mut mem = Memory::new(
2147 Galaxy::Codex,
2148 "The algorithm computes data using a systematic method".to_string(),
2149 );
2150 let original_coord = mem.metadata.coord5d.clone();
2151 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2152
2153 assert_ne!(
2155 mem.metadata.coord5d.x, original_coord.x,
2156 "semantic encoding should change x"
2157 );
2158 assert_ne!(
2159 mem.metadata.coord5d.y, original_coord.y,
2160 "semantic encoding should change y"
2161 );
2162
2163 let retrieved = store.get(Galaxy::Codex, mem.metadata.id).unwrap().unwrap();
2165 assert_eq!(retrieved.metadata.coord5d.x, mem.metadata.coord5d.x);
2166 }
2167
2168 #[test]
2169 fn put_semantic_preserves_temporal_and_importance() {
2170 let tmp = tempfile::tempdir().unwrap();
2171 let store = MemoryStore::open_default(tmp.path()).unwrap();
2172
2173 let mut mem = Memory::new(Galaxy::Codex, "test content".into()).with_importance(0.8);
2174 mem.metadata.coord5d.w = 0.6;
2175 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2176
2177 assert!((mem.metadata.coord5d.w - 0.6).abs() < f32::EPSILON);
2178 assert!((mem.metadata.coord5d.v - 0.8).abs() < f32::EPSILON);
2179 }
2180
2181 #[test]
2182 fn find_similar_returns_nearest_first() {
2183 let tmp = tempfile::tempdir().unwrap();
2184 let store = MemoryStore::open_default(tmp.path()).unwrap();
2185
2186 let mut logic_mem = Memory::new(
2187 Galaxy::Codex,
2188 "The algorithm computes data using systematic logic and analysis".to_string(),
2189 );
2190 store.put_semantic(Galaxy::Codex, &mut logic_mem).unwrap();
2191
2192 let mut emotion_mem = Memory::new(
2193 Galaxy::Codex,
2194 "I feel love and joy with deep passion and empathy in my heart".to_string(),
2195 );
2196 store.put_semantic(Galaxy::Codex, &mut emotion_mem).unwrap();
2197
2198 let results = store
2200 .find_similar(Galaxy::Codex, "algorithm data systematic method", 10)
2201 .unwrap();
2202 assert!(!results.is_empty());
2203 assert_eq!(results[0].0.metadata.id, logic_mem.metadata.id);
2204
2205 let results = store
2207 .find_similar(Galaxy::Codex, "love joy passion heart feeling", 10)
2208 .unwrap();
2209 assert!(!results.is_empty());
2210 assert_eq!(results[0].0.metadata.id, emotion_mem.metadata.id);
2211 }
2212
2213 #[test]
2214 fn find_similar_empty_galaxy() {
2215 let tmp = tempfile::tempdir().unwrap();
2216 let store = MemoryStore::open_default(tmp.path()).unwrap();
2217
2218 let results = store.find_similar(Galaxy::Codex, "anything", 10).unwrap();
2219 assert!(results.is_empty());
2220 }
2221
2222 #[test]
2223 fn find_similar_respects_limit() {
2224 let tmp = tempfile::tempdir().unwrap();
2225 let store = MemoryStore::open_default(tmp.path()).unwrap();
2226
2227 for i in 0..5 {
2228 let mut mem = Memory::new(Galaxy::Codex, format!("algorithm data method {i}"));
2229 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2230 }
2231
2232 let results = store
2233 .find_similar(Galaxy::Codex, "algorithm data", 3)
2234 .unwrap();
2235 assert_eq!(results.len(), 3);
2236 }
2237
2238 #[test]
2239 fn semantic_encoder_accessible() {
2240 let tmp = tempfile::tempdir().unwrap();
2241 let store = MemoryStore::open_default(tmp.path()).unwrap();
2242
2243 let scores = store.semantic_encoder().encode("algorithm data logic");
2244 assert!(scores.x < 0.5);
2246 }
2247
2248 #[test]
2249 fn put_raw_batch_writes_atomically() {
2250 let tmp = tempfile::tempdir().unwrap();
2251 let store = MemoryStore::open_default(tmp.path()).unwrap();
2252
2253 let entries: &[(&[u8], &[u8])] =
2254 &[(b"key1", b"val1"), (b"key2", b"val2"), (b"key3", b"val3")];
2255 store.put_raw_batch(Galaxy::Karma, entries).unwrap();
2256
2257 assert_eq!(
2258 store.get_raw(Galaxy::Karma, b"key1").unwrap().unwrap(),
2259 b"val1"
2260 );
2261 assert_eq!(
2262 store.get_raw(Galaxy::Karma, b"key2").unwrap().unwrap(),
2263 b"val2"
2264 );
2265 assert_eq!(
2266 store.get_raw(Galaxy::Karma, b"key3").unwrap().unwrap(),
2267 b"val3"
2268 );
2269 }
2270
2271 #[test]
2272 fn put_raw_batch_empty_is_noop() {
2273 let tmp = tempfile::tempdir().unwrap();
2274 let store = MemoryStore::open_default(tmp.path()).unwrap();
2275
2276 store.put_raw_batch(Galaxy::Karma, &[]).unwrap();
2277 assert_eq!(store.count(Galaxy::Karma).unwrap(), 0);
2278 }
2279
2280 #[test]
2281 fn entry_limit_rejects_excess_writes() {
2282 let tmp = tempfile::tempdir().unwrap();
2283 let store = MemoryStore::open_default(tmp.path())
2284 .unwrap()
2285 .with_entry_limit(3);
2286
2287 for i in 0..3 {
2288 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
2289 store.put(Galaxy::Codex, &mem).unwrap();
2290 }
2291
2292 let mem = Memory::new(Galaxy::Codex, "overflow memory".to_string());
2294 let result = store.put(Galaxy::Codex, &mem);
2295 assert!(result.is_err(), "write beyond limit should be rejected");
2296 let err_msg = result.unwrap_err().to_string();
2297 assert!(
2298 err_msg.contains("entry limit reached"),
2299 "error should mention entry limit: {err_msg}"
2300 );
2301 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
2302 }
2303
2304 #[test]
2305 fn entry_limit_per_galaxy_independent() {
2306 let tmp = tempfile::tempdir().unwrap();
2307 let store = MemoryStore::open_default(tmp.path())
2308 .unwrap()
2309 .with_entry_limit(2);
2310
2311 for i in 0..2 {
2313 let mem = Memory::new(Galaxy::Codex, format!("codex {i}"));
2314 store.put(Galaxy::Codex, &mem).unwrap();
2315 }
2316
2317 let mem = Memory::new(Galaxy::Research, "science memory".to_string());
2319 let result = store.put(Galaxy::Research, &mem);
2320 assert!(
2321 result.is_ok(),
2322 "different galaxy should not be affected by limit"
2323 );
2324 }
2325
2326 #[test]
2327 fn entry_limit_none_allows_unlimited() {
2328 let tmp = tempfile::tempdir().unwrap();
2329 let store = MemoryStore::open_default(tmp.path()).unwrap();
2330
2331 for i in 0..50 {
2333 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
2334 store.put(Galaxy::Codex, &mem).unwrap();
2335 }
2336 assert_eq!(store.count(Galaxy::Codex).unwrap(), 50);
2337 }
2338
2339 #[test]
2340 fn map_full_error_is_graceful() {
2341 let tmp = tempfile::tempdir().unwrap();
2345 let store = MemoryStore::open(tmp.path(), 512 * 1024).unwrap();
2346
2347 let mut written = 0;
2349 let mut got_map_full = false;
2350 for i in 0..1000 {
2351 let mem = Memory::new(
2352 Galaxy::Codex,
2353 format!("memory content {i} {}", "with padding ".repeat(50)),
2354 );
2355 match store.put(Galaxy::Codex, &mem) {
2356 Ok(()) => written += 1,
2357 Err(e) => {
2358 let msg = e.to_string();
2359 if msg.contains("map full") {
2360 got_map_full = true;
2361 break;
2362 }
2363 break;
2365 }
2366 }
2367 }
2368
2369 assert!(
2370 got_map_full || written < 1000,
2371 "should eventually hit map full or error"
2372 );
2373 assert!(written > 0, "should have written at least some memories");
2374 }
2375
2376 #[test]
2377 fn test_find_across_galaxies() {
2378 let tmp = tempfile::tempdir().unwrap();
2379 let store = MemoryStore::open_default(tmp.path()).unwrap();
2380
2381 let mem = Memory::new(Galaxy::Research, "Cross-galaxy research memo".into());
2382 let id = mem.metadata.id;
2383 store.put(Galaxy::Research, &mem).unwrap();
2384
2385 let found = store.find_across_galaxies(id).unwrap();
2386 assert!(found.is_some());
2387 let (galaxy, retrieved) = found.unwrap();
2388 assert_eq!(galaxy, Galaxy::Research);
2389 assert_eq!(retrieved.metadata.id, id);
2390 assert_eq!(retrieved.content, "Cross-galaxy research memo");
2391
2392 assert!(
2394 store
2395 .find_across_galaxies(uuid::Uuid::new_v4())
2396 .unwrap()
2397 .is_none()
2398 );
2399 }
2400}