minds-wake-each-other: a message from another Mind that names her launches a Mind off the beat, once per message; a Mind naming herself does not

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-31 13:52:27 +01:00
commit c33884fe7a
2 changed files with 34 additions and 2 deletions

View file

@ -426,6 +426,14 @@ fn addresses(who: &PersonaId, delivery: &ChatDelivery) -> Option<&'static str> {
}
}
/// Whether a delivery wakes `who` off the beat: the operator or another Mind of
/// the roster addresses her. A Mind naming herself does not; a member does not.
fn wakes(config: &Config, who: &PersonaId, delivery: &ChatDelivery) -> bool {
let from_operator = config.operators.contains(&delivery.sender);
let from_other_mind = delivery.sender != who.matrix_id && config.personas.iter().any(|mind| mind.matrix_id == delivery.sender);
(from_operator || from_other_mind) && addresses(who, delivery).is_some()
}
/// The mouth pages a pending answer, so fewer deliveries may come than wait.
/// The heartbeat acks nothing: it asks again after the highest seq it has until
/// the answer is empty. A mouth that never empties is cut off at `PENDING_PAGES`.
@ -593,7 +601,7 @@ pub fn poll(organs: &Organs, config: &Config) -> Result<Outcome> {
let mut launches: Vec<String> = Vec::new();
let mut launched: Vec<Slug> = Vec::new();
// Off the beat: the operator naming her, once per message, and her Self run
// Off the beat: the operator or another Mind of the roster naming her, once per message, and her Self run
// having ended since her last turn. Neither waits for the beat; both need her
// idle and released.
for row in &mut rows {
@ -606,7 +614,7 @@ pub fn poll(organs: &Organs, config: &Config) -> Result<Outcome> {
pending.sort_by_key(|delivery| delivery.seq);
pending
.into_iter()
.filter(|delivery| config.operators.contains(&delivery.sender) && addresses(&row.seat.who, delivery).is_some())
.filter(|delivery| wakes(config, &row.seat.who, delivery))
.map(|delivery| format!("{}:{}", slug.0, delivery.event_id))
.find(|key| !state.handled_mentions.contains(key))
};

View file

@ -462,6 +462,30 @@ fn the_operator_naming_a_persona_launches_her_at_once_off_the_beat_and_once_per_
assert_eq!(rig.poll(), Outcome::Polled { launched: vec![Slug("ann".into())] });
}
#[test]
fn another_mind_naming_a_persona_wakes_her_once_off_the_beat() {
let rig = Rig::new();
rig.turn(ANN, 10);
rig.turn(BOB, 10);
rig.store.save(&HeartbeatState { last_beat_at: rig.now(), handled_mentions: vec![] }).unwrap();
rig.say(BOB, delivery(1, "@ann:chat", "Bob, a word", &[]));
assert_eq!(rig.poll(), Outcome::Polled { launched: vec![Slug("bob".into())] }, "a Mind of the roster wakes her at once");
rig.end_run(BOB);
assert_eq!(rig.poll(), Outcome::Polled { launched: vec![] }, "the same message polled again wakes nobody");
assert_eq!(rig.runs(BOB), 2);
}
#[test]
fn a_mind_naming_no_one_or_herself_wakes_nobody() {
let rig = Rig::new();
rig.turn(ANN, 10);
rig.turn(BOB, 10);
rig.store.save(&HeartbeatState { last_beat_at: rig.now(), handled_mentions: vec![] }).unwrap();
rig.say(BOB, delivery(1, "@ann:chat", "nothing to anyone", &[]));
rig.say(BOB, delivery(2, "@bob:chat", "Bob reminds Bob", &["@bob:chat"]));
assert_eq!(rig.poll(), Outcome::Polled { launched: vec![] });
}
#[test]
fn a_member_naming_a_persona_launches_nobody_off_the_beat_but_counts_as_full_urgency_on_it() {
let rig = Rig::new();