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}