From 8a801d09dce0406ffbfd139a8911a66e2df071f8 Mon Sep 17 00:00:00 2001 From: Eureka Date: Wed, 11 Nov 2026 17:11:12 +0000 Subject: [PATCH] persona: an event told in this process is not told again under a new seq Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/persona.rs | 7 ++++++- crates/mind-body/src/persona/tests.rs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 9fd106c..03935b6 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -1364,6 +1364,9 @@ struct PersonaInbox<'a, C: ControlSource, S: ControlSource> { waiting: Vec, /// The highest seq written to the child. told_through: u64, + /// The event ids of every delivery this process told. A restarted mouth numbers an event again; the seq alone would tell it twice. Not + /// the answered set: a batch told and left unanswered is retold by the next process, which starts with none. + told_events: BTreeSet, /// The highest seq an answered result carried in this process. A result acks only through /// the lowest seq still told and unanswered, so this can lie above the mouth's ack until the /// retold batch is answered; a process that ends first leaves the mouth's ack, the truth. @@ -1447,6 +1450,7 @@ impl<'a, C: ControlSource, S: ControlSource> PersonaInbox<'a, C, S> { start: first, waiting: Vec::new(), told_through: 0, + told_events: BTreeSet::new(), answered_through: 0, written: None, echoed: Vec::new(), @@ -1531,6 +1535,7 @@ impl<'a, C: ControlSource, S: ControlSource> PersonaInbox<'a, C, S> { if let Some(through) = batch.iter().map(|delivery| delivery.seq).max() { self.told_through = self.told_through.max(through); } + self.told_events.extend(batch.iter().map(|delivery| delivery.event_id.clone())); self.told.extend(told); self.written = Some(Written { batch, shown, refusals, revision: false, replies }); input @@ -1736,7 +1741,7 @@ impl SessionInbox for PersonaInbox<'_, C, S> if Conversation::of(oldest) != self.conversation { return Ok(Polled { pending: false, other: false, other_first: false, yield_now: true }); } - let untold: Vec = pending.into_iter().filter(|delivery| delivery.seq > self.told_through).take_while(|delivery| Conversation::of(delivery) == self.conversation).collect(); + let untold: Vec = pending.into_iter().filter(|delivery| delivery.seq > self.told_through && !self.told_events.contains(&delivery.event_id)).take_while(|delivery| Conversation::of(delivery) == self.conversation).collect(); // A delivery's stagger runs from the poll that first saw it, so a stream of newer messages moves nobody's clock. The line // is told once any delivery in it is due, whole: she is never shown a message and kept from the ones before it. let now = unix_ms(self.organs.clock.now()); diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 640fbae..734f9ef 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -8840,3 +8840,22 @@ fn events_a_restarted_mouth_renumbers_are_told_once_and_nothing_new_is_lost() { assert_eq!(told.matches(&format!("text=\"{text}\"")).count(), times, "{text} is told {times} time(s): {told}"); } } + +/// An event told and not yet answered, numbered again by a mouth that restarts during the turn, is not told a second time. +#[test] +fn an_event_told_and_unanswered_that_the_mouth_renumbers_is_not_told_again() { + let first = operator_says(1, "first"); + let rig = Rig::new(vec![first.clone()], &[says(&[("operator", "one")]), says(&[("operator", "two")])]); + rig.clock.step.set(0); + let (deliveries, now, count) = (rig.chat.deliveries.clone(), rig.clock.now.clone(), Cell::new(0)); + *rig.model.after_echo.borrow_mut() = Some(Box::new(move || { + count.set(count.get() + 1); + if count.get() == 1 { + now.set(now.get() + 6_000); + *deliveries.borrow_mut() = vec![ChatDelivery { seq: 3, ..first.clone() }]; + } + })); + rig.go(); + let told: String = rig.model.requests.borrow().iter().map(|told| told.input.clone()).collect::>().join("\n"); + assert_eq!(told.matches("text=\"first\"").count(), 1, "{told}"); +}