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 keyring_db: Option<Database>,
195 at_rest: Option<crate::at_rest::AtRestState>,
198 episodic_term_cache: std::sync::Arc<RwLock<HashMap<String, Vec<uuid::Uuid>>>>,
200 episodic_embedder:
202 std::sync::OnceLock<Option<Arc<dyn crate::embedder::Embedder + Send + Sync>>>,
203 episodic_sidecar_ensured: std::sync::OnceLock<()>,
205 episodic_aliases: std::sync::OnceLock<Option<crate::episodic_keys::AdaptiveAliases>>,
207 episodic_enrichment: std::sync::OnceLock<Option<crate::enrichment::VocabularyEnrichment>>,
209}
210
211impl MemoryStore {
212 #[cfg(unix)]
228 pub fn probe_write_lock(store_root: &Path) -> std::io::Result<()> {
229 use rustix::fs::{FlockOperation, fcntl_lock};
230 let lock_path = store_root.join("lmdb").join("lock.mdb");
231 let file = std::fs::OpenOptions::new()
232 .read(true)
233 .write(true)
234 .open(&lock_path)?;
235 match fcntl_lock(&file, FlockOperation::NonBlockingLockExclusive) {
236 Ok(()) => Ok(()),
237 Err(rustix::io::Errno::AGAIN | rustix::io::Errno::ACCESS) => Err(std::io::Error::new(
238 std::io::ErrorKind::WouldBlock,
239 "LMDB writer lock held by another process",
240 )),
241 Err(e) => Err(e.into()),
242 }
243 }
244
245 #[cfg(not(unix))]
248 pub fn probe_write_lock(_store_root: &Path) -> std::io::Result<()> {
249 Ok(())
250 }
251
252 pub fn open(path: impl AsRef<Path>, map_size: usize) -> Result<Self> {
264 Self::open_with_at_rest(path, map_size, &crate::at_rest::AtRestConfig::from_env()?)
265 }
266
267 pub fn open_with_at_rest(
274 path: impl AsRef<Path>,
275 map_size: usize,
276 at_rest_config: &crate::at_rest::AtRestConfig,
277 ) -> Result<Self> {
278 let path = path.as_ref().to_path_buf();
279
280 std::fs::create_dir_all(&path)
282 .map_err(|e| CoreError::Memory(format!("Cannot create store dir: {e}")))?;
283 #[cfg(unix)]
284 {
285 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))
286 .map_err(|e| CoreError::Memory(format!("Cannot set store dir permissions: {e}")))?;
287 }
288
289 let env = Environment::new()
290 .set_map_size(map_size)
291 .set_max_dbs(64)
292 .open(&path)
293 .map_err(|e| CoreError::Memory(format!("LMDB open failed: {e}")))?;
294
295 for galaxy in Galaxy::all() {
297 let db = env
298 .create_db(Some(galaxy.db_name()), DatabaseFlags::default())
299 .map_err(|e| {
300 CoreError::Memory(format!(
301 "LMDB create_db failed for {}: {e}",
302 galaxy.db_name()
303 ))
304 })?;
305 let _ = db;
306 }
307
308 for (name, flags) in crate::indexes::INDEX_DBS {
310 let db = env
311 .create_db(Some(name), *flags)
312 .map_err(|e| CoreError::Memory(format!("LMDB create_db failed for {name}: {e}")))?;
313 let _ = db;
314 }
315
316 let index_dbs = IndexDbs::open(&env)?;
317 let episodic_db = env
318 .create_db(Some("episodic_records"), DatabaseFlags::default())
319 .map_err(|e| {
320 CoreError::Memory(format!("LMDB create_db failed for episodic_records: {e}"))
321 })?;
322 let episodic_terms_v2_db = env
329 .create_db(Some("episodic_terms_v2"), DatabaseFlags::DUP_SORT)
330 .map_err(|e| {
331 CoreError::Memory(format!("LMDB create_db failed for episodic_terms_v2: {e}"))
332 })?;
333 let embedding_cache_db = env
334 .create_db(Some("embedding_cache"), DatabaseFlags::default())
335 .map_err(|e| {
336 CoreError::Memory(format!("LMDB create_db failed for embedding_cache: {e}"))
337 })?;
338 let revisions_db = env
339 .create_db(Some("revisions"), DatabaseFlags::default())
340 .map_err(|e| CoreError::Memory(format!("LMDB create_db failed for revisions: {e}")))?;
341 let attestations_db = env
342 .create_db(
343 Some(crate::attestation::ATTESTATIONS_DB),
344 DatabaseFlags::default(),
345 )
346 .map_err(|e| {
347 CoreError::Memory(format!("LMDB create_db failed for attestations: {e}"))
348 })?;
349 let cold_storage_db = env
350 .create_db(Some("cold_storage"), DatabaseFlags::default())
351 .map_err(|e| {
352 CoreError::Memory(format!("LMDB create_db failed for cold_storage: {e}"))
353 })?;
354 let (keyring_db, at_rest) = crate::at_rest::open_at_rest(&env, &path, at_rest_config)?;
355 Ok(Self {
356 path,
357 env,
358 index_dbs,
359 semantic_encoder: SemanticEncoder::new(),
360 max_entries_per_galaxy: None,
361 mutation_count: AtomicU64::new(0),
362 episodic_db,
363 episodic_terms_v2_db,
364 embedding_cache_db,
365 revisions_db,
366 attestations_db,
367 cold_storage_db,
368 keyring_db,
369 at_rest,
370 episodic_term_cache: std::sync::Arc::new(RwLock::new(HashMap::new())),
371 episodic_embedder: std::sync::OnceLock::new(),
372 episodic_sidecar_ensured: std::sync::OnceLock::new(),
373 episodic_aliases: std::sync::OnceLock::new(),
374 episodic_enrichment: std::sync::OnceLock::new(),
375 })
376 }
377
378 #[must_use]
382 pub fn at_rest_status(&self) -> crate::at_rest::AtRestStatus {
383 if let Some(state) = &self.at_rest {
384 return crate::at_rest::AtRestStatus::Present(state.status());
385 }
386 match &self.keyring_db {
387 None => crate::at_rest::AtRestStatus::Absent,
388 Some(db) => crate::at_rest::read_status(&self.env, *db, &self.path),
389 }
390 }
391
392 #[must_use]
395 pub const fn at_rest_state(&self) -> Option<&crate::at_rest::AtRestState> {
396 self.at_rest.as_ref()
397 }
398
399 pub fn open_readonly_bounded(
409 path: impl AsRef<Path>,
410 timeout: std::time::Duration,
411 ) -> Result<Option<Self>> {
412 let path = path.as_ref().to_path_buf();
413 let (tx, rx) = std::sync::mpsc::channel();
414 std::thread::spawn(move || {
415 let result = Self::open_readonly(&path);
416 let _ = tx.send(result);
417 });
418 match rx.recv_timeout(timeout) {
419 Ok(result) => result.map(Some),
420 Err(_) => Ok(None),
421 }
422 }
423
424 pub fn open_default_bounded(
428 path: impl AsRef<Path>,
429 timeout: std::time::Duration,
430 ) -> Result<Option<Self>> {
431 let path = path.as_ref().to_path_buf();
432 let (tx, rx) = std::sync::mpsc::channel();
433 std::thread::spawn(move || {
434 let result = Self::open_default(&path);
435 let _ = tx.send(result);
436 });
437 match rx.recv_timeout(timeout) {
438 Ok(result) => result.map(Some),
439 Err(_) => Ok(None),
440 }
441 }
442
443 pub fn open_default(path: impl AsRef<Path>) -> Result<Self> {
452 let platform_default = if cfg!(windows) {
457 256 * 1024 * 1024
458 } else {
459 4 * 1024 * 1024 * 1024
460 };
461 let size = std::env::var("WM_DEFAULT_MAP_SIZE")
462 .ok()
463 .and_then(|v| v.parse::<usize>().ok())
464 .filter(|&v| v > 0)
465 .unwrap_or(platform_default);
466 Self::open(path, size)
467 }
468
469 pub fn open_readonly(path: impl AsRef<Path>) -> Result<Self> {
475 let path = path.as_ref().to_path_buf();
476 if !path.is_dir() {
477 return Err(CoreError::Memory(format!(
478 "Read-only LMDB store directory does not exist: {}",
479 path.display()
480 )));
481 }
482 if !path.join("data.mdb").is_file() {
483 return Err(CoreError::Memory(format!(
484 "Read-only LMDB store is missing data.mdb: {}",
485 path.display()
486 )));
487 }
488
489 let env = Environment::new()
490 .set_max_dbs(32)
491 .set_flags(EnvironmentFlags::READ_ONLY)
492 .open(&path)
493 .map_err(|e| CoreError::Memory(format!("Read-only LMDB open failed: {e}")))?;
494
495 let index_dbs = IndexDbs::open(&env)?;
496 let open_named = |name: &str| {
497 env.open_db(Some(name)).map_err(|e| {
498 CoreError::Memory(format!("Read-only LMDB missing database {name}: {e}"))
499 })
500 };
501 let episodic_db = open_named("episodic_records")?;
502 let episodic_terms_v2_db = open_named("episodic_terms_v2")?;
503 let embedding_cache_db = open_named("embedding_cache")?;
504 let revisions_db = open_named("revisions")?;
505 let attestations_db = open_named(crate::attestation::ATTESTATIONS_DB)?;
506 let cold_storage_db = open_named("cold_storage")?;
507 let keyring_db = crate::at_rest::open_keyring_optional(&env)?;
510
511 Ok(Self {
512 path,
513 env,
514 index_dbs,
515 semantic_encoder: SemanticEncoder::new(),
516 max_entries_per_galaxy: None,
517 mutation_count: AtomicU64::new(0),
518 episodic_db,
519 episodic_terms_v2_db,
520 embedding_cache_db,
521 revisions_db,
522 attestations_db,
523 cold_storage_db,
524 keyring_db,
525 at_rest: None,
526 episodic_term_cache: std::sync::Arc::new(RwLock::new(HashMap::new())),
527 episodic_embedder: std::sync::OnceLock::new(),
528 episodic_sidecar_ensured: std::sync::OnceLock::new(),
529 episodic_aliases: std::sync::OnceLock::new(),
530 episodic_enrichment: std::sync::OnceLock::new(),
531 })
532 }
533
534 const NAMED_DBIS: [&'static str; 6] = [
537 "episodic_records",
538 "episodic_terms_v2",
539 "embedding_cache",
540 "revisions",
541 crate::attestation::ATTESTATIONS_DB,
542 "cold_storage",
543 ];
544
545 pub fn open_inspection(path: impl AsRef<Path>) -> Result<Self> {
558 let path = path.as_ref().to_path_buf();
559 if !path.is_dir() {
560 return Err(CoreError::Memory(format!(
561 "Read-only LMDB store directory does not exist: {}",
562 path.display()
563 )));
564 }
565 if !path.join("data.mdb").is_file() {
566 return Err(CoreError::Memory(format!(
567 "Read-only LMDB store is missing data.mdb: {}",
568 path.display()
569 )));
570 }
571
572 let env = Environment::new()
573 .set_max_dbs(32)
574 .set_flags(EnvironmentFlags::READ_ONLY | EnvironmentFlags::NO_LOCK)
575 .open(&path)
576 .map_err(|e| CoreError::Memory(format!("Inspection LMDB open failed: {e}")))?;
577
578 let index_dbs = IndexDbs::open(&env)?;
579 let open_named = |name: &str| {
580 env.open_db(Some(name)).map_err(|e| {
581 CoreError::Memory(format!("Inspection LMDB missing database {name}: {e}"))
582 })
583 };
584 let episodic_db = open_named("episodic_records")?;
585 let episodic_terms_v2_db = open_named("episodic_terms_v2")?;
586 let embedding_cache_db = open_named("embedding_cache")?;
587 let revisions_db = open_named("revisions")?;
588 let attestations_db = open_named(crate::attestation::ATTESTATIONS_DB)?;
589 let cold_storage_db = open_named("cold_storage")?;
590 let keyring_db = crate::at_rest::open_keyring_optional(&env)?;
593
594 Ok(Self {
595 path,
596 env,
597 index_dbs,
598 semantic_encoder: SemanticEncoder::new(),
599 max_entries_per_galaxy: None,
600 mutation_count: AtomicU64::new(0),
601 episodic_db,
602 episodic_terms_v2_db,
603 embedding_cache_db,
604 revisions_db,
605 attestations_db,
606 cold_storage_db,
607 keyring_db,
608 at_rest: None,
609 episodic_term_cache: std::sync::Arc::new(RwLock::new(HashMap::new())),
610 episodic_embedder: std::sync::OnceLock::new(),
611 episodic_sidecar_ensured: std::sync::OnceLock::new(),
612 episodic_aliases: std::sync::OnceLock::new(),
613 episodic_enrichment: std::sync::OnceLock::new(),
614 })
615 }
616
617 pub fn ensure_schema(path: impl AsRef<Path>) -> Result<Vec<String>> {
630 let path = path.as_ref().to_path_buf();
631 if !path.is_dir() {
632 return Err(CoreError::Memory(format!(
633 "Store directory does not exist: {}",
634 path.display()
635 )));
636 }
637 let expected = || {
638 Galaxy::all()
639 .into_iter()
640 .map(|galaxy| galaxy.db_name().to_string())
641 .chain(
642 crate::indexes::INDEX_DBS
643 .iter()
644 .map(|(name, _)| (*name).to_string()),
645 )
646 .chain(Self::NAMED_DBIS.iter().map(|name| (*name).to_string()))
647 };
648 let missing: Vec<String> = {
649 let env = Environment::new()
650 .set_max_dbs(64)
651 .set_flags(EnvironmentFlags::READ_ONLY)
652 .open(&path)
653 .map_err(|e| CoreError::Memory(format!("Read-only LMDB open failed: {e}")))?;
654 expected()
655 .filter(|name| env.open_db(Some(name.as_str())).is_err())
656 .collect()
657 };
658 if missing.is_empty() {
659 return Ok(missing);
660 }
661 let store = Self::open_default(&path)?;
664 drop(store);
665 Ok(missing)
666 }
667
668 #[must_use]
673 pub const fn with_entry_limit(mut self, limit: usize) -> Self {
674 self.max_entries_per_galaxy = Some(limit);
675 self
676 }
677
678 pub fn path(&self) -> &Path {
680 &self.path
681 }
682
683 pub const fn env(&self) -> &Environment {
685 &self.env
686 }
687
688 pub fn mutation_count(&self) -> u64 {
692 self.mutation_count.load(Ordering::Relaxed)
693 }
694
695 pub const fn index_dbs(&self) -> &IndexDbs {
697 &self.index_dbs
698 }
699
700 pub const fn semantic_encoder(&self) -> &SemanticEncoder {
702 &self.semantic_encoder
703 }
704
705 fn ensure_episodic_sidecar(&self) {
710 if self.episodic_sidecar_ensured.get().is_some() {
711 return;
712 }
713 let _ = self.episodic_sidecar_ensured.set(());
714 let view = EpisodicStore::new(
715 &self.env,
716 self.episodic_db,
717 self.episodic_terms_v2_db,
718 self.episodic_term_cache.clone(),
719 &self.mutation_count,
720 );
721 let needs_rebuild = matches!(
722 (view.sidecar_is_empty(), view.record_count()),
723 (Ok(true), Ok(n)) if n > 0
724 );
725 if needs_rebuild {
726 match view.rebuild_sidecar() {
727 Ok(n) => tracing::info!("episodic sidecar rebuilt from {n} records"),
728 Err(e) => {
729 tracing::warn!("episodic sidecar rebuild failed: {e}");
730 }
731 }
732 }
733 }
734
735 #[must_use]
737 pub fn episodic(&self) -> EpisodicStore<'_> {
738 self.ensure_episodic_sidecar();
739 let mut store = EpisodicStore::new(
740 &self.env,
741 self.episodic_db,
742 self.episodic_terms_v2_db,
743 self.episodic_term_cache.clone(),
744 &self.mutation_count,
745 );
746 if let Some(Some(embedder)) = self.episodic_embedder.get() {
747 store = store.with_embedder(embedder.clone());
748 }
749 if let Some(Some(aliases)) = self.episodic_aliases.get() {
750 store = store.with_adaptive_aliases(aliases.clone());
751 }
752 if let Some(Some(enrichment)) = self.episodic_enrichment.get() {
753 store = store.with_enrichment(enrichment.clone());
754 }
755 store
756 }
757
758 pub fn set_episodic_embedder(
760 &self,
761 embedder: Arc<dyn crate::embedder::Embedder + Send + Sync>,
762 ) {
763 let _ = self.episodic_embedder.set(Some(embedder));
764 }
765
766 pub fn set_episodic_aliases(&self, aliases: crate::episodic_keys::AdaptiveAliases) {
768 let _ = self.episodic_aliases.set(Some(aliases));
769 }
770
771 pub fn set_episodic_enrichment(&self, enrichment: crate::enrichment::VocabularyEnrichment) {
773 let _ = self.episodic_enrichment.set(Some(enrichment));
774 }
775
776 pub fn galaxy_db(&self, galaxy: Galaxy) -> Result<Database> {
778 self.env.open_db(Some(galaxy.db_name())).map_err(|e| {
779 CoreError::Memory(format!("LMDB open_db failed for {}: {e}", galaxy.db_name()))
780 })
781 }
782
783 pub fn put(&self, galaxy: Galaxy, memory: &Memory) -> Result<()> {
791 if let Some(limit) = self.max_entries_per_galaxy {
793 let current = self.count(galaxy)?;
794 if current >= limit {
795 return Err(CoreError::Memory(format!(
796 "galaxy {} entry limit reached ({current}/{limit}), write rejected",
797 galaxy.db_name()
798 )));
799 }
800 }
801
802 let db = self.galaxy_db(galaxy)?;
803 let key = memory.metadata.id.as_bytes();
804 let val = rmp_serde::to_vec_named(memory)
805 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
806
807 let mut tx = self
808 .env
809 .begin_rw_txn()
810 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
811
812 let existing = tx
817 .get(db, key)
818 .ok()
819 .and_then(|bytes| crate::codec::decode(bytes).ok());
820
821 match tx.put(db, key, &val, lmdb::WriteFlags::default()) {
822 Ok(()) => {}
823 Err(lmdb::Error::MapFull) => {
824 tx.abort();
825 return Err(CoreError::Memory(format!(
826 "LMDB map full: galaxy {}, consider growing map size or pruning old memories",
827 galaxy.db_name()
828 )));
829 }
830 Err(e) => {
831 tx.abort();
832 return Err(CoreError::Memory(format!("LMDB put failed: {e}")));
833 }
834 }
835 if let Some(existing) = existing {
836 self.index_dbs.remove(&mut tx, galaxy, &existing)?;
837 }
838 self.index_dbs.add(&mut tx, galaxy, memory)?;
839 tx.commit()
840 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
841 self.mutation_count.fetch_add(1, Ordering::Relaxed);
842 Ok(())
843 }
844
845 pub fn get(&self, galaxy: Galaxy, id: uuid::Uuid) -> Result<Option<Memory>> {
847 let db = self.galaxy_db(galaxy)?;
848 let key = id.as_bytes();
849
850 let tx = self
851 .env
852 .begin_ro_txn()
853 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
854 let result = tx.get(db, key);
855 match result {
856 Ok(bytes) => {
857 let memory: Memory = crate::codec::decode(bytes)
858 .map_err(|e| CoreError::Memory(format!("deserialize failed: {e}")))?;
859 tx.commit()
860 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
861 Ok(Some(memory))
862 }
863 Err(lmdb::Error::NotFound) => {
864 tx.commit()
866 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
867 Ok(None)
868 }
869 Err(e) => Err(CoreError::Memory(format!("LMDB get failed: {e}"))),
870 }
871 }
872
873 pub fn find_across_galaxies(&self, id: uuid::Uuid) -> Result<Option<(Galaxy, Memory)>> {
879 for galaxy in Galaxy::memory_galaxies() {
880 if let Some(mem) = self.get(galaxy, id)? {
881 return Ok(Some((galaxy, mem)));
882 }
883 }
884 Ok(None)
885 }
886
887 pub fn delete(&self, galaxy: Galaxy, id: uuid::Uuid) -> Result<bool> {
890 let db = self.galaxy_db(galaxy)?;
891 let key = id.as_bytes();
892
893 let mut tx = self
894 .env
895 .begin_rw_txn()
896 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
897
898 let exists = tx.get(db, key).is_ok();
900 if exists {
901 if let Ok(bytes) = tx.get(db, key) {
903 if let Ok(memory) = crate::codec::decode(bytes) {
904 let _ = self.index_dbs.remove(&mut tx, galaxy, &memory);
905 }
906 }
907 tx.del(db, key, None)
908 .map_err(|e| CoreError::Memory(format!("LMDB del failed: {e}")))?;
909 }
910 tx.commit()
911 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
912 if exists {
913 self.mutation_count.fetch_add(1, Ordering::Relaxed);
914 }
915 Ok(exists)
916 }
917
918 pub fn scan(&self, galaxy: Galaxy, limit: usize) -> Result<Vec<Memory>> {
920 let db = self.galaxy_db(galaxy)?;
921 let tx = self
922 .env
923 .begin_ro_txn()
924 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
925
926 let mut cursor = tx
927 .open_ro_cursor(db)
928 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
929
930 let mut memories = Vec::with_capacity(limit.min(256));
931 for (i, (_key, val)) in cursor.iter().enumerate() {
932 if memories.len() >= limit {
933 break;
934 }
935 match crate::codec::decode(val) {
936 Ok(memory) => memories.push(memory),
937 Err(e) => {
938 tracing::warn!(
939 "Skipping corrupted entry at index {i} in galaxy {:?}: {e}",
940 galaxy
941 );
942 }
943 }
944 }
945
946 drop(cursor);
947 tx.commit()
948 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
949 Ok(memories)
950 }
951
952 pub fn scan_all(&self, galaxy: Galaxy) -> Result<Vec<Memory>> {
957 self.scan_all_impl(galaxy, false)
958 }
959
960 pub fn scan_all_strict(&self, galaxy: Galaxy) -> Result<Vec<Memory>> {
964 self.scan_all_impl(galaxy, true)
965 }
966
967 fn scan_all_impl(&self, galaxy: Galaxy, strict: bool) -> Result<Vec<Memory>> {
968 let db = self.galaxy_db(galaxy)?;
969 let tx = self
970 .env
971 .begin_ro_txn()
972 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
973
974 let mut cursor = tx
975 .open_ro_cursor(db)
976 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
977
978 let mut memories = Vec::new();
979 for (i, (_key, val)) in cursor.iter().enumerate() {
980 match crate::codec::decode(val) {
981 Ok(memory) => memories.push(memory),
982 Err(e) => {
983 if strict {
984 return Err(CoreError::Memory(format!(
985 "refusing incomplete scan of {}: record {i} cannot be decoded: {e}",
986 galaxy.db_name()
987 )));
988 }
989 tracing::warn!(
990 "Skipping corrupted entry at index {i} in galaxy {:?}: {e}",
991 galaxy
992 );
993 }
994 }
995 }
996
997 drop(cursor);
998 tx.commit()
999 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1000 Ok(memories)
1001 }
1002
1003 pub fn count(&self, galaxy: Galaxy) -> Result<usize> {
1005 let db = self.galaxy_db(galaxy)?;
1006 let tx = self
1007 .env
1008 .begin_ro_txn()
1009 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1010 let mut cursor = tx
1011 .open_ro_cursor(db)
1012 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
1013 let count = cursor.iter().count();
1014 drop(cursor);
1015 tx.commit()
1016 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1017 Ok(count)
1018 }
1019
1020 pub fn count_by_tag(&self, galaxy: Galaxy, tag: &str) -> Result<usize> {
1025 let tx = self
1026 .env
1027 .begin_ro_txn()
1028 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1029 let ids = self.index_dbs.find_by_tag(&tx, galaxy, tag)?;
1030 tx.commit()
1031 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1032 Ok(ids.len())
1033 }
1034
1035 pub fn clear_galaxy(&self, galaxy: Galaxy) -> Result<usize> {
1039 let db = self.galaxy_db(galaxy)?;
1040
1041 let mut tx = self
1042 .env
1043 .begin_rw_txn()
1044 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1045
1046 let mut cursor = tx
1047 .open_ro_cursor(db)
1048 .map_err(|e| CoreError::Memory(format!("LMDB cursor failed: {e}")))?;
1049
1050 let mut count = 0usize;
1051 let keys_to_delete: Vec<(Vec<u8>, Memory)> = cursor
1052 .iter()
1053 .filter_map(|(key, val)| {
1054 if let Ok(memory) = crate::codec::decode(val) {
1055 Some((key.to_vec(), memory))
1056 } else {
1057 None
1058 }
1059 })
1060 .collect();
1061
1062 drop(cursor);
1063
1064 for (key, memory) in &keys_to_delete {
1065 let _ = self.index_dbs.remove(&mut tx, galaxy, memory);
1066 tx.del(db, &key, None)
1067 .map_err(|e| CoreError::Memory(format!("LMDB del failed: {e}")))?;
1068 count += 1;
1069 }
1070
1071 tx.commit()
1072 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1073 self.mutation_count
1074 .fetch_add(count as u64, Ordering::Relaxed);
1075 Ok(count)
1076 }
1077
1078 pub fn batch_put(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<usize> {
1081 if memories.is_empty() {
1082 return Ok(0);
1083 }
1084
1085 let db = self.galaxy_db(galaxy)?;
1086
1087 let mut tx = self
1088 .env
1089 .begin_rw_txn()
1090 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1091
1092 let mut count = 0usize;
1093 for memory in memories {
1094 let key = memory.metadata.id.as_bytes();
1095 let val = rmp_serde::to_vec_named(memory)
1096 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
1097 match tx.put(db, key, &val, WriteFlags::default()) {
1098 Ok(()) => {}
1099 Err(lmdb::Error::MapFull) => {
1100 tx.abort();
1101 return Err(CoreError::Memory(format!(
1102 "LMDB map full: galaxy {}, consider growing map size",
1103 galaxy.db_name()
1104 )));
1105 }
1106 Err(e) => {
1107 tx.abort();
1108 return Err(CoreError::Memory(format!("LMDB put failed: {e}")));
1109 }
1110 }
1111 self.index_dbs.add(&mut tx, galaxy, memory)?;
1112 count += 1;
1113 }
1114
1115 tx.commit()
1116 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1117 self.mutation_count
1118 .fetch_add(count as u64, Ordering::Relaxed);
1119 Ok(count)
1120 }
1121
1122 pub fn get_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<Option<Vec<u8>>> {
1124 let db = self.galaxy_db(galaxy)?;
1125 let tx = self
1126 .env
1127 .begin_ro_txn()
1128 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1129 match tx.get(db, &key) {
1130 Ok(bytes) => {
1131 let data = bytes.to_vec();
1132 tx.commit()
1133 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1134 Ok(Some(data))
1135 }
1136 Err(lmdb::Error::NotFound) => {
1137 tx.commit()
1138 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1139 Ok(None)
1140 }
1141 Err(e) => Err(CoreError::Memory(format!("LMDB get_raw failed: {e}"))),
1142 }
1143 }
1144
1145 pub fn put_raw(&self, galaxy: Galaxy, key: &[u8], val: &[u8]) -> Result<()> {
1147 let db = self.galaxy_db(galaxy)?;
1148 let mut tx = self
1149 .env
1150 .begin_rw_txn()
1151 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1152 tx.put(db, &key, &val, lmdb::WriteFlags::default())
1153 .map_err(|e| CoreError::Memory(format!("LMDB put_raw failed: {e}")))?;
1154 tx.commit()
1155 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1156 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1157 Ok(())
1158 }
1159
1160 pub fn delete_raw(&self, galaxy: Galaxy, key: &[u8]) -> Result<bool> {
1163 let db = self.galaxy_db(galaxy)?;
1164 let mut tx = self
1165 .env
1166 .begin_rw_txn()
1167 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1168 let deleted = tx.del(db, &key, None).is_ok();
1169 tx.commit()
1170 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1171 if deleted {
1172 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1173 }
1174 Ok(deleted)
1175 }
1176
1177 pub fn put_raw_batch(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
1180 self.put_raw_batch_impl(galaxy, entries)?;
1181 self.mutation_count
1182 .fetch_add(entries.len() as u64, Ordering::Relaxed);
1183 Ok(())
1184 }
1185
1186 pub fn put_raw_batch_untracked(
1196 &self,
1197 galaxy: Galaxy,
1198 entries: &[(&[u8], &[u8])],
1199 ) -> Result<()> {
1200 self.put_raw_batch_impl(galaxy, entries)
1201 }
1202
1203 fn put_raw_batch_impl(&self, galaxy: Galaxy, entries: &[(&[u8], &[u8])]) -> Result<()> {
1204 let db = self.galaxy_db(galaxy)?;
1205 let mut tx = self
1206 .env
1207 .begin_rw_txn()
1208 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1209 for (key, val) in entries {
1210 tx.put(db, key, val, WriteFlags::default())
1211 .map_err(|e| CoreError::Memory(format!("LMDB put_raw_batch failed: {e}")))?;
1212 }
1213 tx.commit()
1214 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1215 Ok(())
1216 }
1217
1218 pub fn find_by_content_hash(&self, galaxy: Galaxy, hash: &str) -> Result<Option<uuid::Uuid>> {
1224 let tx = self
1225 .env
1226 .begin_ro_txn()
1227 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1228 let result = self.index_dbs.find_by_content_hash(&tx, galaxy, hash)?;
1229 tx.commit()
1230 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1231 Ok(result)
1232 }
1233
1234 pub fn find_by_content_hash_scan(
1236 &self,
1237 galaxy: Galaxy,
1238 hash: &str,
1239 ) -> Result<Option<uuid::Uuid>> {
1240 let memories = self.scan(galaxy, 10_000)?;
1241 for mem in memories {
1242 if mem.metadata.content_hash == hash {
1243 return Ok(Some(mem.metadata.id));
1244 }
1245 }
1246 Ok(None)
1247 }
1248
1249 pub fn put_dedup(&self, galaxy: Galaxy, memory: &Memory) -> Result<uuid::Uuid> {
1253 if let Some(existing_id) =
1254 self.find_by_content_hash(galaxy, &memory.metadata.content_hash)?
1255 {
1256 return Ok(existing_id);
1257 }
1258 let id = memory.metadata.id;
1259 self.put(galaxy, memory)?;
1260 Ok(id)
1261 }
1262
1263 pub fn put_batch(&self, galaxy: Galaxy, memories: &[Memory]) -> Result<()> {
1268 let db = self.galaxy_db(galaxy)?;
1269 let mut tx = self
1270 .env
1271 .begin_rw_txn()
1272 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1273
1274 for memory in memories {
1275 let key = memory.metadata.id.as_bytes();
1276 let val = rmp_serde::to_vec_named(memory)
1277 .map_err(|e| CoreError::Memory(format!("serialize failed: {e}")))?;
1278 tx.put(db, key, &val, WriteFlags::default())
1279 .map_err(|e| CoreError::Memory(format!("LMDB put_batch failed: {e}")))?;
1280 self.index_dbs.add(&mut tx, galaxy, memory)?;
1281 }
1282
1283 tx.commit()
1284 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1285 self.mutation_count
1286 .fetch_add(memories.len() as u64, Ordering::Relaxed);
1287 Ok(())
1288 }
1289
1290 pub fn query(&self, galaxy: Galaxy, query: &MemoryQuery) -> Result<Vec<Memory>> {
1297 if query.content_substring.is_none()
1300 && query.tags.len() == 1
1301 && query.min_importance.is_none()
1302 && query.max_importance.is_none()
1303 && query.created_after.is_none()
1304 && query.created_before.is_none()
1305 {
1306 return self.query_by_tag_indexed(galaxy, &query.tags[0], query.limit);
1307 }
1308
1309 if query.content_substring.is_none()
1310 && query.tags.is_empty()
1311 && let Some(min) = query.min_importance
1312 && let Some(max) = query.max_importance
1313 && query.created_after.is_none()
1314 && query.created_before.is_none()
1315 {
1316 return self.query_by_importance_indexed(galaxy, min, max, query.limit);
1317 }
1318
1319 if query.content_substring.is_none()
1320 && query.tags.is_empty()
1321 && query.min_importance.is_none()
1322 && query.max_importance.is_none()
1323 && let Some(after) = query.created_after
1324 && let Some(before) = query.created_before
1325 {
1326 return self.query_by_time_indexed(galaxy, after, before, query.limit);
1327 }
1328
1329 let memories = self.scan(galaxy, 10_000)?;
1331 let mut results = Vec::new();
1332 for mem in memories {
1333 if query.matches(&mem) {
1334 results.push(mem);
1335 if results.len() >= query.limit {
1336 break;
1337 }
1338 }
1339 }
1340 Ok(results)
1341 }
1342
1343 fn query_by_tag_indexed(&self, galaxy: Galaxy, tag: &str, limit: usize) -> Result<Vec<Memory>> {
1345 let db = self.galaxy_db(galaxy)?;
1346 let tx = self
1347 .env
1348 .begin_ro_txn()
1349 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1350 let ids = self.index_dbs.find_by_tag(&tx, galaxy, tag)?;
1351 let mut results = Vec::new();
1352 for id in &ids {
1353 if results.len() >= limit {
1354 break;
1355 }
1356 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1357 if let Ok(mem) = crate::codec::decode(bytes) {
1358 results.push(mem);
1359 }
1360 }
1361 }
1362 tx.commit()
1363 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1364 Ok(results)
1365 }
1366
1367 fn query_by_importance_indexed(
1369 &self,
1370 galaxy: Galaxy,
1371 min: f32,
1372 max: f32,
1373 limit: usize,
1374 ) -> Result<Vec<Memory>> {
1375 let db = self.galaxy_db(galaxy)?;
1376 let tx = self
1377 .env
1378 .begin_ro_txn()
1379 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1380 let ids = self
1381 .index_dbs
1382 .find_by_importance_range(&tx, galaxy, min, max)?;
1383 let mut results = Vec::new();
1384 for id in &ids {
1385 if results.len() >= limit {
1386 break;
1387 }
1388 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1389 if let Ok(mem) = crate::codec::decode(bytes) {
1390 results.push(mem);
1391 }
1392 }
1393 }
1394 tx.commit()
1395 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1396 Ok(results)
1397 }
1398
1399 fn query_by_time_indexed(
1401 &self,
1402 galaxy: Galaxy,
1403 after: chrono::DateTime<chrono::Utc>,
1404 before: chrono::DateTime<chrono::Utc>,
1405 limit: usize,
1406 ) -> Result<Vec<Memory>> {
1407 let db = self.galaxy_db(galaxy)?;
1408 let tx = self
1409 .env
1410 .begin_ro_txn()
1411 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1412 let ids = self
1413 .index_dbs
1414 .find_by_time_range(&tx, galaxy, after, before)?;
1415 let mut results = Vec::new();
1416 for id in &ids {
1417 if results.len() >= limit {
1418 break;
1419 }
1420 if let Ok(bytes) = tx.get(db, id.as_bytes()) {
1421 if let Ok(mem) = crate::codec::decode(bytes) {
1422 results.push(mem);
1423 }
1424 }
1425 }
1426 tx.commit()
1427 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1428 Ok(results)
1429 }
1430
1431 pub fn put_semantic(&self, galaxy: Galaxy, memory: &mut Memory) -> Result<()> {
1439 let temporal_weight = memory.metadata.coord5d.w;
1440 let importance = memory.metadata.importance;
1441 memory.metadata.coord5d =
1442 self.semantic_encoder
1443 .encode_coordinate(&memory.content, temporal_weight, importance);
1444 self.put(galaxy, memory)
1445 }
1446
1447 pub fn find_similar(
1452 &self,
1453 galaxy: Galaxy,
1454 query_text: &str,
1455 limit: usize,
1456 ) -> Result<Vec<(Memory, f32)>> {
1457 let query_coord = self
1458 .semantic_encoder
1459 .encode_coordinate(query_text, 0.5, 0.5);
1460 let memories = self.scan(galaxy, 10_000)?;
1461 let mut results: Vec<(Memory, f32)> = memories
1462 .into_iter()
1463 .map(|m| {
1464 let dist = query_coord.semantic_distance_to(&m.metadata.coord5d);
1465 (m, dist)
1466 })
1467 .collect();
1468 results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1469 results.truncate(limit);
1470 Ok(results)
1471 }
1472
1473 pub fn put_embedding(&self, memory_id: uuid::Uuid, embedding: &[f32]) -> Result<()> {
1478 let db = self.galaxy_db(Galaxy::Embeddings)?;
1479 let key = memory_id.as_bytes();
1480 let val = encode_embedding(embedding);
1481
1482 let mut tx = self
1483 .env
1484 .begin_rw_txn()
1485 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1486 tx.put(db, key, &val, WriteFlags::default())
1487 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding failed: {e}")))?;
1488 tx.commit()
1489 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1490 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1491 Ok(())
1492 }
1493
1494 pub fn get_embedding(&self, memory_id: uuid::Uuid) -> Result<Option<Vec<f32>>> {
1496 let db = self.galaxy_db(Galaxy::Embeddings)?;
1497 let key = memory_id.as_bytes();
1498
1499 let tx = self
1500 .env
1501 .begin_ro_txn()
1502 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1503 match tx.get(db, key) {
1504 Ok(bytes) => {
1505 let embedding = decode_embedding(bytes);
1506 tx.commit()
1507 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1508 Ok(Some(embedding))
1509 }
1510 Err(lmdb::Error::NotFound) => {
1511 tx.commit()
1512 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1513 Ok(None)
1514 }
1515 Err(e) => Err(CoreError::Memory(format!("LMDB get_embedding failed: {e}"))),
1516 }
1517 }
1518
1519 pub fn delete_embedding(&self, memory_id: uuid::Uuid) -> Result<bool> {
1521 let db = self.galaxy_db(Galaxy::Embeddings)?;
1522 let key = memory_id.as_bytes();
1523
1524 let mut tx = self
1525 .env
1526 .begin_rw_txn()
1527 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1528 let exists = tx.get(db, key).is_ok();
1529 if exists {
1530 tx.del(db, key, None)
1531 .map_err(|e| CoreError::Memory(format!("LMDB del_embedding failed: {e}")))?;
1532 }
1533 tx.commit()
1534 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1535 if exists {
1536 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1537 }
1538 Ok(exists)
1539 }
1540
1541 pub fn put_embedding_cache(&self, cache_key: &str, embedding: &[f32]) -> Result<()> {
1547 let mut tx = self
1548 .env
1549 .begin_rw_txn()
1550 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1551 tx.put(
1552 self.embedding_cache_db,
1553 &cache_key.as_bytes().to_vec(),
1554 &encode_embedding(embedding),
1555 WriteFlags::default(),
1556 )
1557 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1558 tx.commit()
1559 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1560 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1561 Ok(())
1562 }
1563
1564 pub fn put_embedding_cache_batch(&self, entries: &[(String, Vec<f32>)]) -> Result<()> {
1566 if entries.is_empty() {
1567 return Ok(());
1568 }
1569 let mut tx = self
1570 .env
1571 .begin_rw_txn()
1572 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1573 for (key, embedding) in entries {
1574 tx.put(
1575 self.embedding_cache_db,
1576 &key.as_bytes().to_vec(),
1577 &encode_embedding(embedding),
1578 WriteFlags::default(),
1579 )
1580 .map_err(|e| CoreError::Memory(format!("LMDB put_embedding_cache failed: {e}")))?;
1581 }
1582 tx.commit()
1583 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1584 self.mutation_count
1585 .fetch_add(entries.len() as u64, Ordering::Relaxed);
1586 Ok(())
1587 }
1588
1589 pub fn get_embedding_cache(&self, cache_key: &str) -> Result<Option<Vec<f32>>> {
1591 let tx = self
1592 .env
1593 .begin_ro_txn()
1594 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1595 match tx.get(self.embedding_cache_db, &cache_key.as_bytes().to_vec()) {
1596 Ok(bytes) => {
1597 let embedding = decode_embedding(bytes);
1598 tx.commit()
1599 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1600 Ok(Some(embedding))
1601 }
1602 Err(lmdb::Error::NotFound) => {
1603 tx.commit()
1604 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1605 Ok(None)
1606 }
1607 Err(e) => Err(CoreError::Memory(format!(
1608 "LMDB get_embedding_cache failed: {e}"
1609 ))),
1610 }
1611 }
1612
1613 pub fn get_embedding_cache_batch(&self, keys: &[String]) -> Result<Vec<Option<Vec<f32>>>> {
1616 let tx = self
1617 .env
1618 .begin_ro_txn()
1619 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1620 let mut out = Vec::with_capacity(keys.len());
1621 for key in keys {
1622 out.push(
1623 tx.get(self.embedding_cache_db, &key.as_bytes().to_vec())
1624 .ok()
1625 .map(decode_embedding),
1626 );
1627 }
1628 tx.commit()
1629 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1630 Ok(out)
1631 }
1632
1633 pub fn embedding_cache_count(&self) -> Result<u64> {
1635 let tx = self
1636 .env
1637 .begin_ro_txn()
1638 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1639 let mut cursor = tx
1640 .open_ro_cursor(self.embedding_cache_db)
1641 .map_err(|e| CoreError::Memory(format!("LMDB cursor embedding_cache failed: {e}")))?;
1642 let mut count = 0u64;
1643 for _ in cursor.iter() {
1644 count += 1;
1645 }
1646 Ok(count)
1647 }
1648
1649 pub fn record_revision(
1655 &self,
1656 galaxy: Galaxy,
1657 id: MemoryId,
1658 old_hash: &str,
1659 new_hash: &str,
1660 actor: crate::revision::RevisionActor,
1661 ) -> Result<crate::revision::MemoryRevision> {
1662 let seq = self.revisions(galaxy, id)?.len() as u32;
1663 let entry = crate::revision::MemoryRevision {
1664 seq,
1665 timestamp: wm_core::time::now_unix_secs(),
1666 old_hash: old_hash.to_string(),
1667 new_hash: new_hash.to_string(),
1668 actor_session: actor.session,
1669 actor_user: actor.user,
1670 actor_compartment: actor.compartment,
1671 };
1672 let key = crate::revision::revision_key(galaxy, id, seq);
1673 let val = serde_json::to_vec(&entry)
1674 .map_err(|e| CoreError::Memory(format!("revision serialize failed: {e}")))?;
1675 let mut tx = self
1676 .env
1677 .begin_rw_txn()
1678 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1679 tx.put(self.revisions_db, &key, &val, WriteFlags::default())
1680 .map_err(|e| CoreError::Memory(format!("LMDB put revision failed: {e}")))?;
1681 tx.commit()
1682 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1683 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1684 Ok(entry)
1685 }
1686
1687 pub fn revisions(
1690 &self,
1691 galaxy: Galaxy,
1692 id: MemoryId,
1693 ) -> Result<Vec<crate::revision::MemoryRevision>> {
1694 const MDB_GET_CURRENT: u32 = 4;
1699 const MDB_NEXT: u32 = 8;
1700 const MDB_SET_RANGE: u32 = 17;
1701 let prefix = crate::revision::revision_prefix(galaxy, id);
1702 let tx = self
1703 .env
1704 .begin_ro_txn()
1705 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1706 let cursor = tx
1707 .open_ro_cursor(self.revisions_db)
1708 .map_err(|e| CoreError::Memory(format!("LMDB cursor revisions failed: {e}")))?;
1709 let mut out = Vec::new();
1710 if cursor.get(Some(&prefix), None, MDB_SET_RANGE).is_ok() {
1711 while let Ok((key, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1712 if !key.is_some_and(|k| k.starts_with(&prefix)) {
1715 break;
1716 }
1717 let entry: crate::revision::MemoryRevision = serde_json::from_slice(val)
1718 .map_err(|e| CoreError::Memory(format!("revision deserialize failed: {e}")))?;
1719 out.push(entry);
1720 if out.len() >= 10_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1722 break;
1723 }
1724 }
1725 }
1726 drop(cursor);
1727 tx.commit()
1728 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1729 Ok(out)
1730 }
1731
1732 pub fn verify_revision_chain(
1735 &self,
1736 galaxy: Galaxy,
1737 id: MemoryId,
1738 current_hash: &str,
1739 ) -> Result<crate::revision::RevisionChainReport> {
1740 let entries = self.revisions(galaxy, id)?;
1741 Ok(crate::revision::verify_chain(&entries, current_hash))
1742 }
1743
1744 pub fn record_attestation(
1752 &self,
1753 galaxy: Galaxy,
1754 id: MemoryId,
1755 entry: &crate::attestation::RecordAttestation,
1756 ) -> Result<()> {
1757 let key = crate::attestation::attestation_key(galaxy, id);
1758 let val = serde_json::to_vec(entry)
1759 .map_err(|e| CoreError::Memory(format!("attestation serialize failed: {e}")))?;
1760 let mut tx = self
1761 .env
1762 .begin_rw_txn()
1763 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1764 tx.put(self.attestations_db, &key, &val, WriteFlags::default())
1765 .map_err(|e| CoreError::Memory(format!("LMDB put attestation failed: {e}")))?;
1766 tx.commit()
1767 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1768 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1769 Ok(())
1770 }
1771
1772 pub fn attestation(
1775 &self,
1776 galaxy: Galaxy,
1777 id: MemoryId,
1778 ) -> Result<Option<crate::attestation::RecordAttestation>> {
1779 let key = crate::attestation::attestation_key(galaxy, id);
1780 let tx = self
1781 .env
1782 .begin_ro_txn()
1783 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1784 let out =
1785 match tx.get(self.attestations_db, &key) {
1786 Ok(val) => Some(serde_json::from_slice(val).map_err(|e| {
1787 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1788 })?),
1789 Err(lmdb::Error::NotFound) => None,
1790 Err(e) => {
1791 return Err(CoreError::Memory(format!(
1792 "LMDB get attestation failed: {e}"
1793 )));
1794 }
1795 };
1796 drop(tx);
1797 Ok(out)
1798 }
1799
1800 pub fn scan_attestations(&self) -> Result<Vec<crate::attestation::RecordAttestation>> {
1803 const MDB_GET_CURRENT: u32 = 4;
1804 const MDB_NEXT: u32 = 8;
1805 const MDB_FIRST: u32 = 9;
1806 let tx = self
1807 .env
1808 .begin_ro_txn()
1809 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1810 let cursor = tx
1811 .open_ro_cursor(self.attestations_db)
1812 .map_err(|e| CoreError::Memory(format!("LMDB cursor attestations failed: {e}")))?;
1813 let mut out = Vec::new();
1814 if cursor.get(None, None, MDB_FIRST).is_ok() {
1815 while let Ok((_, val)) = cursor.get(None, None, MDB_GET_CURRENT) {
1816 let entry: crate::attestation::RecordAttestation = serde_json::from_slice(val)
1817 .map_err(|e| {
1818 CoreError::Memory(format!("attestation deserialize failed: {e}"))
1819 })?;
1820 out.push(entry);
1821 if out.len() >= 1_000_000 || cursor.get(None, None, MDB_NEXT).is_err() {
1823 break;
1824 }
1825 }
1826 }
1827 drop(cursor);
1828 tx.commit()
1829 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1830 Ok(out)
1831 }
1832
1833 pub fn verify_attestation(
1839 &self,
1840 galaxy: Galaxy,
1841 id: MemoryId,
1842 ) -> Result<crate::attestation::AttestationReport> {
1843 use crate::attestation::AttestationReport;
1844 let Some(att) = self.attestation(galaxy, id)? else {
1845 return Ok(AttestationReport {
1846 attested: false,
1847 signature_valid: false,
1848 matches_head: false,
1849 memory_present: self.get(galaxy, id)?.is_some(),
1850 breaks: vec!["no attestation recorded for this memory".to_string()],
1851 });
1852 };
1853 let mut breaks = Vec::new();
1854 let signature_valid = crate::attestation::verify_attestation(&att);
1855 if !signature_valid {
1856 breaks.push("signature does not verify against recorded pubkey".to_string());
1857 }
1858 let (matches_head, memory_present) = if let Some(memory) = self.get(galaxy, id)? {
1859 let matches = memory.metadata.content_hash == att.record_hash;
1860 if !matches {
1861 breaks.push(
1862 "attested record_hash != live content_hash (memory updated after attestation)"
1863 .to_string(),
1864 );
1865 }
1866 (matches, true)
1867 } else {
1868 breaks.push("attested memory id not present in galaxy".to_string());
1869 (false, false)
1870 };
1871 Ok(AttestationReport {
1872 attested: true,
1873 signature_valid,
1874 matches_head,
1875 memory_present,
1876 breaks,
1877 })
1878 }
1879
1880 pub fn attestation_sweep(
1884 &self,
1885 ) -> Result<
1886 Vec<(
1887 crate::attestation::RecordAttestation,
1888 crate::attestation::AttestationReport,
1889 )>,
1890 > {
1891 use crate::attestation::AttestationReport;
1892 let mut out = Vec::new();
1893 for att in self.scan_attestations()? {
1894 let parsed = match (
1895 Galaxy::from_db_name(&att.galaxy),
1896 uuid::Uuid::parse_str(&att.memory_id),
1897 ) {
1898 (Some(galaxy), Ok(id)) => Some((galaxy, id)),
1899 _ => None,
1900 };
1901 match parsed {
1902 Some((galaxy, id)) => out.push((att, self.verify_attestation(galaxy, id)?)),
1903 None => out.push((
1904 att,
1905 AttestationReport {
1906 attested: true,
1907 signature_valid: false,
1908 matches_head: false,
1909 memory_present: false,
1910 breaks: vec![
1911 "attestation row has unparseable galaxy or memory id".to_string(),
1912 ],
1913 },
1914 )),
1915 }
1916 }
1917 Ok(out)
1918 }
1919
1920 pub fn put_cold_record(&self, record: &crate::cold_storage::ColdRecord) -> Result<()> {
1924 let key = record.id.as_bytes();
1925 let val = rmp_serde::to_vec_named(record)
1926 .map_err(|e| CoreError::Memory(format!("Cold record serialization failed: {e}")))?;
1927 let mut tx = self
1928 .env
1929 .begin_rw_txn()
1930 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1931 tx.put(self.cold_storage_db, key, &val, WriteFlags::default())
1932 .map_err(|e| CoreError::Memory(format!("LMDB put cold_storage failed: {e}")))?;
1933 tx.commit()
1934 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1935 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1936 Ok(())
1937 }
1938
1939 pub fn get_cold_record(&self, id: MemoryId) -> Result<Option<crate::cold_storage::ColdRecord>> {
1941 let key = id.as_bytes();
1942 let tx = self
1943 .env
1944 .begin_ro_txn()
1945 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1946 match tx.get(self.cold_storage_db, key) {
1947 Ok(bytes) => {
1948 let record: crate::cold_storage::ColdRecord = rmp_serde::from_slice(bytes)
1949 .map_err(|e| {
1950 CoreError::Memory(format!("Cold record deserialization failed: {e}"))
1951 })?;
1952 Ok(Some(record))
1953 }
1954 Err(lmdb::Error::NotFound) => Ok(None),
1955 Err(e) => Err(CoreError::Memory(format!(
1956 "LMDB get cold_storage failed: {e}"
1957 ))),
1958 }
1959 }
1960
1961 pub fn delete_cold_record(&self, id: MemoryId) -> Result<bool> {
1963 let key = id.as_bytes();
1964 let mut tx = self
1965 .env
1966 .begin_rw_txn()
1967 .map_err(|e| CoreError::Memory(format!("LMDB rw_txn failed: {e}")))?;
1968 let deleted = match tx.del(self.cold_storage_db, key, None) {
1969 Ok(()) => true,
1970 Err(lmdb::Error::NotFound) => false,
1971 Err(e) => {
1972 return Err(CoreError::Memory(format!(
1973 "LMDB del cold_storage failed: {e}"
1974 )));
1975 }
1976 };
1977 tx.commit()
1978 .map_err(|e| CoreError::Memory(format!("LMDB commit failed: {e}")))?;
1979 if deleted {
1980 self.mutation_count.fetch_add(1, Ordering::Relaxed);
1981 }
1982 Ok(deleted)
1983 }
1984
1985 pub fn count_cold(&self, galaxy: Option<Galaxy>) -> Result<usize> {
1987 let tx = self
1988 .env
1989 .begin_ro_txn()
1990 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
1991 let mut cursor = tx
1992 .open_ro_cursor(self.cold_storage_db)
1993 .map_err(|e| CoreError::Memory(format!("LMDB open_ro_cursor failed: {e}")))?;
1994 let mut count = 0;
1995 for (_key, val) in cursor.iter() {
1996 if let Some(target_g) = galaxy {
1997 let record: crate::cold_storage::ColdRecord =
1998 rmp_serde::from_slice(val).map_err(|e| {
1999 CoreError::Memory(format!("Cold record deserialization failed: {e}"))
2000 })?;
2001 if record.galaxy == target_g {
2002 count += 1;
2003 }
2004 } else {
2005 count += 1;
2006 }
2007 }
2008 Ok(count)
2009 }
2010
2011 pub fn list_cold_records(
2013 &self,
2014 galaxy: Option<Galaxy>,
2015 limit: usize,
2016 ) -> Result<Vec<crate::cold_storage::ColdRecordSummary>> {
2017 let query = crate::cold_storage::ColdQuery {
2018 galaxy,
2019 limit: if limit == 0 { 100 } else { limit },
2020 ..Default::default()
2021 };
2022 self.query_cold_records(&query)
2023 }
2024
2025 pub fn query_cold_records(
2027 &self,
2028 query: &crate::cold_storage::ColdQuery,
2029 ) -> Result<Vec<crate::cold_storage::ColdRecordSummary>> {
2030 let tx = self
2031 .env
2032 .begin_ro_txn()
2033 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
2034 let mut cursor = tx
2035 .open_ro_cursor(self.cold_storage_db)
2036 .map_err(|e| CoreError::Memory(format!("LMDB open_ro_cursor failed: {e}")))?;
2037 let mut results = Vec::new();
2038 let limit = if query.limit == 0 {
2039 usize::MAX
2040 } else {
2041 query.limit
2042 };
2043
2044 for (_key, val) in cursor.iter() {
2045 let record: crate::cold_storage::ColdRecord =
2046 rmp_serde::from_slice(val).map_err(|e| {
2047 CoreError::Memory(format!("Cold record deserialization failed: {e}"))
2048 })?;
2049 let summary = record.summary();
2050 if query.matches(&summary) {
2051 results.push(summary);
2052 if results.len() >= limit {
2053 break;
2054 }
2055 }
2056 }
2057 Ok(results)
2058 }
2059
2060 pub fn find_cold_matching(
2069 &self,
2070 terms: &[String],
2071 galaxy: Option<Galaxy>,
2072 limit: usize,
2073 max_scan: usize,
2074 ) -> Result<crate::cold_storage::ColdDiscoveryOutcome> {
2075 self.find_cold_matching_eligible(terms, galaxy, limit, max_scan, |_| true)
2076 }
2077
2078 pub fn find_cold_matching_eligible(
2081 &self,
2082 terms: &[String],
2083 galaxy: Option<Galaxy>,
2084 limit: usize,
2085 max_scan: usize,
2086 eligible: impl Fn(&Memory) -> bool,
2087 ) -> Result<crate::cold_storage::ColdDiscoveryOutcome> {
2088 use crate::cold_storage::ColdDiscoveryStop;
2089 let mut out = crate::cold_storage::ColdDiscoveryOutcome::default();
2090 if terms.is_empty() || limit == 0 || max_scan == 0 {
2091 return Ok(out);
2092 }
2093 let tx = self
2094 .env
2095 .begin_ro_txn()
2096 .map_err(|e| CoreError::Memory(format!("LMDB ro_txn failed: {e}")))?;
2097 let mut cursor = tx
2098 .open_ro_cursor(self.cold_storage_db)
2099 .map_err(|e| CoreError::Memory(format!("LMDB open_ro_cursor failed: {e}")))?;
2100 let mut iter = cursor.iter();
2101 loop {
2102 if out.scanned >= max_scan {
2103 out.stop_reason = ColdDiscoveryStop::ScanLimit;
2104 break;
2105 }
2106 if out.records.len() >= limit {
2107 out.stop_reason = ColdDiscoveryStop::ResultLimit;
2108 break;
2109 }
2110 let Some((key, val)) = iter.next() else {
2111 out.stop_reason = ColdDiscoveryStop::Exhausted;
2112 break;
2113 };
2114 out.scanned += 1;
2115 let record: crate::cold_storage::ColdRecord = if let Ok(r) = rmp_serde::from_slice(val)
2116 {
2117 r
2118 } else {
2119 out.integrity_rejected += 1;
2120 continue;
2121 };
2122 if let Some(g) = galaxy {
2123 if record.galaxy != g {
2124 continue;
2125 }
2126 }
2127 out.candidates += 1;
2128 let mem = if let Ok(m) = record.decompress() {
2129 m
2130 } else {
2131 out.integrity_rejected += 1;
2132 continue;
2133 };
2134 if mem.metadata.is_private {
2135 out.private_skipped += 1;
2136 continue;
2137 }
2138 if !mem.metadata.validity.is_current() {
2139 out.non_current_skipped += 1;
2140 continue;
2141 }
2142 let integrity_ok = key == record.id.as_bytes()
2143 && mem.metadata.id == record.id
2144 && mem.metadata.galaxy == record.galaxy
2145 && mem.metadata.content_hash == record.content_hash
2146 && crate::content_hash(&mem.content) == record.content_hash;
2147 if !integrity_ok {
2148 out.integrity_rejected += 1;
2149 continue;
2150 }
2151 let haystack = format!(
2152 "{} {}",
2153 mem.content.to_lowercase(),
2154 mem.metadata.tags.join(" ").to_lowercase()
2155 );
2156 if !terms.iter().all(|t| haystack.contains(t.as_str())) {
2157 continue;
2158 }
2159 out.matched += 1;
2160 if !eligible(&mem) {
2161 out.eligibility_skipped += 1;
2162 continue;
2163 }
2164 out.records.push(record);
2165 }
2166 Ok(out)
2167 }
2168
2169 pub fn freeze_to_cold(
2175 &self,
2176 search: Option<&crate::SearchEngine>,
2177 memory_id: MemoryId,
2178 distance: f32,
2179 factors: crate::cold_storage::OuterRimFactors,
2180 digest_id: Option<MemoryId>,
2181 notes: Option<String>,
2182 codec: crate::cold_storage::CompressionCodec,
2183 ) -> Result<crate::cold_storage::ColdRecord> {
2184 let (galaxy, mut mem) = self.find_across_galaxies(memory_id)?.ok_or_else(|| {
2185 CoreError::NotFound(format!("Memory {memory_id} not found in hot store"))
2186 })?;
2187
2188 if mem.metadata.tier != crate::memory::Tier::Archival {
2190 let _ = mem.transition_tier(crate::memory::Tier::Archival);
2191 }
2192
2193 let record =
2194 crate::cold_storage::ColdRecord::new(&mem, distance, factors, digest_id, notes, codec)?;
2195
2196 self.put_cold_record(&record)?;
2198
2199 self.delete(galaxy, memory_id)?;
2201
2202 if let Some(engine) = search {
2204 if let Ok(mut writer_guard) = engine.writer() {
2205 let _ = engine.delete_document(&mut writer_guard, &memory_id.to_string());
2206 let _ = engine.commit(&mut writer_guard);
2207 }
2208 }
2209
2210 Ok(record)
2211 }
2212
2213 pub fn thaw_from_cold(
2219 &self,
2220 search: Option<&crate::SearchEngine>,
2221 memory_id: MemoryId,
2222 ) -> Result<Memory> {
2223 let record = self.get_cold_record(memory_id)?.ok_or_else(|| {
2224 CoreError::NotFound(format!("Memory {memory_id} not found in cold storage"))
2225 })?;
2226
2227 let mut mem = record.decompress()?;
2228
2229 let _ = mem.transition_tier(crate::memory::Tier::Episodic);
2231 mem.metadata.accessed_at = chrono::Utc::now();
2232 mem.metadata.access_count += 1;
2233 mem.metadata.recall_count += 1;
2234 if !mem.metadata.tags.iter().any(|t| t == "thawed:phagic") {
2235 mem.metadata.tags.push("thawed:phagic".to_string());
2236 }
2237
2238 self.put(record.galaxy, &mem)?;
2240
2241 if let Some(engine) = search {
2243 if let Ok(mut writer_guard) = engine.writer() {
2244 let _ = engine.index_memory(&mut writer_guard, &mem);
2245 let _ = engine.commit(&mut writer_guard);
2246 }
2247 }
2248
2249 self.delete_cold_record(memory_id)?;
2251
2252 Ok(mem)
2253 }
2254
2255 pub fn find_anywhere(&self, id: MemoryId) -> Result<Option<(Galaxy, Memory, bool)>> {
2259 if let Some((galaxy, mem)) = self.find_across_galaxies(id)? {
2260 return Ok(Some((galaxy, mem, false)));
2261 }
2262 if let Some(cold_record) = self.get_cold_record(id)? {
2263 let galaxy = cold_record.galaxy;
2264 let mem = cold_record.decompress()?;
2265 return Ok(Some((galaxy, mem, true)));
2266 }
2267 Ok(None)
2268 }
2269}
2270
2271#[cfg(test)]
2272mod tests {
2273 use super::*;
2274 use crate::content_hash;
2275
2276 #[test]
2277 fn open_and_create_galaxies() {
2278 let tmp = tempfile::tempdir().unwrap();
2279 let store = MemoryStore::open_default(tmp.path()).unwrap();
2280 for galaxy in Galaxy::all() {
2281 let _db = store.galaxy_db(galaxy).unwrap();
2282 }
2283 }
2284
2285 #[test]
2286 fn ensure_schema_completes_a_pre_cold_store() {
2287 use lmdb::{DatabaseFlags as LmdbFlags, Environment as LmdbEnv};
2288 use uuid::Uuid;
2289
2290 let tmp = tempfile::tempdir().unwrap();
2293 let path = tmp.path().join("old-store");
2294 std::fs::create_dir_all(&path).unwrap();
2295 {
2296 let env = LmdbEnv::new().set_max_dbs(64).open(&path).unwrap();
2297 for galaxy in Galaxy::all() {
2298 env.create_db(Some(galaxy.db_name()), LmdbFlags::default())
2299 .unwrap();
2300 }
2301 for (name, flags) in crate::indexes::INDEX_DBS {
2302 env.create_db(Some(name), *flags).unwrap();
2303 }
2304 for (name, flags) in [
2305 ("episodic_records", LmdbFlags::default()),
2306 ("episodic_terms_v2", LmdbFlags::DUP_SORT),
2307 ("embedding_cache", LmdbFlags::default()),
2308 ("revisions", LmdbFlags::default()),
2309 (crate::attestation::ATTESTATIONS_DB, LmdbFlags::default()),
2310 ] {
2311 env.create_db(Some(name), flags).unwrap();
2312 }
2313 }
2314
2315 let files_before: Vec<String> = {
2316 let mut names: Vec<String> = std::fs::read_dir(&path)
2317 .unwrap()
2318 .map(|e| e.unwrap().file_name().to_string_lossy().to_string())
2319 .collect();
2320 names.sort();
2321 names
2322 };
2323 let data_before = std::fs::read(path.join("data.mdb")).unwrap();
2324
2325 let error = match MemoryStore::open_readonly(&path) {
2326 Ok(_) => panic!("strict read-only open must refuse an incomplete store"),
2327 Err(e) => e.to_string(),
2328 };
2329 assert!(error.contains("cold_storage"), "{error}");
2330 assert_eq!(
2331 std::fs::read(path.join("data.mdb")).unwrap(),
2332 data_before,
2333 "readonly refusal must not mutate the pre-cold store"
2334 );
2335 let files_after: Vec<String> = {
2336 let mut names: Vec<String> = std::fs::read_dir(&path)
2337 .unwrap()
2338 .map(|e| e.unwrap().file_name().to_string_lossy().to_string())
2339 .collect();
2340 names.sort();
2341 names
2342 };
2343 assert_eq!(
2344 files_after, files_before,
2345 "readonly refusal changed the store directory"
2346 );
2347
2348 let created = MemoryStore::ensure_schema(&path).unwrap();
2349 assert_eq!(created, vec!["cold_storage".to_string()], "{created:?}");
2350
2351 let store = MemoryStore::open_readonly(&path).unwrap();
2352 assert!(store.get_cold_record(Uuid::nil()).unwrap().is_none());
2353
2354 assert!(MemoryStore::ensure_schema(&path).unwrap().is_empty());
2356 }
2357
2358 #[test]
2359 fn cold_discovery_hydrates_verifies_and_respects_visibility() {
2360 use crate::cold_storage::{ColdRecord, CompressionCodec, OuterRimFactors};
2361 let tmp = tempfile::tempdir().unwrap();
2362 let store = MemoryStore::open_default(tmp.path()).unwrap();
2363 let factors = OuterRimFactors {
2364 age_factor: 0.5,
2365 access_factor: 0.5,
2366 resonance_factor: 0.5,
2367 emotional_factor: 0.5,
2368 importance_factor: 0.5,
2369 distance: 0.5,
2370 };
2371
2372 let pub_mem = Memory::new(
2373 Galaxy::Codex,
2374 "public needle zxquniquecoldfact741 buried".into(),
2375 );
2376 let rec = ColdRecord::new(
2377 &pub_mem,
2378 0.5,
2379 factors.clone(),
2380 None,
2381 None,
2382 CompressionCodec::Gzip,
2383 )
2384 .unwrap();
2385 store.put_cold_record(&rec).unwrap();
2386 let out = store
2387 .find_cold_matching(&["zxquniquecoldfact741".to_string()], None, 10, 100)
2388 .unwrap();
2389 assert_eq!(out.matched, 1);
2390 assert_eq!(out.integrity_rejected, 0);
2391 assert_eq!(out.records.len(), 1);
2392
2393 let mut priv_mem = Memory::new(Galaxy::Codex, "private needle zxquniquecoldfact742".into());
2395 priv_mem.metadata.is_private = true;
2396 store
2397 .put_cold_record(
2398 &ColdRecord::new(&priv_mem, 0.5, factors, None, None, CompressionCodec::Gzip)
2399 .unwrap(),
2400 )
2401 .unwrap();
2402 let out_priv = store
2403 .find_cold_matching(&["zxquniquecoldfact742".to_string()], None, 10, 100)
2404 .unwrap();
2405 assert_eq!(out_priv.matched, 0);
2406 assert_eq!(out_priv.private_skipped, 1);
2407
2408 let mut tampered = rec.clone();
2411 let mut bad = Memory::new(Galaxy::Codex, "tampered needle zxquniquecoldfact743".into());
2412 bad.metadata.id = rec.id;
2413 let (payload, size) =
2414 crate::cold_storage::compress_memory(&bad, CompressionCodec::Gzip).unwrap();
2415 tampered.compressed_payload = payload;
2416 tampered.uncompressed_size = size;
2417 store.put_cold_record(&tampered).unwrap();
2418 let out_tamper = store
2419 .find_cold_matching(&["zxquniquecoldfact743".to_string()], None, 10, 100)
2420 .unwrap();
2421 assert_eq!(out_tamper.matched, 0);
2422 assert_eq!(out_tamper.integrity_rejected, 1);
2423
2424 store.delete_cold_record(rec.id).unwrap();
2427 let wrong_key = uuid::Uuid::from_u128(741);
2428 assert_ne!(wrong_key, rec.id);
2429 let value = rmp_serde::to_vec_named(&rec).unwrap();
2430 let mut tx = store.env.begin_rw_txn().unwrap();
2431 tx.put(
2432 store.cold_storage_db,
2433 wrong_key.as_bytes(),
2434 &value,
2435 WriteFlags::default(),
2436 )
2437 .unwrap();
2438 tx.commit().unwrap();
2439 let wrong_key_out = store
2440 .find_cold_matching(&["zxquniquecoldfact741".into()], None, 10, 100)
2441 .unwrap();
2442 assert!(wrong_key_out.records.is_empty());
2443 assert_eq!(wrong_key_out.integrity_rejected, 1);
2444 }
2445
2446 #[test]
2450 fn query_substring_filters_galaxy_wide() {
2451 let tmp = tempfile::tempdir().unwrap();
2452 let store = MemoryStore::open_default(tmp.path()).unwrap();
2453
2454 for (i, content) in [
2455 "the mesh joins at dawn",
2456 "unrelated content entirely",
2457 "MESH joins at dusk",
2458 ]
2459 .iter()
2460 .enumerate()
2461 {
2462 let mut m = Memory::new(Galaxy::Codex, content.to_string());
2463 m.metadata.importance = 0.5 + i as f32 / 10.0;
2464 store.put(Galaxy::Codex, &m).unwrap();
2465 }
2466
2467 let hits = store
2468 .query(
2469 Galaxy::Codex,
2470 &MemoryQuery::new().with_content_substring("mesh joins"),
2471 )
2472 .unwrap();
2473 assert_eq!(hits.len(), 2, "CI substring must match both: {hits:?}");
2474 assert!(
2475 hits.iter()
2476 .all(|m| m.content.to_lowercase().contains("mesh joins"))
2477 );
2478
2479 let none = store
2480 .query(
2481 Galaxy::Codex,
2482 &MemoryQuery::new().with_content_substring("quantum calendar"),
2483 )
2484 .unwrap();
2485 assert!(none.is_empty(), "no match must be an honest empty set");
2486
2487 let mut tagged = Memory::new(Galaxy::Codex, "mesh joins again".to_string());
2489 tagged.metadata.tags = vec!["mesh".into()];
2490 store.put(Galaxy::Codex, &tagged).unwrap();
2491 let combined = store
2492 .query(
2493 Galaxy::Codex,
2494 &MemoryQuery::new()
2495 .with_tags(vec!["mesh".into()])
2496 .with_content_substring("again"),
2497 )
2498 .unwrap();
2499 assert_eq!(combined.len(), 1);
2500 assert_eq!(combined[0].content, "mesh joins again");
2501 }
2502
2503 #[cfg(unix)]
2504 #[test]
2505 fn store_dir_has_restrictive_permissions() {
2506 let tmp = tempfile::tempdir().unwrap();
2507 let store_path = tmp.path().join("lmdb");
2508 let _store = MemoryStore::open_default(&store_path).unwrap();
2509 let perms = std::fs::metadata(&store_path).unwrap().permissions().mode();
2510 assert_eq!(
2511 perms & 0o777,
2512 0o700,
2513 "store directory should have 0700 permissions, got {:o}",
2514 perms & 0o777
2515 );
2516 }
2517
2518 #[test]
2519 fn put_get_delete_memory() {
2520 let tmp = tempfile::tempdir().unwrap();
2521 let store = MemoryStore::open_default(tmp.path()).unwrap();
2522
2523 let mem = Memory::new(Galaxy::Codex, "Hello world".to_string());
2524 let id = mem.metadata.id;
2525
2526 store.put(Galaxy::Codex, &mem).unwrap();
2527 let retrieved = store.get(Galaxy::Codex, id).unwrap();
2528 assert!(retrieved.is_some());
2529 assert_eq!(retrieved.unwrap().content, "Hello world");
2530
2531 let deleted = store.delete(Galaxy::Codex, id).unwrap();
2532 assert!(deleted);
2533
2534 let gone = store.get(Galaxy::Codex, id).unwrap();
2535 assert!(gone.is_none());
2536 }
2537
2538 #[test]
2539 fn scan_memories() {
2540 let tmp = tempfile::tempdir().unwrap();
2541 let store = MemoryStore::open_default(tmp.path()).unwrap();
2542
2543 for i in 0..5 {
2544 let mem = Memory::new(Galaxy::Codex, format!("memory-{i}"));
2545 store.put(Galaxy::Codex, &mem).unwrap();
2546 }
2547
2548 let all = store.scan(Galaxy::Codex, 100).unwrap();
2549 assert_eq!(all.len(), 5);
2550
2551 let limited = store.scan(Galaxy::Codex, 3).unwrap();
2552 assert_eq!(limited.len(), 3);
2553 }
2554
2555 #[test]
2556 fn overwrite_removes_stale_index_entries() {
2557 let tmp = tempfile::tempdir().unwrap();
2558 let store = MemoryStore::open_default(tmp.path()).unwrap();
2559
2560 let mut mem = Memory::new(Galaxy::Codex, "overwrite target".to_string());
2562 mem.metadata.tags = vec!["alpha".to_string()];
2563 mem.metadata.importance = 0.9;
2564 let id = mem.metadata.id;
2565 store.put(Galaxy::Codex, &mem).unwrap();
2566
2567 let mut updated = Memory::new(Galaxy::Codex, "overwritten content".to_string());
2569 updated.metadata.id = id;
2570 updated.metadata.tags = vec!["beta".to_string()];
2571 updated.metadata.importance = 0.1;
2572 store.put(Galaxy::Codex, &updated).unwrap();
2573
2574 let tx = store.env().begin_ro_txn().unwrap();
2576 let by_alpha = store
2577 .index_dbs()
2578 .find_by_tag(&tx, Galaxy::Codex, "alpha")
2579 .unwrap();
2580 let by_beta = store
2581 .index_dbs()
2582 .find_by_tag(&tx, Galaxy::Codex, "beta")
2583 .unwrap();
2584 assert!(
2585 by_alpha.is_empty(),
2586 "stale tag index entries must be removed on overwrite"
2587 );
2588 assert_eq!(by_beta, vec![id]);
2589
2590 let by_importance = store
2591 .index_dbs()
2592 .find_by_importance_range(&tx, Galaxy::Codex, 0.0, 0.2)
2593 .unwrap();
2594 assert!(
2595 by_importance.contains(&id),
2596 "new importance must be indexed"
2597 );
2598 let by_high = store
2599 .index_dbs()
2600 .find_by_importance_range(&tx, Galaxy::Codex, 0.8, 1.0)
2601 .unwrap();
2602 assert!(
2603 !by_high.contains(&id),
2604 "stale importance index entries must be removed on overwrite"
2605 );
2606
2607 let old_hash = content_hash("overwrite target");
2608 let new_hash = content_hash("overwritten content");
2609 assert_eq!(
2610 store
2611 .index_dbs()
2612 .find_by_content_hash(&tx, Galaxy::Codex, &old_hash)
2613 .unwrap(),
2614 None,
2615 "stale content-hash index entry must be removed"
2616 );
2617 assert_eq!(
2618 store
2619 .index_dbs()
2620 .find_by_content_hash(&tx, Galaxy::Codex, &new_hash)
2621 .unwrap(),
2622 Some(id)
2623 );
2624 }
2625
2626 #[test]
2627 fn count_memories() {
2628 let tmp = tempfile::tempdir().unwrap();
2629 let store = MemoryStore::open_default(tmp.path()).unwrap();
2630
2631 assert_eq!(store.count(Galaxy::Codex).unwrap(), 0);
2632
2633 for i in 0..3 {
2634 let mem = Memory::new(Galaxy::Codex, format!("count-{i}"));
2635 store.put(Galaxy::Codex, &mem).unwrap();
2636 }
2637
2638 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
2639 }
2640
2641 #[test]
2645 fn count_by_tag_counts_indexed_records() {
2646 let tmp = tempfile::tempdir().unwrap();
2647 let store = MemoryStore::open_default(tmp.path()).unwrap();
2648
2649 assert_eq!(store.count_by_tag(Galaxy::Sessions, "start").unwrap(), 0);
2650
2651 let mut start = Memory::new(Galaxy::Sessions, "{\"type\":\"session_start\"}".into());
2652 start.metadata.tags = vec!["session".into(), "start".into()];
2653 store.put(Galaxy::Sessions, &start).unwrap();
2654 for i in 0..2 {
2655 let mut turn =
2656 Memory::new(Galaxy::Sessions, format!("{{\"type\":\"turn\",\"i\":{i}}}"));
2657 turn.metadata.tags = vec!["session".into(), "turn".into()];
2658 store.put(Galaxy::Sessions, &turn).unwrap();
2659 }
2660
2661 assert_eq!(store.count(Galaxy::Sessions).unwrap(), 3);
2662 assert_eq!(store.count_by_tag(Galaxy::Sessions, "start").unwrap(), 1);
2663 assert_eq!(store.count_by_tag(Galaxy::Sessions, "turn").unwrap(), 2);
2664 assert_eq!(store.count_by_tag(Galaxy::Sessions, "absent").unwrap(), 0);
2665 }
2666
2667 #[test]
2668 fn get_nonexistent_returns_none() {
2669 let tmp = tempfile::tempdir().unwrap();
2670 let store = MemoryStore::open_default(tmp.path()).unwrap();
2671 let result = store.get(Galaxy::Codex, uuid::Uuid::new_v4()).unwrap();
2672 assert!(result.is_none());
2673 }
2674
2675 #[test]
2676 fn raw_put_get() {
2677 let tmp = tempfile::tempdir().unwrap();
2678 let store = MemoryStore::open_default(tmp.path()).unwrap();
2679
2680 store
2681 .put_raw(Galaxy::Substrate, b"config:key", b"value123")
2682 .unwrap();
2683 let val = store.get_raw(Galaxy::Substrate, b"config:key").unwrap();
2684 assert_eq!(val, Some(b"value123".to_vec()));
2685 }
2686
2687 #[test]
2688 fn put_dedup_prevents_duplicates() {
2689 let tmp = tempfile::tempdir().unwrap();
2690 let store = MemoryStore::open_default(tmp.path()).unwrap();
2691
2692 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
2693 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
2694
2695 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
2696 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
2697
2698 assert_eq!(id1, id2, "dedup should return same ID for same content");
2699 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
2700 }
2701
2702 #[test]
2703 fn put_dedup_allows_different_content() {
2704 let tmp = tempfile::tempdir().unwrap();
2705 let store = MemoryStore::open_default(tmp.path()).unwrap();
2706
2707 let mem1 = Memory::new(Galaxy::Codex, "content A".into());
2708 store.put_dedup(Galaxy::Codex, &mem1).unwrap();
2709
2710 let mem2 = Memory::new(Galaxy::Codex, "content B".into());
2711 store.put_dedup(Galaxy::Codex, &mem2).unwrap();
2712
2713 assert_eq!(store.count(Galaxy::Codex).unwrap(), 2);
2714 }
2715
2716 #[test]
2717 fn put_batch_atomic_write() {
2718 let tmp = tempfile::tempdir().unwrap();
2719 let store = MemoryStore::open_default(tmp.path()).unwrap();
2720
2721 let memories: Vec<Memory> = (0..10)
2722 .map(|i| Memory::new(Galaxy::Codex, format!("batch-{i}")))
2723 .collect();
2724
2725 store.put_batch(Galaxy::Codex, &memories).unwrap();
2726 assert_eq!(store.count(Galaxy::Codex).unwrap(), 10);
2727 }
2728
2729 #[test]
2730 fn query_by_tags() {
2731 let tmp = tempfile::tempdir().unwrap();
2732 let store = MemoryStore::open_default(tmp.path()).unwrap();
2733
2734 let mem1 = Memory::new(Galaxy::Codex, "tagged memory".into())
2735 .with_tags(vec!["rust".into(), "memory".into()]);
2736 let mem2 =
2737 Memory::new(Galaxy::Codex, "other memory".into()).with_tags(vec!["python".into()]);
2738 store.put(Galaxy::Codex, &mem1).unwrap();
2739 store.put(Galaxy::Codex, &mem2).unwrap();
2740
2741 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
2742 let results = store.query(Galaxy::Codex, &query).unwrap();
2743 assert_eq!(results.len(), 1);
2744 assert_eq!(results[0].content, "tagged memory");
2745 }
2746
2747 #[test]
2748 fn query_by_importance_range() {
2749 let tmp = tempfile::tempdir().unwrap();
2750 let store = MemoryStore::open_default(tmp.path()).unwrap();
2751
2752 store
2753 .put(
2754 Galaxy::Codex,
2755 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
2756 )
2757 .unwrap();
2758 store
2759 .put(
2760 Galaxy::Codex,
2761 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
2762 )
2763 .unwrap();
2764 store
2765 .put(
2766 Galaxy::Codex,
2767 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
2768 )
2769 .unwrap();
2770
2771 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
2772 let results = store.query(Galaxy::Codex, &query).unwrap();
2773 assert_eq!(results.len(), 1);
2774 assert_eq!(results[0].content, "mid");
2775 }
2776
2777 #[test]
2778 fn memory_query_one_sided_time_bounds() {
2779 let old = Memory::new(Galaxy::Codex, "old".into());
2782 let mut recent = Memory::new(Galaxy::Codex, "recent".into());
2783 recent.metadata.created_at = old.metadata.created_at + chrono::Duration::days(30);
2784
2785 let cutoff = old.metadata.created_at + chrono::Duration::days(10);
2786 let after = MemoryQuery::new().with_created_after(cutoff);
2787 assert!(!after.matches(&old), "pre-cutoff memory must not match");
2788 assert!(after.matches(&recent), "post-cutoff memory must match");
2789
2790 let before = MemoryQuery::new().with_created_before(cutoff);
2791 assert!(before.matches(&old), "pre-cutoff memory must match");
2792 assert!(
2793 !before.matches(&recent),
2794 "post-cutoff memory must not match"
2795 );
2796
2797 let edge = MemoryQuery::new().with_created_after(cutoff);
2799 let mut at = Memory::new(Galaxy::Codex, "at cutoff".into());
2800 at.metadata.created_at = cutoff;
2801 assert!(edge.matches(&at), "created_at == after bound is inclusive");
2802 }
2803
2804 #[test]
2805 fn embedding_put_get_delete() {
2806 let tmp = tempfile::tempdir().unwrap();
2807 let store = MemoryStore::open_default(tmp.path()).unwrap();
2808
2809 let id = uuid::Uuid::new_v4();
2810 let embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5];
2811
2812 store.put_embedding(id, &embedding).unwrap();
2813 let retrieved = store.get_embedding(id).unwrap();
2814 assert!(retrieved.is_some());
2815 let retrieved = retrieved.unwrap();
2816 assert_eq!(retrieved.len(), 5);
2817 assert!((retrieved[0] - 0.1).abs() < f32::EPSILON);
2818
2819 assert!(store.delete_embedding(id).unwrap());
2820 assert!(store.get_embedding(id).unwrap().is_none());
2821 }
2822
2823 #[test]
2824 fn embedding_cache_roundtrip_batch_and_count() {
2825 let tmp = tempfile::tempdir().unwrap();
2826 let store = MemoryStore::open_default(tmp.path()).unwrap();
2827
2828 let entries: Vec<(String, Vec<f32>)> = (0..5)
2829 .map(|i| (format!("ns:model:{i:016x}"), vec![i as f32; 8]))
2830 .collect();
2831 store.put_embedding_cache_batch(&entries).unwrap();
2832 assert_eq!(store.embedding_cache_count().unwrap(), 5);
2833
2834 let hit = store
2836 .get_embedding_cache("ns:model:0000000000000003")
2837 .unwrap();
2838 assert_eq!(hit.unwrap(), vec![3.0f32; 8]);
2839 assert!(
2840 store
2841 .get_embedding_cache("ns:model:absent")
2842 .unwrap()
2843 .is_none()
2844 );
2845
2846 let keys: Vec<String> = (0..6).map(|i| format!("ns:model:{i:016x}")).collect();
2848 let batch = store.get_embedding_cache_batch(&keys).unwrap();
2849 assert_eq!(batch.len(), 6);
2850 assert!(batch[0..5].iter().all(Option::is_some));
2851 assert!(batch[5].is_none());
2852
2853 store
2855 .put_embedding_cache("ns:model:0000000000000001", &[9.0; 8])
2856 .unwrap();
2857 assert_eq!(store.embedding_cache_count().unwrap(), 5);
2858 assert_eq!(
2859 store
2860 .get_embedding_cache("ns:model:0000000000000001")
2861 .unwrap()
2862 .unwrap(),
2863 vec![9.0f32; 8]
2864 );
2865 }
2866
2867 #[test]
2868 fn embedding_cache_survives_store_reopen() {
2869 let tmp = tempfile::tempdir().unwrap();
2871 {
2872 let store = MemoryStore::open_default(tmp.path()).unwrap();
2873 store
2874 .put_embedding_cache("onnx:bge-small:abc", &[0.5; 384])
2875 .unwrap();
2876 }
2877 let reopened = MemoryStore::open_default(tmp.path()).unwrap();
2878 let cached = reopened.get_embedding_cache("onnx:bge-small:abc").unwrap();
2879 assert_eq!(cached.unwrap(), vec![0.5f32; 384]);
2880 }
2881
2882 #[test]
2883 fn content_hash_is_sha256() {
2884 let hash1 = content_hash("test content");
2885 let hash2 = content_hash("test content");
2886 let hash3 = content_hash("different content");
2887
2888 assert_eq!(hash1, hash2, "same content should produce same hash");
2889 assert_ne!(
2890 hash1, hash3,
2891 "different content should produce different hash"
2892 );
2893 assert_eq!(hash1.len(), 64, "SHA-256 hex should be 64 chars");
2894 }
2895
2896 #[test]
2897 fn query_by_tag_uses_index() {
2898 let tmp = tempfile::tempdir().unwrap();
2899 let store = MemoryStore::open_default(tmp.path()).unwrap();
2900
2901 let mem1 = Memory::new(Galaxy::Codex, "tagged".into())
2902 .with_tags(vec!["rust".into(), "memory".into()]);
2903 let mem2 = Memory::new(Galaxy::Codex, "other".into()).with_tags(vec!["python".into()]);
2904 store.put(Galaxy::Codex, &mem1).unwrap();
2905 store.put(Galaxy::Codex, &mem2).unwrap();
2906
2907 let query = MemoryQuery::new().with_tags(vec!["rust".into()]);
2908 let results = store.query(Galaxy::Codex, &query).unwrap();
2909 assert_eq!(results.len(), 1);
2910 assert_eq!(results[0].content, "tagged");
2911 }
2912
2913 #[test]
2914 fn query_by_importance_uses_index() {
2915 let tmp = tempfile::tempdir().unwrap();
2916 let store = MemoryStore::open_default(tmp.path()).unwrap();
2917
2918 store
2919 .put(
2920 Galaxy::Codex,
2921 &Memory::new(Galaxy::Codex, "low".into()).with_importance(0.1),
2922 )
2923 .unwrap();
2924 store
2925 .put(
2926 Galaxy::Codex,
2927 &Memory::new(Galaxy::Codex, "mid".into()).with_importance(0.5),
2928 )
2929 .unwrap();
2930 store
2931 .put(
2932 Galaxy::Codex,
2933 &Memory::new(Galaxy::Codex, "high".into()).with_importance(0.9),
2934 )
2935 .unwrap();
2936
2937 let query = MemoryQuery::new().with_importance_range(0.4, 0.6);
2938 let results = store.query(Galaxy::Codex, &query).unwrap();
2939 assert_eq!(results.len(), 1);
2940 assert_eq!(results[0].content, "mid");
2941 }
2942
2943 #[test]
2944 fn query_by_time_uses_index() {
2945 let tmp = tempfile::tempdir().unwrap();
2946 let store = MemoryStore::open_default(tmp.path()).unwrap();
2947
2948 let t0 = chrono::Utc::now();
2949 std::thread::sleep(std::time::Duration::from_millis(10));
2950 let mem = Memory::new(Galaxy::Codex, "timed".into());
2951 store.put(Galaxy::Codex, &mem).unwrap();
2952 std::thread::sleep(std::time::Duration::from_millis(10));
2953 let t2 = chrono::Utc::now();
2954
2955 let query = MemoryQuery::new().with_time_range(t0, t2);
2956 let results = store.query(Galaxy::Codex, &query).unwrap();
2957 assert_eq!(results.len(), 1);
2958 assert_eq!(results[0].content, "timed");
2959 }
2960
2961 #[test]
2962 fn delete_removes_index_entries() {
2963 let tmp = tempfile::tempdir().unwrap();
2964 let store = MemoryStore::open_default(tmp.path()).unwrap();
2965
2966 let mem = Memory::new(Galaxy::Codex, "test".into())
2967 .with_tags(vec!["tag1".into()])
2968 .with_importance(0.7);
2969 let id = mem.metadata.id;
2970 let hash = mem.metadata.content_hash.clone();
2971 store.put(Galaxy::Codex, &mem).unwrap();
2972
2973 assert!(
2975 store
2976 .find_by_content_hash(Galaxy::Codex, &hash)
2977 .unwrap()
2978 .is_some()
2979 );
2980
2981 store.delete(Galaxy::Codex, id).unwrap();
2983
2984 assert!(
2986 store
2987 .find_by_content_hash(Galaxy::Codex, &hash)
2988 .unwrap()
2989 .is_none()
2990 );
2991
2992 let query = MemoryQuery::new().with_tags(vec!["tag1".into()]);
2994 let results = store.query(Galaxy::Codex, &query).unwrap();
2995 assert!(results.is_empty());
2996 }
2997
2998 #[test]
2999 fn put_batch_updates_indexes() {
3000 let tmp = tempfile::tempdir().unwrap();
3001 let store = MemoryStore::open_default(tmp.path()).unwrap();
3002
3003 let memories: Vec<Memory> = (0..5)
3004 .map(|i| {
3005 Memory::new(Galaxy::Codex, format!("batch-{i}"))
3006 .with_tags(vec![format!("tag{i}")])
3007 .with_importance(i as f32 * 0.2)
3008 })
3009 .collect();
3010 store.put_batch(Galaxy::Codex, &memories).unwrap();
3011
3012 for i in 0..5 {
3013 let query = MemoryQuery::new().with_tags(vec![format!("tag{i}")]);
3014 let results = store.query(Galaxy::Codex, &query).unwrap();
3015 assert_eq!(results.len(), 1, "tag{i} should have 1 result");
3016 }
3017 }
3018
3019 #[test]
3020 fn find_by_content_hash_indexed_matches_scan() {
3021 let tmp = tempfile::tempdir().unwrap();
3022 let store = MemoryStore::open_default(tmp.path()).unwrap();
3023
3024 let mem = Memory::new(Galaxy::Codex, "dedup test".into());
3025 let id = mem.metadata.id;
3026 let hash = mem.metadata.content_hash.clone();
3027 store.put(Galaxy::Codex, &mem).unwrap();
3028
3029 let indexed = store.find_by_content_hash(Galaxy::Codex, &hash).unwrap();
3030 let scanned = store
3031 .find_by_content_hash_scan(Galaxy::Codex, &hash)
3032 .unwrap();
3033
3034 assert_eq!(indexed, scanned);
3035 assert_eq!(indexed, Some(id));
3036 }
3037
3038 #[test]
3039 fn put_dedup_uses_index() {
3040 let tmp = tempfile::tempdir().unwrap();
3041 let store = MemoryStore::open_default(tmp.path()).unwrap();
3042
3043 let mem1 = Memory::new(Galaxy::Codex, "duplicate content".into());
3044 let id1 = store.put_dedup(Galaxy::Codex, &mem1).unwrap();
3045
3046 let mem2 = Memory::new(Galaxy::Codex, "duplicate content".into());
3047 let id2 = store.put_dedup(Galaxy::Codex, &mem2).unwrap();
3048
3049 assert_eq!(id1, id2, "dedup should return same ID for same content");
3050 assert_eq!(store.count(Galaxy::Codex).unwrap(), 1);
3051 }
3052
3053 #[test]
3054 fn put_semantic_updates_coord5d() {
3055 let tmp = tempfile::tempdir().unwrap();
3056 let store = MemoryStore::open_default(tmp.path()).unwrap();
3057
3058 let mut mem = Memory::new(
3059 Galaxy::Codex,
3060 "The algorithm computes data using a systematic method".to_string(),
3061 );
3062 let original_coord = mem.metadata.coord5d.clone();
3063 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
3064
3065 assert_ne!(
3067 mem.metadata.coord5d.x, original_coord.x,
3068 "semantic encoding should change x"
3069 );
3070 assert_ne!(
3071 mem.metadata.coord5d.y, original_coord.y,
3072 "semantic encoding should change y"
3073 );
3074
3075 let retrieved = store.get(Galaxy::Codex, mem.metadata.id).unwrap().unwrap();
3077 assert_eq!(retrieved.metadata.coord5d.x, mem.metadata.coord5d.x);
3078 }
3079
3080 #[test]
3081 fn put_semantic_preserves_temporal_and_importance() {
3082 let tmp = tempfile::tempdir().unwrap();
3083 let store = MemoryStore::open_default(tmp.path()).unwrap();
3084
3085 let mut mem = Memory::new(Galaxy::Codex, "test content".into()).with_importance(0.8);
3086 mem.metadata.coord5d.w = 0.6;
3087 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
3088
3089 assert!((mem.metadata.coord5d.w - 0.6).abs() < f32::EPSILON);
3090 assert!((mem.metadata.coord5d.v - 0.8).abs() < f32::EPSILON);
3091 }
3092
3093 #[test]
3094 fn find_similar_returns_nearest_first() {
3095 let tmp = tempfile::tempdir().unwrap();
3096 let store = MemoryStore::open_default(tmp.path()).unwrap();
3097
3098 let mut logic_mem = Memory::new(
3099 Galaxy::Codex,
3100 "The algorithm computes data using systematic logic and analysis".to_string(),
3101 );
3102 store.put_semantic(Galaxy::Codex, &mut logic_mem).unwrap();
3103
3104 let mut emotion_mem = Memory::new(
3105 Galaxy::Codex,
3106 "I feel love and joy with deep passion and empathy in my heart".to_string(),
3107 );
3108 store.put_semantic(Galaxy::Codex, &mut emotion_mem).unwrap();
3109
3110 let results = store
3112 .find_similar(Galaxy::Codex, "algorithm data systematic method", 10)
3113 .unwrap();
3114 assert!(!results.is_empty());
3115 assert_eq!(results[0].0.metadata.id, logic_mem.metadata.id);
3116
3117 let results = store
3119 .find_similar(Galaxy::Codex, "love joy passion heart feeling", 10)
3120 .unwrap();
3121 assert!(!results.is_empty());
3122 assert_eq!(results[0].0.metadata.id, emotion_mem.metadata.id);
3123 }
3124
3125 #[test]
3126 fn find_similar_empty_galaxy() {
3127 let tmp = tempfile::tempdir().unwrap();
3128 let store = MemoryStore::open_default(tmp.path()).unwrap();
3129
3130 let results = store.find_similar(Galaxy::Codex, "anything", 10).unwrap();
3131 assert!(results.is_empty());
3132 }
3133
3134 #[test]
3135 fn find_similar_respects_limit() {
3136 let tmp = tempfile::tempdir().unwrap();
3137 let store = MemoryStore::open_default(tmp.path()).unwrap();
3138
3139 for i in 0..5 {
3140 let mut mem = Memory::new(Galaxy::Codex, format!("algorithm data method {i}"));
3141 store.put_semantic(Galaxy::Codex, &mut mem).unwrap();
3142 }
3143
3144 let results = store
3145 .find_similar(Galaxy::Codex, "algorithm data", 3)
3146 .unwrap();
3147 assert_eq!(results.len(), 3);
3148 }
3149
3150 #[test]
3151 fn semantic_encoder_accessible() {
3152 let tmp = tempfile::tempdir().unwrap();
3153 let store = MemoryStore::open_default(tmp.path()).unwrap();
3154
3155 let scores = store.semantic_encoder().encode("algorithm data logic");
3156 assert!(scores.x < 0.5);
3158 }
3159
3160 #[test]
3161 fn put_raw_batch_writes_atomically() {
3162 let tmp = tempfile::tempdir().unwrap();
3163 let store = MemoryStore::open_default(tmp.path()).unwrap();
3164
3165 let entries: &[(&[u8], &[u8])] =
3166 &[(b"key1", b"val1"), (b"key2", b"val2"), (b"key3", b"val3")];
3167 store.put_raw_batch(Galaxy::Karma, entries).unwrap();
3168
3169 assert_eq!(
3170 store.get_raw(Galaxy::Karma, b"key1").unwrap().unwrap(),
3171 b"val1"
3172 );
3173 assert_eq!(
3174 store.get_raw(Galaxy::Karma, b"key2").unwrap().unwrap(),
3175 b"val2"
3176 );
3177 assert_eq!(
3178 store.get_raw(Galaxy::Karma, b"key3").unwrap().unwrap(),
3179 b"val3"
3180 );
3181 }
3182
3183 #[test]
3184 fn put_raw_batch_empty_is_noop() {
3185 let tmp = tempfile::tempdir().unwrap();
3186 let store = MemoryStore::open_default(tmp.path()).unwrap();
3187
3188 store.put_raw_batch(Galaxy::Karma, &[]).unwrap();
3189 assert_eq!(store.count(Galaxy::Karma).unwrap(), 0);
3190 }
3191
3192 #[test]
3193 fn entry_limit_rejects_excess_writes() {
3194 let tmp = tempfile::tempdir().unwrap();
3195 let store = MemoryStore::open_default(tmp.path())
3196 .unwrap()
3197 .with_entry_limit(3);
3198
3199 for i in 0..3 {
3200 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
3201 store.put(Galaxy::Codex, &mem).unwrap();
3202 }
3203
3204 let mem = Memory::new(Galaxy::Codex, "overflow memory".to_string());
3206 let result = store.put(Galaxy::Codex, &mem);
3207 assert!(result.is_err(), "write beyond limit should be rejected");
3208 let err_msg = result.unwrap_err().to_string();
3209 assert!(
3210 err_msg.contains("entry limit reached"),
3211 "error should mention entry limit: {err_msg}"
3212 );
3213 assert_eq!(store.count(Galaxy::Codex).unwrap(), 3);
3214 }
3215
3216 #[test]
3217 fn entry_limit_per_galaxy_independent() {
3218 let tmp = tempfile::tempdir().unwrap();
3219 let store = MemoryStore::open_default(tmp.path())
3220 .unwrap()
3221 .with_entry_limit(2);
3222
3223 for i in 0..2 {
3225 let mem = Memory::new(Galaxy::Codex, format!("codex {i}"));
3226 store.put(Galaxy::Codex, &mem).unwrap();
3227 }
3228
3229 let mem = Memory::new(Galaxy::Research, "science memory".to_string());
3231 let result = store.put(Galaxy::Research, &mem);
3232 assert!(
3233 result.is_ok(),
3234 "different galaxy should not be affected by limit"
3235 );
3236 }
3237
3238 #[test]
3239 fn entry_limit_none_allows_unlimited() {
3240 let tmp = tempfile::tempdir().unwrap();
3241 let store = MemoryStore::open_default(tmp.path()).unwrap();
3242
3243 for i in 0..50 {
3245 let mem = Memory::new(Galaxy::Codex, format!("memory {i}"));
3246 store.put(Galaxy::Codex, &mem).unwrap();
3247 }
3248 assert_eq!(store.count(Galaxy::Codex).unwrap(), 50);
3249 }
3250
3251 #[test]
3252 fn map_full_error_is_graceful() {
3253 let tmp = tempfile::tempdir().unwrap();
3257 let store = MemoryStore::open(tmp.path(), 512 * 1024).unwrap();
3258
3259 let mut written = 0;
3261 let mut got_map_full = false;
3262 for i in 0..1000 {
3263 let mem = Memory::new(
3264 Galaxy::Codex,
3265 format!("memory content {i} {}", "with padding ".repeat(50)),
3266 );
3267 match store.put(Galaxy::Codex, &mem) {
3268 Ok(()) => written += 1,
3269 Err(e) => {
3270 let msg = e.to_string();
3271 if msg.contains("map full") {
3272 got_map_full = true;
3273 break;
3274 }
3275 break;
3277 }
3278 }
3279 }
3280
3281 assert!(
3282 got_map_full || written < 1000,
3283 "should eventually hit map full or error"
3284 );
3285 assert!(written > 0, "should have written at least some memories");
3286 }
3287
3288 #[test]
3289 fn test_find_across_galaxies() {
3290 let tmp = tempfile::tempdir().unwrap();
3291 let store = MemoryStore::open_default(tmp.path()).unwrap();
3292
3293 let mem = Memory::new(Galaxy::Research, "Cross-galaxy research memo".into());
3294 let id = mem.metadata.id;
3295 store.put(Galaxy::Research, &mem).unwrap();
3296
3297 let found = store.find_across_galaxies(id).unwrap();
3298 assert!(found.is_some());
3299 let (galaxy, retrieved) = found.unwrap();
3300 assert_eq!(galaxy, Galaxy::Research);
3301 assert_eq!(retrieved.metadata.id, id);
3302 assert_eq!(retrieved.content, "Cross-galaxy research memo");
3303
3304 assert!(
3306 store
3307 .find_across_galaxies(uuid::Uuid::new_v4())
3308 .unwrap()
3309 .is_none()
3310 );
3311 }
3312}