1use crate::{ConsolidationReport, ConsolidationScope, Engine, EngineError, EngineResult};
4use hippmem_consolidation::hebbian::ActivationLog;
5use hippmem_consolidation::worker::ConsolidationWorker;
6use hippmem_core::ids::MemoryId;
7use hippmem_core::model::unit::MemoryUnit;
8use hippmem_core::time::{Clock, SystemClock};
9use hippmem_model::deterministic::summarize::DeterministicSummarizer;
10use hippmem_store::activation_log::ActivationLogger;
11use hippmem_store::kv::KvStore;
12use hippmem_store::memory_log::MemoryLog;
13use hippmem_store::store::{
14 ACTIVATION_LOG, CAUSAL_INDEX, CONSOLIDATION_QUEUE, CORRECTION_OVERLAY, ENTITY_INDEX,
15 EVENT_INDEX, GOAL_INDEX, LINK_OVERLAY, MEMORY_KV, SUMMARY_OVERLAY, TEMPORAL_INDEX, TOPIC_INDEX,
16};
17use std::time::Instant;
18
19impl Engine {
20 pub fn consolidate(&self, scope: ConsolidationScope) -> EngineResult<ConsolidationReport> {
23 if matches!(scope, ConsolidationScope::Reindex) {
24 return self.consolidate_reindex();
25 }
26 self.consolidate_incremental()
27 }
28
29 fn consolidate_incremental(&self) -> EngineResult<ConsolidationReport> {
31 let start = Instant::now();
32 let clock = SystemClock;
33 let now = clock.now();
34
35 let mut units = crate::retrieve_api::load_all_units(self.store.db_arc());
37
38 let logger = ActivationLogger::new(self.store.db_arc());
40 let mut activation_log = ActivationLog::default();
41 if let Ok(records) = logger.read_all() {
42 for rec in &records {
43 for i in 0..rec.used_memory_ids.len() {
44 for j in (i + 1)..rec.used_memory_ids.len() {
45 let a = MemoryId(rec.used_memory_ids[i] as u128);
46 let b = MemoryId(rec.used_memory_ids[j] as u128);
47 let ts = hippmem_core::time::Timestamp::from_millis(rec.recorded_at_ms);
48 activation_log.record(a, ts, 0.5);
49 activation_log.record(b, ts, 0.5);
50 }
51 }
52 }
53 }
54 let co_activations = activation_log.co_activation_pairs(3_600_000);
55
56 let summarizer = DeterministicSummarizer;
58 let mut worker = ConsolidationWorker::default();
59 let cycle_stats = worker.run_cycle(&mut units, &co_activations, now, Some(&summarizer));
60
61 let kv = KvStore::new(self.store.db_arc());
63 for unit in &units {
64 let bincode_unit = bincode::serde::encode_to_vec(unit, bincode::config::standard())
65 .map_err(|e| EngineError::Internal(e.to_string()))?;
66 kv.put(unit.id.0, &bincode_unit)
67 .map_err(|e| EngineError::Store(e.to_string()))?;
68 }
69
70 if let Some(ref summary_unit) = cycle_stats.summary_unit {
72 let bincode_summary =
73 bincode::serde::encode_to_vec(summary_unit, bincode::config::standard())
74 .map_err(|e| EngineError::Internal(e.to_string()))?;
75 kv.put(summary_unit.id.0, &bincode_summary)
76 .map_err(|e| EngineError::Store(e.to_string()))?;
77 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
78 graph
79 .put_outgoing(summary_unit.id, &summary_unit.links)
80 .map_err(EngineError::Store)?;
81 }
82
83 let elapsed_ms = start.elapsed().as_millis() as u64;
84
85 Ok(ConsolidationReport {
86 memories_processed: units.len() as u64
87 + if cycle_stats.summary_unit.is_some() {
88 1
89 } else {
90 0
91 },
92 edges_decayed: cycle_stats.edges_decayed,
93 edges_archived: cycle_stats.edges_archived,
94 edges_merged: cycle_stats.hebbian_applied,
95 observation_promoted: 0,
96 summaries_created: cycle_stats.summaries_created,
97 contradictions_found: 0,
98 reindexed: false,
99 elapsed_ms,
100 })
101 }
102
103 fn consolidate_reindex(&self) -> EngineResult<ConsolidationReport> {
105 let start = Instant::now();
106
107 let log = MemoryLog::new(self.store.db_arc());
109 let raw_records = log
110 .read_all()
111 .map_err(|e| EngineError::Store(e.to_string()))?;
112 let mut units: Vec<(u128, MemoryUnit)> = Vec::with_capacity(raw_records.len());
113 for (id, data) in &raw_records {
114 let (unit, _): (MemoryUnit, _) =
115 bincode::serde::decode_from_slice(data, bincode::config::standard()).map_err(
116 |e| EngineError::Internal(format!("failed to deserialize MemoryUnit: {}", e)),
117 )?;
118 units.push((*id, unit));
119 }
120 let total = units.len() as u64;
121
122 clear_all_secondary_tables(self.store.db_arc())
124 .map_err(|e| EngineError::Store(e.to_string()))?;
125
126 {
128 let mut ft = self.fulltext_index.lock();
129 let _ = ft.commit();
130 drop(ft);
131 if self.fulltext_dir.exists() {
132 std::fs::remove_dir_all(&self.fulltext_dir).map_err(|e| {
133 EngineError::Store(format!("failed to delete fulltext directory: {}", e))
134 })?;
135 }
136 let new_ft = hippmem_store::fulltext::FulltextIndex::create(&self.fulltext_dir)
137 .map_err(|e| {
138 EngineError::Store(format!("failed to rebuild Tantivy index: {}", e))
139 })?;
140 *self.fulltext_index.lock() = new_ft;
141 }
142
143 {
145 use hippmem_store::semantic::binary::BinaryCodeIndex;
146 use hippmem_store::semantic::hnsw::FlatVectorIndex;
147 *self.binary_code_index.lock() = BinaryCodeIndex::new();
148 *self.dense_vector_index.lock() = FlatVectorIndex::new();
149 }
150
151 for (id, unit) in &units {
153 self.reindex_one(MemoryId(*id), unit)?;
154 }
155
156 let elapsed_ms = start.elapsed().as_millis() as u64;
157
158 Ok(ConsolidationReport {
159 memories_processed: total,
160 edges_decayed: 0,
161 edges_archived: 0,
162 edges_merged: 0,
163 observation_promoted: 0,
164 summaries_created: 0,
165 contradictions_found: 0,
166 reindexed: true,
167 elapsed_ms,
168 })
169 }
170
171 fn reindex_one(&self, id: MemoryId, unit: &MemoryUnit) -> EngineResult<()> {
173 use crate::write_api::write_internal;
174
175 let input = crate::WriteMemoryInput {
176 content: unit.content.raw.clone(),
177 content_type: Some(unit.content.content_type),
178 context: unit.context.clone(),
179 importance_hint: Some(unit.understanding.importance.value()),
180 source_refs: unit.context.source_refs.clone(),
181 };
182 write_internal(self, id, input, true, None)?;
184 Ok(())
185 }
186}
187
188fn clear_all_secondary_tables(
192 db: std::sync::Arc<redb::Database>,
193) -> Result<(), hippmem_store::store::StoreError> {
194 use redb::ReadableTable;
195
196 let txn = db.begin_write()?;
197
198 let u128_tables: &[redb::TableDefinition<u128, &[u8]>] = &[
200 MEMORY_KV,
201 LINK_OVERLAY,
202 SUMMARY_OVERLAY,
203 CORRECTION_OVERLAY,
204 ACTIVATION_LOG,
205 CONSOLIDATION_QUEUE,
206 ];
207 for def in u128_tables {
208 let keys: Vec<u128> = {
209 let table = txn.open_table(*def)?;
210 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
211 };
212 if !keys.is_empty() {
213 let mut table = txn.open_table(*def)?;
214 for k in &keys {
215 let _ = table.remove(*k);
216 }
217 }
218 }
219
220 let u64_tables: &[redb::TableDefinition<u64, &[u8]>] = &[
222 ENTITY_INDEX,
223 TOPIC_INDEX,
224 GOAL_INDEX,
225 EVENT_INDEX,
226 CAUSAL_INDEX,
227 ];
228 for def in u64_tables {
229 let keys: Vec<u64> = {
230 let table = txn.open_table(*def)?;
231 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
232 };
233 if !keys.is_empty() {
234 let mut table = txn.open_table(*def)?;
235 for k in &keys {
236 let _ = table.remove(*k);
237 }
238 }
239 }
240
241 let u32_tables: &[redb::TableDefinition<u32, &[u8]>] = &[TEMPORAL_INDEX];
243 for def in u32_tables {
244 let keys: Vec<u32> = {
245 let table = txn.open_table(*def)?;
246 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
247 };
248 if !keys.is_empty() {
249 let mut table = txn.open_table(*def)?;
250 for k in &keys {
251 let _ = table.remove(*k);
252 }
253 }
254 }
255
256 txn.commit()?;
257 Ok(())
258}