Skip to main content

hippmem_engine/
consolidate_api.rs

1//! Engine::consolidate — consolidation API (05 §5, 09 §4.4).
2
3use 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    /// Runs consolidation: Hebbian→decay→compaction→summary, covering the specified scope.
21    /// Reindex scope: rebuilds all secondary indexes from memory_log (no data loss).
22    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    /// Standard incremental consolidation (Hebbian→decay→compaction→summary).
30    fn consolidate_incremental(&self) -> EngineResult<ConsolidationReport> {
31        let start = Instant::now();
32        let clock = SystemClock;
33        let now = clock.now();
34
35        // 1. Load all data in the store
36        let mut units = crate::retrieve_api::load_all_units(self.store.db_arc());
37
38        // 2. Read activation_log and build co-activation pairs
39        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        // 3. Run consolidation cycle (Hebbian→decay→compaction→summary)
57        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        // 4. Persist the modified units back to the store
62        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        // 4b. Persist summary memory
71        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    /// Reindex: rebuilds all secondary indexes from memory_log (no data loss, MemoryId unchanged).
104    fn consolidate_reindex(&self) -> EngineResult<ConsolidationReport> {
105        let start = Instant::now();
106
107        // 1. Read all raw records from memory_log
108        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        // 2. Clear all secondary tables (preserve MEMORY_LOG)
123        clear_all_secondary_tables(self.store.db_arc())
124            .map_err(|e| EngineError::Store(e.to_string()))?;
125
126        // 3. Clear the Tantivy fulltext index (rebuild after deleting the directory)
127        {
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        // 4. Clear the vector indexes
144        {
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        // 5. Re-write each entry with its original MemoryId
152        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    /// Re-processes a memory with its original MemoryId (used internally by Reindex).
172    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        // skip_memory_log=true: the record already exists in MEMORY_LOG (constitution C7)
183        write_internal(self, id, input, true, None)?;
184        Ok(())
185    }
186}
187
188// ── Table cleanup helpers ──
189
190/// Clears all secondary tables, preserving MEMORY_LOG (constitution C7).
191fn 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    // u128 tables (excluding MEMORY_LOG)
199    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    // u64 tables
221    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    // u32 tables
242    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}