1use crate::{Engine, EngineResult, RetrieveInput, RetrieveOutput};
6use hippmem_core::hash::stable_hash64;
7use hippmem_core::ids::MemoryId;
8use hippmem_core::model::links::{ActivationStep, RecallChannel, RetrievalResult};
9use hippmem_core::model::unit::MemoryUnit;
10use hippmem_core::time::Clock;
11use hippmem_model::deterministic::extract::DeterministicExtractor;
12use hippmem_model::lang::active_locales;
13use hippmem_retrieval::explain::deduce_dimensions;
14use hippmem_retrieval::seeds::{multi_channel_seeds, rrf_fuse};
15use hippmem_retrieval::spreading::spread_multi_hop_fused;
16use hippmem_retrieval::warnings::check_warnings;
17use hippmem_store::activation_log::ActivationLogger;
18use hippmem_store::kv::InvertedIndex;
19use hippmem_store::semantic::vector_index::BinaryIndex;
20use hippmem_store::semantic::vector_index::VectorIndex;
21use std::collections::HashMap;
22
23impl Engine {
24 pub fn retrieve(&self, input: RetrieveInput) -> EngineResult<RetrieveOutput> {
26 let params = self.params.read();
27
28 let extractor = DeterministicExtractor;
30 let query_content = hippmem_core::model::unit::MemoryContent {
31 raw: input.query.clone(),
32 summary: None,
33 normalized: None,
34 language: hippmem_core::model::unit::Language::Zh,
35 content_type: hippmem_core::model::enums::ContentType::UserStatement,
36 };
37 let understanding = extractor
38 .extract_sync_immediate(&query_content)
39 .unwrap_or_else(|_| hippmem_model::traits::ImmediateExtraction {
40 entities: vec![],
41 topics: vec![],
42 explicit_causals: vec![],
43 language: hippmem_core::model::unit::Language::Zh,
44 content_type: None,
45 importance: hippmem_core::score::UnitScore::new(0.0),
46 });
47
48 let inverted = InvertedIndex::new(self.store.db_arc());
50
51 let entity_hits: Vec<(MemoryId, f32)> = understanding
53 .entities
54 .iter()
55 .filter_map(|em| {
56 let key = hippmem_core::hash::stable_hash64(&em.canonical);
57 inverted.get_entity(&key).ok().map(|ids| {
58 ids.into_iter()
59 .map(|id| (MemoryId(id), 0.2f32))
60 .collect::<Vec<_>>()
61 })
62 })
63 .flatten()
64 .collect();
65
66 let topic_hits: Vec<(MemoryId, f32)> = understanding
68 .topics
69 .iter()
70 .filter_map(|t| {
71 let key = hippmem_core::hash::stable_hash64(&t.label);
72 inverted.get_topic(&key).ok().map(|ids| {
73 ids.into_iter()
74 .map(|id| (MemoryId(id), 0.15f32))
75 .collect::<Vec<_>>()
76 })
77 })
78 .flatten()
79 .collect();
80
81 let now = hippmem_core::time::SystemClock.now();
83 let temporal_keys = temporal_bucket_keys(now);
84 let mut temporal_hit_ids = std::collections::HashSet::new();
85 for tk in &temporal_keys {
86 if let Ok(ids) = inverted.get_temporal(tk) {
87 for id in ids {
88 temporal_hit_ids.insert(MemoryId(id));
89 }
90 }
91 }
92 let temporal_hits: Vec<(MemoryId, bool)> =
93 temporal_hit_ids.into_iter().map(|id| (id, true)).collect();
94
95 let bm25_hits: Vec<(MemoryId, f32)> = self
97 .fulltext_index
98 .lock()
99 .search(&input.query, params.seed_per_channel as usize)
100 .unwrap_or_default()
101 .into_iter()
102 .map(|(id, score)| {
103 let norm = (score / params.bm25_norm_factor).tanh();
104 (MemoryId(id), norm)
105 })
106 .collect();
107
108 let semantic_hits: Vec<(MemoryId, f32)> = {
110 let query_texts = vec![input.query.clone()];
111 self.embedder
112 .embed_sync(&query_texts)
113 .ok()
114 .and_then(|vectors| vectors.first().cloned())
115 .map(|query_vec| {
116 let idx = self.dense_vector_index.lock();
117 idx.search(&query_vec, params.seed_per_channel as usize)
118 .unwrap_or_default()
119 .into_iter()
120 .map(|(id, l2_dist)| {
121 let cos_sim = 1.0 / (1.0 + l2_dist);
123 (MemoryId(id), cos_sim)
124 })
125 .filter(|(_, sim)| *sim > 0.0)
126 .collect()
127 })
128 .unwrap_or_default()
129 };
130
131 let binary_hits: Vec<(MemoryId, f32)> = {
133 let query_bc = query_binary_code(&input.query);
134 let idx = self.binary_code_index.lock();
135 idx.search(&query_bc, params.seed_per_channel as usize)
136 .unwrap_or_default()
137 .into_iter()
138 .map(|(id, hamming)| {
139 let sim = 1.0 - (hamming as f32 / 128.0);
140 (MemoryId(id), sim.max(0.0))
141 })
142 .filter(|(_, sim)| *sim > 0.0)
143 .collect()
144 };
145
146 let query_goals = extract_query_goals(&input.query);
148 let goal_hits: Vec<(MemoryId, usize)> = query_goals
149 .iter()
150 .filter_map(|goal| {
151 let key = stable_hash64(goal);
152 inverted.get_goal(&key).ok().map(|ids| {
153 ids.into_iter()
154 .map(|id| (MemoryId(id), 1))
155 .collect::<Vec<_>>()
156 })
157 })
158 .flatten()
159 .collect();
160
161 let query_events = extract_query_events(&input.query);
163 let event_hits: Vec<(MemoryId, usize)> = query_events
164 .iter()
165 .filter_map(|event| {
166 let key = stable_hash64(event);
167 inverted.get_event(&key).ok().map(|ids| {
168 ids.into_iter()
169 .map(|id| (MemoryId(id), 1))
170 .collect::<Vec<_>>()
171 })
172 })
173 .flatten()
174 .collect();
175
176 let causal_hits: Vec<(MemoryId, usize)> = understanding
178 .explicit_causals
179 .iter()
180 .filter_map(|c| {
181 let causal_str = format!("{} -> {}", c.cause, c.effect);
182 let key = stable_hash64(&causal_str);
183 inverted.get_causal(&key).ok().map(|ids| {
184 ids.into_iter()
185 .map(|id| (MemoryId(id), 1))
186 .collect::<Vec<_>>()
187 })
188 })
189 .flatten()
190 .collect();
191
192 let recent_hits: Vec<(MemoryId, f32)> = {
194 let mut recent_map: HashMap<MemoryId, f32> = HashMap::new();
195
196 for mid in &input.context.recent_memory_ids {
198 recent_map
199 .entry(*mid)
200 .and_modify(|s| *s = (*s + 0.3).min(1.0))
201 .or_insert(0.3);
202 }
203
204 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
206 for mid in &input.context.recent_memory_ids {
207 if let Ok(links) = graph.get_outgoing(mid) {
208 for link in links.iter().take(8) {
209 recent_map
210 .entry(link.target_id)
211 .and_modify(|s| *s = (*s + 0.15).min(1.0))
212 .or_insert(0.15);
213 }
214 }
215 }
216
217 let act_log = ActivationLogger::new(self.store.db_arc());
219 if let Ok(records) = act_log.read_all() {
220 let mut freq: HashMap<MemoryId, u32> = HashMap::new();
221 for rec in records.iter() {
222 for mid_u64 in &rec.used_memory_ids {
223 *freq.entry(MemoryId(*mid_u64 as u128)).or_default() += 1;
224 }
225 }
226 let max_freq = freq.values().max().copied().unwrap_or(1) as f32;
227 for (mid, count) in freq {
228 let score = (count as f32 / max_freq) * 0.25;
229 recent_map
230 .entry(mid)
231 .and_modify(|s| *s = (*s + score).min(1.0))
232 .or_insert(score);
233 }
234 }
235
236 let mut hits: Vec<(MemoryId, f32)> = recent_map.into_iter().collect();
237 hits.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
238 hits.truncate(params.seed_per_channel as usize);
239 hits
240 };
241
242 let seed_result = multi_channel_seeds(
243 &input.query,
244 &entity_hits,
245 &temporal_hits,
246 &semantic_hits,
247 &topic_hits,
248 &bm25_hits,
249 &binary_hits,
250 &goal_hits,
251 &event_hits,
252 &causal_hits,
253 &recent_hits,
254 params.seed_per_channel as usize,
255 );
256
257 let fused_scores: HashMap<MemoryId, (f32, RecallChannel)> = if seed_result.seeds.is_empty()
259 {
260 let fallback = load_limited_units(self.store.db_arc(), 50);
262 fallback
263 .into_iter()
264 .map(|u| (u.id, (0.3_f32, RecallChannel::RecentActivation)))
265 .collect()
266 } else {
267 rrf_fuse(&seed_result.seeds, ¶ms)
268 };
269
270 let seed_ids: Vec<MemoryId> = fused_scores.keys().cloned().collect();
272 let mut unit_map: HashMap<MemoryId, MemoryUnit> = HashMap::new();
273 for unit in load_units_by_ids(self.store.db_arc(), &seed_ids) {
274 unit_map.insert(unit.id, unit);
275 }
276
277 let importance_map: HashMap<MemoryId, f32> = unit_map
279 .iter()
280 .map(|(id, unit)| (*id, unit.understanding.importance.value()))
281 .collect();
282
283 let graph = hippmem_store::graph::GraphStore::new(self.store.db_arc());
284 let mut links_map: HashMap<MemoryId, Vec<hippmem_core::model::links::AssociationLink>> =
285 HashMap::new();
286
287 for sid in &seed_ids {
289 if let Ok(links) = graph.get_outgoing(sid) {
290 links_map.insert(*sid, links);
291 }
292 }
293
294 let neighbor_ids: Vec<MemoryId> = links_map
296 .values()
297 .flatten()
298 .map(|l| l.target_id)
299 .filter(|tid| !links_map.contains_key(tid))
300 .collect();
301 for nid in &neighbor_ids {
302 if let Ok(links) = graph.get_outgoing(nid) {
303 links_map.insert(*nid, links);
304 }
305 }
306 for unit in load_units_by_ids(self.store.db_arc(), &neighbor_ids) {
308 unit_map.entry(unit.id).or_insert(unit);
309 }
310
311 let activated = spread_multi_hop_fused(&fused_scores, &links_map, ¶ms, &importance_map);
313 let max_k = input.top_k.min(activated.len());
314
315 let extra_ids: Vec<MemoryId> = activated
317 .iter()
318 .map(|(id, _, _)| *id)
319 .filter(|id| !unit_map.contains_key(id))
320 .collect();
321 for unit in load_units_by_ids(self.store.db_arc(), &extra_ids) {
322 unit_map.insert(unit.id, unit);
323 }
324
325 let loaded_units: Vec<MemoryUnit> = activated
327 .iter()
328 .filter_map(|(id, _, _)| unit_map.get(id).cloned())
329 .collect();
330 let mut reranked = hippmem_retrieval::rerank::rerank_by_energy(&activated, &loaded_units);
331
332 apply_question_aware_boost(&input.query, &mut reranked, ¶ms);
335
336 let results: Vec<RetrievalResult> = reranked
338 .iter()
339 .take(max_k)
340 .map(|(_id, energy, trace, unit)| {
341 let matched = deduce_dimensions(trace);
342 let warns = check_warnings(unit, *energy);
343 RetrievalResult {
344 memory: unit.clone(),
345 final_score: *energy,
346 activation_trace: trace.clone(),
347 matched_dimensions: matched,
348 warnings: warns,
349 }
350 })
351 .collect();
352
353 let channel_contributions: Vec<(RecallChannel, u32)> = {
355 let mut map: HashMap<RecallChannel, u32> = HashMap::new();
356 for seed in &seed_result.seeds {
357 *map.entry(seed.channel).or_default() += 1;
358 }
359 map.into_iter().collect()
360 };
361
362 let retrieval_id = {
365 let act_log = ActivationLogger::new(self.store.db_arc());
366 let used_ids: Vec<u64> = results.iter().map(|r| r.memory.id.0 as u64).collect();
367 let now_ms =
368 if let Ok(t) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
369 t.as_millis() as i64
370 } else {
371 0
372 };
373 let _ = act_log.record(&hippmem_store::activation_log::ActivationRecord {
374 retrieval_id: now_ms as u64,
375 used_memory_ids: used_ids,
376 signal: "retrieve".into(),
377 recorded_at_ms: now_ms,
378 });
379 now_ms as u64
380 };
381
382 Ok(RetrieveOutput {
383 retrieval_id,
384 results,
385 trace: crate::RetrievalTrace {
386 seeds: seed_result
387 .seeds
388 .iter()
389 .map(|s| crate::SeedRecord {
390 id: s.id,
391 channel: s.channel,
392 initial_energy: s.score,
393 rank_in_channel: s.rank_in_channel,
394 })
395 .collect(),
396 steps: activated
397 .iter()
398 .flat_map(|(_, _, trace)| trace.clone())
399 .collect(),
400 hops_used: 0,
401 merged_count: 0,
402 },
403 diagnostics: crate::RetrievalDiagnostics {
404 channel_contributions,
405 reranked: true,
406 pruned_branches: 0,
407 backend_used: crate::BackendUsage {
408 embedder: self.embedder.backend_id().to_string(),
409 reranker: Some("rule".into()),
410 },
411 latency_ms: 0,
412 },
413 })
414 }
415}
416
417#[derive(Debug, Clone, Copy, PartialEq)]
423enum QuestionType {
424 Why,
426 How,
428 What,
430 Correction,
432 Preference,
434 None,
436}
437
438fn detect_question_type(query: &str) -> QuestionType {
444 let q = query.to_lowercase();
445
446 for lang in active_locales() {
448 if let Some((before, after)) = lang.change_pair {
449 if q.contains(before) && q.contains(after) {
450 return QuestionType::Correction;
451 }
452 }
453 }
454
455 for lang in active_locales() {
459 for keyword in lang.q_correction {
460 if q.contains(keyword) {
461 return QuestionType::Correction;
462 }
463 }
464 }
465 for lang in active_locales() {
466 for keyword in lang.q_preference {
467 if q.contains(keyword) {
468 return QuestionType::Preference;
469 }
470 }
471 }
472 for lang in active_locales() {
473 for keyword in lang.q_why {
474 if q.contains(keyword) {
475 return QuestionType::Why;
476 }
477 }
478 }
479 for lang in active_locales() {
480 for keyword in lang.q_how {
481 if q.contains(keyword) {
482 return QuestionType::How;
483 }
484 }
485 }
486 for lang in active_locales() {
487 for keyword in lang.q_what {
488 if q.contains(keyword) {
489 return QuestionType::What;
490 }
491 }
492 }
493 QuestionType::None
494}
495
496fn explanatory_pattern_score(text: &str) -> f32 {
498 let mut score = 0.0f32;
499 for lang in active_locales() {
500 for (pattern, boost) in lang.explanatory {
501 if text.contains(pattern) {
502 score += boost;
503 }
504 }
505 }
506 score.min(0.20) }
508
509fn content_type_boost(query: &str) -> Vec<(hippmem_core::model::unit::ContentType, f32)> {
519 let qt = detect_question_type(query);
520 let mut boosts = Vec::new();
521
522 match qt {
523 QuestionType::Correction => {
524 boosts.push((hippmem_core::model::unit::ContentType::Correction, 0.12));
526 }
527 QuestionType::Preference => {
528 boosts.push((hippmem_core::model::unit::ContentType::Preference, 0.08));
530 boosts.push((hippmem_core::model::unit::ContentType::Decision, 0.04));
532 }
533 QuestionType::Why => {
534 boosts.push((hippmem_core::model::unit::ContentType::Decision, 0.08));
536 boosts.push((hippmem_core::model::unit::ContentType::TaskState, 0.08));
537 }
538 QuestionType::How => {
539 boosts.push((hippmem_core::model::unit::ContentType::TaskState, 0.08));
541 }
542 QuestionType::What => {
543 boosts.push((
549 hippmem_core::model::unit::ContentType::ProjectKnowledge,
550 0.15,
551 ));
552 }
553 QuestionType::None => {
554 }
556 }
557
558 if qt != QuestionType::Correction {
560 let q = query.to_lowercase();
561 let has_correction_signal = active_locales().iter().any(|lang| {
562 lang.q_correction.iter().any(|kw| q.contains(kw))
563 || lang
564 .change_pair
565 .is_some_and(|(b, a)| q.contains(b) && q.contains(a))
566 });
567 if has_correction_signal {
568 boosts.push((hippmem_core::model::unit::ContentType::Correction, 0.10));
569 }
570 }
571
572 boosts
573}
574
575fn apply_question_aware_boost(
585 query: &str,
586 reranked: &mut [(MemoryId, f32, Vec<ActivationStep>, MemoryUnit)],
587 params: &hippmem_core::config::AlgoParams,
588) {
589 let qt = detect_question_type(query);
590 let ct_boosts = content_type_boost(query);
591 let cap = params.seed_energy_cap;
592 let what_subject: Option<String> = if qt == QuestionType::What {
594 extract_subject_for_what_query(query)
595 } else {
596 None
597 };
598
599 match qt {
601 QuestionType::Why => {
602 for (_, energy, _, unit) in reranked.iter_mut() {
603 let boost = explanatory_pattern_score(&unit.content.raw);
604 if boost > 0.0 {
605 *energy = (*energy + boost).min(cap);
606 }
607 }
608 }
609 QuestionType::Correction
610 | QuestionType::Preference
611 | QuestionType::How
612 | QuestionType::What
613 | QuestionType::None => {
614 }
616 }
617
618 if !ct_boosts.is_empty() {
622 for (_, energy, _, unit) in reranked.iter_mut() {
623 for (ct, boost) in &ct_boosts {
624 if unit.content.content_type != *ct {
625 continue;
626 }
627 if qt == QuestionType::What
629 && *ct == hippmem_core::model::unit::ContentType::ProjectKnowledge
630 {
631 if let Some(ref subject) = what_subject {
632 let content_lower = unit.content.raw.to_lowercase();
633 if !content_lower.contains(&subject.to_lowercase()) {
634 break; }
636 }
637 }
638 *energy = (*energy + boost).min(cap);
639 break; }
641 }
642 }
643
644 let keywords = extract_discriminative_keywords(query);
650 if !keywords.is_empty() {
651 for (_, energy, _, unit) in reranked.iter_mut() {
652 let mut kw_bonus = 0.0f32;
653 let content_lower = unit.content.raw.to_lowercase();
654 for kw in &keywords {
655 if content_lower.contains(&kw.to_lowercase()) {
656 kw_bonus += 0.04;
657 }
658 }
659 if kw_bonus > 0.0 {
660 *energy = (*energy + kw_bonus.min(0.08)).min(cap);
661 }
662 }
663 }
664
665 if qt == QuestionType::What {
670 if let Some(ref subject) = extract_subject_for_what_query(query) {
671 let subject_lower = subject.to_lowercase();
672 for (_, energy, _, unit) in reranked.iter_mut() {
673 let content_lower = unit.content.raw.to_lowercase();
674 let has_definition = active_locales().iter().any(|lang| {
675 lang.definition_patterns
676 .iter()
677 .any(|pat| content_lower.contains(&format!("{} {pat}", subject_lower)))
678 });
679 if has_definition {
680 *energy = (*energy + 0.05).min(cap);
681 }
682 }
683 }
684 }
685
686 reranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
688}
689
690fn extract_subject_for_what_query(query: &str) -> Option<String> {
699 let q = query.to_lowercase();
700 for lang in active_locales() {
701 for delimiter in lang.what_delimiters {
702 if let Some(pos) = q.find(delimiter) {
703 let prefix = &q[..pos];
704 let subject = if let Some(particle) = lang.possessive_particle {
705 prefix
708 .rsplit(particle)
709 .next()
710 .unwrap_or("")
711 .rsplit(|c: char| c.is_whitespace() || c == '?' || c == '?')
712 .next()
713 .unwrap_or("")
714 .trim()
715 .to_string()
716 } else {
717 prefix
718 .rsplit(|c: char| c.is_whitespace() || c == '?' || c == '?')
719 .next()
720 .unwrap_or("")
721 .trim()
722 .to_string()
723 };
724 if subject.len() >= 2 {
725 return Some(subject);
726 }
727 return None;
728 }
729 }
730 }
731 None
732}
733
734fn extract_discriminative_keywords(query: &str) -> Vec<String> {
742 let stop_words: Vec<&str> = active_locales()
744 .iter()
745 .flat_map(|lang| lang.stop_words.iter().copied())
746 .collect();
747
748 let mut keywords: Vec<String> = Vec::new();
749 let mut seen = std::collections::HashSet::new();
750
751 for word in query.split(|c: char| !c.is_alphanumeric()) {
753 let is_keyword = (word.len() >= 2 && word.chars().any(|c| c.is_uppercase()))
754 || (word.chars().all(|c| c.is_ascii_alphabetic()) && word.len() >= 3);
755 if is_keyword
756 && !stop_words.contains(&word.to_lowercase().as_str())
757 && seen.insert(word.to_string())
758 {
759 keywords.push(word.to_string());
760 }
761 }
762
763 for word in query
765 .split(|c: char| c.is_whitespace() || c.is_ascii_punctuation() || c == '?' || c == '?')
766 {
767 let trimmed = word.trim();
768 if trimmed.chars().count() >= 2
769 && trimmed.chars().all(|c| c as u32 > 0x2E80) && !stop_words.contains(&trimmed)
771 && seen.insert(trimmed.to_string())
772 {
773 keywords.push(trimmed.to_string());
774 }
775 }
776
777 keywords.truncate(5); keywords
779}
780
781fn query_binary_code(text: &str) -> [u8; 16] {
783 let bc0 = stable_hash64(&format!("bc_0_{}", text));
784 let bc1 = stable_hash64(&format!("bc_1_{}", text));
785 let mut bytes = [0u8; 16];
786 bytes[..8].copy_from_slice(&bc0.to_le_bytes());
787 bytes[8..].copy_from_slice(&bc1.to_le_bytes());
788 bytes
789}
790
791fn temporal_bucket_keys(ts: hippmem_core::time::Timestamp) -> Vec<u32> {
793 let ms = ts.0;
794 vec![
795 (ms / 3_600_000) as u32, (ms / 86_400_000) as u32, (ms / 604_800_000) as u32, ]
799}
800
801pub(crate) fn load_all_units(db: std::sync::Arc<redb::Database>) -> Vec<MemoryUnit> {
802 use redb::ReadableDatabase;
803 use redb::ReadableTable;
804 let mut units = Vec::new();
805 let read_txn = db.begin_read().expect("read transaction should succeed");
806 let table = read_txn
807 .open_table(hippmem_store::store::MEMORY_KV)
808 .expect("memory_kv table should exist");
809 let iter = table.iter().expect("iter should succeed");
810 for entry in iter.flatten() {
811 let (_key, value) = entry;
812 if let Ok((unit, _)) = bincode::serde::decode_from_slice::<MemoryUnit, _>(
813 value.value(),
814 bincode::config::standard(),
815 ) {
816 units.push(unit);
817 }
818 }
819 units
820}
821
822fn load_units_by_ids(db: std::sync::Arc<redb::Database>, ids: &[MemoryId]) -> Vec<MemoryUnit> {
824 if ids.is_empty() {
825 return vec![];
826 }
827 use redb::ReadableDatabase;
828 let mut units = Vec::new();
829 let read_txn = db.begin_read().expect("read transaction should succeed");
830 let table = read_txn
831 .open_table(hippmem_store::store::MEMORY_KV)
832 .expect("memory_kv table should exist");
833 for id in ids {
834 if let Some(value) = table.get(id.0).expect("get should succeed") {
835 if let Ok((unit, _)) = bincode::serde::decode_from_slice::<MemoryUnit, _>(
836 value.value(),
837 bincode::config::standard(),
838 ) {
839 units.push(unit);
840 }
841 }
842 }
843 units
844}
845
846fn extract_query_goals(text: &str) -> Vec<String> {
848 let mut goals = Vec::new();
849 for lang in active_locales() {
850 for m in lang.goal_markers {
851 if text.contains(m) {
852 goals.push(format!("goal_marker:{m}"));
853 }
854 }
855 }
856 goals
857}
858
859fn extract_query_events(text: &str) -> Vec<String> {
861 let mut events = Vec::new();
862 for lang in active_locales() {
863 for m in lang.event_markers {
864 if text.contains(m) {
865 events.push(format!("event_marker:{m}"));
866 }
867 }
868 }
869 events
870}
871
872fn load_limited_units(db: std::sync::Arc<redb::Database>, limit: usize) -> Vec<MemoryUnit> {
874 use redb::ReadableDatabase;
875 use redb::ReadableTable;
876 let mut units = Vec::new();
877 let read_txn = db.begin_read().expect("read transaction should succeed");
878 let table = read_txn
879 .open_table(hippmem_store::store::MEMORY_KV)
880 .expect("memory_kv table should exist");
881 let iter = table.iter().expect("iter should succeed");
882 for entry in iter.flatten().take(limit) {
883 let (_key, value) = entry;
884 if let Ok((unit, _)) = bincode::serde::decode_from_slice::<MemoryUnit, _>(
885 value.value(),
886 bincode::config::standard(),
887 ) {
888 units.push(unit);
889 }
890 }
891 units
892}