writerslogic_warm/core/memory_api/
maintenance.rs1use anyhow::Result;
8
9use super::recall::evaluate;
10use super::stamp::{stable_hash, Stamp};
11use super::types::*;
12use super::{ConsolidationPolicy, FactSummary, ForgettingPolicy, Memory, MemoryError};
13
14impl Memory {
15 pub fn summaries(&self, with_text: bool) -> Vec<FactSummary> {
20 self.try_summaries(with_text)
21 .expect("coherent memory summaries")
22 }
23
24 pub fn try_summaries(&self, with_text: bool) -> Result<Vec<FactSummary>> {
25 let now = self.clock.now_millis();
26 self.summaries_snapshot(with_text, now)
27 .map(|(_, facts)| facts)
28 }
29
30 fn summaries_snapshot(&self, with_text: bool, now: Millis) -> Result<(u64, Vec<FactSummary>)> {
31 self.with_snapshot(|index| {
32 let mut facts = Vec::new();
33 let revision = self.core.scan_documents(|document| {
34 let Some(stamp) = Stamp::from_parts(document.version, document.metadata) else {
35 return;
36 };
37 let edges = index.edges(document.id);
38 let status =
39 evaluate(Some(&stamp), edges, now, None).unwrap_or(FactStatus::Current);
40 facts.push(FactSummary {
41 id: document.id.to_owned(),
42 text: if with_text { document.text() } else { None },
43 status,
44 ingest_time: Some(stamp.ingest),
45 event_time: stamp.event,
46 valid_from: stamp.valid_from,
47 valid_to: stamp.valid_to,
48 effective_from: stamp.effective_from(),
49 subject: stamp.subject.clone(),
50 superseded_at: edges.iter().map(|edge| edge.effective).min(),
51 });
52 });
53 facts.sort_by(|a, b| a.id.cmp(&b.id));
54 check!(
55 index.revision == Some(revision),
56 "snapshot revision changed"
57 );
58 Ok((revision, facts))
59 })
60 }
61
62 pub fn apply_forgetting(&self, options: &ForgetOptions) -> Result<ForgetReport> {
65 self.apply_forgetting_with(self.forgetting.as_ref(), options)
66 }
67
68 pub fn apply_forgetting_with(
69 &self,
70 policy: &dyn ForgettingPolicy,
71 options: &ForgetOptions,
72 ) -> Result<ForgetReport> {
73 let now = self.clock.now_millis();
74 let mut completed = std::collections::BTreeSet::new();
75 for _ in 0..16 {
76 let (revision, facts) = self.summaries_snapshot(policy.needs_text(), now)?;
77 let known: std::collections::HashSet<&str> =
78 facts.iter().map(|fact| fact.id.as_str()).collect();
79 let mut ids: Vec<String> = policy
80 .select(&facts, now)
81 .into_iter()
82 .filter(|id| known.contains(id.as_str()))
83 .collect();
84 ids.sort_unstable();
85 ids.dedup();
86 if let Some(report) = self.forget_at_revision(
87 &ForgetScope::Ids { ids },
88 options,
89 revision,
90 &mut completed,
91 )? {
92 return Ok(report);
93 }
94 }
95 Err(MemoryError::Concurrent(
96 "memory changed during forgetting policy evaluation; retry the operation".into(),
97 )
98 .into())
99 }
100
101 pub fn consolidate(&self, options: &ConsolidateOptions) -> Result<ConsolidationReport> {
103 self.consolidate_with(self.consolidation.as_ref(), options)
104 }
105
106 pub fn consolidate_with(
110 &self,
111 policy: &dyn ConsolidationPolicy,
112 options: &ConsolidateOptions,
113 ) -> Result<ConsolidationReport> {
114 let now = self.clock.now_millis();
115 for _ in 0..16 {
116 let (revision, facts) = self.summaries_snapshot(policy.needs_text(), now)?;
117 let mut inputs = Vec::new();
118 let mut doomed = Vec::new();
119 for action in policy.plan(&facts, now) {
120 if action.sources.is_empty() {
121 continue;
122 }
123 let mut sources = action.sources.clone();
124 sources.sort_unstable();
125 sources.dedup();
126 let mut summary = action.summary;
127 summary.supersedes = Some(sources.clone());
128 if summary.id.is_none() {
129 let parts: Vec<&[u8]> = sources.iter().map(|s| s.as_bytes()).collect();
130 summary.id = Some(format!("mem:merged:{}", stable_hash(&parts)));
131 }
132 if action.forget_sources && options.allow_destructive {
133 doomed.extend(sources);
134 }
135 inputs.push(summary);
136 }
137 if inputs.is_empty() {
138 if !self.core.memory_revision_matches(revision) {
139 continue;
140 }
141 return Ok(ConsolidationReport {
142 created: Vec::new(),
143 forgotten: Vec::new(),
144 dry_run: options.dry_run,
145 });
146 }
147 if options.dry_run {
148 if !self.core.memory_revision_matches(revision) {
149 continue;
150 }
151 let now_for_plan = now;
152 let created = inputs
153 .iter()
154 .map(|input| Remembered {
155 id: input.id.clone().unwrap_or_default(),
156 created: true,
157 supersedes: input.supersedes.clone().unwrap_or_default(),
158 superseded_by: Vec::new(),
159 ingest_time: now_for_plan,
160 })
161 .collect();
162 doomed.sort_unstable();
163 doomed.dedup();
164 return Ok(ConsolidationReport {
165 created,
166 forgotten: doomed,
167 dry_run: true,
168 });
169 }
170 let Some(created) = self.remember_at_revision(inputs, now, revision)? else {
171 continue;
172 };
173 let forgotten = if doomed.is_empty() {
174 Vec::new()
175 } else {
176 let expected =
177 revision.wrapping_add(u64::from(created.iter().any(|fact| fact.created)));
178 let cleanup = self.forget_at_revision(
179 &ForgetScope::Ids { ids: doomed },
180 &ForgetOptions::default(),
181 expected,
182 &mut Default::default(),
183 ).and_then(|report| report.ok_or_else(|| MemoryError::Concurrent(
184 "consolidation committed; sources changed before cleanup; retained sources require reconciliation".into()
185 ).into()));
186 crate::core::error::after_commit("consolidation source cleanup", cleanup)?.forgotten
187 };
188 return Ok(ConsolidationReport {
189 created,
190 forgotten,
191 dry_run: false,
192 });
193 }
194 Err(MemoryError::Concurrent(
195 "memory changed during consolidation policy evaluation; retry the operation".into(),
196 )
197 .into())
198 }
199}