Skip to main content

formal_ai/
dreaming_runtime.rs

1//! Cooperative, default-on dreaming for the core server runtime.
2//!
3//! The worker never runs while a foreground request is active and waits for a
4//! real idle window after the latest request. Without persisted issue-494
5//! consent it may retain newly learned amendments/patterns but strips every
6//! deletion action, so background learning is default-on while freeing remains
7//! default-off.
8
9use std::io;
10use std::path::Path;
11use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
12use std::sync::Once;
13use std::time::{Duration, SystemTime, UNIX_EPOCH};
14
15use crate::dreaming::{apply_dreaming_plan, DreamingOutcome};
16use crate::memory::MemoryStore;
17use crate::storage_policy::{auto_free_space_enabled, plan_for_real_storage};
18
19const DEFAULT_IDLE_SECONDS: u64 = 60;
20const DEFAULT_INTERVAL_SECONDS: u64 = 6 * 60 * 60;
21
22static ACTIVE_FOREGROUND: AtomicUsize = AtomicUsize::new(0);
23static LAST_FOREGROUND_SECONDS: AtomicU64 = AtomicU64::new(0);
24static START: Once = Once::new();
25
26/// Guard one foreground operation. Dropping it wakes the idle clock and lets
27/// the low-priority worker yield cooperatively on every supported platform.
28pub struct ForegroundActivity;
29
30impl ForegroundActivity {
31    #[must_use]
32    pub fn begin() -> Self {
33        ACTIVE_FOREGROUND.fetch_add(1, Ordering::SeqCst);
34        LAST_FOREGROUND_SECONDS.store(now_seconds(), Ordering::SeqCst);
35        Self
36    }
37}
38
39impl Drop for ForegroundActivity {
40    fn drop(&mut self) {
41        LAST_FOREGROUND_SECONDS.store(now_seconds(), Ordering::SeqCst);
42        ACTIVE_FOREGROUND.fetch_sub(1, Ordering::SeqCst);
43    }
44}
45
46/// Whether the core has no foreground work and has passed the idle threshold.
47#[must_use]
48pub fn core_is_idle(idle_for: Duration) -> bool {
49    if ACTIVE_FOREGROUND.load(Ordering::SeqCst) != 0 {
50        return false;
51    }
52    now_seconds().saturating_sub(LAST_FOREGROUND_SECONDS.load(Ordering::SeqCst))
53        >= idle_for.as_secs()
54}
55
56/// Start the one-per-process core worker.
57///
58/// Dreaming is **default-on**: it only stays off when `FORMAL_AI_DREAMING` is
59/// explicitly set to `0`/`off`/`false`. The worker uses the same default shared
60/// memory path as every foreground surface.
61pub fn start_core_dreaming() {
62    if dreaming_disabled() {
63        return;
64    }
65    let path = crate::shared_memory::shared_memory_path();
66    if let Err(error) = crate::shared_memory::ensure_shared_memory_file(&path) {
67        eprintln!(
68            "[dreaming] could not initialize {}: {error}",
69            path.display()
70        );
71        return;
72    }
73    START.call_once(|| {
74        LAST_FOREGROUND_SECONDS.store(now_seconds(), Ordering::SeqCst);
75        std::thread::Builder::new()
76            .name(String::from("formal-ai-dreaming"))
77            .spawn(move || {
78                lower_current_thread_priority();
79                loop {
80                    std::thread::sleep(Duration::from_secs(DEFAULT_IDLE_SECONDS));
81                    if core_is_idle(Duration::from_secs(DEFAULT_IDLE_SECONDS)) {
82                        if let Err(error) = run_core_dreaming_once(&path) {
83                            if std::env::var("FORMAL_AI_DREAMING_DEBUG").as_deref() == Ok("1") {
84                                eprintln!("[dreaming] background run failed: {error}");
85                            }
86                        }
87                        std::thread::sleep(Duration::from_secs(DEFAULT_INTERVAL_SECONDS));
88                    }
89                }
90            })
91            .expect("spawn core dreaming worker");
92    });
93}
94
95/// Drop the calling thread to the lowest OS scheduling priority so dreaming
96/// work never competes with foreground request handling (issue #540 §6). Best
97/// effort: an unsupported platform simply keeps the default priority — the
98/// cooperative [`core_is_idle`] gate still applies either way.
99fn lower_current_thread_priority() {
100    let _ = thread_priority::set_current_thread_priority(thread_priority::ThreadPriority::Min);
101}
102
103/// Execute one idle run, exposed for deterministic integration tests.
104///
105/// The run yields cooperatively: if a foreground request arrives between the
106/// planning and application steps, the run aborts without writing anything and
107/// returns an empty outcome, so dreaming never delays live traffic.
108pub fn run_core_dreaming_once(memory_path: &Path) -> io::Result<DreamingOutcome> {
109    let mut store = MemoryStore::load_from_file(memory_path)?;
110    let mut plan = plan_for_real_storage(&store, memory_path, 0)?;
111    if !auto_free_space_enabled(memory_path) {
112        plan.actions.clear();
113        plan.selected_reclaim_bytes = 0;
114    }
115    // Mid-run cancellation point: planning is the expensive half, so check the
116    // foreground gate again before mutating and persisting the store.
117    if ACTIVE_FOREGROUND.load(Ordering::SeqCst) != 0 {
118        return Ok(DreamingOutcome::default());
119    }
120    let outcome = apply_dreaming_plan(&mut store, &plan);
121    if outcome.removed_events > 0
122        || outcome.learned_amendments > 0
123        || outcome.learned_patterns > 0
124        || outcome.recorded_failures > 0
125        || outcome.recorded_trials > 0
126    {
127        store.save_to_file(memory_path)?;
128        // Keep the composed meta-recipe artifact in step with the amendments
129        // that shape solving (issue #540 §1): the recipe next to the memory
130        // log always reflects the currently retained amendment set.
131        let recipe = crate::dreaming::compose_recipe_with_amendments(store.events());
132        let recipe_path = memory_path.with_extension("recipe.lino");
133        crate::memory::write_locked_atomic(&recipe_path, &recipe)?;
134    }
135    Ok(outcome)
136}
137
138/// Whether the `FORMAL_AI_DREAMING` opt-out is in force. Dreaming is
139/// default-on; only an explicit `0`/`off`/`false` (any case) disables it.
140#[must_use]
141pub fn dreaming_disabled() -> bool {
142    std::env::var("FORMAL_AI_DREAMING")
143        .ok()
144        .is_some_and(|value| matches!(value.to_ascii_lowercase().as_str(), "0" | "off" | "false"))
145}
146
147fn now_seconds() -> u64 {
148    SystemTime::now()
149        .duration_since(UNIX_EPOCH)
150        .unwrap_or_default()
151        .as_secs()
152}