From 987c6ec74de8e165f9472ecefabf6b9349bd53ac Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 6 Nov 2026 23:15:25 +0100 Subject: [PATCH] persona: the mind says whether a heard reply took; tests kill the keyless read and the lost resolution hear_replies attempts each resolution and untell asks the mind which replies are still in force, so an Ok/Err reading of the admission is no second opinion. The out-of-force test holds replies past the first line so a read by kind alone would retell the resolved one. Co-Authored-By: Claude Sonnet 5.5 --- crates/mind-body/src/persona.rs | 23 ++++++++++++----------- crates/mind-body/src/persona/tests.rs | 12 +++++++++--- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 88be021..ea79e1e 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -1215,7 +1215,7 @@ impl<'a, C: ControlSource, S: ControlSource> PersonaInbox<'a, C, S> { } /// Replies that were told and not answered are told again: the line failed, was stale or was refused. A reply no longer in force - /// (resolved by someone, or by a resolution whose answer was lost) leaves for good: it stays in `told_replies`, so no read returns it. + /// (resolved by her answer, by someone else, or by a resolution whose answer was lost) leaves for good: it stays in `told_replies`, so no read returns it. fn untell(&mut self, replies: Vec) { for reply in replies { if matches!(reply_in_force(self.organs.mind, &reply.id), Ok(false)) { @@ -1425,8 +1425,8 @@ impl SessionInbox for PersonaInbox<'_, C, S> match step { Step::Answered(started, intent, opened) => { // The result answers the replies its lines carried: they are heard now, and only now. - let unresolved = hear_replies(self.organs, self.run, heard, started.as_ref().map(|(_, follow_up)| follow_up)); - self.untell(unresolved); + hear_replies(self.organs, self.run, &heard, started.as_ref().map(|(_, follow_up)| follow_up)); + self.untell(heard); // What she admitted this turn is hers to settle or take on for the rest of the session, though no line showed its id. self.seen.add(Shown { people: Vec::new(), open: opened }); // The one ack rule: through the highest answered seq, never past a delivery still told @@ -1838,10 +1838,13 @@ fn unheard_replies(mind: &dyn MindPort, told: &BTreeSet, waiting: &[Hear } /// The result that answers a line resolves each reply the line carried, one by one: Answered by the follow-up her answer's -/// `start_self_run` admitted, else Recorded as heard. A reply resolved is out of force, so no later process tells it. A reply whose -/// resolution fails does not hold the others hostage: it is returned, to be told again if it is still in force. -fn hear_replies(organs: &Organs, run: &PipelineRef, heard: Vec, follow_up: Option<&PipelineRef>) -> Vec { - heard.into_iter().filter(|reply| resolve_reply(organs, run, reply, follow_up).is_err()).collect() +/// `start_self_run` admitted, else Recorded as heard. A resolved reply is out of force, so no later process tells it. Whether it took is +/// read from the mind (`reply_in_force`, in `untell`), not from this answer: a reply whose resolution failed stays in force and is told +/// again, one that someone else resolved, or whose answer was lost, is not; and one failure holds no other reply hostage. +fn hear_replies(organs: &Organs, run: &PipelineRef, heard: &[HeardReply], follow_up: Option<&PipelineRef>) { + for reply in heard { + let _ = resolve_reply(organs, run, reply, follow_up); + } } fn resolve_reply(organs: &Organs, run: &PipelineRef, reply: &HeardReply, follow_up: Option<&PipelineRef>) -> Result<()> { @@ -1857,10 +1860,8 @@ fn resolve_reply(organs: &Organs, run: &PipelineRef, reply: &HeardReply, follow_ rationale: "Her Persona heard this reply of her Self in a turn and answered it.".into(), resolved_on: date(organs.clock.now()), }); - match organs.mind.admit(AGENT, label_of(run), vec![resolution])? { - PipelineAdmissionOutcome::Committed { .. } | PipelineAdmissionOutcome::AlreadyAdmitted { .. } => Ok(()), - _ => bail!("the reply could not be resolved"), - } + organs.mind.admit(AGENT, label_of(run), vec![resolution])?; + Ok(()) } /// Whether the reply is still in force in her mind. diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 17ae31c..b73b55b 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -4483,7 +4483,9 @@ fn a_refused_result_leaves_its_replies_in_force_and_they_are_told_again() { fn a_reply_put_out_of_force_after_it_was_told_leaves_for_good_and_holds_no_other_reply_hostage() { let rig = Rig::new(vec![operator_says(1, "hello")], &[says(&[]), says(&[]), says(&[]), says(&[])]); rig.mind.committed(her_self_asked()); - rig.mind.committed(vec![self_reply("self-r-r1", "One."), self_reply("self-r-r2", "Two.")]); + // Five long replies after the first: the first line cannot hold them all, so some wait for a later line while the first is untold. + let long = "x".repeat(3900); + rig.mind.committed(vec![self_reply("self-r-r1", "One."), self_reply("self-r-r2", &long), self_reply("self-r-r3", &long), self_reply("self-r-r4", &long), self_reply("self-r-r5", &long), self_reply("self-r-r6", &long)]); rig.clock.step.set(1); rig.model.linger.set(true); rig.open_the_run(); @@ -4512,10 +4514,14 @@ fn a_reply_put_out_of_force_after_it_was_told_leaves_for_good_and_holds_no_other assert_eq!(failure_of(rig.start_over(&timeline, &config())), None); let lines = told(&rig); let told_of = |label: &str| all_replies(&lines).iter().filter(|line| line.contains(&reply_id(label))).count(); - assert_eq!((told_of("self-r-r1"), told_of("self-r-r2")), (1, 1), "each told once: {lines:?}"); + for n in 1..=6 { + assert_eq!(told_of(&format!("self-r-r{n}")), 1, "r{n} told once: {}", lines.len()); + } assert!(lines.len() >= 3, "later lines followed: {lines:?}"); assert_eq!(outcome_of(&rig, &reply_id("self-r-r1")), Some(ResolutionOutcome::Recorded { reason: "elsewhere".into() })); - assert_eq!(outcome_of(&rig, &reply_id("self-r-r2")), heard_outcome(), "the reply beside it was heard"); + for n in 2..=6 { + assert_eq!(outcome_of(&rig, &reply_id(&format!("self-r-r{n}"))), heard_outcome(), "r{n}, beside it or on a later line, was heard"); + } } /// A reply that reaches a session with no chat pending is read at the next interval, told on the line it makes, and resolved.