lib.rsannotatedlib.rssource647 lines · 28.2 KB · raw
1//! Everything a shell needs, behind one object: the cat, the conversation, the
2//! parents' view and the voice allowance. The shell reports touches and what
3//! the microphone heard, and speaks and draws what comes back. This is the
4//! boundary the UniFFI crate exports.
5
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
8use std::time::{Instant, SystemTime, UNIX_EPOCH};
9
10use log::{debug, error, info, trace, warn};
11
12use whiskers_core::{HouseholdConfig, TimeKeeper, TimeStatus, 
13    ChatError, Clock, Config, Conversation, Digest, DirPictures, Embedder, Fact, Guard, Heard,
14    Image, JsonChat, JsonMemory, JsonlLog, Model, Outcome, Parts, PictureId, Recall, ReflectInput, Reflector,
15    ReflectorConfig, SharedChat, SharedProfile, SharedLog, SharedMemory, read_shared_log,
16};
17use whiskers_gateway::Gateway;
18use whiskers_guard::{RemoteEmbedder, RemoteGuard, RemoteIcons, RemoteRanker, RemoteSync};
19pub use whiskers_pet::{Mood, Phase, PetView, Touch, TouchKind};
20
21pub struct EngineConfig {
22    pub data_dir: PathBuf,
23    pub gateway_url: String,
24    pub guard_url: String,
25    pub model: String,
26    /// The parents' own prompt; `None` uses the default, written for the child's profile.
27    pub system_prompt: Option<String>,
28}
29
30/// What Whiskers says. The text is the only thing a shell may speak.
31pub struct Spoken {
32    pub text: String,
33    pub outcome: Outcome,
34}
35
36pub struct SystemClock;
37impl Clock for SystemClock {
38    fn now_ms(&self) -> u64 {
39        SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_millis() as u64)
40    }
41}
42
43/// The parents' view of a stretch of time.
44pub struct DigestView {
45    pub exchanges: Vec<ExchangeView>,
46    pub facts_learned: Vec<String>,
47    pub answered: u32,
48    pub stopped: u32,
49    /// Log lines that could not be read (a crash mid-write).
50    pub unreadable_lines: u32,
51}
52
53pub struct ExchangeView {
54    pub at_ms: u64,
55    pub heard: String,
56    pub pictures: Vec<String>,
57    pub model_wrote: Option<String>,
58    pub said: String,
59    pub outcome: Outcome,
60    pub needs_a_grown_up: bool,
61    pub notes: Vec<String>,
62}
63
64pub struct Engine {
65    pet: Mutex<whiskers_pet::Pet>,
66    convo: Mutex<Conversation>,
67    /// Exchanges waiting for the memory to consider them (see `reflect`).
68    pending: Mutex<Vec<ReflectInput>>,
69    reflect_lock: Mutex<()>,
70    reflector: Reflector,
71    log_path: PathBuf,
72    pictures: DirPictures,
73    summarizer: Box<dyn Model>,
74    time: Mutex<TimeKeeper>,
75    /// `None` for an engine built without a service (tests).
76    sync: Option<RemoteSync>,
77    /// One sync at a time: two at once would each append the same lines.
78    sync_lock: Mutex<()>,
79    device: String,
80    /// The copy of the service's log that sync keeps (every device's entries, one line each).
81    shared_log: PathBuf,
82    /// The child, kept level with the household document (settings change it, sync can too).
83    profile: SharedProfile,
84}
85
86/// Names this device in the day's time count and nowhere else. Made once and kept.
87use std::hash::BuildHasher as _;
88fn device_id(dir: &std::path::Path) -> Result<String, String> {
89    let path = dir.join("device.id");
90    if let Ok(id) = std::fs::read_to_string(&path) {
91        if !id.trim().is_empty() {
92            trace!("device id read from {}", path.display());
93            return Ok(id.trim().to_owned());
94        }
95    }
96    let id = format!("{:x}{:x}", SystemClock.now_ms(), std::process::id())
97        + &std::collections::hash_map::RandomState::new().hash_one(SystemClock.now_ms()).to_string();
98    std::fs::write(&path, &id).map_err(|e| {
99        error!("cannot write the device id to {}: {e}", path.display());
100        e.to_string()
101    })?;
102    info!("this device has a new id ({} chars)", id.len());
103    Ok(id)
104}
105
106/// The whole lines of a log file; a last line without its newline is a write in progress.
107fn complete_lines(path: &std::path::Path) -> Vec<String> {
108    let bytes = std::fs::read(path).unwrap_or_else(|e| {
109        debug!("{} not readable ({e}); treated as empty", path.display());
110        Vec::new()
111    });
112    let text = String::from_utf8_lossy(&bytes);
113    match text.rfind('\n') {
114        Some(i) => text[..=i].lines().map(str::to_owned).collect(),
115        None => Vec::new(),
116    }
117}
118
119/// How many exchanges wait to be thought about: a long outage must not queue the whole evening.
120const PENDING_LIMIT: usize = 8;
121
122fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
123    // A panic while holding a lock must not take the cat's face down with it.
124    m.lock().unwrap_or_else(PoisonError::into_inner)
125}
126
127impl Engine {
128    pub fn new(cfg: EngineConfig) -> Result<Self, String> {
129        info!("engine starting: model {}, data dir {}", cfg.model, cfg.data_dir.display());
130        std::fs::create_dir_all(&cfg.data_dir).map_err(|e| {
131            error!("cannot create the data directory {}: {e}", cfg.data_dir.display());
132            e.to_string()
133        })?;
134        let summarizer = Box::new(Gateway::new(&cfg.gateway_url, &cfg.model, 600));
135        let embedder: Arc<RemoteEmbedder> = Arc::new(RemoteEmbedder::new(&cfg.guard_url));
136        let ranker = Arc::new(RemoteRanker::new(&cfg.guard_url));
137        let model: Arc<dyn Model> = Arc::new(Gateway::new(&cfg.gateway_url, &cfg.model, 400));
138        let guard: Arc<dyn Guard> = Arc::new(RemoteGuard::new(&cfg.guard_url));
139        // A state file that cannot be read is kept aside and the app starts clean, rather than
140        // being unable to start at all; the parents' log says it happened.
141        let mut set_aside = Vec::new();
142        let (chat_store, aside) = JsonChat::open_or_set_aside(&cfg.data_dir.join("chat.json")).map_err(|e: ChatError| e.0)?;
143        set_aside.extend(aside.map(|p| ("the chat", p)));
144        let chat = SharedChat::open(Box::new(chat_store)).map_err(|e: ChatError| e.0)?;
145        let (memory_store, aside) = JsonMemory::open_or_set_aside(&cfg.data_dir.join("memory.json")).map_err(|e| e.0)?;
146        set_aside.extend(aside.map(|p| ("what Whiskers remembers", p)));
147        let log = SharedLog::new(Box::new(JsonlLog::open(&cfg.data_dir.join("log.jsonl")).map_err(|e| e.0)?));
148        for (what, kept_at) in set_aside {
149            warn!("{what}: the damaged file was set aside at {}", kept_at.display());
150            let _ = log.append(&whiskers_core::Entry {
151                at_ms: SystemClock.now_ms(),
152                event: whiskers_core::Event::MemoryFailed {
153                    error: format!("the file for {what} was damaged; it is kept at {} and Whiskers started that part afresh", kept_at.display()),
154                },
155            });
156        }
157        let parts = Parts {
158            model,
159            guard,
160            log,
161            pictures: Box::new(DirPictures::open(&cfg.data_dir.join("pictures")).map_err(|e| e.0)?),
162            memory: SharedMemory::new(Box::new(memory_store)),
163            chat,
164            recall: Arc::new(Recall::new(embedder.clone(), ranker)),
165            clock: Arc::new(SystemClock),
166            profile: SharedProfile::default(),
167        };
168        let service = cfg.guard_url.clone();
169        let mut engine = Self::with_parts(cfg, parts, embedder, summarizer)?;
170        engine.reflector.icons = Arc::new(RemoteIcons::new(&service));
171        engine.sync = Some(RemoteSync::new(service));
172        info!("engine ready (device {})", engine.device);
173        Ok(engine)
174    }
175
176    /// For tests and shells that bring their own adapters.
177    pub fn with_parts(
178        cfg: EngineConfig,
179        parts: Parts,
180        embedder: Arc<dyn Embedder>,
181        summarizer: Box<dyn Model>,
182    ) -> Result<Self, String> {
183        debug!("engine parts assembled; system prompt is {}", if cfg.system_prompt.is_some() { "the parents'" } else { "the default" });
184        let cfg_data_dir = cfg.data_dir.clone();
185        let profile = parts.profile.clone();
186        let reflector = Reflector {
187            model: parts.model.clone(),
188            guard: parts.guard.clone(),
189            embedder,
190            log: parts.log.clone(),
191            memory: parts.memory.clone(),
192            chat: parts.chat.clone(),
193            clock: parts.clock.clone(),
194            profile: parts.profile.clone(),
195            config: ReflectorConfig { memory_limit: 200, compress_after: 24, keep_turns: 10 },
196            icons: Arc::new(whiskers_core::NoIcons),
197            iconified: Default::default(),
198        };
199        let convo = Conversation::new(
200            Config {
201                system_prompt: cfg.system_prompt,
202                history_turns: 24,
203                greet_after_ms: 30 * 60 * 1000,
204            },
205            parts,
206        );
207        let time = TimeKeeper::open(&cfg_data_dir.join("household.json"), &device_id(&cfg_data_dir)?);
208        profile.set(time.config().child);
209        Ok(Self {
210            pet: Mutex::new(whiskers_pet::Pet::new()),
211            convo: Mutex::new(convo),
212            pending: Mutex::new(Vec::new()),
213            reflect_lock: Mutex::new(()),
214            reflector,
215            log_path: cfg.data_dir.join("log.jsonl"),
216            pictures: DirPictures::open(&cfg.data_dir.join("pictures")).map_err(|e| e.0)?,
217            summarizer,
218            time: Mutex::new(time),
219            profile,
220            sync: None,
221            sync_lock: Mutex::new(()),
222            shared_log: cfg_data_dir.join("shared-log.jsonl"),
223            device: device_id(&cfg_data_dir)?,
224        })
225    }
226
227    // ---- the cat --------------------------------------------------------
228
229    pub fn touch(&self, t: Touch) {
230        lock(&self.pet).touch(t);
231    }
232
233    pub fn set_phase(&self, phase: Phase, now_ms: u64) {
234        trace!("engine set_phase {phase:?}");
235        lock(&self.pet).set_phase(phase, now_ms);
236    }
237
238    pub fn tick(&self, now_ms: u64) -> PetView {
239        lock(&self.pet).tick(now_ms)
240    }
241
242    // ---- the conversation (blocking: call off the main thread) -----------
243
244    /// Whether Whiskers should say hello: the first time ever, or after a long quiet. A phone
245    /// folded or unfolded, or an app brought back within half an hour, is not a new chat.
246    pub fn greeting_due(&self) -> bool {
247        lock(&self.convo).greeting_due()
248    }
249
250    pub fn greet(&self) -> Spoken {
251        debug!("engine greet");
252        let r = lock(&self.convo).greet();
253        Spoken { text: r.say.as_str().to_owned(), outcome: r.outcome }
254    }
255
256    pub fn hear(&self, text: &str, pictures: Vec<Image>) -> Spoken {
257        // Held for the turn only; the slow memory work is `reflect`, which does not take this lock.
258        let started = Instant::now();
259        debug!("engine hear: {} chars, {} picture(s)", text.len(), pictures.len());
260        let r = lock(&self.convo).respond(Heard { text: text.to_owned(), pictures });
261        info!("engine hear done in {} ms: {:?}", started.elapsed().as_millis(), r.outcome);
262        if let Some(input) = r.reflect {
263            let mut pending = lock(&self.pending);
264            pending.push(input);
265            // Bounded: a long outage must not queue the whole evening.
266            if pending.len() > PENDING_LIMIT {
267                warn!("{} exchanges wait for the memory; dropping the oldest", pending.len());
268                pending.remove(0);
269            }
270            debug!("{} exchange(s) wait for the memory", pending.len());
271        }
272        Spoken { text: r.say.as_str().to_owned(), outcome: r.outcome }
273    }
274
275    /// The slow work after a turn: deciding what to remember, filing it, folding the old chat
276    /// into its summary. Blocking, but it never holds the conversation, so she can talk again
277    /// at once. Returns what was learned. Only one runs at a time; a call that finds another
278    /// running returns nothing and leaves its work queued for the next.
279    pub fn reflect(&self) -> Vec<Fact> {
280        let _running = match self.reflect_lock.try_lock() {
281            Ok(g) => g,
282            // A panic inside an earlier pass poisons the lock; that must not end remembering for good.
283            Err(std::sync::TryLockError::Poisoned(p)) => p.into_inner(),
284            Err(std::sync::TryLockError::WouldBlock) => {
285                debug!("reflect: another pass is running; leaving the work queued");
286                return Vec::new();
287            }
288        };
289        let started = Instant::now();
290        let mut learned = Vec::new();
291        loop {
292            let inputs: Vec<ReflectInput> = std::mem::take(&mut *lock(&self.pending));
293            debug!("reflect pass over {} exchange(s)", inputs.len());
294            if inputs.is_empty() {
295                self.reflector.run(None);
296            }
297            let mut retry = Vec::new();
298            for input in inputs {
299                let r = self.reflector.reflect(Some(&input));
300                learned.extend(r.learned);
301                if r.retry {
302                    retry.push(input);
303                }
304            }
305            // Exchanges that arrived while this pass was busy: a caller that found the lock held
306            // returned at once, so nobody else will pick these up.
307            let newer = lock(&self.pending).len();
308            if !retry.is_empty() {
309                warn!("reflect: {} exchange(s) will be tried again later (model unreachable)", retry.len());
310                // The model was unreachable: put these back ahead of anything newer, within the bound.
311                let mut pending = lock(&self.pending);
312                retry.append(&mut pending);
313                *pending = retry;
314                let excess = pending.len().saturating_sub(PENDING_LIMIT);
315                pending.drain(..excess);
316            }
317            // Retried ones wait for the next turn; going round again for them would spin on an outage.
318            if newer == 0 {
319                break;
320            }
321        }
322        info!("reflect finished in {} ms: {} fact(s) learned", started.elapsed().as_millis(), learned.len());
323        learned
324    }
325
326    // ---- the grown-ups' time limits -----------------------------------------
327
328    /// Every choice the grown-ups have made, as this device last heard of them.
329    pub fn household_config(&self) -> HouseholdConfig {
330        lock(&self.time).config()
331    }
332
333    /// The grown-ups changed a choice. Call [`sync`](Self::sync) soon so the other devices hear.
334    pub fn set_household_config(&self, config: HouseholdConfig) {
335        info!("engine: the grown-ups changed the household choices");
336        self.profile.set(config.child.clone());
337        lock(&self.time).set_config(config, SystemClock.now_ms());
338    }
339
340    /// The pictures that go with remembered facts follow the facts: the ones this device lacks
341    /// are fetched, and the ones the service lacks are sent. Returns how many arrived here.
342    fn sync_pictures(&self, remote: &RemoteSync, wanted: &[PictureId]) -> Result<u32, String> {
343        let mut arrived = 0;
344        debug!("sync pictures: {} wanted", wanted.len());
345        for id in wanted.iter().filter(|id| !self.pictures.has(id)) {
346            if let Some(bytes) = remote.get_picture(&id.0)? {
347                self.pictures.store(id, &bytes).map_err(|e| {
348                    error!("sync pictures: cannot keep {}: {}", id.0, e.0);
349                    e.0
350                })?;
351                arrived += 1;
352            } else {
353                warn!("sync pictures: {} is wanted but the service does not have it", id.0);
354            }
355        }
356        let here: Vec<String> = wanted.iter().filter(|id| self.pictures.has(id)).map(|id| id.0.clone()).collect();
357        if !here.is_empty() {
358            for id in remote.pictures_missing(&here)? {
359                if let Some(bytes) = self.pictures.read(&PictureId(id.clone())) {
360                    remote.put_picture(&id, &bytes)?;
361                } else {
362                    warn!("sync pictures: the service lacks {id} but it is not readable here");
363                }
364            }
365        }
366        if arrived > 0 {
367            info!("sync pictures: {arrived} picture(s) arrived");
368        }
369        Ok(arrived)
370    }
371
372    /// Brings this device level with the service: what Whiskers remembers, and the grown-ups'
373    /// choices and today's time. Returns how many things changed here. Blocking, and safe to
374    /// call whenever there is a network; an unreachable service is an `Err` and changes nothing.
375    /// Every picture some device kept for a memory or for a conversation, which every device wants.
376    fn pictures_everyone_wants(&self) -> Vec<PictureId> {
377        let mut v: Vec<PictureId> = self.reflector.memory.snapshot().facts.iter().flat_map(|f| f.pictures.iter().cloned()).collect();
378        if let Ok((sources, _)) = read_shared_log(&self.log_path, &self.shared_log, &self.device) {
379            for e in sources.iter().flatten() {
380                if let whiskers_core::Event::Heard { pictures, .. } = &e.event {
381                    v.extend(pictures.iter().cloned());
382                }
383            }
384        }
385        v.retain(PictureId::is_safe);
386        v.sort_by(|a, b| a.0.cmp(&b.0));
387        v.dedup();
388        v
389    }
390
391    /// This device's new log entries go up to the service and the one log comes down, so the
392    /// parents see every conversation wherever it happened. The log only ever grows, so this is
393    /// just "send what the other side lacks". Returns how many lines arrived here.
394    fn sync_journal(&self, remote: &RemoteSync) -> Result<u32, String> {
395        let mine = complete_lines(&self.log_path);
396        let mut have = remote.journal_have(&self.device)?;
397        debug!("sync journal: {} local line(s), the service has {have}", mine.len());
398        if have > mine.len() {
399            warn!("sync journal: the service has {have} lines from this device but only {} exist here", mine.len());
400        }
401        while have < mine.len() {
402            let end = (have + 200).min(mine.len());
403            let now = remote.journal_push(&self.device, have, &mine[have..end])?;
404            if now <= have {
405                error!("sync journal: pushed from line {have} but the service now reports {now}");
406                return Err("the service did not take the log".into());
407            }
408            have = now;
409        }
410        let mut arrived = 0;
411        loop {
412            let held = complete_lines(&self.shared_log).len();
413            let got = remote.journal_pull(held)?;
414            if got.total < held {
415                // Our copy is longer than the log it copies, so it is not a copy of it (it can only
416                // have been damaged): throw it away and take the log again from the start.
417                warn!("sync journal: our copy has {held} lines but the log has {}; rebuilding the copy from the start", got.total);
418                if let Err(e) = std::fs::remove_file(&self.shared_log) {
419                    error!("sync journal: cannot remove the damaged copy: {e}");
420                }
421                continue;
422            }
423            // Only lines that continue exactly where our copy ends are taken.
424            if got.lines.is_empty() || got.from != held {
425                if got.from != held {
426                    warn!("sync journal: pulled lines start at {} but our copy ends at {held}; taking none", got.from);
427                }
428                break;
429            }
430            use std::io::Write as _;
431            let mut text = got.lines.join("\n");
432            text.push('\n');
433            let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&self.shared_log).map_err(|e| {
434                error!("sync journal: cannot open the shared log copy: {e}");
435                e.to_string()
436            })?;
437            f.write_all(text.as_bytes()).and_then(|()| f.sync_data()).map_err(|e| {
438                error!("sync journal: cannot append to the shared log copy: {e}");
439                e.to_string()
440            })?;
441            arrived += got.lines.len() as u32;
442        }
443        info!("sync journal: {arrived} line(s) arrived");
444        Ok(arrived)
445    }
446
447    /// Brings this device level with the service: what Whiskers remembers, every device's
448    /// conversations, the pictures that go with both, and the grown-ups' choices and today's
449    /// time. Returns how many things changed here. Blocking. Each part is tried even if another
450    /// failed; an unreachable service is an `Err` and changes nothing.
451    pub fn sync(&self) -> Result<u32, String> {
452        let Some(remote) = &self.sync else {
453            warn!("sync asked for but there is no service configured");
454            return Err("no service to sync with".into());
455        };
456        // If another sync is under way it is already doing this; there is nothing to add.
457        let Ok(_one_at_a_time) = self.sync_lock.try_lock() else {
458            debug!("sync skipped: another is under way");
459            return Ok(0);
460        };
461        info!("sync starts");
462        let started = Instant::now();
463        let mut changed = 0u32;
464        let mut errors: Vec<String> = Vec::new();
465        let mut step = |what: &str, r: Result<u32, String>| match r {
466            Ok(n) => {
467                debug!("sync {what}: {n} change(s)");
468                changed += n;
469            }
470            Err(e) => {
471                warn!("sync {what} failed: {e}");
472                errors.push(format!("{what}: {e}"));
473            }
474        };
475        let memory = self.reflector.memory.clone();
476        step("memory", remote.memory(&memory.snapshot()).and_then(|theirs| memory.merge(theirs).map(|n| n as u32).map_err(|e| e.0)));
477        // The one session: whichever copy has had the most said in it is the one everyone continues.
478        let chat = self.reflector.chat.clone();
479        step("session", remote.chat(&chat.snapshot()).and_then(|theirs| chat.adopt(theirs).map(u32::from).map_err(|e| e.0)));
480        step("conversations", self.sync_journal(remote));
481        step("pictures", self.sync_pictures(remote, &self.pictures_everyone_wants()));
482        let mine = lock(&self.time).household();
483        step("settings", remote.household(&mine).map(|theirs| u32::from(lock(&self.time).merge(&theirs))));
484        // Settings from another device may have changed the child; the next turn uses it.
485        self.profile.set(lock(&self.time).config().child);
486        // Last, because it is the slow part (an embedding and a Jev question) and the rest must not wait for it:
487        // a few facts per sync are given a picture, and the next sync carries them to every device.
488        self.reflector.give_icons();
489        if errors.is_empty() {
490            info!("sync finished in {} ms: {changed} change(s)", started.elapsed().as_millis());
491            Ok(changed)
492        } else {
493            error!("sync finished in {} ms with {} failed part(s), {changed} change(s)", started.elapsed().as_millis(), errors.len());
494            Err(errors.join("; "))
495        }
496    }
497
498    /// `delta_ms` was spent with Whiskers on `day` (a number that changes at midnight).
499    pub fn tick_time(&self, day: u32, delta_ms: u64) {
500        trace!("engine tick_time day {day} +{delta_ms} ms");
501        lock(&self.time).tick(day, delta_ms);
502    }
503
504    pub fn time_status(&self, day: u32, minute_of_day: u16) -> TimeStatus {
505        let s = lock(&self.time).status(day, minute_of_day);
506        trace!("engine time_status day {day} minute {minute_of_day}: {s:?}");
507        s
508    }
509
510    /// The grown-ups give `minutes` more today.
511    pub fn grant_time(&self, day: u32, minutes: u32) {
512        debug!("engine grant_time day {day} +{minutes} min");
513        lock(&self.time).grant(day, minutes);
514    }
515
516    pub fn time_used_minutes(&self, day: u32) -> u32 {
517        lock(&self.time).used_minutes(day)
518    }
519
520    // ---- the parents' view ------------------------------------------------
521
522    pub fn digest(&self, from_ms: u64, to_ms: u64) -> Result<DigestView, String> {
523        debug!("engine digest [{from_ms}, {to_ms})");
524        let (sources, unreadable) = read_shared_log(&self.log_path, &self.shared_log, &self.device).map_err(|e| e.0)?;
525        let d = Digest::between_all(&sources, from_ms, to_ms);
526        let v = view(&d, unreadable);
527        debug!("engine digest: {} exchange(s), {unreadable} unreadable line(s)", v.exchanges.len());
528        Ok(v)
529    }
530
531    pub fn summarize(&self, from_ms: u64, to_ms: u64) -> Result<String, String> {
532        info!("engine summarize [{from_ms}, {to_ms})");
533        let (sources, _) = read_shared_log(&self.log_path, &self.shared_log, &self.device).map_err(|e| e.0)?;
534        Digest::between_all(&sources, from_ms, to_ms).summarize(self.summarizer.as_ref(), &self.profile.audience())
535            .inspect(|s| debug!("engine summarize: note of {} chars", s.len()))
536            .map_err(|e| {
537                error!("engine summarize failed: {}", e.reason);
538                e.reason
539            })
540    }
541
542    /// The child's journal. Reads the memory directly, so the parents' view never waits on a turn.
543    pub fn facts(&self) -> Vec<Fact> {
544        trace!("engine facts");
545        self.reflector.memory.facts()
546    }
547
548    /// What she may see of what Whiskers remembers: the facts she has not put away.
549    pub fn facts_she_sees(&self) -> Vec<Fact> {
550        trace!("engine facts she sees");
551        self.reflector.memory.usable()
552    }
553
554    /// A fact as it is read out to her, in the words of the profile as it stands now.
555    pub fn aloud(&self, fact: &Fact) -> String {
556        self.profile.audience().read_fact_aloud(&fact.text)
557    }
558
559    /// She puts a fact away: Whiskers stops using it and she stops seeing it, the parents still see it
560    /// and can restore it. The log records that she did. Not `forget`, which is the parents' and permanent.
561    pub fn put_away(&self, id: u64) -> bool {
562        match self.reflector.memory.hide(id, SystemClock.now_ms()) {
563            Ok(fact) => {
564                info!("engine put_away: fact {id} put away by her");
565                self.record_visibility(whiskers_core::Event::PutAway { fact: fact.text });
566                true
567            }
568            Err(e) => {
569                warn!("engine put_away: fact {id} not put away: {}", e.0);
570                false
571            }
572        }
573    }
574
575    /// A parent restores a fact she put away.
576    pub fn restore(&self, id: u64) -> bool {
577        match self.reflector.memory.restore(id, SystemClock.now_ms()) {
578            Ok(fact) => {
579                info!("engine restore: fact {id} restored by a parent");
580                self.record_visibility(whiskers_core::Event::Restored { fact: fact.text });
581                true
582            }
583            Err(e) => {
584                warn!("engine restore: fact {id} not restored: {}", e.0);
585                false
586            }
587        }
588    }
589
590    fn record_visibility(&self, event: whiskers_core::Event) {
591        let entry = whiskers_core::Entry { at_ms: SystemClock.now_ms(), event };
592        if let Err(e) = self.reflector.log.append(&entry) {
593            warn!("engine: a fact's visibility changed but the record could not be written: {}", e.0);
594        }
595    }
596
597    /// A parent removes something Whiskers remembers; the log keeps a record that they did.
598    pub fn forget(&self, id: u64) -> bool {
599        match self.reflector.memory.forget(id) {
600            Ok(fact) => {
601                info!("engine forget: fact {id} removed by a parent");
602                let entry = whiskers_core::Entry { at_ms: SystemClock.now_ms(), event: whiskers_core::Event::Forgot { fact: fact.text } };
603                if let Err(e) = self.reflector.log.append(&entry) {
604                    warn!("engine forget: fact {id} removed but the record could not be written: {}", e.0);
605                }
606                true
607            }
608            Err(e) => {
609                warn!("engine forget: fact {id} not removed: {}", e.0);
610                false
611            }
612        }
613    }
614
615    pub fn picture_path(&self, id: &str) -> PathBuf {
616        self.pictures.path_of(&PictureId(id.to_owned()))
617    }
618
619}
620
621fn view(d: &Digest, unreadable: usize) -> DigestView {
622    let (answered, stopped) = d.counts();
623    let urgent: Vec<u64> = d.needs_a_grown_up().iter().map(|e| e.at_ms).collect();
624    let mut exchanges: Vec<ExchangeView> = d
625        .exchanges
626        .iter()
627        .map(|e| ExchangeView {
628            at_ms: e.at_ms,
629            heard: e.heard.clone(),
630            pictures: e.pictures.iter().map(|p| p.0.clone()).collect(),
631            model_wrote: e.model_wrote.clone(),
632            said: e.said.clone(),
633            outcome: e.outcome,
634            needs_a_grown_up: urgent.contains(&e.at_ms),
635            notes: e.notes.clone(),
636        })
637        .collect();
638    // Needs-a-grown-up first, then the rest in the order they happened.
639    exchanges.sort_by_key(|e| (!e.needs_a_grown_up, e.at_ms));
640    DigestView {
641        exchanges,
642        facts_learned: d.facts_learned.clone(),
643        answered: answered as u32,
644        stopped: stopped as u32,
645        unreadable_lines: unreadable as u32,
646    }
647}