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::time::Instant;
20
21impl Engine {
22 pub fn consolidate(&self, scope: ConsolidationScope) -> EngineResult<ConsolidationReport> {
25 if matches!(scope, ConsolidationScope::Reindex) {
26 return self.consolidate_reindex();
27 }
28 self.consolidate_incremental()
29 }
30
31 fn consolidate_incremental(&self) -> EngineResult<ConsolidationReport> {
33 let start = Instant::now();
34 let clock = SystemClock;
35 let now = clock.now();
36
37 let mut units = crate::retrieve_api::load_all_units(self.store.db_arc());
39
40 let logger = ActivationLogger::new(self.store.db_arc());
43 let mut activation_log = ActivationLog::default();
44 if let Ok(records) = logger.read_all() {
45 for rec in &records {
46 if !is_positive_signal(&rec.signal) {
47 continue;
48 }
49 for i in 0..rec.used_memory_ids.len() {
50 for j in (i + 1)..rec.used_memory_ids.len() {
51 let a = MemoryId(rec.used_memory_ids[i]);
53 let b = MemoryId(rec.used_memory_ids[j]);
54 let ts = hippmem_core::time::Timestamp::from_millis(rec.recorded_at_ms);
55 activation_log.record(a, ts, 0.5);
56 activation_log.record(b, ts, 0.5);
57 }
58 }
59 }
60 }
61 let co_activations = activation_log.co_activation_pairs(3_600_000);
62
63 let mut worker = ConsolidationWorker::default();
65 let cycle_stats = worker.run_cycle(&mut units, &co_activations, now);
66
67 let params = self.params.read();
70 let clusters = plan_summary_clusters(
71 &units,
72 params.summary_similarity_threshold,
73 params.summary_trigger_count,
74 params.summary_low_importance_threshold,
75 );
76 let mut summaries: Vec<MemoryUnit> = Vec::new();
77 for cluster in &clusters {
78 let members: Vec<MemoryUnit> = cluster
79 .iter()
80 .filter_map(|id| units.iter().find(|u| u.id == *id).cloned())
81 .collect();
82 if members.len() != cluster.len() {
83 continue; }
85 let summary_unit = build_summary_unit(&members, &DeterministicSummarizer);
86 if summary_unit.understanding.confidence.value() >= 0.35 {
88 summaries.push(summary_unit);
89 }
90 }
91 for summary_unit in &summaries {
93 for unit in units.iter_mut() {
94 if summary_unit.context.preceding_memory_ids.contains(&unit.id) {
95 unit.lifecycle = MemoryLifecycle::Compressed {
96 into: summary_unit.id,
97 };
98 }
99 }
100 }
101
102 let kv = KvStore::new(self.store.db_arc());
104 for unit in &units {
105 let bincode_unit = bincode::serde::encode_to_vec(unit, bincode::config::standard())
106 .map_err(|e| EngineError::Internal(e.to_string()))?;
107 kv.put(unit.id.0, &bincode_unit)
108 .map_err(|e| EngineError::Store(e.to_string()))?;
109 }
110
111 for summary_unit in &summaries {
114 let input = crate::WriteMemoryInput {
115 content: summary_unit.content.raw.clone(),
116 content_type: Some(summary_unit.content.content_type),
117 context: summary_unit.context.clone(),
118 importance_hint: Some(summary_unit.understanding.importance.value()),
119 source_refs: summary_unit.context.source_refs.clone(),
120 };
121 crate::write_api::write_internal(self, summary_unit.id, input, false, None)?;
122
123 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
128 graph
129 .put_outgoing(summary_unit.id, &summary_unit.links)
130 .map_err(EngineError::Store)?;
131 if let Some(raw) = kv
132 .get(&summary_unit.id.0)
133 .map_err(|e| EngineError::Store(e.to_string()))?
134 {
135 let (mut patched, _): (MemoryUnit, _) =
136 bincode::serde::decode_from_slice(&raw, bincode::config::standard())
137 .map_err(|e| EngineError::Internal(e.to_string()))?;
138 patched.links = summary_unit.links.clone();
139 patched.provenance = summary_unit.provenance.clone();
140 patched.stage = summary_unit.stage;
141 patched.content.summary = summary_unit.content.summary.clone();
142 let re_bincode =
143 bincode::serde::encode_to_vec(&patched, bincode::config::standard())
144 .map_err(|e| EngineError::Internal(e.to_string()))?;
145 kv.put(summary_unit.id.0, &re_bincode)
146 .map_err(|e| EngineError::Store(e.to_string()))?;
147 }
148 }
149
150 let elapsed_ms = start.elapsed().as_millis() as u64;
151
152 Ok(ConsolidationReport {
153 memories_processed: units.len() as u64 + summaries.len() as u64,
154 edges_decayed: cycle_stats.edges_decayed,
155 edges_archived: cycle_stats.edges_archived,
156 edges_merged: cycle_stats.hebbian_applied,
157 observation_promoted: 0,
158 summaries_created: summaries.len() as u64,
159 contradictions_found: 0,
160 reindexed: false,
161 elapsed_ms,
162 })
163 }
164
165 fn consolidate_reindex(&self) -> EngineResult<ConsolidationReport> {
167 let start = Instant::now();
168
169 let log = MemoryLog::new(self.store.db_arc());
171 let raw_records = log
172 .read_all()
173 .map_err(|e| EngineError::Store(e.to_string()))?;
174 let mut units: Vec<(u128, MemoryUnit)> = Vec::with_capacity(raw_records.len());
175 for (id, data) in &raw_records {
176 let (unit, _): (MemoryUnit, _) =
177 bincode::serde::decode_from_slice(data, bincode::config::standard()).map_err(
178 |e| EngineError::Internal(format!("failed to deserialize MemoryUnit: {}", e)),
179 )?;
180 units.push((*id, unit));
181 }
182 let total = units.len() as u64;
183
184 clear_all_secondary_tables(self.store.db_arc())
186 .map_err(|e| EngineError::Store(e.to_string()))?;
187
188 {
190 let mut ft = self.fulltext_index.lock();
191 let _ = ft.commit();
192 drop(ft);
193 if self.fulltext_dir.exists() {
194 std::fs::remove_dir_all(&self.fulltext_dir).map_err(|e| {
195 EngineError::Store(format!("failed to delete fulltext directory: {}", e))
196 })?;
197 }
198 let new_ft = hippmem_store::fulltext::FulltextIndex::create(&self.fulltext_dir)
199 .map_err(|e| {
200 EngineError::Store(format!("failed to rebuild Tantivy index: {}", e))
201 })?;
202 *self.fulltext_index.lock() = new_ft;
203 }
204
205 {
207 use hippmem_store::semantic::binary::BinaryCodeIndex;
208 use hippmem_store::semantic::hnsw::FlatVectorIndex;
209 *self.binary_code_index.lock() = BinaryCodeIndex::new();
210 *self.dense_vector_index.lock() = FlatVectorIndex::new();
211 }
212
213 for (id, unit) in &units {
215 self.reindex_one(MemoryId(*id), unit)?;
216 }
217
218 let elapsed_ms = start.elapsed().as_millis() as u64;
219
220 Ok(ConsolidationReport {
221 memories_processed: total,
222 edges_decayed: 0,
223 edges_archived: 0,
224 edges_merged: 0,
225 observation_promoted: 0,
226 summaries_created: 0,
227 contradictions_found: 0,
228 reindexed: true,
229 elapsed_ms,
230 })
231 }
232
233 fn reindex_one(&self, id: MemoryId, unit: &MemoryUnit) -> EngineResult<()> {
235 use crate::write_api::write_internal;
236
237 let input = crate::WriteMemoryInput {
238 content: unit.content.raw.clone(),
239 content_type: Some(unit.content.content_type),
240 context: unit.context.clone(),
241 importance_hint: Some(unit.understanding.importance.value()),
242 source_refs: unit.context.source_refs.clone(),
243 };
244 write_internal(self, id, input, true, None)?;
246 Ok(())
247 }
248}
249
250fn clear_all_secondary_tables(
254 db: std::sync::Arc<redb::Database>,
255) -> Result<(), hippmem_store::store::StoreError> {
256 use redb::ReadableTable;
257
258 let txn = db.begin_write()?;
259
260 let u128_tables: &[redb::TableDefinition<u128, &[u8]>] = &[
262 MEMORY_KV,
263 LINK_OVERLAY,
264 SUMMARY_OVERLAY,
265 CORRECTION_OVERLAY,
266 ACTIVATION_LOG,
267 CONSOLIDATION_QUEUE,
268 ];
269 for def in u128_tables {
270 let keys: Vec<u128> = {
271 let table = txn.open_table(*def)?;
272 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
273 };
274 if !keys.is_empty() {
275 let mut table = txn.open_table(*def)?;
276 for k in &keys {
277 let _ = table.remove(*k);
278 }
279 }
280 }
281
282 let u64_tables: &[redb::TableDefinition<u64, &[u8]>] = &[
284 ENTITY_INDEX,
285 TOPIC_INDEX,
286 GOAL_INDEX,
287 EVENT_INDEX,
288 CAUSAL_INDEX,
289 ];
290 for def in u64_tables {
291 let keys: Vec<u64> = {
292 let table = txn.open_table(*def)?;
293 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
294 };
295 if !keys.is_empty() {
296 let mut table = txn.open_table(*def)?;
297 for k in &keys {
298 let _ = table.remove(*k);
299 }
300 }
301 }
302
303 let u32_tables: &[redb::TableDefinition<u32, &[u8]>] = &[TEMPORAL_INDEX];
305 for def in u32_tables {
306 let keys: Vec<u32> = {
307 let table = txn.open_table(*def)?;
308 table.iter()?.flatten().map(|(k, _)| k.value()).collect()
309 };
310 if !keys.is_empty() {
311 let mut table = txn.open_table(*def)?;
312 for k in &keys {
313 let _ = table.remove(*k);
314 }
315 }
316 }
317
318 txn.commit()?;
319 Ok(())
320}