1use lmdb::{
7 Cursor, Database, DatabaseFlags, Environment, EnvironmentFlags, Transaction, WriteFlags,
8};
9use std::collections::HashMap;
10use std::path::Path;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, RwLock};
13use wm_core::{CoreError, Galaxy, Result};
14
15#[cfg(unix)]
16use std::os::unix::fs::PermissionsExt;
17
18use crate::episodic::EpisodicStore;
19use crate::indexes::IndexDbs;
20use crate::memory::{Memory, MemoryId, decode_embedding, encode_embedding};
21use crate::semantic::SemanticEncoder;
22
23#[derive(Debug, Clone, Default)]
25pub struct MemoryQuery {
26 pub tags: Vec<String>,
28 pub min_importance: Option<f32>,
30 pub max_importance: Option<f32>,
32 pub created_after: Option<chrono::DateTime<chrono::Utc>>,
34 pub created_before: Option<chrono::DateTime<chrono::Utc>>,
36 pub content_substring: Option<String>,
39 pub limit: usize,
41}
42
43impl MemoryQuery {
44 #[must_use]
46 pub fn new() -> Self {
47 Self {
48 limit: 100,
49 ..Default::default()
50 }
51 }
52
53 #[must_use]
55 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
56 self.tags = tags;
57 self
58 }
59
60 #[must_use]
62 pub const fn with_importance_range(mut self, min: f32, max: f32) -> Self {
63 self.min_importance = Some(min);
64 self.max_importance = Some(max);
65 self
66 }
67
68 #[must_use]
70 pub const fn with_time_range(
71 mut self,
72 after: chrono::DateTime<chrono::Utc>,
73 before: chrono::DateTime<chrono::Utc>,
74 ) -> Self {
75 self.created_after = Some(after);
76 self.created_before = Some(before);
77 self
78 }
79
80 #[must_use]
83 pub const fn with_created_after(mut self, after: chrono::DateTime<chrono::Utc>) -> Self {
84 self.created_after = Some(after);
85 self
86 }
87
88 #[must_use]
91 pub const fn with_created_before(mut self, before: chrono::DateTime<chrono::Utc>) -> Self {
92 self.created_before = Some(before);
93 self
94 }
95
96 #[must_use]
98 pub const fn with_limit(mut self, limit: usize) -> Self {
99 self.limit = limit;
100 self
101 }
102
103 #[must_use]
105 pub fn with_content_substring(mut self, substring: impl Into<String>) -> Self {
106 self.content_substring = Some(substring.into().to_lowercase());
107 self
108 }
109
110 #[must_use]
112 pub fn matches(&self, mem: &Memory) -> bool {
113 if !self.tags.is_empty() {
115 for tag in &self.tags {
116 if !mem.metadata.tags.iter().any(|t| t == tag) {
117 return false;
118 }
119 }
120 }
121
122 if let Some(min) = self.min_importance {
124 if mem.metadata.importance < min {
125 return false;
126 }
127 }
128 if let Some(max) = self.max_importance {
129 if mem.metadata.importance > max {
130 return false;
131 }
132 }
133
134 if let Some(after) = self.created_after {
136 if mem.metadata.created_at < after {
137 return false;
138 }
139 }
140 if let Some(before) = self.created_before {
141 if mem.metadata.created_at > before {
142 return false;
143 }
144 }
145
146 if let Some(sub) = &self.content_substring {
148 if !mem.content.to_lowercase().contains(sub) {
149 return false;
150 }
151 }
152
153 true
154 }
155}
156
157pub struct MemoryStore {
159 path: std::path::PathBuf,
161 env: Environment,
163 index_dbs: IndexDbs,
165 semantic_encoder: SemanticEncoder,
167 max_entries_per_galaxy: Option<usize>,
169 mutation_count: AtomicU64,
173 episodic_db: Database,
175 episodic_terms_v2_db: Database,
177 embedding_cache_db: Database,
182 revisions_db: Database,
186 attestations_db: Database,
190 pub(crate) cold_storage_db: Database,
192 episodic_term_cache: std::sync::Arc<RwLock<HashMap<String, Vec<uuid::Uuid>>>>,
194 episodic_embedder:
196 std::sync::OnceLock<Option<Arc<dyn crate::embedder::Embedder + Send + Sync>>>,
197 episodic_sidecar_ensured: std::sync::OnceLock<()>,
199 episodic_aliases: std::sync::OnceLock<Option<crate::episodic_keys::AdaptiveAliases>>,
201 episodic_enrichment: std::sync::OnceLock<Option<crate::enrichment::VocabularyEnrichment>>,
203}
204
205impl MemoryStore {
206 pub fn open(path: impl AsRef<Path>, map_size: usize) -> Result<Self> {
212 let path = path.as_ref().to_path_buf();
213
214 std::fs::create_dir_all(&path)
216 .map_err(|e| CoreError::Memory(format!("Cannot create store dir: {e}")))?;
217 #[cfg(unix)]
218 {
219 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))
220 .map_err(|e| CoreError::Memory(format!("Cannot set store dir permissions: {e}")))?;
221 }
222
223 let env = Environment::new()
224 .set_map_size(map_size)
225 .set_max_dbs(64)
226 .open(&path)
227 .map_err(|e| CoreError::Memory(format!("LMDB open failed: {e}")))?;
228
229 for galaxy in Galaxy::all() {
231 let db = env
232 .create_db(Some(galaxy.db_name()), DatabaseFlags::default())
233 .map_err(|e| {
234 CoreError::Memory(format!(
235 "LMDB create_db failed for {}: {e}",
236 galaxy.db_name()
237 ))
238 })?;
239 let _ = db;
240 }
241
242 for (name, flags) in crate::indexes::INDEX_DBS {
244 let db = env
245 .create_db(Some(name), *flags)
246 .map_err(|e| CoreError::Memory(format!("LMDB create_db failed for {name}: {e}")))?;
247 let _ = db;
248 }
249
250 let index_dbs = IndexDbs::open(&env)?;
251 let episodic_db = env
252 .create_db(Some("episodic_records"), DatabaseFlags::default())
253 .map_err(|e| {
254 CoreError::Memory(format!("LMDB create_db failed for episodic_records: {e}"))
255 })?;
256 let episodic_terms_v2_db = env
263 .create_db(Some("episodic_terms_v2"), DatabaseFlags::DUP_SORT)
264 .map_err(|e| {
265 CoreError::Memory(format!("LMDB create_db failed for episodic_terms_v2: {e}"))
266 })?;
267 let embedding_cache_db = env
268 .create_db(Some("embedding_cache"), DatabaseFlags::default())
269 .map_err(|e| {
270 CoreError::Memory(format!("LMDB create_db failed for embedding_cache: {e}"))
271 })?;
272 let revisions_db = env
273 .create_db(Some("revisions"), DatabaseFlags::default())
274 .map_err(|e| CoreError::Memory(format!("LMDB create_db failed for revisions: {e}")))?;
275 let attestations_db = env
276 .create_db(
277 Some(crate::attestation::ATTESTATIONS_DB),
278 DatabaseFlags::default(),
279 )
280 .map_err(|e| {
281 CoreError::Memory(format!("LMDB create_db failed for attestations: {e}"))
282 })?;
283 let cold_storage_db = env
284 .create_db(Some("cold_storage"), DatabaseFlags::default())
285 .map_err(|e| {
286 CoreError::Memory(format!("LMDB create_db failed for cold_storage: {e}"))
287 })?;
288 Ok(Self {
289 path,
290 env,
291 index_dbs,
292 semantic_encoder: SemanticEncoder::new(),
293 max_entries_per_galaxy: None,
294 mutation_count: AtomicU64::new(0),
295 episodic_db,
296 episodic_terms_v2_db,
297 embedding_cache_db,
298 revisions_db,
299 attestations_db,
300 cold_storage_db,
301 episodic_term_cache: std::sync::Arc::new(RwLock::new(HashMap::new())),
302 episodic_embedder: std::sync::OnceLock::new(),
303 episodic_sidecar_ensured: std::sync::OnceLock::new(),
304 episodic_aliases: std::sync::OnceLock::new(),
305 episodic_enrichment: std::sync::OnceLock::new(),
306 })
307 }
308
309 pub fn open_default(path: impl AsRef<Path>) -> Result<Self> {
318 let platform_default = if cfg!(windows) {
323 256 * 1024 * 1024
324 } else {
325 4 * 1024 * 1024 * 1024
326 };
327 let size = std::env::var("WM_DEFAULT_MAP_SIZE")
328 .ok()
329 .and_then(|v| v.parse::<usize>().ok())
330 .filter(|&v| v > 0)
331 .unwrap_or(platform_default);
332 Self::open(path, size)
333 }
334
335 pub fn open_readonly(path: impl AsRef<Path>) -> Result<Self> {
341 let path = path.as_ref().to_path_buf();
342 if !path.is_dir() {
343 return Err(CoreError::Memory(format!(
344 "Read-only LMDB store directory does not exist: {}",
345 path.display()
346 )));
347 }
348 if !path.join("data.mdb").is_file() {
349 return Err(CoreError::Memory(format!(
350 "Read-only LMDB store is missing data.mdb: {}",
351 path.display()
352 )));
353 }
354
355 let env = Environment::new()
356 .set_max_dbs(32)
357 .set_flags(EnvironmentFlags::READ_ONLY)
358 .open(&path)
359 .map_err(|e| CoreError::Memory(format!("Read-only LMDB open failed: {e}")))?;
360
361 let index_dbs = IndexDbs::open(&env)?;
362 let open_named = |name: &str| {
363 env.open_db(Some(name)).map_err(|e| {
364 CoreError::Memory(format!("Read-only LMDB missing database {name}: {e}"))
365 })
366 };
367 let episodic_db = open_named("episodic_records")?;
368 let episodic_terms_v2_db = open_named("episodic_terms_v2")?;
369 let embedding_cache_db = open_named("embedding_cache")?;
370 let revisions_db = open_named("revisions")?;
371 let attestations_db = open_named(crate::attestation::ATTESTATIONS_DB)?;
372 let cold_storage_db = open_named("cold_storage")?;
373
374 Ok(Self {
375 path,
376 env,
377 index_dbs,
378 semantic_encoder: SemanticEncoder::new(),
379 max_entries_per_galaxy: None,
380 mutation_count: AtomicU64::new(0),
381 episodic_db,
382 episodic_terms_v2_db,
383 embedding_cache_db,
384 revisions_db,
385 attestations_db,
386 cold_storage_db,
387 episodic_term_cache: std::sync::Arc::new(RwLock::new(HashMap::new())),
388 episodic_embedder: std::sync::OnceLock::new(),
389 episodic_sidecar_ensured: std::sync::OnceLock::new(),
390 episodic_aliases: std::sync::OnceLock::new(),
391 episodic_enrichment: std::sync::OnceLock::new(),
392 })
393 }
394
395 const NAMED_DBIS: [&'static str; 6] = [
398 "episodic_records",
399 "episodic_terms_v2",
400 "embedding_cache",
401 "revisions",
402 crate::attestation::ATTESTATIONS_DB,
403 "cold_storage",
404 ];
405
406 pub fn ensure_schema(path: impl AsRef<Path>) -> Result<Vec<String>> {
415 let path = path.as_ref().to_path_buf();
416 if !path.is_dir() {
417 return Err(CoreError::Memory(format!(
418 "Store directory does not exist: {}",
419 path.display()
420 )));
421 }
422 let expected = || {
423 Galaxy::all()
424 .into_iter()
425 .map(|galaxy| galaxy.db_name().to_string())
426 .chain(
427 crate::indexes::INDEX_DBS
428 .iter()
429 .map(|(name, _)| (*name).to_string()),
430 )
431 .chain(Self::NAMED_DBIS.iter().map(|name| (*name).to_string()))
432 };
433 let missing: Vec<String> = {
434 let env = Environment::new()
435 .set_max_dbs(64)
436 .set_flags(EnvironmentFlags::READ_ONLY)
437 .open(&path)
438 .map_err(|e| CoreError::Memory(format!("Read-only LMDB open failed: {e}")))?;
439 expected()
440 .filter(|name| env.open_db(Some(name.as_str())).is_err())
441 .collect()
442 };
443 if missing.is_empty() {
444 return Ok(missing);
445 }
446 let store = Self::open_default(&path)?;
449 drop(store);
450 Ok(missing)
451 }
452
453 #[must_use]
458 pub const fn with_entry_limit(mut self, limit: usize) -> Self {
459 self.max_entries_per_galaxy = Some(limit);
460 self
461 }
462
463 pub fn path(&self) -> &Path {
465 &self.path
466 }
467
468 pub const fn env(&self) -> &Environment {
470 &self.env
471 }
472
473 pub fn mutation_count(&self) -> u64 {
477 self.mutation_count.load(Ordering::Relaxed)
478 }
479
480 pub const fn index_dbs(&self) -> &IndexDbs {
482 &self.index_dbs
483 }
484
485 pub const fn semantic_encoder(&self) -> &SemanticEncoder {
487 &self.semantic_encoder
488 }
489
490 fn ensure_episodic_sidecar(&self) {
495 if self.episodic_sidecar_ensured.get().is_some() {
496 return;
497 }
498 let _ = self.episodic_sidecar_ensured.set(());
499 let view = EpisodicStore::new(
500 &self.env,
501 self.episodic_db,
502 self.episodic_terms_v2_db,
503 self.episodic_term_cache.clone(),
504 &self.mutation_count,
505 );
506 let needs_rebuild = matches!(
507 (view.sidecar_is_empty(), view.record_count()),
508 (Ok(true), Ok(n)) if n > 0
509 );
510 if needs_rebuild {
511 match view.rebuild_sidecar() {
512 Ok(n) => tracing::info!("episodic sidecar rebuilt from {n} records"),
513 Err(e) => {
514 tracing::warn!("episodic sidecar rebuild failed: {e}");
515 }
516 }
517 }
518 }
519
520 #[must_use]
522 pub fn episodic(&self) -> EpisodicStore<'_> {
523 self.ensure_episodic_sidecar();
524 let mut store = EpisodicStore::new(
525 &self.env,
526 self.episodic_db,
527 self.episodic_terms_v2_db,
528 self.episodic_term_cache.clone(),
529 &self.mutation_count,
530 );
531 if let Some(Some(embedder)) = self.episodic_embedder.get() {
532 store = store.with_embedder(embedder.clone());
533 }
534 if let Some(Some(aliases)) = self.episodic_aliases.get() {
535 store = store.with_adaptive_aliases(aliases.clone());
536 }
537 if let Some(Some(enrichment)) = self.episodic_enrichment.get() {
538 store = store.with_enrichment(enrichment.clone());
539 }
540 store
541 }
542
543 pub fn set_episodic_embedder(
545 &self,
546 embedder: Arc<dyn crate::embedder::Embedder + Send + Sync>,
547 ) {
548 let _ = self.episodic_embedder.set(Some(embedder));
549 }
550
551 pub fn set_episodic_aliases(&self, aliases: crate::episodic_keys::AdaptiveAliases) {
553 let _ = self.episodic_aliases.set(Some(aliases));
554 }
555
556 pub fn set_episodic_enrichment(&self, enrichment: crate::enrichment::VocabularyEnrichment) {
558 let _ = self.episodic_enrichment.set(Some(enrichment));
559 }
560
561 pub fn galaxy_db(&self, galaxy: Galaxy) -> Result<Database> {
563 self.env.open_db(Some(galaxy.db_name())).map_err(|e| {
564 CoreError::Memory(format!("LMDB open_db failed for {}: {e}", galaxy.db_name()))
565 })
566 }
567
568 pub fn put(&self, galaxy: Galaxy, memory: &Memory) -> Result<()> {
576 if let Some(limit) = self.max_entries_per_galaxy {
578 let current = self.count(galaxy)?;
579 if current >= limit {
580 return Err(CoreError::Memory(format!(
581 "galaxy {} entry limit reached ({current}/{limit}), write rejected",
582 galaxy.db_name()
583 )));
584 }
585 }
586
587 let db = self.galaxy_db(galaxy)?;
588 let key = memory.metadata.id.as_bytes();
589 let val = rmp_serde::to_vec_named(memory)
590 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
591
592 let mut tx = self
593 .env
594 .begin_rw_txn()
595 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
596
597 let existing = tx
602 .get(db, key)
603 .ok()
604 .and_then(|bytes| crate::codec::decode(bytes).ok());
605
606 match tx.put(db, key, &val, lmdb::WriteFlags::default()) {
607 Ok(()) => {}
608 Err(lmdb::Error::MapFull) => {
609 tx.abort();
610 return Err(CoreError::Memory(format!(
611 "LMDB map full: galaxy {}, consider growing map size or pruning old memories",
612 galaxy.db_name()
613 )));
614 }
615 Err(e) => {
616 tx.abort();
617 return Err(CoreError::Memory(format!("LMDB put failed: {e}")));
618 }
619 }
620 if let Some(existing) = existing {
621 self.index_dbs.remove(&mut tx, galaxy, &existing)?;
622 }
623 self.index_dbs.add(&mut tx, galaxy, memory)?;
624 tx.commit()
625 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
626 self.mutation_count.fetch_add(1, Ordering::Relaxed);
627 Ok(())
628 }
629
630 pub fn get(&self, galaxy: Galaxy, id: uuid::Uuid) -> Result<Option<Memory>> {
632 let db = self.galaxy_db(galaxy)?;
633 let key = id.as_bytes();
634
635 let tx = self
636 .env
637 .begin_ro_txn()
638 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
639 let result = tx.get(db, key);
640 match result {
641 Ok(bytes) => {
642 let memory: Memory = crate::codec::decode(bytes)
643 .map_err(|e| CoreError::Memory(format!("deserialize failed: {e}")))?;
644 tx.commit()
645 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
646 Ok(Some(memory))
647 }
648 Err(lmdb::Error::NotFound) => {
649 tx.commit()
651 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
652 Ok(None)
653 }
654 Err(e) => Err(CoreError::Memory(format!("LMDB get failed: {e}"))),
655 }
656 }
657
658 pub fn find_across_galaxies(&self, id: uuid::Uuid) -> Result<Option<(Galaxy, Memory)>> {
664 for galaxy in Galaxy::memory_galaxies() {
665 if let Some(mem) = self.get(galaxy, id)? {
666 return Ok(Some((galaxy, mem)));
667 }
668 }
669 Ok(None)
670 }
671
672 pub fn delete(&self, galaxy: Galaxy, id: uuid::Uuid) -> Result<bool> {
675 let db = self.galaxy_db(galaxy)?;
676 let key = id.as_bytes();
677
678 let mut tx = self
679 .env
680 .begin_rw_txn()
681 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
682
683 let exists = tx.get(db, key).is_ok();
685 if exists {
686 if let Ok(bytes) = tx.get(db, key) {
688 if let Ok(memory) = crate::codec::decode(bytes) {
689 let _ = self.index_dbs.remove(&mut tx, galaxy, &memory);
690 }
691 }
692 tx.del(db, key, None)
693 .map_err(|e| CoreError::Memory(format!("LMDB del failed: {e}")))?;
694 }
695 tx.commit()
696 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
697 if exists {
698 self.mutation_count.fetch_add(1, Ordering::Relaxed);
699 }
700 Ok(exists)
701 }
702
703 pub fn scan(&self, galaxy: Galaxy, limit: usize) -> Result<Vec<Memory>> {
705 let db = self.galaxy_db(galaxy)?;
706 let tx = self
707 .env
708 .begin_ro_txn()
709 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
710
711 let mut cursor = tx
712 .open_ro_cursor(db)
713 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
714
715 let mut memories = Vec::with_capacity(limit.min(256));
716 for (i, (_key, val)) in cursor.iter().enumerate() {
717 if memories.len() >= limit {
718 break;
719 }
720 match crate::codec::decode(val) {
721 Ok(memory) => memories.push(memory),
722 Err(e) => {
723 tracing::warn!(
724 "Skipping corrupted entry at index {i} in galaxy {:?}: {e}",
725 galaxy
726 );
727 }
728 }
729 }
730
731 drop(cursor);
732 tx.commit()
733 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
734 Ok(memories)
735 }
736
737 pub fn scan_all(&self, galaxy: Galaxy) -> Result<Vec<Memory>> {
742 self.scan_all_impl(galaxy, false)
743 }
744
745 pub fn scan_all_strict(&self, galaxy: Galaxy) -> Result<Vec<Memory>> {
749 self.scan_all_impl(galaxy, true)
750 }
751
752 fn scan_all_impl(&self, galaxy: Galaxy, strict: bool) -> Result<Vec<Memory>> {
753 let db = self.galaxy_db(galaxy)?;
754 let tx = self
755 .env
756 .begin_ro_txn()
757 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
758
759 let mut cursor = tx
760 .open_ro_cursor(db)
761 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
762
763 let mut memories = Vec::new();
764 for (i, (_key, val)) in cursor.iter().enumerate() {
765 match crate::codec::decode(val) {
766 Ok(memory) => memories.push(memory),
767 Err(e) => {
768 if strict {
769 return Err(CoreError::Memory(format!(
770 "refusing incomplete scan of {}: record {i} cannot be decoded: {e}",
771 galaxy.db_name()
772 )));
773 }
774 tracing::warn!(
775 "Skipping corrupted entry at index {i} in galaxy {:?}: {e}",
776 galaxy
777 );
778 }
779 }
780 }
781
782 drop(cursor);
783 tx.commit()
784 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
785 Ok(memories)
786 }
787
788 pub fn count(&self, galaxy: Galaxy) -> Result<usize> {
790 let db = self.galaxy_db(galaxy)?;
791 let tx = self
792 .env
793 .begin_ro_txn()
794 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
795 let mut cursor = tx
796 .open_ro_cursor(db)
797 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
798 let count = cursor.iter().count();
799 drop(cursor);
800 tx.commit()
801 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
802 Ok(count)
803 }
804
805 pub fn clear_galaxy(&self, galaxy: Galaxy) -> Result<usize> {
809 let db = self.galaxy_db(galaxy)?;
810
811 let mut tx = self
812 .env
813 .begin_rw_txn()
814 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
815
816 let mut cursor = tx
817 .open_ro_cursor(db)
818 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
819
820 let mut count = 0usize;
821 let keys_to_delete: Vec<(Vec<u8>, Memory)> = cursor
822 .iter()
823 .filter_map(|(key, val)| {
824 if let Ok(memory) = crate::codec::decode(val) {
825 Some((key.to_vec(), memory))
826 } else {
827 None
828 }
829 })
830 .collect();
831
832 drop(cursor);
833
834 for (key, memory) in &keys_to_delete {
835 let _ = self.index_dbs.remove(&mut tx, galaxy, memory);
836 tx.del(db, &key, None)
837 .map_err(|e| CoreError::Memory(format!("LMDB del failed: {e}")))?;
838 count += 1;
839 }
840
841 tx.commit()
842 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
843 self.mutation_count
844 .fetch_add(count as u64, Ordering::Relaxed);
845 Ok(count)
846 }
847
848 pub fn batch_put(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<usize> {
851 if memories.is_empty() {
852 return Ok(0);
853 }
854
855 let db = self.galaxy_db(galaxy)?;
856
857 let mut tx = self
858 .env
859 .begin_rw_txn()
860 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
861
862 let mut count = 0usize;
863 for memory in memories {
864 let key = memory.metadata.id.as_bytes();
865 let val = rmp_serde::to_vec_named(memory)
866 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
867 match tx.put(db, key, &val, WriteFlags::default()) {
868 Ok(()) => {}
869 Err(lmdb::Error::MapFull) => {
870 tx.abort();
871 return Err(CoreError::Memory(format!(
872 "LMDB map full: galaxy {}, consider growing map size",
873 galaxy.db_name()
874 )));
875 }
876 Err(e) => {
877 tx.abort();
878 return Err(CoreError::Memory(format!("LMDB put failed: {e}")));
879 }
880 }
881 self.index_dbs.add(&mut tx, galaxy, memory)?;
882 count += 1;
883 }
884
885 tx.commit()
886 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
887 self.mutation_count
888 .fetch_add(count as u64, Ordering::Relaxed);
889 Ok(count)
890 }
891
892 pub fn get_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<Option<Vec<u8>>> {
894 let db = self.galaxy_db(galaxy)?;
895 let tx = self
896 .env
897 .begin_ro_txn()
898 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
899 match tx.get(db, &key) {
900 Ok(bytes) => {
901 let data = bytes.to_vec();
902 tx.commit()
903 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
904 Ok(Some(data))
905 }
906 Err(lmdb::Error::NotFound) => {
907 tx.commit()
908 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
909 Ok(None)
910 }
911 Err(e) => Err(CoreError::Memory(format!("LMDB get_raw failed: {e}"))),
912 }
913 }
914
915 pub fn put_raw(&self, galaxy: Galaxy, key: &[u8], val: &[u8]) -> Result<()> {
917 let db = self.galaxy_db(galaxy)?;
918 let mut tx = self
919 .env
920 .begin_rw_txn()
921 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
922 tx.put(db, &key, &val, lmdb::WriteFlags::default())
923 .map_err(|e| CoreError::Memory(format!("LMDB put_raw failed: {e}")))?;
924 tx.commit()
925 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
926 self.mutation_count.fetch_add(1, Ordering::Relaxed);
927 Ok(())
928 }
929
930 pub fn delete_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<bool> {
933 let db = self.galaxy_db(galaxy)?;
934 let mut tx = self
935 .env
936 .begin_rw_txn()
937 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
938 let deleted = tx.del(db, &key, None).is_ok();
939 tx.commit()
940 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
941 if deleted {
942 self.mutation_count.fetch_add(1, Ordering::Relaxed);
943 }
944 Ok(deleted)
945 }
946
947 pub fn put_raw_batch(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
950 self.put_raw_batch_impl(galaxy, entries)?;
951 self.mutation_count
952 .fetch_add(entries.len() as u64, Ordering::Relaxed);
953 Ok(())
954 }
955
956 pub fn put_raw_batch_untracked(
966 &self,
967 galaxy: Galaxy,
968 entries: &[(&[u8], &[u8])],
969 ) -> Result<()> {
970 self.put_raw_batch_impl(galaxy, entries)
971 }
972
973 fn put_raw_batch_impl(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
974 let db = self.galaxy_db(galaxy)?;
975 let mut tx = self
976 .env
977 .begin_rw_txn()
978 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
979 for (key, val) in entries {
980 tx.put(db, key, val, WriteFlags::default())
981 .map_err(|e| CoreError::Memory(format!("LMDB put_raw_batch failed: {e}")))?;
982 }
983 tx.commit()
984 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
985 Ok(())
986 }
987
988 pub fn find_by_content_hash(&self, galaxy: Galaxy, hash: &str) -> Result<Option<uuid::Uuid>> {
994 let tx = self
995 .env
996 .begin_ro_txn()
997 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
998 let result = self.index_dbs.find_by_content_hash(&tx, galaxy, hash)?;
999 tx.commit()
1000 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1001 Ok(result)
1002 }
1003
1004 pub fn find_by_content_hash_scan(
1006 &self,
1007 galaxy: Galaxy,
1008 hash: &str,
1009 ) -> Result<Option<uuid::Uuid>> {
1010 let memories = self.scan(galaxy, 10_000)?;
1011 for mem in memories {
1012 if mem.metadata.content_hash == hash {
1013 return Ok(Some(mem.metadata.id));
1014 }
1015 }
1016 Ok(None)
1017 }
1018
1019 pub fn put_dedup(&self, galaxy: Galaxy, memory: &Memory) -> Result<uuid::Uuid> {
1023 if let Some(existing_id) =
1024 self.find_by_content_hash(galaxy, &memory.metadata.content_hash)?
1025 {
1026 return Ok(existing_id);
1027 }
1028 let id = memory.metadata.id;
1029 self.put(galaxy, memory)?;
1030 Ok(id)
1031 }
1032
1033 pub fn put_batch(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<()> {
1038 let db = self.galaxy_db(galaxy)?;
1039 let mut tx = self
1040 .env
1041 .begin_rw_txn()
1042 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1043
1044 for memory in memories {
1045 let key = memory.metadata.id.as_bytes();
1046 let val = rmp_serde::to_vec_named(memory)
1047 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
1048 tx.put(db, key, &val, WriteFlags::default())
1049 .map_err(|e| CoreError::Memory(format!("LMDB put_batch failed: {e}")))?;
1050 self.index_dbs.add(&mut tx, galaxy, memory)?;
1051 }
1052
1053 tx.commit()
1054 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1055 self.mutation_count
1056 .fetch_add(memories.len() as u64, Ordering::Relaxed);
1057 Ok(())
1058 }
1059
1060 pub fn query(&self, galaxy: Galaxy, query: &MemoryQuery) -> Result<Vec<Memory>> {
1067 if query.content_substring.is_none()
1070 && query.tags.len() == 1
1071 && query.min_importance.is_none()
1072 && query.max_importance.is_none()
1073 && query.created_after.is_none()
1074 && query.created_before.is_none()
1075 {
1076 return self.query_by_tag_indexed(galaxy, &query.tags[0], query.limit);
1077 }
1078
1079 if query.content_substring.is_none()
1080 && query.tags.is_empty()
1081 && let Some(min) = query.min_importance
1082 && let Some(max) = query.max_importance
1083 && query.created_after.is_none()
1084 && query.created_before.is_none()
1085 {
1086 return self.query_by_importance_indexed(galaxy, min, max, query.limit);
1087 }
1088
1089 if query.content_substring.is_none()
1090 && query.tags.is_empty()
1091 && query.min_importance.is_none()
1092 && query.max_importance.is_none()
1093 && let Some(after) = query.created_after
1094 && let Some(before) = query.created_before
1095 {
1096 return self.query_by_time_indexed(galaxy, after, before, query.limit);
1097 }
1098
1099 let memories = self.scan(galaxy, 10_000)?;
1101 let mut results = Vec::new();
1102 for mem in memories {
1103 if query.matches(&mem) {
1104 results.push(mem);
1105 if results.len() >= query.limit {
1106 break;
1107 }
1108 }
1109 }
1110 Ok(results)
1111 }
1112
1113 fn query_by_tag_indexed(&self, galaxy: Galaxy, tag: &str, limit: usize) -> Result<Vec<Memory>> {
1115 let db = self.galaxy_db(galaxy)?;
1116 let tx = self
1117 .env
1118 .begin_ro_txn()
1119 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1120 let ids = self.index_dbs.find_by_tag(&tx, galaxy, tag)?;
1121 let mut results = Vec::new();
1122 for id in &ids {
1123 if results.len() >= limit {
1124 break;
1125 }
1126 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1127 if let Ok(mem) = crate::codec::decode(bytes) {
1128 results.push(mem);
1129 }
1130 }
1131 }
1132 tx.commit()
1133 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1134 Ok(results)
1135 }
1136
1137 fn query_by_importance_indexed(
1139 &self,
1140 galaxy: Galaxy,
1141 min: f32,
1142 max: f32,
1143 limit: usize,
1144 ) -> Result<Vec<Memory>> {
1145 let db = self.galaxy_db(galaxy)?;
1146 let tx = self
1147 .env
1148 .begin_ro_txn()
1149 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1150 let ids = self
1151 .index_dbs
1152 .find_by_importance_range(&tx, galaxy, min, max)?;
1153 let mut results = Vec::new();
1154 for id in &ids {
1155 if results.len() >= limit {
1156 break;
1157 }
1158 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1159 if let Ok(mem) = crate::codec::decode(bytes) {
1160 results.push(mem);
1161 }
1162 }
1163 }
1164 tx.commit()
1165 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1166 Ok(results)
1167 }
1168
1169 fn query_by_time_indexed(
1171 &self,
1172 galaxy: Galaxy,
1173 after: chrono::DateTime<chrono::Utc>,
1174 before: chrono::DateTime<chrono::Utc>,
1175 limit: usize,
1176 ) -> Result<Vec<Memory>> {
1177 let db = self.galaxy_db(galaxy)?;
1178 let tx = self
1179 .env
1180 .begin_ro_txn()
1181 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1182 let ids = self
1183 .index_dbs
1184 .find_by_time_range(&tx, galaxy, after, before)?;
1185 let mut results = Vec::new();
1186 for id in &ids {
1187 if results.len() >= limit {
1188 break;
1189 }
1190 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1191 if let Ok(mem) = crate::codec::decode(bytes) {
1192 results.push(mem);
1193 }
1194 }
1195 }
1196 tx.commit()
1197 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1198 Ok(results)
1199 }
1200
1201 pub fn put_semantic(&self, galaxy: Galaxy, memory: &mut Memory) -> Result<()> {
1209 let temporal_weight = memory.metadata.coord5d.w;
1210 let importance = memory.metadata.importance;
1211 memory.metadata.coord5d =
1212 self.semantic_encoder
1213 .encode_coordinate(&memory.content, temporal_weight, importance);
1214 self.put(galaxy, memory)
1215 }
1216
1217 pub fn find_similar(
1222 &self,
1223 galaxy: Galaxy,
1224 query_text: &str,
1225 limit: usize,
1226 ) -> Result<Vec<(Memory, f32)>> {
1227 let query_coord = self
1228 .semantic_encoder
1229 .encode_coordinate(query_text, 0.5, 0.5);
1230 let memories = self.scan(galaxy, 10_000)?;
1231 let mut results: Vec<(Memory, f32)> = memories
1232 .into_iter()
1233 .map(|m| {
1234 let dist = query_coord.semantic_distance_to(&m.metadata.coord5d);
1235 (m, dist)
1236 })
1237 .collect();
1238 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1239 results.truncate(limit);
1240 Ok(results)
1241 }
1242
1243 pub fn put_embedding(&self, memory_id: uuid::Uuid, embedding: &[f32]) -> Result<()> {
1248 let db = self.galaxy_db(Galaxy::Embeddings)?;
1249 let key = memory_id.as_bytes();
1250 let val = encode_embedding(embedding);
1251
1252 let mut tx = self
1253 .env
1254 .begin_rw_txn()
1255 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1256 tx.put(db, key, &val, WriteFlags::default())
1257 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding failed: {e}")))?;
1258 tx.commit()
1259 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1260 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1261 Ok(())
1262 }
1263
1264 pub fn get_embedding(&self, memory_id: uuid::Uuid) -> Result<Option<Vec<f32>>> {
1266 let db = self.galaxy_db(Galaxy::Embeddings)?;
1267 let key = memory_id.as_bytes();
1268
1269 let tx = self
1270 .env
1271 .begin_ro_txn()
1272 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1273 match tx.get(db, key) {
1274 Ok(bytes) => {
1275 let embedding = decode_embedding(bytes);
1276 tx.commit()
1277 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1278 Ok(Some(embedding))
1279 }
1280 Err(lmdb::Error::NotFound) => {
1281 tx.commit()
1282 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1283 Ok(None)
1284 }
1285 Err(e) => Err(CoreError::Memory(format!("LMDB get_embedding failed: {e}"))),
1286 }
1287 }
1288
1289 pub fn delete_embedding(&self, memory_id: uuid::Uuid) -> Result<bool> {
1291 let db = self.galaxy_db(Galaxy::Embeddings)?;
1292 let key = memory_id.as_bytes();
1293
1294 let mut tx = self
1295 .env
1296 .begin_rw_txn()
1297 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1298 let exists = tx.get(db, key).is_ok();
1299 if exists {
1300 tx.del(db, key, None)
1301 .map_err(|e| CoreError::Memory(format!("LMDB del_embedding failed: {e}")))?;
1302 }
1303 tx.commit()
1304 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1305 if exists {
1306 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1307 }
1308 Ok(exists)
1309 }
1310
1311 pub fn put_embedding_cache(&self, cache_key: &str, embedding: &[f32]) -> Result<()> {
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(
1322 self.embedding_cache_db,
1323 &cache_key.as_bytes().to_vec(),
1324 &encode_embedding(embedding),
1325 WriteFlags::default(),
1326 )
1327 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1328 tx.commit()
1329 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1330 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1331 Ok(())
1332 }
1333
1334 pub fn put_embedding_cache_batch(&self, entries: &[(String, Vec<f32>)]) -> Result<()> {
1336 if entries.is_empty() {
1337 return Ok(());
1338 }
1339 let mut tx = self
1340 .env
1341 .begin_rw_txn()
1342 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1343 for (key, embedding) in entries {
1344 tx.put(
1345 self.embedding_cache_db,
1346 &key.as_bytes().to_vec(),
1347 &encode_embedding(embedding),
1348 WriteFlags::default(),
1349 )
1350 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1351 }
1352 tx.commit()
1353 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1354 self.mutation_count
1355 .fetch_add(entries.len() as u64, Ordering::Relaxed);
1356 Ok(())
1357 }
1358
1359 pub fn get_embedding_cache(&self, cache_key: &str) -> Result<Option<Vec<f32>>> {
1361 let tx = self
1362 .env
1363 .begin_ro_txn()
1364 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1365 match tx.get(self.embedding_cache_db, &cache_key.as_bytes().to_vec()) {
1366 Ok(bytes) => {
1367 let embedding = decode_embedding(bytes);
1368 tx.commit()
1369 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1370 Ok(Some(embedding))
1371 }
1372 Err(lmdb::Error::NotFound) => {
1373 tx.commit()
1374 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1375 Ok(None)
1376 }
1377 Err(e) => Err(CoreError::Memory(format!(
1378 "LMDB get_embedding_cache failed: {e}"
1379 ))),
1380 }
1381 }
1382
1383 pub fn get_embedding_cache_batch(&self, keys: &[String]) -> Result<Vec<Option<Vec<f32>>>> {
1386 let tx = self
1387 .env
1388 .begin_ro_txn()
1389 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1390 let mut out = Vec::with_capacity(keys.len());
1391 for key in keys {
1392 out.push(
1393 tx.get(self.embedding_cache_db, &key.as_bytes().to_vec())
1394 .ok()
1395 .map(decode_embedding),
1396 );
1397 }
1398 tx.commit()
1399 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1400 Ok(out)
1401 }
1402
1403 pub fn embedding_cache_count(&self) -> Result<u64> {
1405 let tx = self
1406 .env
1407 .begin_ro_txn()
1408 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1409 let mut cursor = tx
1410 .open_ro_cursor(self.embedding_cache_db)
1411 .map_err(|e| CoreError::Memory(format!("LMDB cursor embedding_cache failed: {e}")))?;
1412 let mut count = 0u64;
1413 for _ in cursor.iter() {
1414 count += 1;
1415 }
1416 Ok(count)
1417 }
1418
1419 pub fn record_revision(
1425 &self,
1426 galaxy: Galaxy,
1427 id: MemoryId,
1428 old_hash: &str,
1429 new_hash: &str,
1430 actor: crate::revision::RevisionActor,
1431 ) -> Result<crate::revision::MemoryRevision> {
1432 let seq = self.revisions(galaxy, id)?.len() as u32;
1433 let entry = crate::revision::MemoryRevision {
1434 seq,
1435 timestamp: wm_core::time::now_unix_secs(),
1436 old_hash: old_hash.to_string(),
1437 new_hash: new_hash.to_string(),
1438 actor_session: actor.session,
1439 actor_user: actor.user,
1440 actor_compartment: actor.compartment,
1441 };
1442 let key = crate::revision::revision_key(galaxy, id, seq);
1443 let val = serde_json::to_vec(&entry)
1444 .map_err(|e| CoreError::Memory(format!("revision serialize failed: {e}")))?;
1445 let mut tx = self
1446 .env
1447 .begin_rw_txn()
1448 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1449 tx.put(self.revisions_db, &key, &val, WriteFlags::default())
1450 .map_err(|e| CoreError::Memory(format!("LMDB put revision failed: {e}")))?;
1451 tx.commit()
1452 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1453 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1454 Ok(entry)
1455 }
1456
1457 pub fn revisions(
1460 &self,
1461 galaxy: Galaxy,
1462 id: MemoryId,
1463 ) -> Result<Vec<crate::revision::MemoryRevision>> {
1464 const MDB_GET_CURRENT: u32 = 4;
1469 const MDB_NEXT: u32 = 8;
1470 const MDB_SET_RANGE: u32 = 17;
1471 let prefix = crate::revision::revision_prefix(galaxy, id);
1472 let tx = self
1473 .env
1474 .begin_ro_txn()
1475 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1476 let cursor = tx
1477 .open_ro_cursor(self.revisions_db)
1478 .map_err(|e| CoreError::Memory(format!("LMDB cursor revisions failed: {e}")))?;
1479 let mut out = Vec::new();
1480 if cursor.get(Some(&prefix), None, MDB_SET_RANGE).is_ok() {
1481 while let Ok((key, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1482 if !key.is_some_and(|k| k.starts_with(&prefix)) {
1485 break;
1486 }
1487 let entry: crate::revision::MemoryRevision = serde_json::from_slice(val)
1488 .map_err(|e| CoreError::Memory(format!("revision deserialize failed: {e}")))?;
1489 out.push(entry);
1490 if out.len() >= 10_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1492 break;
1493 }
1494 }
1495 }
1496 drop(cursor);
1497 tx.commit()
1498 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1499 Ok(out)
1500 }
1501
1502 pub fn verify_revision_chain(
1505 &self,
1506 galaxy: Galaxy,
1507 id: MemoryId,
1508 current_hash: &str,
1509 ) -> Result<crate::revision::RevisionChainReport> {
1510 let entries = self.revisions(galaxy, id)?;
1511 Ok(crate::revision::verify_chain(&entries, current_hash))
1512 }
1513
1514 pub fn record_attestation(
1522 &self,
1523 galaxy: Galaxy,
1524 id: MemoryId,
1525 entry: &crate::attestation::RecordAttestation,
1526 ) -> Result<()> {
1527 let key = crate::attestation::attestation_key(galaxy, id);
1528 let val = serde_json::to_vec(entry)
1529 .map_err(|e| CoreError::Memory(format!("attestation serialize failed: {e}")))?;
1530 let mut tx = self
1531 .env
1532 .begin_rw_txn()
1533 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1534 tx.put(self.attestations_db, &key, &val, WriteFlags::default())
1535 .map_err(|e| CoreError::Memory(format!("LMDB put attestation failed: {e}")))?;
1536 tx.commit()
1537 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1538 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1539 Ok(())
1540 }
1541
1542 pub fn attestation(
1545 &self,
1546 galaxy: Galaxy,
1547 id: MemoryId,
1548 ) -> Result<Option<crate::attestation::RecordAttestation>> {
1549 let key = crate::attestation::attestation_key(galaxy, id);
1550 let tx = self
1551 .env
1552 .begin_ro_txn()
1553 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1554 let out =
1555 match tx.get(self.attestations_db, &key) {
1556 Ok(val) => Some(serde_json::from_slice(val).map_err(|e| {
1557 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1558 })?),
1559 Err(lmdb::Error::NotFound) => None,
1560 Err(e) => {
1561 return Err(CoreError::Memory(format!(
1562 "LMDB get attestation failed: {e}"
1563 )));
1564 }
1565 };
1566 drop(tx);
1567 Ok(out)
1568 }
1569
1570 pub fn scan_attestations(&self) -> Result<Vec<crate::attestation::RecordAttestation>> {
1573 const MDB_GET_CURRENT: u32 = 4;
1574 const MDB_NEXT: u32 = 8;
1575 const MDB_FIRST: u32 = 9;
1576 let tx = self
1577 .env
1578 .begin_ro_txn()
1579 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1580 let cursor = tx
1581 .open_ro_cursor(self.attestations_db)
1582 .map_err(|e| CoreError::Memory(format!("LMDB cursor attestations failed: {e}")))?;
1583 let mut out = Vec::new();
1584 if cursor.get(None, None, MDB_FIRST).is_ok() {
1585 while let Ok((_, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1586 let entry: crate::attestation::RecordAttestation = serde_json::from_slice(val)
1587 .map_err(|e| {
1588 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1589 })?;
1590 out.push(entry);
1591 if out.len() >= 1_000_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1593 break;
1594 }
1595 }
1596 }
1597 drop(cursor);
1598 tx.commit()
1599 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1600 Ok(out)
1601 }
1602
1603 pub fn verify_attestation(
1609 &self,
1610 galaxy: Galaxy,
1611 id: MemoryId,
1612 ) -> Result<crate::attestation::AttestationReport> {
1613 use crate::attestation::AttestationReport;
1614 let Some(att) = self.attestation(galaxy, id)? else {
1615 return Ok(AttestationReport {
1616 attested: false,
1617 signature_valid: false,
1618 matches_head: false,
1619 memory_present: self.get(galaxy, id)?.is_some(),
1620 breaks: vec!["no attestation recorded for this memory".to_string()],
1621 });
1622 };
1623 let mut breaks = Vec::new();
1624 let signature_valid = crate::attestation::verify_attestation(&att);
1625 if !signature_valid {
1626 breaks.push("signature does not verify against recorded pubkey".to_string());
1627 }
1628 let (matches_head, memory_present) = if let Some(memory) = self.get(galaxy, id)? {
1629 let matches = memory.metadata.content_hash == att.record_hash;
1630 if !matches {
1631 breaks.push(
1632 "attested record_hash != live content_hash (memory updated after attestation)"
1633 .to_string(),
1634 );
1635 }
1636 (matches, true)
1637 } else {
1638 breaks.push("attested memory id not present in galaxy".to_string());
1639 (false, false)
1640 };
1641 Ok(AttestationReport {
1642 attested: true,
1643 signature_valid,
1644 matches_head,
1645 memory_present,
1646 breaks,
1647 })
1648 }
1649
1650 pub fn attestation_sweep(
1654 &self,
1655 ) -> Result<
1656 Vec<(
1657 crate::attestation::RecordAttestation,
1658 crate::attestation::AttestationReport,
1659 )>,
1660 > {
1661 use crate::attestation::AttestationReport;
1662 let mut out = Vec::new();
1663 for att in self.scan_attestations()? {
1664 let parsed = match (
1665 Galaxy::from_db_name(&att.galaxy),
1666 uuid::Uuid::parse_str(&att.memory_id),
1667 ) {
1668 (Some(galaxy), Ok(id)) => Some((galaxy, id)),
1669 _ => None,
1670 };
1671 match parsed {
1672 Some((galaxy, id)) => out.push((att, self.verify_attestation(galaxy, id)?)),
1673 None => out.push((
1674 att,
1675 AttestationReport {
1676 attested: true,
1677 signature_valid: false,
1678 matches_head: false,
1679 memory_present: false,
1680 breaks: vec![
1681 "attestation row has unparseable galaxy or memory id".to_string(),
1682 ],
1683 },
1684 )),
1685 }
1686 }
1687 Ok(out)
1688 }
1689
1690 pub fn put_cold_record(&self, record: &crate::cold_storage::ColdRecord) -> Result<()> {
1694 let key = record.id.as_bytes();
1695 let val = rmp_serde::to_vec_named(record)
1696 .map_err(|e| CoreError::Memory(format!("Cold record serialization failed: {e}")))?;
1697 let mut tx = self
1698 .env
1699 .begin_rw_txn()
1700 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1701 tx.put(self.cold_storage_db, key, &val, WriteFlags::default())
1702 .map_err(|e| CoreError::Memory(format!("LMDB put cold_storage failed: {e}")))?;
1703 tx.commit()
1704 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1705 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1706 Ok(())
1707 }
1708
1709 pub fn get_cold_record(&self, id: MemoryId) -> Result<Option<crate::cold_storage::ColdRecord>> {
1711 let key = id.as_bytes();
1712 let tx = self
1713 .env
1714 .begin_ro_txn()
1715 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1716 match tx.get(self.cold_storage_db, key) {
1717 Ok(bytes) => {
1718 let record: crate::cold_storage::ColdRecord = rmp_serde::from_slice(bytes)
1719 .map_err(|e| {
1720 CoreError::Memory(format!("Cold record deserialization failed: {e}"))
1721 })?;
1722 Ok(Some(record))
1723 }
1724 Err(lmdb::Error::NotFound) => Ok(None),
1725 Err(e) => Err(CoreError::Memory(format!(
1726 "LMDB get cold_storage failed: {e}"
1727 ))),
1728 }
1729 }
1730
1731 pub fn delete_cold_record(&self, id: MemoryId) -> Result<bool> {
1733 let key = id.as_bytes();
1734 let mut tx = self
1735 .env
1736 .begin_rw_txn()
1737 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1738 let deleted = match tx.del(self.cold_storage_db, key, None) {
1739 Ok(()) => true,
1740 Err(lmdb::Error::NotFound) => false,
1741 Err(e) => {
1742 return Err(CoreError::Memory(format!(
1743 "LMDB del cold_storage failed: {e}"
1744 )));
1745 }
1746 };
1747 tx.commit()
1748 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1749 if deleted {
1750 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1751 }
1752 Ok(deleted)
1753 }
1754
1755 pub fn count_cold(&self, galaxy: Option<Galaxy>) -> Result<usize> {
1757 let tx = self
1758 .env
1759 .begin_ro_txn()
1760 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1761 let mut cursor = tx
1762 .open_ro_cursor(self.cold_storage_db)
1763 .map_err(|e| CoreError::Memory(format!("LMDB open_ro_cursor failed: {e}")))?;
1764 let mut count = 0;
1765 for (_key, val) in cursor.iter() {
1766 if let Some(target_g) = galaxy {
1767 let record: crate::cold_storage::ColdRecord =
1768 rmp_serde::from_slice(val).map_err(|e| {
1769 CoreError::Memory(format!("Cold record deserialization failed: {e}"))
1770 })?;
1771 if record.galaxy == target_g {
1772 count += 1;
1773 }
1774 } else {
1775 count += 1;
1776 }
1777 }
1778 Ok(count)
1779 }
1780
1781 pub fn list_cold_records(
1783 &self,
1784 galaxy: Option<Galaxy>,
1785 limit: usize,
1786 ) -> Result<Vec<crate::cold_storage::ColdRecordSummary>> {
1787 let query = crate::cold_storage::ColdQuery {
1788 galaxy,
1789 limit: if limit == 0 { 100 } else { limit },
1790 ..Default::default()
1791 };
1792 self.query_cold_records(&query)
1793 }
1794
1795 pub fn query_cold_records(
1797 &self,
1798 query: &crate::cold_storage::ColdQuery,
1799 ) -> Result<Vec<crate::cold_storage::ColdRecordSummary>> {
1800 let tx = self
1801 .env
1802 .begin_ro_txn()
1803 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1804 let mut cursor = tx
1805 .open_ro_cursor(self.cold_storage_db)
1806 .map_err(|e| CoreError::Memory(format!("LMDB open_ro_cursor failed: {e}")))?;
1807 let mut results = Vec::new();
1808 let limit = if query.limit == 0 {
1809 usize::MAX
1810 } else {
1811 query.limit
1812 };
1813
1814 for (_key, val) in cursor.iter() {
1815 let record: crate::cold_storage::ColdRecord =
1816 rmp_serde::from_slice(val).map_err(|e| {
1817 CoreError::Memory(format!("Cold record deserialization failed: {e}"))
1818 })?;
1819 let summary = record.summary();
1820 if query.matches(&summary) {
1821 results.push(summary);
1822 if results.len() >= limit {
1823 break;
1824 }
1825 }
1826 }
1827 Ok(results)
1828 }
1829
1830 pub fn find_cold_matching(
1839 &self,
1840 terms: &[String],
1841 galaxy: Option<Galaxy>,
1842 limit: usize,
1843 max_scan: usize,
1844 ) -> Result<crate::cold_storage::ColdDiscoveryOutcome> {
1845 self.find_cold_matching_eligible(terms, galaxy, limit, max_scan, |_| true)
1846 }
1847
1848 pub fn find_cold_matching_eligible(
1851 &self,
1852 terms: &[String],
1853 galaxy: Option<Galaxy>,
1854 limit: usize,
1855 max_scan: usize,
1856 eligible: impl Fn(&Memory) -> bool,
1857 ) -> Result<crate::cold_storage::ColdDiscoveryOutcome> {
1858 use crate::cold_storage::ColdDiscoveryStop;
1859 let mut out = crate::cold_storage::ColdDiscoveryOutcome::default();
1860 if terms.is_empty() || limit == 0 || max_scan == 0 {
1861 return Ok(out);
1862 }
1863 let tx = self
1864 .env
1865 .begin_ro_txn()
1866 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1867 let mut cursor = tx
1868 .open_ro_cursor(self.cold_storage_db)
1869 .map_err(|e| CoreError::Memory(format!("LMDB open_ro_cursor failed: {e}")))?;
1870 let mut iter = cursor.iter();
1871 loop {
1872 if out.scanned >= max_scan {
1873 out.stop_reason = ColdDiscoveryStop::ScanLimit;
1874 break;
1875 }
1876 if out.records.len() >= limit {
1877 out.stop_reason = ColdDiscoveryStop::ResultLimit;
1878 break;
1879 }
1880 let Some((key, val)) = iter.next() else {
1881 out.stop_reason = ColdDiscoveryStop::Exhausted;
1882 break;
1883 };
1884 out.scanned += 1;
1885 let record: crate::cold_storage::ColdRecord = if let Ok(r) = rmp_serde::from_slice(val)
1886 {
1887 r
1888 } else {
1889 out.integrity_rejected += 1;
1890 continue;
1891 };
1892 if let Some(g) = galaxy {
1893 if record.galaxy != g {
1894 continue;
1895 }
1896 }
1897 out.candidates += 1;
1898 let mem = if let Ok(m) = record.decompress() {
1899 m
1900 } else {
1901 out.integrity_rejected += 1;
1902 continue;
1903 };
1904 if mem.metadata.is_private {
1905 out.private_skipped += 1;
1906 continue;
1907 }
1908 if !mem.metadata.validity.is_current() {
1909 out.non_current_skipped += 1;
1910 continue;
1911 }
1912 let integrity_ok = key == record.id.as_bytes()
1913 && mem.metadata.id == record.id
1914 && mem.metadata.galaxy == record.galaxy
1915 && mem.metadata.content_hash == record.content_hash
1916 && crate::content_hash(&mem.content) == record.content_hash;
1917 if !integrity_ok {
1918 out.integrity_rejected += 1;
1919 continue;
1920 }
1921 let haystack = format!(
1922 "{} {}",
1923 mem.content.to_lowercase(),
1924 mem.metadata.tags.join(" ").to_lowercase()
1925 );
1926 if !terms.iter().all(|t| haystack.contains(t.as_str())) {
1927 continue;
1928 }
1929 out.matched += 1;
1930 if !eligible(&mem) {
1931 out.eligibility_skipped += 1;
1932 continue;
1933 }
1934 out.records.push(record);
1935 }
1936 Ok(out)
1937 }
1938
1939 pub fn freeze_to_cold(
1945 &self,
1946 search: Option<&crate::SearchEngine>,
1947 memory_id: MemoryId,
1948 distance: f32,
1949 factors: crate::cold_storage::OuterRimFactors,
1950 digest_id: Option<MemoryId>,
1951 notes: Option<String>,
1952 codec: crate::cold_storage::CompressionCodec,
1953 ) -> Result<crate::cold_storage::ColdRecord> {
1954 let (galaxy, mut mem) = self.find_across_galaxies(memory_id)?.ok_or_else(|| {
1955 CoreError::NotFound(format!("Memory {memory_id} not found in hot store"))
1956 })?;
1957
1958 if mem.metadata.tier != crate::memory::Tier::Archival {
1960 let _ = mem.transition_tier(crate::memory::Tier::Archival);
1961 }
1962
1963 let record =
1964 crate::cold_storage::ColdRecord::new(&mem, distance, factors, digest_id, notes, codec)?;
1965
1966 self.put_cold_record(&record)?;
1968
1969 self.delete(galaxy, memory_id)?;
1971
1972 if let Some(engine) = search {
1974 if let Ok(mut writer_guard) = engine.writer() {
1975 let _ = engine.delete_document(&mut writer_guard, &memory_id.to_string());
1976 let _ = engine.commit(&mut writer_guard);
1977 }
1978 }
1979
1980 Ok(record)
1981 }
1982
1983 pub fn thaw_from_cold(
1989 &self,
1990 search: Option<&crate::SearchEngine>,
1991 memory_id: MemoryId,
1992 ) -> Result<Memory> {
1993 let record = self.get_cold_record(memory_id)?.ok_or_else(|| {
1994 CoreError::NotFound(format!("Memory {memory_id} not found in cold storage"))
1995 })?;
1996
1997 let mut mem = record.decompress()?;
1998
1999 let _ = mem.transition_tier(crate::memory::Tier::Episodic);
2001 mem.metadata.accessed_at = chrono::Utc::now();
2002 mem.metadata.access_count += 1;
2003 mem.metadata.recall_count += 1;
2004 if !mem.metadata.tags.iter().any(|t| t == "thawed:phagic") {
2005 mem.metadata.tags.push("thawed:phagic".to_string());
2006 }
2007
2008 self.put(record.galaxy, &mem)?;
2010
2011 if let Some(engine) = search {
2013 if let Ok(mut writer_guard) = engine.writer() {
2014 let _ = engine.index_memory(&mut writer_guard, &mem);
2015 let _ = engine.commit(&mut writer_guard);
2016 }
2017 }
2018
2019 self.delete_cold_record(memory_id)?;
2021
2022 Ok(mem)
2023 }
2024
2025 pub fn find_anywhere(&self, id: MemoryId) -> Result<Option<(Galaxy, Memory, bool)>> {
2029 if let Some((galaxy, mem)) = self.find_across_galaxies(id)? {
2030 return Ok(Some((galaxy, mem, false)));
2031 }
2032 if let Some(cold_record) = self.get_cold_record(id)? {
2033 let galaxy = cold_record.galaxy;
2034 let mem = cold_record.decompress()?;
2035 return Ok(Some((galaxy, mem, true)));
2036 }
2037 Ok(None)
2038 }
2039}
2040
2041#[cfg(test)]
2042mod tests {
2043 use super::*;
2044 use crate::content_hash;
2045
2046 #[test]
2047 fn open_and_create_galaxies() {
2048 let tmp = tempfile::tempdir().unwrap();
2049 let store = MemoryStore::open_default(tmp.path()).unwrap();
2050 for galaxy in Galaxy::all() {
2051 let _db = store.galaxy_db(galaxy).unwrap();
2052 }
2053 }
2054
2055 #[test]
2056 fn ensure_schema_completes_a_pre_cold_store() {
2057 use lmdb::{DatabaseFlags as LmdbFlags, Environment as LmdbEnv};
2058 use uuid::Uuid;
2059
2060 let tmp = tempfile::tempdir().unwrap();
2063 let path = tmp.path().join("old-store");
2064 std::fs::create_dir_all(&path).unwrap();
2065 {
2066 let env = LmdbEnv::new().set_max_dbs(64).open(&path).unwrap();
2067 for galaxy in Galaxy::all() {
2068 env.create_db(Some(galaxy.db_name()), LmdbFlags::default())
2069 .unwrap();
2070 }
2071 for (name, flags) in crate::indexes::INDEX_DBS {
2072 env.create_db(Some(name), *flags).unwrap();
2073 }
2074 for (name, flags) in [
2075 ("episodic_records", LmdbFlags::default()),
2076 ("episodic_terms_v2", LmdbFlags::DUP_SORT),
2077 ("embedding_cache", LmdbFlags::default()),
2078 ("revisions", LmdbFlags::default()),
2079 (crate::attestation::ATTESTATIONS_DB, LmdbFlags::default()),
2080 ] {
2081 env.create_db(Some(name), flags).unwrap();
2082 }
2083 }
2084
2085 let error = match MemoryStore::open_readonly(&path) {
2086 Ok(_) => panic!("strict read-only open must refuse an incomplete store"),
2087 Err(e) => e.to_string(),
2088 };
2089 assert!(error.contains("cold_storage"), "{error}");
2090
2091 let created = MemoryStore::ensure_schema(&path).unwrap();
2092 assert_eq!(created, vec!["cold_storage".to_string()], "{created:?}");
2093
2094 let store = MemoryStore::open_readonly(&path).unwrap();
2095 assert!(store.get_cold_record(Uuid::nil()).unwrap().is_none());
2096
2097 assert!(MemoryStore::ensure_schema(&path).unwrap().is_empty());
2099 }
2100
2101 #[test]
2102 fn cold_discovery_hydrates_verifies_and_respects_visibility() {
2103 use crate::cold_storage::{ColdRecord, CompressionCodec, OuterRimFactors};
2104 let tmp = tempfile::tempdir().unwrap();
2105 let store = MemoryStore::open_default(tmp.path()).unwrap();
2106 let factors = OuterRimFactors {
2107 age_factor: 0.5,
2108 access_factor: 0.5,
2109 resonance_factor: 0.5,
2110 emotional_factor: 0.5,
2111 importance_factor: 0.5,
2112 distance: 0.5,
2113 };
2114
2115 let pub_mem = Memory::new(
2116 Galaxy::Codex,
2117 "public needle zxquniquecoldfact741 buried".into(),
2118 );
2119 let rec = ColdRecord::new(
2120 &pub_mem,
2121 0.5,
2122 factors.clone(),
2123 None,
2124 None,
2125 CompressionCodec::Gzip,
2126 )
2127 .unwrap();
2128 store.put_cold_record(&rec).unwrap();
2129 let out = store
2130 .find_cold_matching(&["zxquniquecoldfact741".to_string()], None, 10, 100)
2131 .unwrap();
2132 assert_eq!(out.matched, 1);
2133 assert_eq!(out.integrity_rejected, 0);
2134 assert_eq!(out.records.len(), 1);
2135
2136 let mut priv_mem = Memory::new(Galaxy::Codex, "private needle zxquniquecoldfact742".into());
2138 priv_mem.metadata.is_private = true;
2139 store
2140 .put_cold_record(
2141 &ColdRecord::new(&priv_mem, 0.5, factors, None, None, CompressionCodec::Gzip)
2142 .unwrap(),
2143 )
2144 .unwrap();
2145 let out_priv = store
2146 .find_cold_matching(&["zxquniquecoldfact742".to_string()], None, 10, 100)
2147 .unwrap();
2148 assert_eq!(out_priv.matched, 0);
2149 assert_eq!(out_priv.private_skipped, 1);
2150
2151 let mut tampered = rec.clone();
2154 let mut bad = Memory::new(Galaxy::Codex, "tampered needle zxquniquecoldfact743".into());
2155 bad.metadata.id = rec.id;
2156 let (payload, size) =
2157 crate::cold_storage::compress_memory(&bad, CompressionCodec::Gzip).unwrap();
2158 tampered.compressed_payload = payload;
2159 tampered.uncompressed_size = size;
2160 store.put_cold_record(&tampered).unwrap();
2161 let out_tamper = store
2162 .find_cold_matching(&["zxquniquecoldfact743".to_string()], None, 10, 100)
2163 .unwrap();
2164 assert_eq!(out_tamper.matched, 0);
2165 assert_eq!(out_tamper.integrity_rejected, 1);
2166
2167 store.delete_cold_record(rec.id).unwrap();
2170 let wrong_key = uuid::Uuid::from_u128(741);
2171 assert_ne!(wrong_key, rec.id);
2172 let value = rmp_serde::to_vec_named(&rec).unwrap();
2173 let mut tx = store.env.begin_rw_txn().unwrap();
2174 tx.put(
2175 store.cold_storage_db,
2176 wrong_key.as_bytes(),
2177 &value,
2178 WriteFlags::default(),
2179 )
2180 .unwrap();
2181 tx.commit().unwrap();
2182 let wrong_key_out = store
2183 .find_cold_matching(&["zxquniquecoldfact741".into()], None, 10, 100)
2184 .unwrap();
2185 assert!(wrong_key_out.records.is_empty());
2186 assert_eq!(wrong_key_out.integrity_rejected, 1);
2187 }
2188
2189 #[test]
2193 fn query_substring_filters_galaxy_wide() {
2194 let tmp = tempfile::tempdir().unwrap();
2195 let store = MemoryStore::open_default(tmp.path()).unwrap();
2196
2197 for (i, content) in [
2198 "the mesh joins at dawn",
2199 "unrelated content entirely",
2200 "MESH joins at dusk",
2201 ]
2202 .iter()
2203 .enumerate()
2204 {
2205 let mut m = Memory::new(Galaxy::Codex, content.to_string());
2206 m.metadata.importance = 0.5 + i as f32 / 10.0;
2207 store.put(Galaxy::Codex, &m).unwrap();
2208 }
2209
2210 let hits = store
2211 .query(
2212 Galaxy::Codex,
2213 &MemoryQuery::new().with_content_substring("mesh joins"),
2214 )
2215 .unwrap();
2216 assert_eq!(hits.len(), 2, "CI substring must match both: {hits:?}");
2217 assert!(
2218 hits.iter()
2219 .all(|m| m.content.to_lowercase().contains("mesh joins"))
2220 );
2221
2222 let none = store
2223 .query(
2224 Galaxy::Codex,
2225 &MemoryQuery::new().with_content_substring("quantum calendar"),
2226 )
2227 .unwrap();
2228 assert!(none.is_empty(), "no match must be an honest empty set");
2229
2230 let mut tagged = Memory::new(Galaxy::Codex, "mesh joins again".to_string());
2232 tagged.metadata.tags = vec!["mesh".into()];
2233 store.put(Galaxy::Codex, &tagged).unwrap();
2234 let combined = store
2235 .query(
2236 Galaxy::Codex,
2237 &MemoryQuery::new()
2238 .with_tags(vec!["mesh".into()])
2239 .with_content_substring("again"),
2240 )
2241 .unwrap();
2242 assert_eq!(combined.len(), 1);
2243 assert_eq!(combined[0].content, "mesh joins again");
2244 }
2245
2246 #[cfg(unix)]
2247 #[test]
2248 fn store_dir_has_restrictive_permissions() {
2249 let tmp = tempfile::tempdir().unwrap();
2250 let store_path = tmp.path().join("lmdb");
2251 let _store = MemoryStore::open_default(&store_path).unwrap();
2252 let perms = std::fs::metadata(&store_path).unwrap().permissions().mode();
2253 assert_eq!(
2254 perms & 0o777,
2255 0o700,
2256 "store directory should have 0700 permissions, got {:o}",
2257 perms & 0o777
2258 );
2259 }
2260
2261 #[test]
2262 fn put_get_delete_memory() {
2263 let tmp = tempfile::tempdir().unwrap();
2264 let store = MemoryStore::open_default(tmp.path()).unwrap();
2265
2266 let mem = Memory::new(Galaxy::Codex, "Hello world".to_string());
2267 let id = mem.metadata.id;
2268
2269 store.put(Galaxy::Codex, &mem).unwrap();
2270 let retrieved = store.get(Galaxy::Codex, id).unwrap();
2271 assert!(retrieved.is_some());
2272 assert_eq!(retrieved.unwrap().content, "Hello world");
2273
2274 let deleted = store.delete(Galaxy::Codex, id).unwrap();
2275 assert!(deleted);
2276
2277 let gone = store.get(Galaxy::Codex, id).unwrap();
2278 assert!(gone.is_none());
2279 }
2280
2281 #[test]
2282 fn scan_memories() {
2283 let tmp = tempfile::tempdir().unwrap();
2284 let store = MemoryStore::open_default(tmp.path()).unwrap();
2285
2286 for i in 0..5 {
2287 let mem = Memory::new(Galaxy::Codex, format!("memory-{i}"));
2288 store.put(Galaxy::Codex, &mem).unwrap();
2289 }
2290
2291 let all = store.scan(Galaxy::Codex, 100).unwrap();
2292 assert_eq!(all.len(), 5);
2293
2294 let limited = store.scan(Galaxy::Codex, 3).unwrap();
2295 assert_eq!(limited.len(), 3);
2296 }
2297
2298 #[test]
2299 fn overwrite_removes_stale_index_entries() {
2300 let tmp = tempfile::tempdir().unwrap();
2301 let store = MemoryStore::open_default(tmp.path()).unwrap();
2302
2303 let mut mem = Memory::new(Galaxy::Codex, "overwrite target".to_string());
2305 mem.metadata.tags = vec!["alpha".to_string()];
2306 mem.metadata.importance = 0.9;
2307 let id = mem.metadata.id;
2308 store.put(Galaxy::Codex, &mem).unwrap();
2309
2310 let mut updated = Memory::new(Galaxy::Codex, "overwritten content".to_string());
2312 updated.metadata.id = id;
2313 updated.metadata.tags = vec!["beta".to_string()];
2314 updated.metadata.importance = 0.1;
2315 store.put(Galaxy::Codex, &updated).unwrap();
2316
2317 let tx = store.env().begin_ro_txn().unwrap();
2319 let by_alpha = store
2320 .index_dbs()
2321 .find_by_tag(&tx, Galaxy::Codex, "alpha")
2322 .unwrap();
2323 let by_beta = store
2324 .index_dbs()
2325 .find_by_tag(&tx, Galaxy::Codex, "beta")
2326 .unwrap();
2327 assert!(
2328 by_alpha.is_empty(),
2329 "stale tag index entries must be removed on overwrite"
2330 );
2331 assert_eq!(by_beta, vec![id]);
2332
2333 let by_importance = store
2334 .index_dbs()
2335 .find_by_importance_range(&tx, Galaxy::Codex, 0.0, 0.2)
2336 .unwrap();
2337 assert!(
2338 by_importance.contains(&id),
2339 "new importance must be indexed"
2340 );
2341 let by_high = store
2342 .index_dbs()
2343 .find_by_importance_range(&tx, Galaxy::Codex, 0.8, 1.0)
2344 .unwrap();
2345 assert!(
2346 !by_high.contains(&id),
2347 "stale importance index entries must be removed on overwrite"
2348 );
2349
2350 let old_hash = content_hash("overwrite target");
2351 let new_hash = content_hash("overwritten content");
2352 assert_eq!(
2353 store
2354 .index_dbs()
2355 .find_by_content_hash(&tx, Galaxy::Codex, &old_hash)
2356 .unwrap(),
2357 None,
2358 "stale content-hash index entry must be removed"
2359 );
2360 assert_eq!(
2361 store
2362 .index_dbs()
2363 .find_by_content_hash(&tx, Galaxy::Codex, &new_hash)
2364 .unwrap(),
2365 Some(id)
2366 );
2367 }
2368
2369 #[test]
2370 fn count_memories() {
2371 let tmp = tempfile::tempdir().unwrap();
2372 let store = MemoryStore::open_default(tmp.path()).unwrap();
2373
2374 assert_eq!(store.count(Galaxy::Codex).unwrap(), 0);
2375
2376 for i in 0..3 {
2377 let mem = Memory::new(Galaxy::Codex, format!("count-{i}"));
2378 store.put(Galaxy::Codex, &mem).unwrap();
2379 }
2380
2381 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
2382 }
2383
2384 #[test]
2385 fn get_nonexistent_returns_none() {
2386 let tmp = tempfile::tempdir().unwrap();
2387 let store = MemoryStore::open_default(tmp.path()).unwrap();
2388 let result = store.get(Galaxy::Codex, uuid::Uuid::new_v4()).unwrap();
2389 assert!(result.is_none());
2390 }
2391
2392 #[test]
2393 fn raw_put_get() {
2394 let tmp = tempfile::tempdir().unwrap();
2395 let store = MemoryStore::open_default(tmp.path()).unwrap();
2396
2397 store
2398 .put_raw(Galaxy::Substrate, b"config:key", b"value123")
2399 .unwrap();
2400 let val = store.get_raw(Galaxy::Substrate, b"config:key").unwrap();
2401 assert_eq!(val, Some(b"value123".to_vec()));
2402 }
2403
2404 #[test]
2405 fn put_dedup_prevents_duplicates() {
2406 let tmp = tempfile::tempdir().unwrap();
2407 let store = MemoryStore::open_default(tmp.path()).unwrap();
2408
2409 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
2410 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
2411
2412 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
2413 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
2414
2415 assert_eq!(id1, id2, "dedup should return same ID for same content");
2416 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
2417 }
2418
2419 #[test]
2420 fn put_dedup_allows_different_content() {
2421 let tmp = tempfile::tempdir().unwrap();
2422 let store = MemoryStore::open_default(tmp.path()).unwrap();
2423
2424 let mem1 = Memory::new(Galaxy::Codex, "content A".into());
2425 store.put_dedup(Galaxy::Codex, &mem1).unwrap();
2426
2427 let mem2 = Memory::new(Galaxy::Codex, "content B".into());
2428 store.put_dedup(Galaxy::Codex, &mem2).unwrap();
2429
2430 assert_eq!(store.count(Galaxy::Codex).unwrap(), 2);
2431 }
2432
2433 #[test]
2434 fn put_batch_atomic_write() {
2435 let tmp = tempfile::tempdir().unwrap();
2436 let store = MemoryStore::open_default(tmp.path()).unwrap();
2437
2438 let memories: Vec<Memory> = (0..10)
2439 .map(|i| Memory::new(Galaxy::Codex, format!("batch-{i}")))
2440 .collect();
2441
2442 store.put_batch(Galaxy::Codex, &memories).unwrap();
2443 assert_eq!(store.count(Galaxy::Codex).unwrap(), 10);
2444 }
2445
2446 #[test]
2447 fn query_by_tags() {
2448 let tmp = tempfile::tempdir().unwrap();
2449 let store = MemoryStore::open_default(tmp.path()).unwrap();
2450
2451 let mem1 = Memory::new(Galaxy::Codex, "tagged memory".into())
2452 .with_tags(vec!["rust".into(), "memory".into()]);
2453 let mem2 =
2454 Memory::new(Galaxy::Codex, "other memory".into()).with_tags(vec!["python".into()]);
2455 store.put(Galaxy::Codex, &mem1).unwrap();
2456 store.put(Galaxy::Codex, &mem2).unwrap();
2457
2458 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
2459 let results = store.query(Galaxy::Codex, &query).unwrap();
2460 assert_eq!(results.len(), 1);
2461 assert_eq!(results[0].content, "tagged memory");
2462 }
2463
2464 #[test]
2465 fn query_by_importance_range() {
2466 let tmp = tempfile::tempdir().unwrap();
2467 let store = MemoryStore::open_default(tmp.path()).unwrap();
2468
2469 store
2470 .put(
2471 Galaxy::Codex,
2472 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
2473 )
2474 .unwrap();
2475 store
2476 .put(
2477 Galaxy::Codex,
2478 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
2479 )
2480 .unwrap();
2481 store
2482 .put(
2483 Galaxy::Codex,
2484 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
2485 )
2486 .unwrap();
2487
2488 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
2489 let results = store.query(Galaxy::Codex, &query).unwrap();
2490 assert_eq!(results.len(), 1);
2491 assert_eq!(results[0].content, "mid");
2492 }
2493
2494 #[test]
2495 fn memory_query_one_sided_time_bounds() {
2496 let old = Memory::new(Galaxy::Codex, "old".into());
2499 let mut recent = Memory::new(Galaxy::Codex, "recent".into());
2500 recent.metadata.created_at = old.metadata.created_at + chrono::Duration::days(30);
2501
2502 let cutoff = old.metadata.created_at + chrono::Duration::days(10);
2503 let after = MemoryQuery::new().with_created_after(cutoff);
2504 assert!(!after.matches(&old), "pre-cutoff memory must not match");
2505 assert!(after.matches(&recent), "post-cutoff memory must match");
2506
2507 let before = MemoryQuery::new().with_created_before(cutoff);
2508 assert!(before.matches(&old), "pre-cutoff memory must match");
2509 assert!(
2510 !before.matches(&recent),
2511 "post-cutoff memory must not match"
2512 );
2513
2514 let edge = MemoryQuery::new().with_created_after(cutoff);
2516 let mut at = Memory::new(Galaxy::Codex, "at cutoff".into());
2517 at.metadata.created_at = cutoff;
2518 assert!(edge.matches(&at), "created_at == after bound is inclusive");
2519 }
2520
2521 #[test]
2522 fn embedding_put_get_delete() {
2523 let tmp = tempfile::tempdir().unwrap();
2524 let store = MemoryStore::open_default(tmp.path()).unwrap();
2525
2526 let id = uuid::Uuid::new_v4();
2527 let embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5];
2528
2529 store.put_embedding(id, &embedding).unwrap();
2530 let retrieved = store.get_embedding(id).unwrap();
2531 assert!(retrieved.is_some());
2532 let retrieved = retrieved.unwrap();
2533 assert_eq!(retrieved.len(), 5);
2534 assert!((retrieved[0] - 0.1).abs() < f32::EPSILON);
2535
2536 assert!(store.delete_embedding(id).unwrap());
2537 assert!(store.get_embedding(id).unwrap().is_none());
2538 }
2539
2540 #[test]
2541 fn embedding_cache_roundtrip_batch_and_count() {
2542 let tmp = tempfile::tempdir().unwrap();
2543 let store = MemoryStore::open_default(tmp.path()).unwrap();
2544
2545 let entries: Vec<(String, Vec<f32>)> = (0..5)
2546 .map(|i| (format!("ns:model:{i:016x}"), vec![i as f32; 8]))
2547 .collect();
2548 store.put_embedding_cache_batch(&entries).unwrap();
2549 assert_eq!(store.embedding_cache_count().unwrap(), 5);
2550
2551 let hit = store
2553 .get_embedding_cache("ns:model:0000000000000003")
2554 .unwrap();
2555 assert_eq!(hit.unwrap(), vec![3.0f32; 8]);
2556 assert!(
2557 store
2558 .get_embedding_cache("ns:model:absent")
2559 .unwrap()
2560 .is_none()
2561 );
2562
2563 let keys: Vec<String> = (0..6).map(|i| format!("ns:model:{i:016x}")).collect();
2565 let batch = store.get_embedding_cache_batch(&keys).unwrap();
2566 assert_eq!(batch.len(), 6);
2567 assert!(batch[0..5].iter().all(Option::is_some));
2568 assert!(batch[5].is_none());
2569
2570 store
2572 .put_embedding_cache("ns:model:0000000000000001", &[9.0; 8])
2573 .unwrap();
2574 assert_eq!(store.embedding_cache_count().unwrap(), 5);
2575 assert_eq!(
2576 store
2577 .get_embedding_cache("ns:model:0000000000000001")
2578 .unwrap()
2579 .unwrap(),
2580 vec![9.0f32; 8]
2581 );
2582 }
2583
2584 #[test]
2585 fn embedding_cache_survives_store_reopen() {
2586 let tmp = tempfile::tempdir().unwrap();
2588 {
2589 let store = MemoryStore::open_default(tmp.path()).unwrap();
2590 store
2591 .put_embedding_cache("onnx:bge-small:abc", &[0.5; 384])
2592 .unwrap();
2593 }
2594 let reopened = MemoryStore::open_default(tmp.path()).unwrap();
2595 let cached = reopened.get_embedding_cache("onnx:bge-small:abc").unwrap();
2596 assert_eq!(cached.unwrap(), vec![0.5f32; 384]);
2597 }
2598
2599 #[test]
2600 fn content_hash_is_sha256() {
2601 let hash1 = content_hash("test content");
2602 let hash2 = content_hash("test content");
2603 let hash3 = content_hash("different content");
2604
2605 assert_eq!(hash1, hash2, "same content should produce same hash");
2606 assert_ne!(
2607 hash1, hash3,
2608 "different content should produce different hash"
2609 );
2610 assert_eq!(hash1.len(), 64, "SHA-256 hex should be 64 chars");
2611 }
2612
2613 #[test]
2614 fn query_by_tag_uses_index() {
2615 let tmp = tempfile::tempdir().unwrap();
2616 let store = MemoryStore::open_default(tmp.path()).unwrap();
2617
2618 let mem1 = Memory::new(Galaxy::Codex, "tagged".into())
2619 .with_tags(vec!["rust".into(), "memory".into()]);
2620 let mem2 = Memory::new(Galaxy::Codex, "other".into()).with_tags(vec!["python".into()]);
2621 store.put(Galaxy::Codex, &mem1).unwrap();
2622 store.put(Galaxy::Codex, &mem2).unwrap();
2623
2624 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
2625 let results = store.query(Galaxy::Codex, &query).unwrap();
2626 assert_eq!(results.len(), 1);
2627 assert_eq!(results[0].content, "tagged");
2628 }
2629
2630 #[test]
2631 fn query_by_importance_uses_index() {
2632 let tmp = tempfile::tempdir().unwrap();
2633 let store = MemoryStore::open_default(tmp.path()).unwrap();
2634
2635 store
2636 .put(
2637 Galaxy::Codex,
2638 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
2639 )
2640 .unwrap();
2641 store
2642 .put(
2643 Galaxy::Codex,
2644 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
2645 )
2646 .unwrap();
2647 store
2648 .put(
2649 Galaxy::Codex,
2650 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
2651 )
2652 .unwrap();
2653
2654 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
2655 let results = store.query(Galaxy::Codex, &query).unwrap();
2656 assert_eq!(results.len(), 1);
2657 assert_eq!(results[0].content, "mid");
2658 }
2659
2660 #[test]
2661 fn query_by_time_uses_index() {
2662 let tmp = tempfile::tempdir().unwrap();
2663 let store = MemoryStore::open_default(tmp.path()).unwrap();
2664
2665 let t0 = chrono::Utc::now();
2666 std::thread::sleep(std::time::Duration::from_millis(10));
2667 let mem = Memory::new(Galaxy::Codex, "timed".into());
2668 store.put(Galaxy::Codex, &mem).unwrap();
2669 std::thread::sleep(std::time::Duration::from_millis(10));
2670 let t2 = chrono::Utc::now();
2671
2672 let query = MemoryQuery::new().with_time_range(t0, t2);
2673 let results = store.query(Galaxy::Codex, &query).unwrap();
2674 assert_eq!(results.len(), 1);
2675 assert_eq!(results[0].content, "timed");
2676 }
2677
2678 #[test]
2679 fn delete_removes_index_entries() {
2680 let tmp = tempfile::tempdir().unwrap();
2681 let store = MemoryStore::open_default(tmp.path()).unwrap();
2682
2683 let mem = Memory::new(Galaxy::Codex, "test".into())
2684 .with_tags(vec!["tag1".into()])
2685 .with_importance(0.7);
2686 let id = mem.metadata.id;
2687 let hash = mem.metadata.content_hash.clone();
2688 store.put(Galaxy::Codex, &mem).unwrap();
2689
2690 assert!(
2692 store
2693 .find_by_content_hash(Galaxy::Codex, &hash)
2694 .unwrap()
2695 .is_some()
2696 );
2697
2698 store.delete(Galaxy::Codex, id).unwrap();
2700
2701 assert!(
2703 store
2704 .find_by_content_hash(Galaxy::Codex, &hash)
2705 .unwrap()
2706 .is_none()
2707 );
2708
2709 let query = MemoryQuery::new().with_tags(vec!["tag1".into()]);
2711 let results = store.query(Galaxy::Codex, &query).unwrap();
2712 assert!(results.is_empty());
2713 }
2714
2715 #[test]
2716 fn put_batch_updates_indexes() {
2717 let tmp = tempfile::tempdir().unwrap();
2718 let store = MemoryStore::open_default(tmp.path()).unwrap();
2719
2720 let memories: Vec<Memory> = (0..5)
2721 .map(|i| {
2722 Memory::new(Galaxy::Codex, format!("batch-{i}"))
2723 .with_tags(vec![format!("tag{i}")])
2724 .with_importance(i as f32 * 0.2)
2725 })
2726 .collect();
2727 store.put_batch(Galaxy::Codex, &memories).unwrap();
2728
2729 for i in 0..5 {
2730 let query = MemoryQuery::new().with_tags(vec![format!("tag{i}")]);
2731 let results = store.query(Galaxy::Codex, &query).unwrap();
2732 assert_eq!(results.len(), 1, "tag{i} should have 1 result");
2733 }
2734 }
2735
2736 #[test]
2737 fn find_by_content_hash_indexed_matches_scan() {
2738 let tmp = tempfile::tempdir().unwrap();
2739 let store = MemoryStore::open_default(tmp.path()).unwrap();
2740
2741 let mem = Memory::new(Galaxy::Codex, "dedup test".into());
2742 let id = mem.metadata.id;
2743 let hash = mem.metadata.content_hash.clone();
2744 store.put(Galaxy::Codex, &mem).unwrap();
2745
2746 let indexed = store.find_by_content_hash(Galaxy::Codex, &hash).unwrap();
2747 let scanned = store
2748 .find_by_content_hash_scan(Galaxy::Codex, &hash)
2749 .unwrap();
2750
2751 assert_eq!(indexed, scanned);
2752 assert_eq!(indexed, Some(id));
2753 }
2754
2755 #[test]
2756 fn put_dedup_uses_index() {
2757 let tmp = tempfile::tempdir().unwrap();
2758 let store = MemoryStore::open_default(tmp.path()).unwrap();
2759
2760 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
2761 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
2762
2763 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
2764 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
2765
2766 assert_eq!(id1, id2, "dedup should return same ID for same content");
2767 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
2768 }
2769
2770 #[test]
2771 fn put_semantic_updates_coord5d() {
2772 let tmp = tempfile::tempdir().unwrap();
2773 let store = MemoryStore::open_default(tmp.path()).unwrap();
2774
2775 let mut mem = Memory::new(
2776 Galaxy::Codex,
2777 "The algorithm computes data using a systematic method".to_string(),
2778 );
2779 let original_coord = mem.metadata.coord5d.clone();
2780 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2781
2782 assert_ne!(
2784 mem.metadata.coord5d.x, original_coord.x,
2785 "semantic encoding should change x"
2786 );
2787 assert_ne!(
2788 mem.metadata.coord5d.y, original_coord.y,
2789 "semantic encoding should change y"
2790 );
2791
2792 let retrieved = store.get(Galaxy::Codex, mem.metadata.id).unwrap().unwrap();
2794 assert_eq!(retrieved.metadata.coord5d.x, mem.metadata.coord5d.x);
2795 }
2796
2797 #[test]
2798 fn put_semantic_preserves_temporal_and_importance() {
2799 let tmp = tempfile::tempdir().unwrap();
2800 let store = MemoryStore::open_default(tmp.path()).unwrap();
2801
2802 let mut mem = Memory::new(Galaxy::Codex, "test content".into()).with_importance(0.8);
2803 mem.metadata.coord5d.w = 0.6;
2804 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2805
2806 assert!((mem.metadata.coord5d.w - 0.6).abs() < f32::EPSILON);
2807 assert!((mem.metadata.coord5d.v - 0.8).abs() < f32::EPSILON);
2808 }
2809
2810 #[test]
2811 fn find_similar_returns_nearest_first() {
2812 let tmp = tempfile::tempdir().unwrap();
2813 let store = MemoryStore::open_default(tmp.path()).unwrap();
2814
2815 let mut logic_mem = Memory::new(
2816 Galaxy::Codex,
2817 "The algorithm computes data using systematic logic and analysis".to_string(),
2818 );
2819 store.put_semantic(Galaxy::Codex, &mut logic_mem).unwrap();
2820
2821 let mut emotion_mem = Memory::new(
2822 Galaxy::Codex,
2823 "I feel love and joy with deep passion and empathy in my heart".to_string(),
2824 );
2825 store.put_semantic(Galaxy::Codex, &mut emotion_mem).unwrap();
2826
2827 let results = store
2829 .find_similar(Galaxy::Codex, "algorithm data systematic method", 10)
2830 .unwrap();
2831 assert!(!results.is_empty());
2832 assert_eq!(results[0].0.metadata.id, logic_mem.metadata.id);
2833
2834 let results = store
2836 .find_similar(Galaxy::Codex, "love joy passion heart feeling", 10)
2837 .unwrap();
2838 assert!(!results.is_empty());
2839 assert_eq!(results[0].0.metadata.id, emotion_mem.metadata.id);
2840 }
2841
2842 #[test]
2843 fn find_similar_empty_galaxy() {
2844 let tmp = tempfile::tempdir().unwrap();
2845 let store = MemoryStore::open_default(tmp.path()).unwrap();
2846
2847 let results = store.find_similar(Galaxy::Codex, "anything", 10).unwrap();
2848 assert!(results.is_empty());
2849 }
2850
2851 #[test]
2852 fn find_similar_respects_limit() {
2853 let tmp = tempfile::tempdir().unwrap();
2854 let store = MemoryStore::open_default(tmp.path()).unwrap();
2855
2856 for i in 0..5 {
2857 let mut mem = Memory::new(Galaxy::Codex, format!("algorithm data method {i}"));
2858 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
2859 }
2860
2861 let results = store
2862 .find_similar(Galaxy::Codex, "algorithm data", 3)
2863 .unwrap();
2864 assert_eq!(results.len(), 3);
2865 }
2866
2867 #[test]
2868 fn semantic_encoder_accessible() {
2869 let tmp = tempfile::tempdir().unwrap();
2870 let store = MemoryStore::open_default(tmp.path()).unwrap();
2871
2872 let scores = store.semantic_encoder().encode("algorithm data logic");
2873 assert!(scores.x < 0.5);
2875 }
2876
2877 #[test]
2878 fn put_raw_batch_writes_atomically() {
2879 let tmp = tempfile::tempdir().unwrap();
2880 let store = MemoryStore::open_default(tmp.path()).unwrap();
2881
2882 let entries: &[(&[u8], &[u8])] =
2883 &[(b"key1", b"val1"), (b"key2", b"val2"), (b"key3", b"val3")];
2884 store.put_raw_batch(Galaxy::Karma, entries).unwrap();
2885
2886 assert_eq!(
2887 store.get_raw(Galaxy::Karma, b"key1").unwrap().unwrap(),
2888 b"val1"
2889 );
2890 assert_eq!(
2891 store.get_raw(Galaxy::Karma, b"key2").unwrap().unwrap(),
2892 b"val2"
2893 );
2894 assert_eq!(
2895 store.get_raw(Galaxy::Karma, b"key3").unwrap().unwrap(),
2896 b"val3"
2897 );
2898 }
2899
2900 #[test]
2901 fn put_raw_batch_empty_is_noop() {
2902 let tmp = tempfile::tempdir().unwrap();
2903 let store = MemoryStore::open_default(tmp.path()).unwrap();
2904
2905 store.put_raw_batch(Galaxy::Karma, &[]).unwrap();
2906 assert_eq!(store.count(Galaxy::Karma).unwrap(), 0);
2907 }
2908
2909 #[test]
2910 fn entry_limit_rejects_excess_writes() {
2911 let tmp = tempfile::tempdir().unwrap();
2912 let store = MemoryStore::open_default(tmp.path())
2913 .unwrap()
2914 .with_entry_limit(3);
2915
2916 for i in 0..3 {
2917 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
2918 store.put(Galaxy::Codex, &mem).unwrap();
2919 }
2920
2921 let mem = Memory::new(Galaxy::Codex, "overflow memory".to_string());
2923 let result = store.put(Galaxy::Codex, &mem);
2924 assert!(result.is_err(), "write beyond limit should be rejected");
2925 let err_msg = result.unwrap_err().to_string();
2926 assert!(
2927 err_msg.contains("entry limit reached"),
2928 "error should mention entry limit: {err_msg}"
2929 );
2930 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
2931 }
2932
2933 #[test]
2934 fn entry_limit_per_galaxy_independent() {
2935 let tmp = tempfile::tempdir().unwrap();
2936 let store = MemoryStore::open_default(tmp.path())
2937 .unwrap()
2938 .with_entry_limit(2);
2939
2940 for i in 0..2 {
2942 let mem = Memory::new(Galaxy::Codex, format!("codex {i}"));
2943 store.put(Galaxy::Codex, &mem).unwrap();
2944 }
2945
2946 let mem = Memory::new(Galaxy::Research, "science memory".to_string());
2948 let result = store.put(Galaxy::Research, &mem);
2949 assert!(
2950 result.is_ok(),
2951 "different galaxy should not be affected by limit"
2952 );
2953 }
2954
2955 #[test]
2956 fn entry_limit_none_allows_unlimited() {
2957 let tmp = tempfile::tempdir().unwrap();
2958 let store = MemoryStore::open_default(tmp.path()).unwrap();
2959
2960 for i in 0..50 {
2962 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
2963 store.put(Galaxy::Codex, &mem).unwrap();
2964 }
2965 assert_eq!(store.count(Galaxy::Codex).unwrap(), 50);
2966 }
2967
2968 #[test]
2969 fn map_full_error_is_graceful() {
2970 let tmp = tempfile::tempdir().unwrap();
2974 let store = MemoryStore::open(tmp.path(), 512 * 1024).unwrap();
2975
2976 let mut written = 0;
2978 let mut got_map_full = false;
2979 for i in 0..1000 {
2980 let mem = Memory::new(
2981 Galaxy::Codex,
2982 format!("memory content {i} {}", "with padding ".repeat(50)),
2983 );
2984 match store.put(Galaxy::Codex, &mem) {
2985 Ok(()) => written += 1,
2986 Err(e) => {
2987 let msg = e.to_string();
2988 if msg.contains("map full") {
2989 got_map_full = true;
2990 break;
2991 }
2992 break;
2994 }
2995 }
2996 }
2997
2998 assert!(
2999 got_map_full || written < 1000,
3000 "should eventually hit map full or error"
3001 );
3002 assert!(written > 0, "should have written at least some memories");
3003 }
3004
3005 #[test]
3006 fn test_find_across_galaxies() {
3007 let tmp = tempfile::tempdir().unwrap();
3008 let store = MemoryStore::open_default(tmp.path()).unwrap();
3009
3010 let mem = Memory::new(Galaxy::Research, "Cross-galaxy research memo".into());
3011 let id = mem.metadata.id;
3012 store.put(Galaxy::Research, &mem).unwrap();
3013
3014 let found = store.find_across_galaxies(id).unwrap();
3015 assert!(found.is_some());
3016 let (galaxy, retrieved) = found.unwrap();
3017 assert_eq!(galaxy, Galaxy::Research);
3018 assert_eq!(retrieved.metadata.id, id);
3019 assert_eq!(retrieved.content, "Cross-galaxy research memo");
3020
3021 assert!(
3023 store
3024 .find_across_galaxies(uuid::Uuid::new_v4())
3025 .unwrap()
3026 .is_none()
3027 );
3028 }
3029}