1use anyhow::Result;
7use serde_json::{Map, Value};
8use std::collections::{BTreeSet, HashMap, HashSet};
9
10use super::stamp::{stable_hash, Placement, Stamp, KEY};
11use super::types::*;
12use super::{FactView, IdConflict, Memory, MemoryError};
13use crate::core::documents::{chunk_document, DocumentInput};
14
15const MAX_BATCH: usize = 4096;
16const WRITE_ATTEMPTS: usize = 16;
17const MAX_TEXT_BYTES: usize = 64 * 1024;
18const CHUNK_WORDS: u32 = 4096;
19const MAX_ID_BYTES: usize = 4096;
20const MAX_LABEL_BYTES: usize = 1024;
21const MAX_LABELS: usize = 256;
22const MAX_LINKS: usize = 256;
23
24struct Planned {
26 id: String,
27 input: RememberInput,
28 stamp: Stamp,
29 idempotent: bool,
31 replaces: bool,
33 previous: Option<Stamp>,
35 document: Option<DocumentInput>,
36}
37
38fn apply_placement(stamp: &Stamp, metadata: &mut Value) -> Option<String> {
41 if let Some(map) = metadata.as_object_mut() {
42 map.remove(KEY);
43 }
44 match stamp.placement() {
45 Placement::Version(version) => Some(version),
46 Placement::Metadata(value) => {
47 metadata[KEY] = value;
48 None
49 }
50 }
51}
52
53fn valid_id(id: &str) -> bool {
54 !id.is_empty()
55 && id.len() <= MAX_ID_BYTES
56 && !id.chars().any(char::is_control)
57 && !id.starts_with("hms:")
58}
59
60fn label(value: &str, what: &str) -> Result<String> {
61 let value = value.trim();
62 check!(
63 !value.is_empty() && value.len() <= MAX_LABEL_BYTES && !value.contains('\0'),
64 "{what} must contain 1..={MAX_LABEL_BYTES} bytes"
65 );
66 Ok(value.to_owned())
67}
68
69fn push_unique(list: &mut Vec<String>, value: String) {
70 if !list.contains(&value) {
71 list.push(value);
72 }
73}
74
75fn time_part(value: Option<Millis>) -> Vec<u8> {
76 value.map_or_else(|| b"-".to_vec(), |v| v.to_string().into_bytes())
77}
78
79impl Memory {
80 pub fn remember(&self, input: RememberInput) -> Result<Remembered> {
82 let mut outcomes = self.remember_many(vec![input])?;
83 Ok(outcomes.remove(0))
84 }
85
86 pub fn remember_many(&self, inputs: Vec<RememberInput>) -> Result<Vec<Remembered>> {
91 check!(
92 !inputs.is_empty() && inputs.len() <= MAX_BATCH,
93 "remember requires 1..={MAX_BATCH} facts"
94 );
95 let now = self.clock.now_millis();
96 for _ in 0..WRITE_ATTEMPTS {
97 let Some(before) = self.planning_revision() else {
98 continue;
99 };
100 match self.remember_at_revision(inputs.clone(), now, before) {
101 Ok(Some(outcomes)) => return Ok(outcomes),
102 Ok(None) => {}
103 Err(error) => return Err(error),
104 }
105 }
106 Err(MemoryError::Concurrent(
107 "memory changed while planning remember; retry the operation".into(),
108 )
109 .into())
110 }
111
112 pub(super) fn remember_at_revision(
113 &self,
114 inputs: Vec<RememberInput>,
115 now: Millis,
116 before: u64,
117 ) -> Result<Option<Vec<Remembered>>> {
118 check!(
119 !inputs.is_empty() && inputs.len() <= MAX_BATCH,
120 "remember requires 1..={MAX_BATCH} facts"
121 );
122 let planning = (|| -> Result<Option<Vec<Planned>>> {
123 if self.index.read().revision != Some(before) {
124 return Ok(None);
125 }
126 let mut planned: Vec<Planned> = Vec::with_capacity(inputs.len());
127 let mut seen = HashSet::with_capacity(inputs.len());
128 for input in inputs {
129 let fact = self.plan_one(&planned, input, now)?;
130 check!(
131 seen.insert(fact.id.clone()),
132 "fact id {:?} appears twice in one call",
133 fact.id
134 );
135 planned.push(fact);
136 }
137 Ok(Some(planned))
138 })();
139 let mut planned = match planning {
140 Ok(Some(planned)) => planned,
141 Ok(None) => return Ok(None),
142 Err(_) if self.core.revision() != before => return Ok(None),
143 Err(error) => return Err(error),
144 };
145 let documents: Vec<DocumentInput> = planned
146 .iter_mut()
147 .filter_map(|fact| fact.document.take())
148 .collect();
149 let changed = !documents.is_empty();
150 if !self.core.change_memory_documents(documents, &[], before)? {
151 return Ok(None);
152 }
153 let written: Vec<&Planned> = planned.iter().filter(|fact| !fact.idempotent).collect();
154 if !written.is_empty() {
155 self.settle(before, u64::from(changed), |index| {
156 let linked = written.iter().any(|fact| {
160 fact.replaces
161 && (!index.edges(&fact.id).is_empty()
162 || !index.direct_predecessors(&fact.id).is_empty())
163 });
164 if linked {
165 return false;
166 }
167 for fact in &written {
168 if let Some(old) = &fact.previous {
169 index.remove(&fact.id, old);
170 }
171 }
172 let batch: HashMap<&str, (Millis, Millis)> = written
173 .iter()
174 .map(|f| (f.id.as_str(), (f.stamp.effective_from(), f.stamp.ingest)))
175 .collect();
176 for fact in &written {
177 let lookup = |id: &str| {
178 batch.get(id).copied().or_else(|| {
179 fact.stamp
180 .subject
181 .as_deref()
182 .and_then(|subject| index.member(subject, id))
183 })
184 };
185 let known: HashMap<String, (Millis, Millis)> = fact
187 .stamp
188 .superseded_by
189 .iter()
190 .filter_map(|id| lookup(id).map(|times| (id.clone(), times)))
191 .collect();
192 index.insert(&fact.id, &fact.stamp, &|id| known.get(id).copied());
193 }
194 true
195 });
196 }
197 let outcomes: Vec<Remembered> = planned
198 .iter()
199 .map(|fact| Remembered {
200 id: fact.id.clone(),
201 created: !fact.idempotent,
202 supersedes: fact.stamp.supersedes.clone(),
203 superseded_by: fact.stamp.superseded_by.clone(),
204 ingest_time: fact.stamp.ingest,
205 })
206 .collect();
207 if let Some(observer) = &self.observer {
208 for (fact, outcome) in planned.iter().zip(&outcomes) {
209 if outcome.created {
210 observer.on_remembered(outcome, &fact.input);
211 }
212 }
213 }
214 Ok(Some(outcomes))
215 }
216
217 fn plan_one(
218 &self,
219 earlier: &[Planned],
220 mut input: RememberInput,
221 now: Millis,
222 ) -> Result<Planned> {
223 check!(
224 !input.text.trim().is_empty() && input.text.len() <= MAX_TEXT_BYTES,
225 "text must contain 1..={MAX_TEXT_BYTES} bytes"
226 );
227 if let (Some(from), Some(to)) = (input.valid_from, input.valid_to) {
228 check!(from < to, "validFrom must be before validTo");
229 }
230 let user_metadata = match input.metadata.take() {
231 None | Some(Value::Null) => Map::new(),
232 Some(Value::Object(map)) => map,
233 Some(_) => invalid!("metadata must be a JSON object"),
234 };
235 check!(
236 !user_metadata.contains_key(KEY),
237 "metadata key {KEY:?} is reserved"
238 );
239 let subject = input
240 .subject
241 .as_deref()
242 .map(|s| label(s, "subject"))
243 .transpose()?;
244 let mut entities = Vec::new();
245 for entity in input.entities.iter().flatten() {
246 push_unique(&mut entities, label(entity, "entity")?);
247 }
248 if let Some(extractor) = &self.entities {
249 for entity in extractor.entities(&input.text) {
250 if let Ok(entity) = label(&entity, "entity") {
251 push_unique(&mut entities, entity);
252 }
253 }
254 }
255 check!(
256 entities.len() <= MAX_LABELS,
257 "at most {MAX_LABELS} entities"
258 );
259 let mut explicit = Vec::new();
260 for id in input.supersedes.iter().flatten() {
261 check!(valid_id(id), "supersedes holds an invalid fact id");
262 push_unique(&mut explicit, id.clone());
263 }
264 explicit.sort_unstable();
265 check!(
266 explicit.len() <= MAX_LINKS,
267 "at most {MAX_LINKS} supersedes"
268 );
269 let metadata_bytes = serde_json::to_vec(&user_metadata)?;
270 let embedding_bytes: Vec<u8> = input
271 .embedding
272 .iter()
273 .flatten()
274 .flat_map(|value| value.to_bits().to_le_bytes())
275 .collect();
276 let hash = stable_hash(&[
277 input.text.as_bytes(),
278 &metadata_bytes,
279 &time_part(input.event_time),
280 &time_part(input.valid_from),
281 &time_part(input.valid_to),
282 subject.as_deref().unwrap_or("").as_bytes(),
283 explicit.join("\u{1}").as_bytes(),
284 entities.join("\u{1}").as_bytes(),
285 input.source_uri.as_deref().unwrap_or("").as_bytes(),
286 &embedding_bytes,
287 ]);
288 let id = match input.id.as_deref() {
289 Some(id) => {
290 check!(
291 valid_id(id),
292 "id must be 1..={MAX_ID_BYTES} bytes, without control characters or the \"hms:\" prefix"
293 );
294 id.to_owned()
295 }
296 None => format!("mem:{hash}"),
297 };
298 check!(!explicit.contains(&id), "a fact cannot supersede itself");
299
300 let mut replaces = false;
301 let mut previous = None;
302 let mut idempotent_stamp = None;
303 if let Some((version, metadata, _)) = self.core.document_snapshot(&id) {
304 match Stamp::from_parts(&version, &metadata) {
305 Some(stamp) if stamp.hash.as_deref() == Some(hash.as_str()) => {
306 idempotent_stamp = Some(stamp);
307 }
308 existing => {
309 if self.config.on_id_conflict != IdConflict::Replace {
310 return Err(MemoryError::Conflict(format!(
311 "fact {id:?} already exists with different content; use another id \
312 or set onIdConflict to \"replace\""
313 ))
314 .into());
315 }
316 check!(
317 existing.is_some() || self.index.read().edges(&id).is_empty(),
318 "document {id:?} is not a memory fact and is superseded by others"
319 );
320 replaces = true;
321 previous = existing;
322 }
323 }
324 }
325 if let Some(stamp) = idempotent_stamp {
326 return Ok(Planned {
327 id,
328 input,
329 stamp,
330 idempotent: true,
331 replaces: false,
332 previous: None,
333 document: None,
334 });
335 }
336
337 for old in &explicit {
338 check!(
339 self.core.has_document(old) || earlier.iter().any(|fact| &fact.id == old),
340 "cannot supersede unknown fact {old:?}"
341 );
342 }
343 let mut supersedes = explicit;
344 let mut superseded_by: Vec<String> = Vec::new();
345 if self.supersession_active {
346 if let Some(subject) = &subject {
347 let mut group: Vec<FactView> = self
348 .index
349 .read()
350 .members(subject)
351 .into_iter()
352 .filter(|member| member.id != id)
353 .map(|member| FactView {
354 id: member.id,
355 subject: Some(subject.clone()),
356 effective_from: member.effective,
357 ingest_time: member.ingest,
358 })
359 .collect();
360 for fact in earlier
361 .iter()
362 .filter(|f| f.stamp.subject.as_ref() == Some(subject))
363 {
364 group.retain(|member| member.id != fact.id);
365 group.push(FactView {
366 id: fact.id.clone(),
367 subject: Some(subject.clone()),
368 effective_from: fact.stamp.effective_from(),
369 ingest_time: fact.stamp.ingest,
370 });
371 }
372 group.sort_by(|a, b| {
373 (a.effective_from, a.ingest_time, &a.id).cmp(&(
374 b.effective_from,
375 b.ingest_time,
376 &b.id,
377 ))
378 });
379 let incoming = FactView {
380 id: id.clone(),
381 subject: Some(subject.clone()),
382 effective_from: input.valid_from.or(input.event_time).unwrap_or(now),
383 ingest_time: now,
384 };
385 let resolution = self.supersession.resolve(&incoming, &group);
386 for linked in resolution
387 .supersedes
388 .iter()
389 .chain(&resolution.superseded_by)
390 {
391 check!(
392 group.iter().any(|member| &member.id == linked),
393 "supersession policy returned {linked:?}, which is not a stored fact of subject {subject:?}"
394 );
395 }
396 for old in resolution.supersedes {
397 push_unique(&mut supersedes, old);
398 }
399 for newer in resolution.superseded_by {
400 push_unique(&mut superseded_by, newer);
401 }
402 supersedes.sort_unstable();
403 superseded_by.sort_unstable();
404 }
405 }
406 for old in &supersedes {
407 check!(
408 !self.index.read().supersedes_transitively(old, &id),
409 "superseding {old:?} would make a cycle"
410 );
411 }
412 for newer in &superseded_by {
413 check!(
414 !self.index.read().supersedes_transitively(&id, newer),
415 "being superseded by {newer:?} would make a cycle"
416 );
417 }
418
419 let stamp = Stamp {
420 ingest: now,
421 event: input.event_time,
422 valid_from: input.valid_from,
423 valid_to: input.valid_to,
424 subject,
425 supersedes,
426 superseded_by,
427 entities,
428 hash: Some(hash),
429 };
430 let mut metadata = Value::Object(user_metadata);
431 let version = apply_placement(&stamp, &mut metadata);
432 let document = DocumentInput {
433 id: id.clone(),
434 text: input.text.clone(),
435 source_uri: input.source_uri.clone(),
436 version,
437 metadata: Some(metadata),
438 chunk_words: Some(CHUNK_WORDS),
439 overlap_words: Some(0),
440 store_text: Some(true),
441 embeddings: input.embedding.clone().map(|embedding| vec![embedding]),
442 title_weight: None,
443 };
444 let chunks = chunk_document(&document)
445 .map_err(|e| MemoryError::Invalid(format!("invalid fact: {e}")))?;
446 check!(
447 chunks.len() == 1,
448 "text must fit one chunk of at most {CHUNK_WORDS} words; remember it as several facts"
449 );
450 Ok(Planned {
451 id,
452 input,
453 stamp,
454 idempotent: false,
455 replaces,
456 previous,
457 document: Some(document),
458 })
459 }
460
461 pub fn forget(&self, scope: &ForgetScope, options: &ForgetOptions) -> Result<ForgetReport> {
470 let mut completed = BTreeSet::new();
471 for _ in 0..WRITE_ATTEMPTS {
472 let Some(before) = self.planning_revision() else {
473 continue;
474 };
475 match self.forget_at_revision(scope, options, before, &mut completed) {
476 Ok(Some(report)) => return Ok(report),
477 Ok(None) => {}
478 Err(error) => return Err(error),
479 }
480 }
481 Err(MemoryError::Concurrent(
482 "memory changed during forgetting; retry to finish any committed prefix".into(),
483 )
484 .into())
485 }
486
487 pub(super) fn forget_at_revision(
488 &self,
489 scope: &ForgetScope,
490 options: &ForgetOptions,
491 before: u64,
492 completed: &mut BTreeSet<String>,
493 ) -> Result<Option<ForgetReport>> {
494 let mut ids = self.resolve_scope(scope)?;
495 if options.include_chain {
496 let index = self.index.read();
497 let mut chained = BTreeSet::new();
498 for id in &ids {
499 chained.extend(index.component(id));
500 }
501 ids = chained;
502 }
503 let mut stamps: Vec<(String, Stamp)> = Vec::new();
504 for id in &ids {
505 if let Some((version, metadata, _)) = self.core.document_snapshot(id) {
506 stamps.push((
507 id.clone(),
508 Stamp::from_parts(&version, &metadata).unwrap_or_default(),
509 ));
510 }
511 }
512 let forgotten: Vec<String> = stamps.iter().map(|(id, _)| id.clone()).collect();
513 if options.dry_run || forgotten.is_empty() {
514 if !self.core.memory_revision_matches(before) {
515 return Ok(None);
516 }
517 let forgotten = if options.dry_run {
518 forgotten
519 } else {
520 completed.iter().cloned().collect()
521 };
522 let purged = if !options.dry_run && !completed.is_empty() {
523 crate::core::error::after_commit(
524 "forgotten-text synchronization",
525 self.core.sync_text_segment(),
526 )?;
527 if options.purge {
528 crate::core::error::after_commit(
529 "forgotten-data purge",
530 self.core.compact_scrubbing(),
531 )?;
532 }
533 if let Some(observer) = &self.observer {
534 observer.on_forgotten(&forgotten);
535 }
536 options.purge
537 } else {
538 false
539 };
540 return Ok(Some(ForgetReport {
541 forgotten,
542 dry_run: options.dry_run,
543 purged,
544 }));
545 }
546 let rewrites = self.bridge_survivors(&forgotten)?;
547 let mut revision = before;
548 let mut commits = 0;
549 for batch in rewrites.chunks(MAX_BATCH) {
552 if !self
553 .core
554 .change_memory_documents(batch.to_vec(), &[], revision)?
555 {
556 return Ok(None);
557 }
558 revision = revision.wrapping_add(1);
559 commits += 1;
560 }
561 for batch in forgotten.chunks(MAX_BATCH) {
562 if !self
563 .core
564 .change_memory_documents(Vec::new(), batch, revision)?
565 {
566 return Ok(None);
567 }
568 completed.extend(batch.iter().cloned());
569 revision = revision.wrapping_add(1);
570 commits += 1;
571 }
572 crate::core::error::after_commit(
573 "forgotten-text synchronization",
574 self.core.sync_text_segment(),
575 )?;
576 if rewrites.is_empty() {
577 self.settle(before, commits, |index| {
578 for (id, stamp) in &stamps {
579 index.remove(id, stamp);
580 }
581 true
582 });
583 } else {
584 self.index.write().revision = None;
585 }
586 let mut purged = false;
587 if options.purge {
588 crate::core::error::after_commit(
589 "forgotten-data purge",
590 self.core.compact_scrubbing(),
591 )?;
592 self.index.write().revision = None;
593 purged = true;
594 }
595 let forgotten: Vec<String> = completed.iter().cloned().collect();
596 if let Some(observer) = &self.observer {
597 observer.on_forgotten(&forgotten);
598 }
599 Ok(Some(ForgetReport {
600 forgotten,
601 dry_run: false,
602 purged,
603 }))
604 }
605
606 fn bridge_survivors(&self, forgotten: &[String]) -> Result<Vec<DocumentInput>> {
610 let dead: BTreeSet<String> = forgotten.iter().cloned().collect();
611 let index = self.index.read();
612 let mut affected: BTreeSet<String> = BTreeSet::new();
613 for id in forgotten {
614 affected.extend(
615 index
616 .edges(id)
617 .iter()
618 .map(|edge| edge.by.clone())
619 .filter(|by| !dead.contains(by)),
620 );
621 }
622 let mut rewrites = Vec::new();
623 for survivor in affected {
624 let Some(parts) = self.core.document_parts(&survivor) else {
625 continue;
626 };
627 let Some(mut stamp) = Stamp::from_parts(&parts.version, &parts.metadata) else {
628 continue;
629 };
630 let mut supersedes: BTreeSet<String> = stamp
631 .supersedes
632 .iter()
633 .filter(|id| !dead.contains(*id))
634 .cloned()
635 .collect();
636 supersedes.extend(index.bridge(&survivor, &dead));
637 let supersedes: Vec<String> = supersedes.into_iter().collect();
638 let hints: Vec<String> = stamp
639 .superseded_by
640 .iter()
641 .filter(|id| !dead.contains(*id))
642 .cloned()
643 .collect();
644 let mut current = stamp.supersedes.clone();
645 current.sort_unstable();
646 if supersedes == current && hints == stamp.superseded_by {
647 continue;
648 }
649 stamp.supersedes = supersedes;
650 stamp.superseded_by = hints;
651 let mut metadata = parts.metadata;
652 let version = apply_placement(&stamp, &mut metadata);
653 rewrites.push(DocumentInput {
654 id: survivor,
655 text: parts.text.unwrap_or_default(),
656 source_uri: parts.source_uri,
657 version,
658 metadata: Some(metadata),
659 chunk_words: Some(CHUNK_WORDS),
660 overlap_words: Some(0),
661 store_text: Some(true),
662 embeddings: parts.embedding.map(|embedding| vec![embedding]),
663 title_weight: None,
664 });
665 }
666 Ok(rewrites)
667 }
668
669 fn resolve_scope(&self, scope: &ForgetScope) -> Result<BTreeSet<String>> {
670 match scope {
671 ForgetScope::Ids { ids } => {
672 check!(
673 ids.iter().all(|id| valid_id(id)),
674 "ids must be valid fact ids"
675 );
676 Ok(ids.iter().cloned().collect())
677 }
678 ForgetScope::Subject { subject } => Ok(self
679 .index
680 .read()
681 .subject_ids(subject.trim())
682 .into_iter()
683 .collect()),
684 ForgetScope::Entity { entity } => Ok(self
685 .index
686 .read()
687 .entity_ids(entity.trim())
688 .iter()
689 .cloned()
690 .collect()),
691 ForgetScope::IngestedBefore { time } => {
692 Ok(self.scan_ids(|_, stamp| stamp.is_some_and(|stamp| stamp.ingest < *time)))
693 }
694 ForgetScope::EventBefore { time } => Ok(self.scan_ids(|_, stamp| {
695 stamp.is_some_and(|stamp| {
696 stamp.event.or(stamp.valid_from).unwrap_or(stamp.ingest) < *time
697 })
698 })),
699 ForgetScope::Matching { filter } => {
700 let Some(filter) = filter.as_object().filter(|f| !f.is_empty()) else {
701 invalid!("filter must be a non-empty JSON object");
702 };
703 check!(
704 !filter.contains_key(KEY),
705 "metadata key {KEY:?} is reserved"
706 );
707 let mut ids = BTreeSet::new();
708 self.core.scan_documents(|document| {
709 if Stamp::from_parts(document.version, document.metadata).is_some()
710 && filter
711 .iter()
712 .all(|(key, value)| document.metadata.get(key) == Some(value))
713 {
714 ids.insert(document.id.to_owned());
715 }
716 });
717 Ok(ids)
718 }
719 }
720 }
721
722 fn scan_ids(&self, keep: impl Fn(&str, Option<&Stamp>) -> bool) -> BTreeSet<String> {
723 let mut ids = BTreeSet::new();
724 self.core.scan_documents(|document| {
725 let stamp = Stamp::from_parts(document.version, document.metadata);
726 if stamp.is_some() && keep(document.id, stamp.as_ref()) {
727 ids.insert(document.id.to_owned());
728 }
729 });
730 ids
731 }
732}