From 8b184342827486e53812c60ba63344415eee70e0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 11 Nov 2026 23:23:36 +0100 Subject: [PATCH 1/2] mind-body: pin that a flush answer does not spend a stale-send revision she was never told Fails at base: the flush acks delivery 1 and drops the revision. Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/persona/tests.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 47126ad..124842c 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -6568,6 +6568,33 @@ fn a_process_that_ends_with_a_revision_waiting_leaves_the_batch_unacked() { }); } +#[test] +fn a_flush_answer_does_not_spend_a_revision_she_was_never_told() { + let rig = Rig::new(vec![], &[]); + rig.clock.step.set(0); + let (ask, answer) = (delivery(1, "swarm", "@op:chat", "question"), delivery(2, "swarm", "@libby:chat", "answer")); + arrives(&rig, &ask); + with_inbox(&rig, &config(), vec![ask.clone()], |inbox| { + inbox.start_line().unwrap(); + inbox.echoed(); + arrives(&rig, &answer); + inbox.result(&reply_result(&says(&[("swarm", "mine")])), true, false).unwrap(); + assert!(inbox.revision.is_some()); + inbox.flush_text(Why::Idle); + inbox.echoed(); + inbox.result(&reply_result(&says(&[("swarm", "last words")])), true, false).unwrap(); + assert_eq!(rig.chat.sent.borrow().last().unwrap().basis, None, "a flush send is never refused"); + assert!(inbox.revision.is_some(), "the flush never showed her the revision line"); + }); + assert!(rig.chat.acked.borrow().is_empty()); + let next = next_batch(&config(), &[], rig.chat.pending(0).unwrap().deliveries, 0); + assert_eq!(next.len(), 2); + with_inbox(&rig, &config(), next, |inbox| { + let line = inbox.start_line().unwrap(); + assert!(line.contains("question") && line.contains("answer") && !line.contains("was not sent"), "{line}"); + }); +} + #[test] fn an_undecodable_revision_stays_final() { let rig = Rig::new(vec![], &[]); From 51a13b7e821e528d34cb49fa2e7248d46b983bcb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 11 Nov 2026 23:28:19 +0100 Subject: [PATCH 2/2] mind-body: only an answer to a revision line spends a waiting stale-send revision A flush answer never showed her the revision, so spending it acked a batch she had not answered. final_reply stays the send-basis switch. Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/persona.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 28e2e1d..5198bf7 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -1856,8 +1856,8 @@ impl SessionInbox for PersonaInbox<'_, C, S> // failed (a batch's third refusal leaves no retell to hold the ack back). Deliveries are // told in seq order, so while one is unanswered below an answered seq, everything // answered lies above it and nothing is acked; the ack follows its answer. - // Her final answer lands after she took in every line, so a revision still waiting is answered by it and its note, that a send was not sent, would be false. - let spent = if final_reply { self.revision.take().map(|revision| revision.batch) } else { None }; + // Only an answer to the revision line spends a waiting revision: a flush answer never saw it, so its batch stays unacked for the next process. + let spent = if revised { self.revision.take().map(|revision| revision.batch) } else { None }; self.answered_through = self.answered_through.max(batch.iter().chain(spent.iter().flatten()).map(|delivery| delivery.seq).max().unwrap_or(0)); if self.progress.refused.as_ref().is_some_and(|refused| batch.iter().any(|delivery| delivery.seq == refused.lowest)) { self.progress.refused = None;