1use crate::signals::is_positive_signal;
4use crate::{ConsolidationReport, ConsolidationScope, Engine, EngineError, EngineResult};
5use hippmem_consolidation::hebbian::ActivationLog;
6use hippmem_consolidation::summarize::{build_summary_unit, plan_summary_clusters};
7use hippmem_consolidation::worker::ConsolidationWorker;
8use hippmem_core::ids::MemoryId;
9use hippmem_core::model::unit::{MemoryLifecycle, MemoryUnit};
10use hippmem_core::time::{Clock, SystemClock};
11use hippmem_model::deterministic::summarize::DeterministicSummarizer;
12use hippmem_store::activation_log::ActivationLogger;
13use hippmem_store::kv::KvStore;
14use hippmem_store::memory_log::MemoryLog;
15use hippmem_store::store::{
16 ACTIVATION_LOG, CAUSAL_INDEX, CONSOLIDATION_QUEUE, CORRECTION_OVERLAY, ENTITY_INDEX,
17 EVENT_INDEX, GOAL_INDEX, LINK_OVERLAY, MEMORY_KV, SUMMARY_OVERLAY, TEMPORAL_INDEX, TOPIC_INDEX,
18};
19use std::collections::HashMap;
20use std::time::Instant;
21
22impl Engine {
23 pub fn consolidate(&self, scope: ConsolidationScope) -> EngineResult<ConsolidationReport> {
26 if matches!(scope, ConsolidationScope::Reindex) {
27 return self.consolidate_reindex();
28 }
29 self.consolidate_incremental()
30 }
31
32 fn consolidate_incremental(&self) -> EngineResult<ConsolidationReport> {
34 let start = Instant::now();
35 let clock = SystemClock;
36 let now = clock.now();
37
38 let mut units = crate::retrieve_api::load_all_units(self.store.db_arc());
40
41 let logger = ActivationLogger::new(self.store.db_arc());
49 let mut activation_log = ActivationLog::default();
50 let mut rejected_ids: Vec<MemoryId> = Vec::new();
51 if let Ok(records) = logger.read_all() {
52 for rec in &records {
53 if is_positive_signal(&rec.signal) {
54 let weight = match rec.signal.as_str() {
55 "Referenced" => 0.5,
56 "TaskSucceeded" => 0.8,
57 _ => 1.0, };
59 for i in 0..rec.used_memory_ids.len() {
60 for j in (i + 1)..rec.used_memory_ids.len() {
61 let a = MemoryId(rec.used_memory_ids[i]);
63 let b = MemoryId(rec.used_memory_ids[j]);
64 let ts = hippmem_core::time::Timestamp::from_millis(rec.recorded_at_ms);
65 activation_log.record(a, ts, weight);
66 activation_log.record(b, ts, weight);
67 }
68 }
69 } else if rec.signal == "UserRejected" && !rec.used_memory_ids.is_empty() {
70 rejected_ids.extend(rec.used_memory_ids.iter().map(|id| MemoryId(*id)));
72 }
73 }
74 }
75 let co_activations = activation_log.co_activation_pairs(3_600_000);
76
77 let mut worker = ConsolidationWorker::default();
79 let cycle_stats = worker.run_cycle(&mut units, &co_activations, &rejected_ids, now);
80
81 let params = self.params.read();
84 let clusters = plan_summary_clusters(
85 &units,
86 params.summary_similarity_threshold,
87 params.summary_trigger_count,
88 params.summary_low_importance_threshold,
89 );
90 let mut summaries: Vec<MemoryUnit> = Vec::new();
91 for cluster in &clusters {
92 let members: Vec<MemoryUnit> = cluster
93 .iter()
94 .filter_map(|id| units.iter().find(|u| u.id == *id).cloned())
95 .collect();
96 if members.len() != cluster.len() {
97 continue; }
99 let summary_unit = build_summary_unit(&members, &DeterministicSummarizer);
100 if summary_unit.understanding.confidence.value() >= 0.35 {
102 summaries.push(summary_unit);
103 }
104 }
105 for summary_unit in &summaries {
107 for unit in units.iter_mut() {
108 if summary_unit.context.preceding_memory_ids.contains(&unit.id) {
109 unit.lifecycle = MemoryLifecycle::Compressed {
110 into: summary_unit.id,
111 };
112 }
113 }
114 }
115
116 let mut compressed_into: HashMap<MemoryId, MemoryId> = HashMap::new();
124 for summary_unit in &summaries {
125 for source_id in &summary_unit.context.preceding_memory_ids {
126 compressed_into.insert(*source_id, summary_unit.id);
127 }
128 }
129 if !compressed_into.is_empty() {
130 for unit in units.iter_mut() {
131 for link in unit.links.iter_mut() {
132 if let Some(&summary_id) = compressed_into.get(&link.target_id) {
133 link.target_id = summary_id;
134 match link.evidence.note.as_mut() {
135 Some(note) => note.push_str(" [redirected to summary]"),
136 None => link.evidence.note = Some("redirected to summary".into()),
137 }
138 }
139 }
140 }
141 }
142
143 let kv = KvStore::new(self.store.db_arc());
149 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
150 for unit in &units {
151 let bincode_unit = bincode::serde::encode_to_vec(unit, bincode::config::standard())
152 .map_err(|e| EngineError::Internal(e.to_string()))?;
153 kv.put(unit.id.0, &bincode_unit)
154 .map_err(|e| EngineError::Store(e.to_string()))?;
155 graph
156 .put_outgoing(unit.id, &unit.links)
157 .map_err(EngineError::Store)?;
158 }
159
160 for summary_unit in &summaries {
163 let input = crate::WriteMemoryInput {
164 content: summary_unit.content.raw.clone(),
165 content_type: Some(summary_unit.content.content_type),
166 context: summary_unit.context.clone(),
167 importance_hint: Some(summary_unit.understanding.importance.value()),
168 source_refs: summary_unit.context.source_refs.clone(),
169 };
170 crate::write_api::write_internal(self, summary_unit.id, input, false, None)?;
171
172 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
179 let summary_links: Vec<hippmem_core::model::links::AssociationLink> = summary_unit
180 .links
181 .iter()
182 .filter(|l| !compressed_into.contains_key(&l.target_id))
183 .cloned()
184 .collect();
185 graph
186 .put_outgoing(summary_unit.id, &summary_links)
187 .map_err(EngineError::Store)?;
188 if let Some(raw) = kv
189 .get(&summary_unit.id.0)
190 .map_err(|e| EngineError::Store(e.to_string()))?
191 {
192 let (mut patched, _): (MemoryUnit, _) =
193 bincode::serde::decode_from_slice(&raw, bincode::config::standard())
194 .map_err(|e| EngineError::Internal(e.to_string()))?;
195 patched.links = summary_links.clone();
196 patched.provenance = summary_unit.provenance.clone();
197 patched.stage = summary_unit.stage;
198 patched.content.summary = summary_unit.content.summary.clone();
199 let re_bincode =
200 bincode::serde::encode_to_vec(&patched, bincode::config::standard())
201 .map_err(|e| EngineError::Internal(e.to_string()))?;
202 kv.put(summary_unit.id.0, &re_bincode)
203 .map_err(|e| EngineError::Store(e.to_string()))?;
204 }
205 }
206
207 let elapsed_ms = start.elapsed().as_millis() as u64;
208
209 Ok(ConsolidationReport {
210 memories_processed: units.len() as u64 + summaries.len() as u64,
211 edges_decayed: cycle_stats.edges_decayed,
212 edges_archived: cycle_stats.edges_archived,
213 edges_merged: cycle_stats.hebbian_applied,
214 observation_promoted: 0,
215 summaries_created: summaries.len() as u64,
216 contradictions_found: 0,
217 reindexed: false,
218 elapsed_ms,
219 })
220 }
221
222 fn consolidate_reindex(&self) -> EngineResult<ConsolidationReport> {
224 let start = Instant::now();
225
226 let log = MemoryLog::new(self.store.db_arc());
228 let raw_records = log
229 .read_all()
230 .map_err(|e| EngineError::Store(e.to_string()))?;
231 let mut units: Vec<(u128, MemoryUnit)> = Vec::with_capacity(raw_records.len());
232 for (id, data) in &raw_records {
233 let (unit, _): (MemoryUnit, _) =
234 bincode::serde::decode_from_slice(data, bincode::config::standard()).map_err(
235 |e| EngineError::Internal(format!("failed to deserialize MemoryUnit: {}", e)),
236 )?;
237 units.push((*id, unit));
238 }
239 let total = units.len() as u64;
240
241 clear_all_secondary_tables(self.store.db_arc())
243 .map_err(|e| EngineError::Store(e.to_string()))?;
244
245 {
247 let mut ft = self.fulltext_index.lock();
248 let _ = ft.commit();
249 drop(ft);
250 if self.fulltext_dir.exists() {
251 std::fs::remove_dir_all(&self.fulltext_dir).map_err(|e| {
252 EngineError::Store(format!("failed to delete fulltext directory: {}", e))
253 })?;
254 }
255 let new_ft = hippmem_store::fulltext::FulltextIndex::create(&self.fulltext_dir)
256 .map_err(|e| {
257 EngineError::Store(format!("failed to rebuild Tantivy index: {}", e))
258 })?;
259 *self.fulltext_index.lock() = new_ft;
260 }
261
262 {
264 use hippmem_store::semantic::binary::BinaryCodeIndex;
265 use hippmem_store::semantic::hnsw::FlatVectorIndex;
266 *self.binary_code_index.lock() = BinaryCodeIndex::new();
267 *self.dense_vector_index.lock() = FlatVectorIndex::new();
268 }
269
270 for (id, unit) in &units {
272 self.reindex_one(MemoryId(*id), unit)?;
273 }
274
275 let elapsed_ms = start.elapsed().as_millis() as u64;
276
277 Ok(ConsolidationReport {
278 memories_processed: total,
279 edges_decayed: 0,
280 edges_archived: 0,
281 edges_merged: 0,
282 observation_promoted: 0,
283 summaries_created: 0,
284 contradictions_found: 0,
285 reindexed: true,
286 elapsed_ms,
287 })
288 }
289
290 fn reindex_one(&self, id: MemoryId, unit: &MemoryUnit) -> EngineResult<()> {
292 use crate::write_api::write_internal;
293
294 let input = crate::WriteMemoryInput {
295 content: unit.content.raw.clone(),
296 content_type: Some(unit.content.content_type),
297 context: unit.context.clone(),
298 importance_hint: Some(unit.understanding.importance.value()),
299 source_refs: unit.context.source_refs.clone(),
300 };
301 write_internal(self, id, input, true, None)?;
303 Ok(())
304 }
305}
306
307fn clear_all_secondary_tables(
311 db: std::sync::Arc<redb::Database>,
312) -> Result<(), hippmem_store::store::StoreError> {
313 use redb::ReadableTable;
314
315 let txn = db.begin_write()?;
316
317 let u128_tables: &[redb::TableDefinition<u128, &[u8]>] = &[
319 MEMORY_KV,
320 LINK_OVERLAY,
321 SUMMARY_OVERLAY,
322 CORRECTION_OVERLAY,
323 ACTIVATION_LOG,
324 CONSOLIDATION_QUEUE,
325 ];
326 for def in u128_tables {
327 let keys: Vec<u128> = {
328 let table = txn.open_table(*def)?;
329 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
330 };
331 if !keys.is_empty() {
332 let mut table = txn.open_table(*def)?;
333 for k in &keys {
334 let _ = table.remove(*k);
335 }
336 }
337 }
338
339 let u64_tables: &[redb::TableDefinition<u64, &[u8]>] = &[
341 ENTITY_INDEX,
342 TOPIC_INDEX,
343 GOAL_INDEX,
344 EVENT_INDEX,
345 CAUSAL_INDEX,
346 ];
347 for def in u64_tables {
348 let keys: Vec<u64> = {
349 let table = txn.open_table(*def)?;
350 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
351 };
352 if !keys.is_empty() {
353 let mut table = txn.open_table(*def)?;
354 for k in &keys {
355 let _ = table.remove(*k);
356 }
357 }
358 }
359
360 let u32_tables: &[redb::TableDefinition<u32, &[u8]>] = &[TEMPORAL_INDEX];
362 for def in u32_tables {
363 let keys: Vec<u32> = {
364 let table = txn.open_table(*def)?;
365 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
366 };
367 if !keys.is_empty() {
368 let mut table = txn.open_table(*def)?;
369 for k in &keys {
370 let _ = table.remove(*k);
371 }
372 }
373 }
374
375 txn.commit()?;
376 Ok(())
377}