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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-11-02 22:25:19 +01:00
commit 632fc4291b
2 changed files with 48 additions and 6 deletions

View file

@ -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<Retell>,
/// 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<PipelineRef>,
/// 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<C: ControlSource, S: ControlSource> 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<C: ControlSource, S: ControlSource> 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;

View file

@ -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() {