From 632fc4291b2cc608e0e39ac23265c6f288d6fb25 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 2 Nov 2026 22:25:19 +0100 Subject: [PATCH] Her own start joins what the slot was last seen holding, so she hears it end however short it is 57 of the Minds' 157 Self runs lived under a minute. A start's answer names the run with no read, so a run she started that opens and ends between two reads of the slot still gets its one notice. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/persona.rs | 16 ++++++----- crates/mind-body/src/persona/tests.rs | 38 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index e656108..9997f89 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -82,8 +82,8 @@ pub const AGENT: &str = "mind-persona"; pub const PERSONA_POLL_S: i64 = 5; /// How often a live session reads who holds her Self run slot, slower than chat's poll. On Eureka's 14.7k-document mind that read -/// cost 1.4 to 2 s of daemon CPU (2026-11-02): 28-41% of a core at the 5 s poll, 9-14% here. A run shorter than the interval can pass -/// unseen; over the Minds' 157 Self runs so far, about 10 would at 15 s, 8 at 5 s and 35 at 60 s. +/// cost 1.4 to 2 s of daemon CPU (2026-11-02): 28-41% of a core at the 5 s poll, 9-14% here. A run she did not start and shorter than the +/// interval can pass unseen; over the Minds' 157 Self runs so far, about 10 would at 15 s, 8 at 5 s and 35 at 60 s. const SLOT_READ_EVERY_S: u64 = 15; /// The directory of her sandbox inside the unit's state directory. @@ -1027,7 +1027,7 @@ struct PersonaInbox<'a, C: ControlSource, S: ControlSource> { /// What refused results left to be told again. retell: Option, /// Her Self runs last seen holding the slot, each until a read shows it gone and how it closed: whoever started it, her, the - /// heartbeat or an earlier process. A failed read leaves this as it stood. + /// heartbeat or an earlier process. Her own start joins it unread. A failed read leaves this as it stood. slot_seen: Vec, /// When the slot is next read; 0 reads it at the first chance, the session's first line. slot_read_due_ms: u64, @@ -1082,7 +1082,7 @@ impl PersonaInbox<'_, C, S> { /// A run that was in her Self run slot and has left it becomes one notice. The slot is read at most once in /// `SLOT_READ_EVERY_S`, and a run that left has its closing read once. A read that fails keeps what was last seen for the next /// read: a mind that does not answer delays a notice and costs her neither the notice nor her chat, which needs no mind. - /// A run that opens and ends between two reads is seen by neither and makes no notice; the next first line still shows it. + /// A run she did not start that opens and ends between two reads is seen by neither and makes no notice; the next first line still shows it. fn note_ended_runs(&mut self) { let now = unix_ms(self.organs.clock.now()); if now < self.slot_read_due_ms { @@ -1215,8 +1215,12 @@ impl SessionInbox for PersonaInbox<'_, C, S> } self.progress.acked = self.answered_through; } - if started.is_some() { - self.progress.self_run = started; + if let Some(run) = started { + // The start's answer is a sight of the slot that costs no read, so her own run is heard however short it is. + if !self.slot_seen.contains(&run) { + self.slot_seen.push(run.clone()); + } + self.progress.self_run = Some(run); } self.progress.last_intent = Some(intent); self.progress.answered += 1; diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 17d455a..b005a7c 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -4156,6 +4156,44 @@ fn assert_one_notice_on(lines: &[String], line: usize, run: &str) { assert!(notices_in(&lines[line])[0].contains(&format!("Your Self run {run} ended")), "{}", lines[line]); } +/// A run she starts that opens and ends between two reads of the slot, so no read ever holds it: the start's answer is enough, and she hears it. +#[test] +fn a_run_she_started_that_ended_between_two_slot_reads_makes_one_notice() { + let rig = Rig::new(vec![operator_says(1, "please start")], &[starting_a_run(), says(&[]), says(&[]), says(&[]), says(&[])]); + rig.clock.step.set(1); + rig.model.linger.set(true); + rig.open_the_run(); + let timeline = Timeline::over(&rig.mind); + let (now, linger, deliveries, echoes, doing_then) = (rig.clock.now.clone(), rig.model.linger.clone(), rig.chat.deliveries.clone(), Cell::new(0), timeline.doing.clone()); + *rig.model.after_echo.borrow_mut() = Some(Box::new(move || { + echoes.set(echoes.get() + 1); + match echoes.get() { + // This poll's read comes before her reply opens the run. + 1 => { + now.set(now.get() + PAST_THE_SLOT_READ_MS); + deliveries.borrow_mut().push(operator_says(2, "and then")); + } + // The run closes at the next read's own query, so that read finds it already gone. + 2 => { + now.set(now.get() + PAST_THE_SLOT_READ_MS); + doing_then.borrow_mut().push_back(Elsewhere::CloseTheSelfRuns); + deliveries.borrow_mut().push(operator_says(3, "and more")); + } + 3 => { + now.set(now.get() + PAST_THE_SLOT_READ_MS); + deliveries.borrow_mut().push(operator_says(4, "still here")); + } + _ => linger.set(false), + } + })); + let Outcome::Served { failure, self_run, .. } = rig.start_over(&timeline, &config()) else { panic!("served") }; + assert_eq!(failure, None); + let label = label_of(&self_run.expect("the run she started")).to_string(); + let lines = told(&rig); + assert!(lines.len() > 3, "{lines:?}"); + assert_one_notice_on(&lines, 2, &label); +} + /// A run she did not start and that was live when the session began: the first line calls it open, and she hears it end. #[test] fn a_run_live_at_the_session_start_makes_one_notice_when_it_ends() {