1use std::collections::{BTreeMap, BTreeSet};
23
24use crate::associative_persistence::AssociativeMemory;
25use crate::memory::{MemoryEvent, MemoryStore};
26use support::{
27 contains_any, estimate_event_bytes, lower_opt, normalized, percent_ceil,
28 required_reclaim_bytes, selected_bytes,
29};
30
31pub mod cues;
32mod learning;
33pub mod lexicon;
34mod support;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct DreamingConfig {
38 pub daydreaming_enabled: bool,
40 pub target_free_ratio_percent: u8,
42 pub storage_capacity_bytes: Option<u64>,
44 pub free_bytes: Option<u64>,
46 pub incoming_bytes: u64,
48}
49
50impl Default for DreamingConfig {
51 fn default() -> Self {
52 Self {
53 daydreaming_enabled: true,
54 target_free_ratio_percent: 20,
55 storage_capacity_bytes: None,
56 free_bytes: None,
57 incoming_bytes: 0,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
63pub enum DreamingDurability {
64 IrreplaceableRaw,
67 RetainedLearning,
69 DeletedConversation,
71 RecomputableCache,
73 RecomputableIntermediate,
75}
76
77impl DreamingDurability {
78 #[must_use]
79 pub const fn is_reclaimable(self) -> bool {
80 matches!(
81 self,
82 Self::DeletedConversation | Self::RecomputableCache | Self::RecomputableIntermediate
83 )
84 }
85
86 const fn pressure_priority(self) -> u8 {
87 match self {
88 Self::DeletedConversation => 0,
89 Self::RecomputableCache => 1,
90 Self::RecomputableIntermediate => 2,
91 Self::RetainedLearning | Self::IrreplaceableRaw => 9,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum DreamingActionKind {
98 PurgeDeletedConversation,
99 RemoveDuplicateRecomputable,
100 EvictLowUseRecomputable,
101 ForgetCoveredSpecific,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct TopicFrequency {
110 pub topic: String,
111 pub interactions: usize,
113 pub task_events: usize,
115 pub requirement_events: usize,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct LearnedRequirement {
123 pub topic: String,
124 pub statement: String,
125 pub source_event_ids: Vec<String>,
126 pub occurrences: usize,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct MetaAlgorithmAmendment {
137 pub id: String,
138 pub topic: String,
139 pub rule: String,
140 pub source_requirement_ids: Vec<String>,
141 pub covered_event_ids: Vec<String>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct DreamingCandidateTask {
147 pub topic: String,
148 pub source_event_id: String,
149 pub input: String,
150 pub expected_output: String,
151 pub simulated_output: String,
152 pub passed: bool,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct DreamingPattern {
159 pub topic: String,
160 pub structure: String,
161 pub occurrences: usize,
162 pub source_event_ids: Vec<String>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct DreamingSynthesizedTask {
173 pub topic: String,
174 pub structure: String,
175 pub input: String,
176 pub answer: String,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct DreamingAction {
181 pub kind: DreamingActionKind,
182 pub event_id: String,
183 pub conversation_id: Option<String>,
184 pub estimated_bytes: u64,
185 pub usage_count: usize,
186 pub reason: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct DreamingEventObservation {
191 pub event_id: String,
192 pub durability: DreamingDurability,
193 pub usage_count: usize,
194 pub estimated_bytes: u64,
195 pub duplicate_key: Option<String>,
196 pub topic: Option<String>,
198 pub covered_by_amendment: bool,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct DreamingPlan {
205 pub daydreaming_enabled: bool,
206 pub target_free_ratio_percent: u8,
207 pub storage_capacity_bytes: Option<u64>,
208 pub free_bytes: Option<u64>,
209 pub incoming_bytes: u64,
210 pub target_free_bytes: Option<u64>,
211 pub required_reclaim_bytes: u64,
212 pub selected_reclaim_bytes: u64,
213 pub total_reclaimable_bytes: u64,
214 pub requires_bigger_storage: bool,
215 pub actions: Vec<DreamingAction>,
216 pub observations: Vec<DreamingEventObservation>,
217 pub topics: Vec<TopicFrequency>,
219 pub learned_requirements: Vec<LearnedRequirement>,
221 pub amendments: Vec<MetaAlgorithmAmendment>,
223 pub candidate_tasks: Vec<DreamingCandidateTask>,
225 pub patterns: Vec<DreamingPattern>,
227 pub synthesized_tasks: Vec<DreamingSynthesizedTask>,
230}
231
232impl DreamingPlan {
233 #[must_use]
234 pub fn event_usage(&self, event_id: &str) -> Option<usize> {
235 self.observations
236 .iter()
237 .find(|observation| observation.event_id == event_id)
238 .map(|observation| observation.usage_count)
239 }
240
241 #[must_use]
243 pub fn topic_interactions(&self, topic: &str) -> Option<usize> {
244 self.topics
245 .iter()
246 .find(|frequency| frequency.topic == topic)
247 .map(|frequency| frequency.interactions)
248 }
249
250 #[must_use]
252 pub fn amendment_for(&self, topic: &str) -> Option<&MetaAlgorithmAmendment> {
253 self.amendments
254 .iter()
255 .find(|amendment| amendment.topic == topic)
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Default)]
260pub struct DreamingOutcome {
261 pub removed_events: usize,
262 pub estimated_reclaimed_bytes: u64,
263 pub learned_amendments: usize,
265 pub learned_patterns: usize,
267 pub recorded_failures: usize,
270 pub recorded_trials: usize,
273}
274
275#[must_use]
276pub fn plan_memory_dreaming(events: &[MemoryEvent], config: &DreamingConfig) -> DreamingPlan {
277 let target_free_ratio_percent = config.target_free_ratio_percent.min(100);
278 let target_free_bytes = config
279 .storage_capacity_bytes
280 .map(|capacity| percent_ceil(capacity, target_free_ratio_percent));
281 let required_reclaim_bytes =
282 required_reclaim_bytes(target_free_bytes, config.free_bytes, config.incoming_bytes);
283
284 if !config.daydreaming_enabled {
285 return DreamingPlan {
286 daydreaming_enabled: false,
287 target_free_ratio_percent,
288 storage_capacity_bytes: config.storage_capacity_bytes,
289 free_bytes: config.free_bytes,
290 incoming_bytes: config.incoming_bytes,
291 target_free_bytes,
292 required_reclaim_bytes,
293 selected_reclaim_bytes: 0,
294 total_reclaimable_bytes: 0,
295 requires_bigger_storage: required_reclaim_bytes > 0,
296 actions: Vec::new(),
297 observations: Vec::new(),
298 topics: Vec::new(),
299 learned_requirements: Vec::new(),
300 amendments: Vec::new(),
301 candidate_tasks: Vec::new(),
302 patterns: Vec::new(),
303 synthesized_tasks: Vec::new(),
304 };
305 }
306
307 let deleted_conversations = deleted_conversation_ids(events);
308 let usage_counts = usage_counts(events);
309 let mut observations = Vec::with_capacity(events.len());
310 let mut reclaimable_candidates = Vec::new();
311 let mut duplicate_groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
312
313 for (index, event) in events.iter().enumerate() {
314 let durability = classify_event(event, &deleted_conversations);
315 let estimated_bytes = estimate_event_bytes(event);
316 let duplicate_key = duplicate_key(event, durability);
317 if let Some(key) = duplicate_key.clone() {
318 duplicate_groups.entry(key).or_default().push(index);
319 }
320 if durability.is_reclaimable() && !event.id.is_empty() {
321 reclaimable_candidates.push(index);
322 }
323 observations.push(DreamingEventObservation {
324 event_id: event.id.clone(),
325 durability,
326 usage_count: usage_counts[index],
327 estimated_bytes,
328 duplicate_key,
329 topic: learning::event_topic(event),
330 covered_by_amendment: false,
331 });
332 }
333
334 let learning = learning::learn_from_memory(events, &observations, &deleted_conversations);
337 let topics = learning.topics;
338 let learned_requirements = learning.requirements;
339 let amendments = learning.amendments;
340 let candidate_tasks = learning.candidate_tasks;
341 let patterns = learning.patterns;
342 let synthesized_tasks = learning.synthesized_tasks;
343 let covered_event_ids: BTreeSet<&str> = amendments
344 .iter()
345 .flat_map(|amendment| amendment.covered_event_ids.iter().map(String::as_str))
346 .collect();
347 for observation in &mut observations {
348 observation.covered_by_amendment =
349 covered_event_ids.contains(observation.event_id.as_str());
350 }
351
352 let total_reclaimable_bytes = reclaimable_candidates
353 .iter()
354 .map(|index| observations[*index].estimated_bytes)
355 .sum();
356 let mut actions = Vec::new();
357 let mut selected_event_ids = BTreeSet::new();
358
359 for event_index in deleted_event_indices(events, &deleted_conversations) {
368 push_action_once(
369 &mut actions,
370 &mut selected_event_ids,
371 DreamingAction {
372 kind: DreamingActionKind::PurgeDeletedConversation,
373 event_id: events[event_index].id.clone(),
374 conversation_id: events[event_index].conversation_id.clone(),
375 estimated_bytes: observations[event_index].estimated_bytes,
376 usage_count: observations[event_index].usage_count,
377 reason: String::from("event belongs to a conversation already marked deleted"),
378 },
379 );
380 }
381
382 for group in duplicate_groups.values() {
383 if group.len() < 2 {
384 continue;
385 }
386 let keep = group
387 .iter()
388 .copied()
389 .max_by(|left, right| {
390 usage_counts[*left]
391 .cmp(&usage_counts[*right])
392 .then_with(|| {
393 observations[*left]
394 .estimated_bytes
395 .cmp(&observations[*right].estimated_bytes)
396 })
397 .then_with(|| right.cmp(left))
398 })
399 .unwrap_or(group[0]);
400 for event_index in group.iter().copied().filter(|index| *index != keep) {
401 push_action_once(
402 &mut actions,
403 &mut selected_event_ids,
404 DreamingAction {
405 kind: DreamingActionKind::RemoveDuplicateRecomputable,
406 event_id: events[event_index].id.clone(),
407 conversation_id: events[event_index].conversation_id.clone(),
408 estimated_bytes: observations[event_index].estimated_bytes,
409 usage_count: observations[event_index].usage_count,
410 reason: format!(
411 "duplicate recomputable event; retained {} with usage {}",
412 events[keep].id, usage_counts[keep]
413 ),
414 },
415 );
416 }
417 }
418
419 let mut selected_reclaim_bytes = selected_bytes(&actions);
420 if required_reclaim_bytes > selected_reclaim_bytes {
421 let mut pressure_candidates = reclaimable_candidates
422 .into_iter()
423 .filter(|index| !selected_event_ids.contains(events[*index].id.as_str()))
424 .collect::<Vec<_>>();
425 pressure_candidates.sort_by(|left, right| {
426 observations[*right]
428 .covered_by_amendment
429 .cmp(&observations[*left].covered_by_amendment)
430 .then_with(|| {
431 observations[*left]
432 .usage_count
433 .cmp(&observations[*right].usage_count)
434 })
435 .then_with(|| {
436 observations[*left]
437 .durability
438 .pressure_priority()
439 .cmp(&observations[*right].durability.pressure_priority())
440 })
441 .then_with(|| {
442 observations[*right]
443 .estimated_bytes
444 .cmp(&observations[*left].estimated_bytes)
445 })
446 .then_with(|| left.cmp(right))
447 });
448
449 for event_index in pressure_candidates {
450 if selected_reclaim_bytes >= required_reclaim_bytes {
451 break;
452 }
453 let (kind, reason) = if observations[event_index].covered_by_amendment {
454 (
455 DreamingActionKind::ForgetCoveredSpecific,
456 String::from(
457 "specific task/test-run record reproducible from a retained meta-algorithm amendment",
458 ),
459 )
460 } else {
461 (
462 DreamingActionKind::EvictLowUseRecomputable,
463 String::from(
464 "lowest-use recomputable event selected to satisfy the free-space target",
465 ),
466 )
467 };
468 push_action_once(
469 &mut actions,
470 &mut selected_event_ids,
471 DreamingAction {
472 kind,
473 event_id: events[event_index].id.clone(),
474 conversation_id: events[event_index].conversation_id.clone(),
475 estimated_bytes: observations[event_index].estimated_bytes,
476 usage_count: observations[event_index].usage_count,
477 reason,
478 },
479 );
480 selected_reclaim_bytes = selected_bytes(&actions);
481 }
482 }
483
484 selected_reclaim_bytes = selected_bytes(&actions);
485 DreamingPlan {
486 daydreaming_enabled: true,
487 target_free_ratio_percent,
488 storage_capacity_bytes: config.storage_capacity_bytes,
489 free_bytes: config.free_bytes,
490 incoming_bytes: config.incoming_bytes,
491 target_free_bytes,
492 required_reclaim_bytes,
493 selected_reclaim_bytes,
494 total_reclaimable_bytes,
495 requires_bigger_storage: required_reclaim_bytes > selected_reclaim_bytes,
496 actions,
497 observations,
498 topics,
499 learned_requirements,
500 amendments,
501 candidate_tasks,
502 patterns,
503 synthesized_tasks,
504 }
505}
506
507#[must_use]
508pub fn apply_dreaming_plan(store: &mut MemoryStore, plan: &DreamingPlan) -> DreamingOutcome {
509 let selected_ids = plan
510 .actions
511 .iter()
512 .map(|action| action.event_id.as_str())
513 .collect::<BTreeSet<_>>();
514
515 let existing_ids: BTreeSet<String> = store
519 .events()
520 .iter()
521 .map(|event| event.id.clone())
522 .collect();
523 let new_amendments: Vec<MemoryEvent> = plan
524 .amendments
525 .iter()
526 .filter(|amendment| !existing_ids.contains(&amendment.id))
527 .map(amendment_event)
528 .collect();
529 let learned_amendments = new_amendments.len();
530 let new_patterns = plan
531 .patterns
532 .iter()
533 .map(pattern_event)
534 .filter(|event| !existing_ids.contains(&event.id))
535 .collect::<Vec<_>>();
536 let learned_patterns = new_patterns.len();
537 let new_failures = plan
540 .candidate_tasks
541 .iter()
542 .filter(|candidate| !candidate.passed)
543 .map(candidate_failure_event)
544 .filter(|event| !existing_ids.contains(&event.id))
545 .collect::<Vec<_>>();
546 let recorded_failures = new_failures.len();
547 let new_trials = plan
548 .synthesized_tasks
549 .iter()
550 .map(synthesized_trial_event)
551 .filter(|event| !existing_ids.contains(&event.id))
552 .collect::<Vec<_>>();
553 let recorded_trials = new_trials.len();
554
555 if selected_ids.is_empty()
556 && new_amendments.is_empty()
557 && new_patterns.is_empty()
558 && new_failures.is_empty()
559 && new_trials.is_empty()
560 {
561 return DreamingOutcome {
562 removed_events: 0,
563 estimated_reclaimed_bytes: 0,
564 learned_amendments: 0,
565 learned_patterns: 0,
566 recorded_failures: 0,
567 recorded_trials: 0,
568 };
569 }
570
571 let initial_len = store.len();
572 let mut retained = store
573 .events()
574 .iter()
575 .filter(|event| !selected_ids.contains(event.id.as_str()))
576 .cloned()
577 .collect::<Vec<_>>();
578 let removed_events = initial_len - retained.len();
579 retained.extend(new_amendments);
580 retained.extend(new_patterns);
581 retained.extend(new_failures);
582 retained.extend(new_trials);
583 *store = MemoryStore::from_events(retained);
584 DreamingOutcome {
585 removed_events,
586 estimated_reclaimed_bytes: selected_bytes(&plan.actions),
587 learned_amendments,
588 learned_patterns,
589 recorded_failures,
590 recorded_trials,
591 }
592}
593
594fn candidate_failure_event(candidate: &DreamingCandidateTask) -> MemoryEvent {
599 let identity = format!("{}\0{}", candidate.source_event_id, candidate.input);
600 MemoryEvent {
601 id: crate::engine::stable_id("dreaming_failure", &identity),
602 kind: Some(String::from("dreaming_candidate_failure")),
603 role: Some(String::from("system")),
604 intent: Some(String::from("generalize")),
605 inputs: Some(candidate.input.clone()),
606 outputs: Some(candidate.expected_output.clone()),
607 content: Some(format!(
608 "Replay of {} on topic {} diverged from the stored output; kept for refinement. Simulated: {}",
609 candidate.source_event_id, candidate.topic, candidate.simulated_output
610 )),
611 demo_label: Some(candidate.topic.clone()),
612 evidence: vec![candidate.source_event_id.clone()],
613 ..MemoryEvent::default()
614 }
615}
616
617fn synthesized_trial_event(trial: &DreamingSynthesizedTask) -> MemoryEvent {
619 let identity = format!("{}\0{}", trial.topic, trial.input);
620 MemoryEvent {
621 id: crate::engine::stable_id("dreaming_trial", &identity),
622 kind: Some(String::from("dreaming_trial")),
623 role: Some(String::from("system")),
624 intent: Some(String::from("solve")),
625 inputs: Some(trial.input.clone()),
626 outputs: Some(trial.answer.clone()),
627 content: Some(format!(
628 "Trial synthesized from recurring structure {} on most-used topic {}",
629 trial.structure, trial.topic
630 )),
631 demo_label: Some(trial.topic.clone()),
632 evidence: Vec::new(),
633 ..MemoryEvent::default()
634 }
635}
636
637fn pattern_event(pattern: &DreamingPattern) -> MemoryEvent {
638 let identity = format!("{}\0{}", pattern.topic, pattern.structure);
639 MemoryEvent {
640 id: crate::engine::stable_id("dreaming_pattern", &identity),
641 kind: Some(String::from("dreaming_pattern")),
642 role: Some(String::from("system")),
643 intent: Some(String::from("generalize")),
644 inputs: Some(format!("topic={}", pattern.topic)),
645 outputs: Some(format!("structure={}", pattern.structure)),
646 content: Some(format!(
647 "Recurring structure {} observed {} time(s)",
648 pattern.structure, pattern.occurrences
649 )),
650 demo_label: Some(pattern.topic.clone()),
651 evidence: pattern.source_event_ids.clone(),
652 ..MemoryEvent::default()
653 }
654}
655
656fn amendment_event(amendment: &MetaAlgorithmAmendment) -> MemoryEvent {
658 MemoryEvent {
659 id: amendment.id.clone(),
660 kind: Some(String::from("meta_algorithm_amendment")),
661 role: Some(String::from("system")),
662 intent: Some(String::from("generalize")),
663 content: Some(amendment.rule.clone()),
664 inputs: Some(format!("topic={}", amendment.topic)),
665 outputs: Some(format!("rule={}", amendment.rule)),
666 demo_label: Some(amendment.topic.clone()),
667 conversation_title: Some(amendment.topic.clone()),
668 evidence: amendment
669 .source_requirement_ids
670 .iter()
671 .cloned()
672 .chain([String::from("recipe:data/meta/dreaming-recipe.lino")])
673 .collect(),
674 ..MemoryEvent::default()
675 }
676}
677
678#[must_use]
679pub fn render_dreaming_plan(plan: &DreamingPlan) -> String {
680 let mut lines = Vec::new();
681 lines.push(String::from("memory_dreaming_plan"));
682 lines.push(format!(" enabled: {}", plan.daydreaming_enabled));
683 lines.push(format!(
684 " target_free_ratio_percent: {}",
685 plan.target_free_ratio_percent
686 ));
687 if let Some(capacity) = plan.storage_capacity_bytes {
688 lines.push(format!(" storage_capacity_bytes: {capacity}"));
689 }
690 if let Some(free) = plan.free_bytes {
691 lines.push(format!(" free_bytes: {free}"));
692 }
693 if let Some(target) = plan.target_free_bytes {
694 lines.push(format!(" target_free_bytes: {target}"));
695 }
696 lines.push(format!(" incoming_bytes: {}", plan.incoming_bytes));
697 lines.push(format!(
698 " required_reclaim_bytes: {}",
699 plan.required_reclaim_bytes
700 ));
701 lines.push(format!(
702 " selected_reclaim_bytes: {}",
703 plan.selected_reclaim_bytes
704 ));
705 lines.push(format!(
706 " total_reclaimable_bytes: {}",
707 plan.total_reclaimable_bytes
708 ));
709 lines.push(format!(
710 " requires_bigger_storage: {}",
711 plan.requires_bigger_storage
712 ));
713 if plan.actions.is_empty() {
714 lines.push(String::from(" action: none"));
715 } else {
716 for action in &plan.actions {
717 lines.push(format!(
718 " action {:?} event={} bytes={} usage={} reason={}",
719 action.kind,
720 action.event_id,
721 action.estimated_bytes,
722 action.usage_count,
723 action.reason
724 ));
725 }
726 }
727
728 if plan.topics.is_empty() {
729 lines.push(String::from(" topic: none"));
730 } else {
731 for topic in &plan.topics {
732 lines.push(format!(
733 " topic {} interactions={} tasks={} requirements={}",
734 topic.topic, topic.interactions, topic.task_events, topic.requirement_events
735 ));
736 }
737 }
738
739 for requirement in &plan.learned_requirements {
740 lines.push(format!(
741 " learned_requirement topic={} occurrences={} statement={}",
742 requirement.topic, requirement.occurrences, requirement.statement
743 ));
744 }
745
746 for amendment in &plan.amendments {
747 lines.push(format!(
748 " meta_algorithm_amendment id={} topic={} covers={} rule={}",
749 amendment.id,
750 amendment.topic,
751 amendment.covered_event_ids.len(),
752 amendment.rule
753 ));
754 }
755 for candidate in &plan.candidate_tasks {
756 lines.push(format!(
757 " candidate_task topic={} source={} passed={}",
758 candidate.topic, candidate.source_event_id, candidate.passed
759 ));
760 }
761 for pattern in &plan.patterns {
762 lines.push(format!(
763 " learned_pattern topic={} occurrences={} structure={}",
764 pattern.topic, pattern.occurrences, pattern.structure
765 ));
766 }
767 for trial in &plan.synthesized_tasks {
768 lines.push(format!(
769 " synthesized_trial topic={} structure={} input={}",
770 trial.topic, trial.structure, trial.input
771 ));
772 }
773
774 lines.join("\n")
775}
776
777#[must_use]
785pub fn compose_recipe_with_amendments(events: &[MemoryEvent]) -> String {
786 let base = lexicon::load_data_document(
787 "dreaming-recipe.lino",
788 include_str!("../data/meta/dreaming-recipe.lino"),
789 );
790 let mut composed = base.trim_end().to_owned();
791 for amendment in crate::dreaming_application::retained_amendments(events) {
792 let _ = std::fmt::Write::write_fmt(
793 &mut composed,
794 format_args!(
795 "\n meta_amendment {}\n topic: {}\n rule: {}\n applied_by: fn_solve_with_standing_requirements\n",
796 amendment.id, amendment.topic, amendment.rule
797 ),
798 );
799 }
800 composed.push('\n');
801 composed
802}
803
804fn push_action_once(
805 actions: &mut Vec<DreamingAction>,
806 selected_event_ids: &mut BTreeSet<String>,
807 action: DreamingAction,
808) {
809 if action.event_id.is_empty() || !selected_event_ids.insert(action.event_id.clone()) {
810 return;
811 }
812 actions.push(action);
813}
814
815fn deleted_conversation_ids(events: &[MemoryEvent]) -> BTreeSet<String> {
816 events
817 .iter()
818 .filter(|event| event.kind.as_deref() == Some("conversation_deleted"))
819 .filter_map(|event| event.conversation_id.as_deref())
820 .map(ToOwned::to_owned)
821 .collect()
822}
823
824fn deleted_event_indices(
825 events: &[MemoryEvent],
826 deleted_conversations: &BTreeSet<String>,
827) -> Vec<usize> {
828 events
829 .iter()
830 .enumerate()
831 .filter(|(_, event)| {
832 event
833 .conversation_id
834 .as_deref()
835 .is_some_and(|id| deleted_conversations.contains(id))
836 })
837 .map(|(index, _)| index)
838 .collect()
839}
840
841fn classify_event(
842 event: &MemoryEvent,
843 deleted_conversations: &BTreeSet<String>,
844) -> DreamingDurability {
845 if event
846 .conversation_id
847 .as_deref()
848 .is_some_and(|id| deleted_conversations.contains(id))
849 {
850 return DreamingDurability::DeletedConversation;
851 }
852
853 let cues = lexicon::lexicon();
856 let kind = lower_opt(event.kind.as_deref());
857 let tool = lower_opt(event.tool.as_deref());
858 let content = lower_opt(event.content.as_deref());
859 let evidence = event.evidence.join("\n").to_lowercase();
860 if contains_any(&kind, &cues.learning_kind_cues)
861 || contains_any(&content, &cues.learning_content_cues)
862 || evidence.contains("learning")
863 {
864 return DreamingDurability::RetainedLearning;
865 }
866
867 if contains_any(&kind, &cues.cache_kind_cues) || contains_any(&tool, &cues.cache_tool_cues) {
868 return DreamingDurability::RecomputableCache;
869 }
870
871 if contains_any(&kind, &cues.intermediate_kind_cues) {
872 return DreamingDurability::RecomputableIntermediate;
873 }
874
875 DreamingDurability::IrreplaceableRaw
876}
877
878fn duplicate_key(event: &MemoryEvent, durability: DreamingDurability) -> Option<String> {
879 if !matches!(
880 durability,
881 DreamingDurability::RecomputableCache | DreamingDurability::RecomputableIntermediate
882 ) {
883 return None;
884 }
885 Some(format!(
886 "kind={}|role={}|intent={}|tool={}|inputs={}|outputs={}|content={}",
887 normalized(event.kind.as_deref()),
888 normalized(event.role.as_deref()),
889 normalized(event.intent.as_deref()),
890 normalized(event.tool.as_deref()),
891 normalized(event.inputs.as_deref()),
892 normalized(event.outputs.as_deref()),
893 normalized(event.content.as_deref()),
894 ))
895}
896
897fn usage_counts(events: &[MemoryEvent]) -> Vec<usize> {
898 let associative = AssociativeMemory::from_memory_events(events);
899 events
900 .iter()
901 .map(|event| usize::try_from(associative.retention_score(&event.id)).unwrap_or(usize::MAX))
902 .collect()
903}