Skip to main content

formal_ai/
dreaming.rs

1//! Low-priority memory maintenance planning **and** self-generalization.
2//!
3//! Dreaming is deliberately split into a pure planner and an explicit apply
4//! helper. The planner can run by default in background contexts because it
5//! only reads memory and proposes work. Physical deletion remains a caller
6//! decision guarded by the same confirmation/backup flow as other maintenance
7//! commands.
8//!
9//! Beyond garbage collection, dreaming is where Formal AI *learns from its own
10//! stored experience* (issue #540). While idle, the planner recalculates which
11//! topics the user interacts with most, extracts the durable requirements the
12//! user has stated on those topics, and **generalizes** them into
13//! [`MetaAlgorithmAmendment`] records — changes baked into how similar future
14//! tasks are solved so the user never has to repeat a requirement. Because the
15//! amendment can re-derive the specific task/test-run records it subsumes, those
16//! specifics become safe to forget under storage pressure while the generalized
17//! amendment is retained forever. This is the issue's core rule: "as soon as
18//! \[a generalization\] is working, \[the\] specific algorithm can be forgotten",
19//! yet "our general meta algorithm must keep changes that allow it to solve all
20//! other tasks".
21
22use 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    /// Background planning is on by default; disabling it returns an empty plan.
39    pub daydreaming_enabled: bool,
40    /// Desired free space after satisfying the next known incoming write.
41    pub target_free_ratio_percent: u8,
42    /// Total capacity of the storage area that holds the memory file.
43    pub storage_capacity_bytes: Option<u64>,
44    /// Current free bytes in the storage area.
45    pub free_bytes: Option<u64>,
46    /// Bytes the caller expects to need for the next write/fetch.
47    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    /// Raw user/assistant/system experience that cannot be reconstructed from a
65    /// public source.
66    IrreplaceableRaw,
67    /// Promoted lessons, skill ledgers, and learned experience.
68    RetainedLearning,
69    /// Events already attached to a soft-deleted conversation.
70    DeletedConversation,
71    /// Public-source cache or tool output that can be fetched again.
72    RecomputableCache,
73    /// Summaries and conclusions that can be derived from retained inputs.
74    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    /// A specific task/test-run record that a retained meta-algorithm amendment
102    /// can reproduce, so it is forgotten first under pressure.
103    ForgetCoveredSpecific,
104}
105
106/// How often the user has interacted with one topic, recalculated from the
107/// current memory links. Topics are the unit dreaming learns about.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct TopicFrequency {
110    pub topic: String,
111    /// Total events attributed to the topic.
112    pub interactions: usize,
113    /// Events that look like concrete tasks / test runs on the topic.
114    pub task_events: usize,
115    /// Events that state a durable user requirement on the topic.
116    pub requirement_events: usize,
117}
118
119/// A durable requirement the user stated on a topic. Dreaming remembers these so
120/// the user never has to repeat them.
121#[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/// A generalization baked into Formal AI's meta-algorithm: when solving tasks on
130/// `topic`, always apply `rule`.
131///
132/// Materialized into memory as a retained, never-reclaimable learning record;
133/// the specifics it `covered_event_ids` subsumes become safe to forget under
134/// pressure.
135#[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/// A prior task replayed by dreaming against the generalized meta-algorithm.
145#[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/// A recurring task structure mined without relying on natural-language cue
156/// words. Patterns are retained learning and can seed later amendments.
157#[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/// A brand-new task dreamed up on a most-used topic while idle.
166///
167/// It is built from a mined pattern and solved through the production
168/// amendment path. Applying the plan retains it as a `dreaming_trial` event,
169/// so patterns and topic frequencies have a real consumer instead of being
170/// write-only statistics.
171#[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    /// The topic this event belongs to, if one could be recalculated.
197    pub topic: Option<String>,
198    /// True when a retained meta-algorithm amendment can reproduce this record,
199    /// so it is safe to forget first under pressure.
200    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    /// Topics ranked by recalculated interaction frequency (most-used first).
218    pub topics: Vec<TopicFrequency>,
219    /// Durable user requirements recovered from memory.
220    pub learned_requirements: Vec<LearnedRequirement>,
221    /// Meta-algorithm generalizations baked in from the learned requirements.
222    pub amendments: Vec<MetaAlgorithmAmendment>,
223    /// Prior tasks discovered on frequent topics and replayed in simulation.
224    pub candidate_tasks: Vec<DreamingCandidateTask>,
225    /// Recurring structures mined across task inputs independently of language.
226    pub patterns: Vec<DreamingPattern>,
227    /// New tasks synthesized from patterns on the most-used topics and solved
228    /// through the production amendment path while dreaming.
229    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    /// The recalculated interaction count for a topic, if it was observed.
242    #[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    /// The amendment learned for a topic, if any.
251    #[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    /// Meta-algorithm amendments newly materialized into the store.
264    pub learned_amendments: usize,
265    /// Recurring task structures newly materialized as retained learning.
266    pub learned_patterns: usize,
267    /// Failed replay candidates preserved as `dreaming_candidate_failure`
268    /// records so future dreaming rounds can keep learning from them.
269    pub recorded_failures: usize,
270    /// Synthesized trials on the most-used topics materialized as
271    /// `dreaming_trial` records.
272    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    // Self-generalization: learn topics and durable requirements from the same
335    // memory links, then bake each requirement into a meta-algorithm amendment.
336    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    // Deleted-conversation purges and duplicate restructuring are planned even
360    // without storage pressure — deliberately (issue #540 §4). Neither loses
361    // information: purged events belong to conversations the user already
362    // deleted explicitly, and deduplication always retains one canonical copy
363    // of the recomputable record. They are consented housekeeping (nothing is
364    // applied without --apply/--confirm or persisted auto-free-space consent);
365    // only the pressure loop below is sized to "just enough for the next
366    // write" and stops at the reclaim target.
367    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            // Specifics a retained amendment can reproduce are forgotten first.
427            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    // Bake each learned generalization into memory as a retained learning record
516    // *before* forgetting the specifics it covers. Applying an unchanged plan
517    // twice must not duplicate amendments, so we skip ids already present.
518    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    // Failed simulations are learning material, not noise: preserve each one as
538    // a retained record so later dreaming rounds (and refinement) can consume it.
539    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
594/// Preserve a failed replay as retained learning material. The record keeps
595/// the diverging outputs so the next dreaming round's refinement pass (and any
596/// human inspecting memory) can see exactly what the meta-algorithm still gets
597/// wrong.
598fn 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
617/// Materialize a synthesized trial dreamed on a most-used topic.
618fn 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
656/// Render a learned generalization as a durable, never-reclaimable memory event.
657fn 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/// Compose the meta-algorithm recipe **as data**.
778///
779/// The result is the base `data/meta/dreaming-recipe.lino` document plus one
780/// `meta_amendment` record per retained amendment currently in memory. The
781/// dreaming runtime writes this next to the memory file after every run, so
782/// learning literally changes the recipe artifact the amendments cite instead
783/// of leaving it static.
784#[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    // Durability cues are grounded as data (multilingual) in
854    // data/meta/dreaming-lexicon.lino instead of hardcoded English keywords.
855    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}