1use alloc::format;
17use alloc::string::String;
18use alloc::vec::Vec;
19
20use plugmem_arena::{
21 Arena, ArenaCfg, BlobHeap, BlobHeapCfg, BlobId, ChunkPool, ChunkPoolCfg, Interner, ListHandle,
22 ShardMode, TermId, key,
23};
24
25use crate::config::Config;
26use crate::error::Error;
27use crate::id::{EntityId, FactId, NONE_U32};
28use crate::index::IdListIndex;
29use crate::index::bm25::Bm25Index;
30use crate::index::hnsw::HnswGraph;
31use crate::index::vecpool::VecPool;
32use crate::journal::{JournalScan, Op, scan};
33use crate::model::{
34 EdgeSlot, EntityByName, EntityRecord, FactAux, FactRecord, TemporalSlot, VALID_TO_OPEN,
35 fact_flags,
36};
37use crate::storage::Storage;
38use crate::tokenizer::Tokenizer;
39
40const SIMILAR_CANDIDATE_CAP: usize = 32;
42
43mod maintain;
44mod persist;
45mod recall;
46
47pub use maintain::MaintainReport;
48pub use recall::{RecallQuery, RecallResult, RecallScratch, RecalledEdge, RecalledFact, source};
49
50#[derive(Clone, Copy, Debug)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize))]
53pub struct RememberInput<'a> {
54 pub now: u64,
56 pub text: &'a str,
58 pub entity: Option<&'a str>,
60 pub tags: &'a [&'a str],
62 pub links: &'a [(&'a str, &'a str)],
64 pub vector: Option<&'a [f32]>,
68 pub valid_from: Option<u64>,
70 pub metadata: Option<&'a [(&'a str, &'a str)]>,
75}
76
77impl<'a> RememberInput<'a> {
78 pub fn text(now: u64, text: &'a str) -> Self {
80 Self {
81 now,
82 text,
83 entity: None,
84 tags: &[],
85 links: &[],
86 vector: None,
87 valid_from: None,
88 metadata: None,
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize))]
96pub struct LinkInput<'a> {
97 pub now: u64,
99 pub src: &'a str,
101 pub rel: &'a str,
103 pub dst: &'a str,
105 pub provenance: Option<FactId>,
107}
108
109#[derive(Clone, Debug, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct RememberOutcome {
113 pub id: FactId,
115 pub entity: Option<EntityId>,
117 pub similar: Vec<Similar>,
122}
123
124#[derive(Clone, Copy, Debug, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct Similar {
128 pub id: FactId,
130 pub score: f32,
133 pub reason: SimilarReason,
135}
136
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
140pub enum SimilarReason {
141 LexicalOverlap,
144 VectorCosine,
147}
148
149#[derive(Clone, Copy, Debug)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize))]
152pub struct FactView<'a> {
153 pub record: FactRecord,
155 pub text: &'a str,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
165pub enum FactFault {
166 Text,
168 Vector,
171 Metadata,
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181#[non_exhaustive]
182pub struct Stats {
183 pub facts: usize,
186 pub entities: usize,
188 pub terms: usize,
190 pub edges: usize,
193 pub vectors: usize,
195 pub next_fact: u32,
198 pub next_entity: u32,
200 pub db_uuid: u128,
203 pub pool_bytes: usize,
206}
207
208#[derive(Clone, Debug, Default, PartialEq, Eq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211pub struct OpenReport {
212 pub replayed: usize,
214 pub skipped: usize,
216 pub truncated_tail: bool,
218}
219
220pub struct Memory<'a> {
227 cfg: Config,
228 facts: Arena<'a, FactRecord>,
230 fact_aux: Arena<'a, FactAux>,
231 entities: Arena<'a, EntityRecord>,
232 by_name: Arena<'a, EntityByName>,
233 edges_out: Arena<'a, EdgeSlot>,
234 edges_in: Arena<'a, EdgeSlot>,
235 temporal: Arena<'a, TemporalSlot>,
236 texts: BlobHeap<'a>,
238 metas: BlobHeap<'a>,
243 terms: Interner<'a>,
246 tag_lists: ChunkPool<'a>,
248 bm25: Bm25Index<'a>,
250 tags_idx: IdListIndex<'a>,
251 entity_facts: IdListIndex<'a>,
252 vecs: VecPool<'a>,
254 hnsw: HnswGraph<'a>,
258 next_fact: u32,
260 next_entity: u32,
261 tokenizer: Tokenizer,
263 tf_scratch: Vec<(u32, u8)>,
264 name_scratch: String,
265}
266
267impl<'a> Memory<'a> {
268 pub fn new(cfg: Config) -> Result<Self, Error> {
275 cfg.validate()?;
276 let uni =
277 |shards: usize| ArenaCfg::new(shards, ShardMode::Uniform).with_max_bytes(cfg.max_bytes);
278 let ord =
279 |shards: usize| ArenaCfg::new(shards, ShardMode::Ordered).with_max_bytes(cfg.max_bytes);
280 let blob = BlobHeapCfg::new()
281 .with_max_bytes(cfg.max_bytes)
282 .with_max_blob(cfg.max_blob);
283 Ok(Self {
284 facts: Arena::new(uni(cfg.shards_facts))?,
285 fact_aux: Arena::new(uni(cfg.shards_facts))?,
286 entities: Arena::new(uni(cfg.shards_entities))?,
287 by_name: Arena::new(ord(cfg.shards_entities))?,
288 edges_out: Arena::new(ord(cfg.shards_edges))?,
289 edges_in: Arena::new(ord(cfg.shards_edges))?,
290 temporal: Arena::new(ord(cfg.shards_temporal))?,
291 texts: BlobHeap::new(blob),
292 metas: BlobHeap::new(blob),
293 terms: Interner::new(blob),
294 tag_lists: ChunkPool::new(ChunkPoolCfg::new().with_max_bytes(cfg.max_bytes)),
295 bm25: Bm25Index::new(cfg.shards_postings, cfg.max_bytes)?,
296 tags_idx: IdListIndex::new(cfg.shards_postings, cfg.max_bytes)?,
297 entity_facts: IdListIndex::new(cfg.shards_entities, cfg.max_bytes)?,
298 vecs: VecPool::new(cfg.dim, cfg.max_bytes),
299 hnsw: HnswGraph::new(cfg.hnsw_m, cfg.hnsw_m0, cfg.max_bytes)?,
300 next_fact: 0,
301 next_entity: 0,
302 tokenizer: Tokenizer::new(),
303 tf_scratch: Vec::new(),
304 name_scratch: String::new(),
305 cfg,
306 })
307 }
308
309 pub fn open<S: Storage>(store: &mut S, cfg: Config) -> Result<(Self, OpenReport), Error> {
313 let snapshot = store
314 .read_snapshot()
315 .map_err(|e| Error::Storage(format!("{e:?}")))?;
316 let journal = store
317 .read_journal()
318 .map_err(|e| Error::Storage(format!("{e:?}")))?;
319 Self::from_bytes(snapshot.as_deref(), &journal, cfg)
320 }
321
322 pub fn from_bytes(
324 snapshot: Option<&[u8]>,
325 journal: &[u8],
326 cfg: Config,
327 ) -> Result<(Self, OpenReport), Error> {
328 let mut mem = match snapshot {
329 Some(bytes) => Self::load_snapshot(bytes, cfg)?,
330 None => Self::new(cfg)?,
331 };
332 let report = mem.replay(journal)?;
333 Ok((mem, report))
334 }
335
336 pub fn from_bytes_borrowed(
347 snapshot: &'a [u8],
348 journal: &[u8],
349 cfg: Config,
350 ) -> Result<Self, Error> {
351 let mem = Self::load_snapshot_borrowed(snapshot, cfg)?;
352 let JournalScan { entries, .. } = scan(journal)?;
353 if !entries.is_empty() {
354 return Err(Error::Invalid(
355 "read-only open requires a checkpointed (empty) journal",
356 ));
357 }
358 Ok(mem)
359 }
360
361 pub fn from_bytes_overlay(
376 snapshot: &'a [u8],
377 journal: &[u8],
378 cfg: Config,
379 ) -> Result<(Self, OpenReport), Error> {
380 let mut mem = Self::load_snapshot_borrowed(snapshot, cfg)?;
381 let report = mem.replay(journal)?;
382 Ok((mem, report))
383 }
384
385 fn replay(&mut self, journal: &[u8]) -> Result<OpenReport, Error> {
390 let JournalScan {
391 entries,
392 truncated_tail,
393 } = scan(journal)?;
394 let mut report = OpenReport {
395 truncated_tail,
396 ..OpenReport::default()
397 };
398 for entry in entries {
399 let op = Op::decode(entry.op, entry.payload)?;
400 match op {
401 Op::Remember {
402 now,
403 valid_from,
404 entity,
405 text,
406 ref tags,
407 ref links,
408 ref vector,
409 ref metadata,
410 revises,
411 assigned,
412 } => {
413 if assigned.0 < self.next_fact {
414 report.skipped += 1;
415 continue;
416 }
417 if assigned.0 != self.next_fact {
418 return Err(Error::Corrupt("journal fact ids are not contiguous"));
419 }
420 if !vector.is_empty() && vector.len() != self.cfg.dim {
421 return Err(Error::Corrupt(
422 "journal vector dimension disagrees with dim",
423 ));
424 }
425 if let Some(target) = revises.some() {
426 self.check_revisable(target)
427 .map_err(|_| Error::Corrupt("journal revises an unrevisable fact"))?;
428 }
429 self.apply_remember(
430 &RememberInput {
431 now,
432 text,
433 entity,
434 tags: &tags.to_vec(),
435 links: &links.to_vec(),
436 vector: (!vector.is_empty()).then_some(vector.as_slice()),
437 valid_from: Some(valid_from),
438 metadata: (!metadata.is_empty()).then_some(metadata.as_slice()),
439 },
440 revises,
441 )?;
442 if let Some(target) = revises.some() {
443 self.close_target(target, valid_from);
444 }
445 report.replayed += 1;
446 }
447 Op::Forget { fact, .. } => {
448 match self.apply_forget(fact) {
451 Ok(_) => report.replayed += 1,
452 Err(Error::NotFound(_)) => {
453 return Err(Error::Corrupt("journal forgets an unknown fact"));
454 }
455 Err(e) => return Err(e),
456 }
457 }
458 Op::Link {
459 now,
460 src,
461 rel,
462 dst,
463 provenance,
464 } => {
465 self.apply_link(now, src, rel, dst, provenance)?;
466 report.replayed += 1;
467 }
468 Op::Maintain { .. } => {
469 self.replay_maintain()?;
473 report.replayed += 1;
474 }
475 }
476 }
477 Ok(report)
478 }
479
480 pub fn remember<S: Storage>(
482 &mut self,
483 store: &mut S,
484 input: RememberInput<'_>,
485 ) -> Result<RememberOutcome, Error> {
486 self.validate_input(&input)?;
487 let mut outcome = self.apply_remember(&input, FactId::NONE)?;
488 self.find_similar(&mut outcome);
489 self.journal_remember(store, &input, FactId::NONE, outcome.id)?;
490 Ok(outcome)
491 }
492
493 pub fn remember_batch<S: Storage>(
501 &mut self,
502 store: &mut S,
503 inputs: &[RememberInput<'_>],
504 skip_similar: bool,
505 ) -> Result<Vec<RememberOutcome>, Error> {
506 let mut out = Vec::with_capacity(inputs.len());
507 for input in inputs {
508 self.validate_input(input)?;
509 let mut outcome = self.apply_remember(input, FactId::NONE)?;
510 if !skip_similar {
511 self.find_similar(&mut outcome);
512 }
513 self.journal_remember(store, input, FactId::NONE, outcome.id)?;
514 out.push(outcome);
515 }
516 Ok(out)
517 }
518
519 fn find_similar(&mut self, outcome: &mut RememberOutcome) {
526 let Some(entity) = outcome.entity else { return };
527 let new_terms: Vec<u32> = self.tf_scratch.iter().map(|&(t, _)| t).collect();
530 let new_vec = self
532 .fact(outcome.id)
533 .filter(|r| r.has_vector())
534 .map(|r| r.vector);
535 if new_terms.is_empty() && new_vec.is_none() {
536 return;
537 }
538 let mut ring = [FactId::NONE; SIMILAR_CANDIDATE_CAP];
540 let mut n = 0usize;
541 for (fact, _) in self.entity_facts.entries(entity.0) {
542 if fact != outcome.id {
543 ring[n % SIMILAR_CANDIDATE_CAP] = fact;
544 n += 1;
545 }
546 }
547 let mut cand_terms: Vec<u32> = Vec::new();
548 for &fact in ring.iter().take(n.min(SIMILAR_CANDIDATE_CAP)) {
549 let Some(record) = self.fact(fact) else {
550 continue;
551 };
552 if record.is_tombstone() || record.is_closed() {
553 continue;
554 }
555
556 let mut lexical = None;
558 if !new_terms.is_empty()
561 && let Ok(text) = core::str::from_utf8(self.texts.get(record.text))
562 {
563 cand_terms.clear();
564 let terms = &self.terms;
565 let cand = &mut cand_terms;
566 self.tokenizer.tokenize(text, &mut |token| {
567 if let Some(term) = terms.lookup(token)
568 && !cand.contains(&term.0)
569 {
570 cand.push(term.0);
571 }
572 });
573 if !cand_terms.is_empty() {
574 let both = cand_terms.iter().filter(|t| new_terms.contains(t)).count();
575 let union = cand_terms.len() + new_terms.len() - both;
576 let jaccard = both as f32 / union as f32;
577 if jaccard > self.cfg.similar_jaccard {
578 lexical = Some(jaccard);
579 }
580 }
581 }
582
583 let mut vector = None;
585 if let (Some(a), true) = (new_vec, record.has_vector()) {
586 let cos = self.vecs.cosine_slots(a, record.vector);
587 if cos > self.cfg.similar_cos {
588 vector = Some(cos);
589 }
590 }
591
592 let best = match (lexical, vector) {
594 (Some(l), Some(v)) if v > l => Some((v, SimilarReason::VectorCosine)),
595 (Some(l), _) => Some((l, SimilarReason::LexicalOverlap)),
596 (None, Some(v)) => Some((v, SimilarReason::VectorCosine)),
597 (None, None) => None,
598 };
599 if let Some((score, reason)) = best {
600 outcome.similar.push(Similar {
601 id: fact,
602 score,
603 reason,
604 });
605 }
606 }
607 outcome
608 .similar
609 .sort_unstable_by(|a, b| b.score.total_cmp(&a.score).then(a.id.cmp(&b.id)));
610 outcome.similar.truncate(8);
611 }
612
613 pub fn revise<S: Storage>(
617 &mut self,
618 store: &mut S,
619 target: FactId,
620 input: RememberInput<'_>,
621 ) -> Result<RememberOutcome, Error> {
622 self.validate_input(&input)?;
623 self.check_revisable(target)?;
627 let outcome = self.apply_remember(&input, target)?;
628 let valid_from = input.valid_from.unwrap_or(input.now);
629 self.close_target(target, valid_from);
630 self.journal_remember(store, &input, target, outcome.id)?;
631 Ok(outcome)
632 }
633
634 pub fn forget<S: Storage>(
638 &mut self,
639 store: &mut S,
640 now: u64,
641 id: FactId,
642 ) -> Result<bool, Error> {
643 let fresh = self.apply_forget(id)?;
644 let mut entry = Vec::new();
645 Op::Forget { now, fact: id }.encode(&mut entry);
646 store
647 .append_journal(&entry)
648 .map_err(|e| Error::Storage(format!("{e:?}")))?;
649 Ok(fresh)
650 }
651
652 pub fn link<S: Storage>(&mut self, store: &mut S, input: LinkInput<'_>) -> Result<(), Error> {
655 self.apply_link(
656 input.now,
657 input.src,
658 input.rel,
659 input.dst,
660 FactId::from_opt(input.provenance),
661 )?;
662 let mut entry = Vec::new();
663 Op::Link {
664 now: input.now,
665 src: input.src,
666 rel: input.rel,
667 dst: input.dst,
668 provenance: FactId::from_opt(input.provenance),
669 }
670 .encode(&mut entry);
671 store
672 .append_journal(&entry)
673 .map_err(|e| Error::Storage(format!("{e:?}")))?;
674 Ok(())
675 }
676
677 pub fn get(&self, id: FactId) -> Option<FactView<'_>> {
680 let record = self.fact(id)?;
681 if record.is_tombstone() {
682 return None;
683 }
684 let text = core::str::from_utf8(self.texts.get(record.text)).ok()?;
688 Some(FactView { record, text })
689 }
690
691 pub fn tags_of(&self, id: FactId, out: &mut Vec<TermId>) {
694 let Some(record) = self.fact(id) else { return };
695 if record.is_tombstone() {
696 return;
697 }
698 let Some(aux) = self.fact_aux.get(&id.0.to_be_bytes()) else {
699 return;
700 };
701 for chunk in self.tag_lists.iter(&aux.tags) {
702 for raw in chunk.chunks_exact(4) {
703 out.push(TermId(u32::from_be_bytes(raw.try_into().unwrap())));
704 }
705 }
706 }
707
708 pub fn metadata_of<'s>(&'s self, id: FactId, out: &mut Vec<(&'s str, &'s str)>) -> bool {
719 out.clear();
720 let Some(record) = self.fact(id) else {
721 return false;
722 };
723 if record.is_tombstone() {
724 return false;
725 }
726 let Some(aux) = self.fact_aux.get(&id.0.to_be_bytes()) else {
727 return false;
728 };
729 if aux.meta.0 == NONE_U32 || aux.meta.0 >= self.metas.len() as u32 {
730 return false;
731 }
732 crate::metadata::decode(self.metas.get(aux.meta), out).is_ok() && !out.is_empty()
733 }
734
735 pub fn entity(&mut self, name: &str) -> Option<EntityId> {
737 let mut norm = core::mem::take(&mut self.name_scratch);
738 normalize_name(&mut self.tokenizer, name, &mut norm);
739 let found = if norm.is_empty() {
740 None
741 } else {
742 self.lookup_entity_by_norm(&norm)
744 };
745 self.name_scratch = norm;
746 found
747 }
748
749 pub fn term(&self, id: TermId) -> &str {
751 self.terms.resolve(id)
752 }
753
754 pub fn entity_name(&self, id: EntityId) -> Option<&str> {
759 let record = self.entities.get(&id.0.to_be_bytes())?;
760 core::str::from_utf8(self.texts.get(record.name)).ok()
763 }
764
765 pub fn facts_len(&self) -> usize {
769 self.facts.len()
770 }
771
772 pub fn entities_len(&self) -> usize {
774 self.entities.len()
775 }
776
777 pub fn cfg(&self) -> &Config {
779 &self.cfg
780 }
781
782 pub fn stats(&self) -> Stats {
784 Stats {
785 facts: self.facts.len(),
786 entities: self.entities.len(),
787 terms: self.terms.len(),
788 edges: self.edges_out.len(),
789 vectors: self.vecs.len(),
790 next_fact: self.next_fact,
791 next_entity: self.next_entity,
792 db_uuid: self.cfg.db_uuid,
793 pool_bytes: self.facts.pool_bytes()
794 + self.fact_aux.pool_bytes()
795 + self.entities.pool_bytes()
796 + self.by_name.pool_bytes()
797 + self.edges_out.pool_bytes()
798 + self.edges_in.pool_bytes()
799 + self.temporal.pool_bytes()
800 + self.texts.pool_bytes()
801 + self.terms.pool_bytes()
802 + self.tag_lists.pool_bytes()
803 + self.bm25.pool_bytes()
804 + self.tags_idx.pool_bytes()
805 + self.entity_facts.pool_bytes()
806 + self.vecs.pool_bytes()
807 + self.hnsw.pool_bytes(),
808 }
809 }
810
811 fn fact(&self, id: FactId) -> Option<FactRecord> {
814 self.facts.get(&id.0.to_be_bytes())
815 }
816
817 fn validate_input(&self, input: &RememberInput<'_>) -> Result<(), Error> {
819 if input.text.len() > self.cfg.max_text {
820 return Err(Error::TooLarge {
821 what: "text",
822 len: input.text.len(),
823 max: self.cfg.max_text,
824 });
825 }
826 if input.tags.len() > 32 {
827 return Err(Error::TooLarge {
828 what: "tags",
829 len: input.tags.len(),
830 max: 32,
831 });
832 }
833 if input.links.len() > 16 {
834 return Err(Error::TooLarge {
835 what: "links",
836 len: input.links.len(),
837 max: 16,
838 });
839 }
840 if input.tags.iter().any(|t| t.is_empty()) {
841 return Err(Error::Invalid("empty tag"));
842 }
843 if !input.links.is_empty() && input.entity.is_none() {
844 return Err(Error::Invalid("links require a subject entity"));
845 }
846 if let Some(v) = input.vector {
847 if self.cfg.dim == 0 {
848 return Err(Error::Invalid("vector given but dim is 0"));
849 }
850 if v.len() != self.cfg.dim {
851 return Err(Error::DimMismatch {
852 got: v.len(),
853 want: self.cfg.dim,
854 });
855 }
856 }
857 Ok(())
858 }
859
860 fn check_revisable(&self, target: FactId) -> Result<(), Error> {
862 let record = self.fact(target).ok_or(Error::NotFound(target))?;
863 if record.is_tombstone() {
864 return Err(Error::NotFound(target));
865 }
866 if record.is_closed() {
867 return Err(Error::AlreadyClosed(target));
868 }
869 Ok(())
870 }
871
872 fn close_target(&mut self, target: FactId, valid_to: u64) {
875 let record = self.fact(target).expect("checked revisable");
876 let payload = self
877 .facts
878 .payload_mut(&target.0.to_be_bytes())
879 .expect("record fetched above");
880 let flags = record.flags | fact_flags::CLOSED;
883 payload[4..6].copy_from_slice(&flags.to_be_bytes());
884 payload[36..44].copy_from_slice(&valid_to.to_be_bytes());
885 }
886
887 fn apply_remember(
890 &mut self,
891 input: &RememberInput<'_>,
892 revises: FactId,
893 ) -> Result<RememberOutcome, Error> {
894 let id = FactId(self.next_fact);
895 let entity = match input.entity {
896 Some(name) => Some(self.resolve_or_create_entity(name, input.now)?),
897 None => None,
898 };
899 let text_id = self.texts.push(input.text.as_bytes())?;
900
901 let mut tfs = core::mem::take(&mut self.tf_scratch);
903 tfs.clear();
904 let terms = &mut self.terms;
905 let mut intern_err = None;
906 self.tokenizer.tokenize(input.text, &mut |token| {
907 if intern_err.is_some() {
908 return;
909 }
910 match terms.intern(token) {
911 Ok(term) => match tfs.iter_mut().find(|(t, _)| *t == term.0) {
912 Some((_, tf)) => *tf = tf.saturating_add(1),
913 None => tfs.push((term.0, 1)),
914 },
915 Err(e) => intern_err = Some(e),
916 }
917 });
918 if let Some(e) = intern_err {
919 self.tf_scratch = tfs;
920 return Err(Error::Arena(e));
921 }
922 self.bm25.index_doc(id, &tfs)?;
923 self.tf_scratch = tfs;
924
925 let meta = match input.metadata {
929 Some(pairs) if !pairs.is_empty() => {
930 self.metas.push(&crate::metadata::encode(pairs)?)?
931 }
932 _ => BlobId(NONE_U32),
933 };
934
935 let mut aux = FactAux {
938 id,
939 tags: ListHandle::EMPTY,
940 meta,
941 };
942 let mut seen_tags: [u32; 32] = [NONE_U32; 32];
943 let mut seen_cnt = 0usize;
944 for tag in input.tags {
945 let term = self.terms.intern(tag)?;
946 if seen_tags[..seen_cnt].contains(&term.0) {
947 continue;
948 }
949 seen_tags[seen_cnt] = term.0;
950 seen_cnt += 1;
951 self.tag_lists.push(&mut aux.tags, &term.0.to_be_bytes())?;
952 self.tags_idx.push(term.0, id, 0)?;
953 }
954 self.fact_aux.insert(&aux)?;
955
956 if let Some(src) = entity {
958 for &(rel, dst_name) in input.links {
959 let dst = self.resolve_or_create_entity(dst_name, input.now)?;
960 let rel = self.terms.intern(rel)?;
961 self.upsert_edge(src, rel, dst, id)?;
962 }
963 self.entity_facts.push(src.0, id, 0)?;
964 }
965
966 let (vector, flags) = match input.vector {
970 Some(v) => (self.vecs.push(id, v)?, fact_flags::HAS_VECTOR),
971 None => (NONE_U32, 0),
972 };
973
974 let recorded_at = input.now;
975 let valid_from = input.valid_from.unwrap_or(input.now);
976 self.facts.insert(&FactRecord {
977 id,
978 entity: EntityId::from_opt(entity),
979 flags,
980 kind: 0,
981 text: text_id,
982 vector,
983 revises,
984 recorded_at,
985 valid_from,
986 valid_to: VALID_TO_OPEN,
987 })?;
988 self.temporal.insert(&TemporalSlot {
989 recorded_at,
990 fact: id,
991 })?;
992 self.next_fact += 1;
993 Ok(RememberOutcome {
994 id,
995 entity,
996 similar: Vec::new(),
997 })
998 }
999
1000 fn apply_forget(&mut self, id: FactId) -> Result<bool, Error> {
1001 let record = self.fact(id).ok_or(Error::NotFound(id))?;
1002 if record.is_tombstone() {
1003 return Ok(false);
1004 }
1005 let payload = self
1006 .facts
1007 .payload_mut(&id.0.to_be_bytes())
1008 .expect("record fetched above");
1009 let flags = record.flags | fact_flags::TOMBSTONE;
1010 payload[4..6].copy_from_slice(&flags.to_be_bytes());
1011 Ok(true)
1012 }
1013
1014 fn apply_link(
1015 &mut self,
1016 now: u64,
1017 src: &str,
1018 rel: &str,
1019 dst: &str,
1020 provenance: FactId,
1021 ) -> Result<(), Error> {
1022 let src = self.resolve_or_create_entity(src, now)?;
1023 let dst = self.resolve_or_create_entity(dst, now)?;
1024 let rel = self.terms.intern(rel)?;
1025 self.upsert_edge(src, rel, dst, provenance)
1026 }
1027
1028 fn upsert_edge(
1029 &mut self,
1030 src: EntityId,
1031 rel: TermId,
1032 dst: EntityId,
1033 fact: FactId,
1034 ) -> Result<(), Error> {
1035 for (arena, a, b) in [
1036 (&mut self.edges_out, src, dst),
1037 (&mut self.edges_in, dst, src),
1038 ] {
1039 let slot = EdgeSlot { a, rel, b, fact };
1040 if !arena.insert(&slot)? {
1041 let mut kb = [0u8; 12];
1042 key::write_u32(&mut kb, a.0);
1043 key::write_u32(&mut kb[4..], rel.0);
1044 key::write_u32(&mut kb[8..], b.0);
1045 let payload = arena.payload_mut(&kb).expect("insert reported a duplicate");
1046 payload.copy_from_slice(&fact.0.to_be_bytes());
1047 }
1048 }
1049 Ok(())
1050 }
1051
1052 fn lookup_entity_by_norm(&self, norm: &str) -> Option<EntityId> {
1055 let term = self.terms.lookup(norm)?;
1056 let mut from = [0u8; 8];
1057 key::write_u32(&mut from, term.0);
1058 let mut to = [0u8; 8];
1059 key::write_u32(&mut to, term.0);
1060 to[4..].copy_from_slice(&u32::MAX.to_be_bytes());
1061 self.by_name.range(&from, &to).next().map(|e| e.id)
1062 }
1063
1064 fn resolve_or_create_entity(&mut self, name: &str, now: u64) -> Result<EntityId, Error> {
1065 let mut norm = core::mem::take(&mut self.name_scratch);
1066 normalize_name(&mut self.tokenizer, name, &mut norm);
1067 if norm.is_empty() {
1068 self.name_scratch = norm;
1069 return Err(Error::Invalid("entity name has no indexable characters"));
1070 }
1071 let result = (|| {
1072 if let Some(found) = self.lookup_entity_by_norm(&norm) {
1073 return Ok(found);
1074 }
1075 let term = self.terms.intern(&norm)?;
1076 let id = EntityId(self.next_entity);
1077 let name_id = self.texts.push(name.as_bytes())?;
1078 self.entities.insert(&EntityRecord {
1079 id,
1080 name: name_id,
1081 name_term: term,
1082 created_at: now,
1083 flags: 0,
1084 })?;
1085 self.by_name.insert(&EntityByName {
1086 name_term: term,
1087 id,
1088 })?;
1089 self.next_entity += 1;
1090 Ok(id)
1091 })();
1092 self.name_scratch = norm;
1093 result
1094 }
1095
1096 fn journal_remember<S: Storage>(
1097 &mut self,
1098 store: &mut S,
1099 input: &RememberInput<'_>,
1100 revises: FactId,
1101 assigned: FactId,
1102 ) -> Result<(), Error> {
1103 let mut entry = Vec::new();
1104 Op::Remember {
1105 now: input.now,
1106 valid_from: input.valid_from.unwrap_or(input.now),
1107 entity: input.entity,
1108 text: input.text,
1109 tags: input.tags.to_vec(),
1110 links: input.links.to_vec(),
1111 vector: input.vector.map(<[f32]>::to_vec).unwrap_or_default(),
1112 metadata: input.metadata.map(<[_]>::to_vec).unwrap_or_default(),
1113 revises,
1114 assigned,
1115 }
1116 .encode(&mut entry);
1117 store
1118 .append_journal(&entry)
1119 .map_err(|e| Error::Storage(format!("{e:?}")))
1120 }
1121}
1122
1123impl core::fmt::Debug for Memory<'_> {
1124 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1127 f.debug_struct("Memory")
1128 .field("facts", &self.facts.len())
1129 .field("entities", &self.entities.len())
1130 .field("terms", &self.terms.len())
1131 .finish()
1132 }
1133}
1134
1135fn normalize_name(tokenizer: &mut Tokenizer, name: &str, out: &mut String) {
1139 out.clear();
1140 tokenizer.tokenize(name, &mut |token| {
1141 if !out.is_empty() {
1142 out.push(' ');
1143 }
1144 out.push_str(token);
1145 });
1146}