Address-only wake: a DM is hers whatever the basis; the quiet ack reads her projects' names from her mind; no quiet ack while a stale send waits; a refused ack in a poll is retried
Soul findings F1, F2, F3 and F6 on PR 43. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
50d8e60102
commit
2f04e13ea5
5 changed files with 92 additions and 29 deletions
|
|
@ -53,10 +53,10 @@ pub(crate) fn addresses(who: &Addressee, delivery: &ChatDelivery) -> bool {
|
|||
delivery.form == Form::Message && (delivery.mentions.contains(&who.matrix_id) || who.names.iter().any(|name| names_whole_word(&delivery.text, &name.text, name.exact)))
|
||||
}
|
||||
|
||||
/// Whether a delivery the mouth gave `own` is for her: Matrix's word that it is her DM, a tag of hers or a reply to her, or what it
|
||||
/// says (`addresses`). The basis `Room` (`@room`) is for no one, and no basis is unproven, not "not for her": the names still decide.
|
||||
/// Whether a delivery the mouth gave `own` is for her: her DM (`is_direct`, which every mouth sends, so an old mouth's DM is hers too),
|
||||
/// Matrix's word that it is a tag of hers or a reply to her, or what it says (`addresses`). The basis `Room` (`@room`) is for no one, and no basis is unproven, not "not for her": the names still decide.
|
||||
pub(crate) fn is_for(own: &Addressee, delivery: &ChatDelivery) -> bool {
|
||||
(delivery.form == Form::Message && matches!(delivery.basis, Some(Basis::Direct | Basis::Tag | Basis::Reply))) || addresses(own, delivery)
|
||||
(delivery.form == Form::Message && (delivery.is_direct || matches!(delivery.basis, Some(Basis::Direct | Basis::Tag | Basis::Reply)))) || addresses(own, delivery)
|
||||
}
|
||||
|
||||
/// The chat names of one stewarded repo. Listed names are exactly those, each matched exactly as the steward wrote it (capitalization
|
||||
|
|
|
|||
|
|
@ -128,3 +128,12 @@ fn only_a_message_can_address_and_the_mouths_basis_stands_for_the_polling_mind_a
|
|||
assert!(is_for(&nibu, &with(None, Form::Message, "nibu, look", &[])), "unproven is not 'not for her': her name decides");
|
||||
assert!(is_for(&nibu, &with(Some(Basis::Room), Form::Message, "nibu, look", &[])), "an @room that also names her is named");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_direct_message_is_for_her_whatever_the_basis() {
|
||||
let nibu = steward("GameCult/Thing", &[]);
|
||||
let dm = ChatDelivery { is_direct: true, ..said("nothing named", &[]) };
|
||||
assert!(is_for(&nibu, &dm), "an old mouth sends no basis; the DM is hers");
|
||||
assert!(!is_for(&nibu, &ChatDelivery { form: Form::Notice, ..dm.clone() }));
|
||||
assert!(!is_for(&nibu, &ChatDelivery { form: Form::Edit, ..dm }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ fn newest(mind: &dyn MindPort, selection: Selection) -> Result<Option<PipelineDo
|
|||
}
|
||||
|
||||
/// The chat names of every repo she stewards in force: one query on her mind.
|
||||
fn project_names_of(mind: &dyn MindPort) -> Result<Vec<Name>> {
|
||||
pub(crate) fn project_names_of(mind: &dyn MindPort) -> Result<Vec<Name>> {
|
||||
let mut names = Vec::new();
|
||||
for header in headers(mind, &in_force(of_kind(PipelineKind::Stewardship)))? {
|
||||
if let PipelineFacts::Stewardship { repo, names: listed, .. } = &header.facts {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ use crate::chat_image::{self, Image, MAX_PER_LINE};
|
|||
use crate::chat_client::{Chat, ChatDelivery, MouthRefused, ChatOpenDirectRequest, ChatRead, DirectOutcome, ChatSendRequest, ContextLine, Refusal, SendBasis};
|
||||
use crate::connector::render_plan;
|
||||
use crate::control::{ControlSource, Effective, HeldReason, derive};
|
||||
use crate::heartbeat::{HeartbeatView, PollTrace};
|
||||
use crate::heartbeat::{HeartbeatView, PollTrace, project_names_of};
|
||||
use crate::lease_client::{Lease, LeaseRequest, Lender, Lent};
|
||||
use crate::launch::{Clock, LaunchOutcome, LaunchRequest, Launcher, Ports, holders, open_and_launch};
|
||||
use crate::queue::{MindPort, any_of, citing, headers, in_force, live_claims, of_kind};
|
||||
|
|
@ -894,8 +894,11 @@ pub fn carry(organs: &Organs, control: &impl ControlSource, swarm: &impl Control
|
|||
let routes = reply_routes(organs.mind, &BTreeSet::new()).unwrap_or_default();
|
||||
let addressing = addressing_of(config, organs.heartbeat, &own_addressee(config));
|
||||
pending.sort_by_key(|delivery| delivery.seq);
|
||||
let own = addressing.iter().find(|mind| mind.matrix_id() == config.own_sender).cloned().unwrap_or_else(|| own_addressee(config));
|
||||
if let Some(through) = quiet_through(&own, &pending, 0) {
|
||||
// Her names as the heartbeat's table has them, and her projects' names as her mind holds them now: the table is saved after the
|
||||
// heartbeat launches her, so it may not yet hold a project a message names.
|
||||
let table = addressing.iter().find(|mind| mind.matrix_id() == config.own_sender).cloned().unwrap_or_else(|| own_addressee(config));
|
||||
let fresh = own_addressee(config).with_projects(project_names_of(organs.mind).unwrap_or_default());
|
||||
if let Some(through) = quiet_through(&[&table, &fresh], &pending, 0) {
|
||||
if organs.chat.ack(through).is_err() {
|
||||
progress.failure = Some(Failure::Ack);
|
||||
break;
|
||||
|
|
@ -1608,12 +1611,14 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for PersonaInbox<'_, C, S>
|
|||
if !pending.is_empty() {
|
||||
self.addressing = addressing_of(self.config, self.organs.heartbeat, &self.addressee);
|
||||
}
|
||||
let own = self.addressing.iter().find(|mind| mind.matrix_id() == self.config.own_sender).cloned().unwrap_or_else(|| self.addressee.clone());
|
||||
if let Some(through) = quiet_through(&own, &pending, self.told_through) {
|
||||
if self.organs.chat.ack(through).is_err() {
|
||||
self.progress.failure.get_or_insert(Failure::Ack);
|
||||
return Ok(Polled { pending: false, other: false, other_first: false, yield_now: true });
|
||||
}
|
||||
let row = self.addressing.iter().find(|mind| mind.matrix_id() == self.config.own_sender).cloned().unwrap_or_else(|| self.addressee.clone());
|
||||
let own = [&row, &self.addressee];
|
||||
// Not while a stale send waits for the messages that interrupted it: they are what the revision line shows her. A failed ack is
|
||||
// tried again at the next poll; it is not a reason to end the session.
|
||||
if self.revision.is_none()
|
||||
&& let Some(through) = quiet_through(&own, &pending, self.told_through)
|
||||
&& self.organs.chat.ack(through).is_ok()
|
||||
{
|
||||
self.progress.acked = through;
|
||||
pending.retain(|delivery| delivery.seq > through);
|
||||
}
|
||||
|
|
@ -1635,7 +1640,7 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for PersonaInbox<'_, C, S>
|
|||
self.first_seen.entry(delivery.seq).or_insert(now);
|
||||
}
|
||||
let any_untold = !untold.is_empty();
|
||||
let due = revision_ready || untold.iter().any(|delivery| is_for(&own, delivery) && now >= self.first_seen[&delivery.seq] + stagger_ms(self.config, &self.addressing, delivery));
|
||||
let due = revision_ready || untold.iter().any(|delivery| own.iter().any(|who| is_for(who, delivery)) && now >= self.first_seen[&delivery.seq] + stagger_ms(self.config, &self.addressing, delivery));
|
||||
self.waiting = if due { one_line_of(untold, |batch| message_lines(self.config, &self.addressee, &self.addressing, batch, &self.earlier(batch)).len()) } else { Vec::new() };
|
||||
Ok(self.polled(!self.waiting.is_empty() || self.retell.is_some() || revision_ready || !self.notices.is_empty() || !self.waiting_replies.is_empty(), any_untold))
|
||||
}
|
||||
|
|
@ -1993,12 +1998,13 @@ fn act<C: ControlSource, S: ControlSource>(inbox: &PersonaInbox<'_, C, S>, batch
|
|||
Ok(Step::Answered(started, reply.intent, opened, notice))
|
||||
}
|
||||
|
||||
/// The seq an ack may pass because nothing in it is for her: the leading run of `pending` (oldest first) that is not addressed to
|
||||
/// `own` and lies above `told_through`, so no delivery she was told and has not answered is passed. None when the front is addressed,
|
||||
/// or already told. Such a delivery wakes and tells no one; it is acked in sequence so the mouth's position moves (it stops reading
|
||||
/// at 500 unacked) and is not lost: Matrix keeps it, and the `context` of the next addressed delivery shows it.
|
||||
fn quiet_through(own: &Addressee, pending: &[ChatDelivery], told_through: u64) -> Option<u64> {
|
||||
let run: Vec<&ChatDelivery> = pending.iter().take_while(|delivery| delivery.seq > told_through && !is_for(own, delivery)).collect();
|
||||
/// The seq an ack may pass because nothing in it is for her: the leading run of `pending` (oldest first) that is not addressed to any of
|
||||
/// `own` (her rows as the heartbeat's table, her projects' names and her session know them) and lies above `told_through`, so no
|
||||
/// delivery she was told and has not answered is passed. None when the front is addressed, or already told. Such a delivery wakes and
|
||||
/// tells no one; it is acked in sequence so the mouth's position moves (it stops reading at 500 unacked) and is not lost: Matrix keeps
|
||||
/// it, and the `context` of the next addressed delivery shows it.
|
||||
fn quiet_through(own: &[&Addressee], pending: &[ChatDelivery], told_through: u64) -> Option<u64> {
|
||||
let run: Vec<&ChatDelivery> = pending.iter().take_while(|delivery| delivery.seq > told_through && !own.iter().any(|who| is_for(who, delivery))).collect();
|
||||
run.last().map(|last| last.seq)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ struct FakeChat {
|
|||
speaker: RefCell<String>,
|
||||
/// Acks are recorded and the deliveries stay, where several sessions share one pending.
|
||||
keep_on_ack: Cell<bool>,
|
||||
/// The mouth refuses every ack.
|
||||
fail_ack: Cell<bool>,
|
||||
/// The opens asked of the mouth, in order.
|
||||
opens: RefCell<Vec<ChatOpenDirectRequest>>,
|
||||
/// How the fake mouth answers an open; none is a transport error.
|
||||
|
|
@ -169,6 +171,9 @@ impl Chat for FakeChat {
|
|||
}
|
||||
|
||||
fn ack(&self, through_seq: u64) -> Result<()> {
|
||||
if self.fail_ack.get() {
|
||||
return Err(anyhow!("the mouth is down"));
|
||||
}
|
||||
self.log.borrow_mut().push("ack".into());
|
||||
self.acked.borrow_mut().push(through_seq);
|
||||
if !self.keep_on_ack.get() {
|
||||
|
|
@ -459,6 +464,7 @@ impl Rig {
|
|||
landed: RefCell::default(),
|
||||
speaker: RefCell::new("@eureka:chat".into()),
|
||||
keep_on_ack: Cell::new(false),
|
||||
fail_ack: Cell::new(false),
|
||||
opens: RefCell::default(),
|
||||
open_outcome: Cell::new(Some(DirectOutcome::Opened)),
|
||||
open_unserved: Cell::new(false),
|
||||
|
|
@ -7947,13 +7953,13 @@ fn a_leading_run_of_what_is_not_hers_is_quiet_and_an_ack_never_passes_what_is_to
|
|||
let own = own_addressee(&config());
|
||||
let room = |seq| heard(seq, "anyone around", Some(Basis::Room), Form::Message);
|
||||
let unproven = |seq| heard(seq, "anyone around", None, Form::Message);
|
||||
assert_eq!(quiet_through(&own, &[unproven(1), room(2), unproven(3)], 0), Some(3), "a nil basis and an @room are not hers");
|
||||
assert_eq!(quiet_through(&[&own], &[unproven(1), room(2), unproven(3)], 0), Some(3), "a nil basis and an @room are not hers");
|
||||
let hers = heard(4, "anyone around", Some(Basis::Reply), Form::Message);
|
||||
assert_eq!(quiet_through(&own, &[unproven(1), hers.clone(), unproven(5)], 0), Some(1), "the run stops at the first that is hers");
|
||||
assert_eq!(quiet_through(&own, &[hers, unproven(5)], 0), None);
|
||||
assert_eq!(quiet_through(&own, &[unproven(1), unproven(2)], 1), None, "the front was told and is not answered: nothing passes it");
|
||||
assert_eq!(quiet_through(&own, &[unproven(5), unproven(6)], 5), None);
|
||||
assert_eq!(quiet_through(&own, &[], 0), None);
|
||||
assert_eq!(quiet_through(&[&own], &[unproven(1), hers.clone(), unproven(5)], 0), Some(1), "the run stops at the first that is hers");
|
||||
assert_eq!(quiet_through(&[&own], &[hers, unproven(5)], 0), None);
|
||||
assert_eq!(quiet_through(&[&own], &[unproven(1), unproven(2)], 1), None, "the front was told and is not answered: nothing passes it");
|
||||
assert_eq!(quiet_through(&[&own], &[unproven(5), unproven(6)], 5), None);
|
||||
assert_eq!(quiet_through(&[&own], &[], 0), None);
|
||||
}
|
||||
|
||||
/// A notice or an edit never addresses her, whatever else it says; a nil basis does not mean "not for her": her name still does.
|
||||
|
|
@ -7964,10 +7970,10 @@ fn a_notice_and_an_edit_are_quiet_whatever_they_say_and_a_nil_basis_with_her_nam
|
|||
for (basis, form) in [(Some(Basis::Direct), Form::Notice), (Some(Basis::Tag), Form::Edit), (Some(Basis::Reply), Form::Notice), (None, Form::Edit)] {
|
||||
let mut line = heard(1, "eureka, look", basis, form);
|
||||
line.mentions = vec!["@eureka:chat".into()];
|
||||
assert_eq!(quiet_through(&own, &[line], 0), Some(1), "{basis:?} {form:?}");
|
||||
assert_eq!(quiet_through(&[&own], &[line], 0), Some(1), "{basis:?} {form:?}");
|
||||
}
|
||||
assert_eq!(quiet_through(&own, &[heard(1, "Eureka, look", None, Form::Message)], 0), None, "an old mouth's delivery, named in the text");
|
||||
assert_eq!(quiet_through(&own, &[heard(1, "look", Some(Basis::Direct), Form::Message)], 0), None);
|
||||
assert_eq!(quiet_through(&[&own], &[heard(1, "Eureka, look", None, Form::Message)], 0), None, "an old mouth's delivery, named in the text");
|
||||
assert_eq!(quiet_through(&[&own], &[heard(1, "look", Some(Basis::Direct), Form::Message)], 0), None);
|
||||
}
|
||||
|
||||
/// Two unaddressed lines ahead of an addressed one are acked at the start, in one ack, and the turn is told the addressed one alone.
|
||||
|
|
@ -8011,3 +8017,45 @@ fn a_v1_mouths_deliveries_still_wake_her_by_name_and_by_tag() {
|
|||
assert_eq!(acked, [2]);
|
||||
assert!(first_line(&writes).contains("Eureka, look"));
|
||||
}
|
||||
|
||||
/// An old mouth sends no basis, but every mouth sends `is_direct`: a DM that names no one is hers, is told, and is not acked as quiet.
|
||||
#[test]
|
||||
fn a_direct_message_from_an_old_mouth_is_hers_with_no_basis() {
|
||||
use crate::chat_client::Form;
|
||||
let dm = ChatDelivery { is_direct: true, ..heard(1, "can you look at this", None, Form::Message) };
|
||||
assert_eq!(quiet_through(&[&own_addressee(&config())], &[dm.clone()], 0), None);
|
||||
let rig = Rig::new(vec![dm], &[says(&[("operator", "ok")])]);
|
||||
rig.clock.step.set(0);
|
||||
let (child, writes) = answering(&rig, reply_saying("ok"));
|
||||
let (_, acked) = carry_children(&rig, vec![child]);
|
||||
assert!(first_line(&writes).contains("can you look at this"));
|
||||
assert_eq!(acked, [1]);
|
||||
}
|
||||
|
||||
/// A message naming only a project she stewards is hers by her mind's records, though no heartbeat table holds the name yet: it is told, not
|
||||
/// acked as quiet at the start.
|
||||
#[test]
|
||||
fn a_message_naming_only_her_project_is_told_though_no_table_holds_the_name() {
|
||||
use crate::chat_client::Form;
|
||||
let rig = Rig::new(vec![heard(1, "is the bad day simulator green?", None, Form::Message)], &[says(&[])]);
|
||||
rig.mind.committed(vec![steward_naming("GameCult/BadDaySimulator", &[])]);
|
||||
rig.clock.step.set(0);
|
||||
let (child, writes) = answering(&rig, reply_saying("ok"));
|
||||
let (_, acked) = carry_children(&rig, vec![child]);
|
||||
assert!(first_line(&writes).contains("bad day simulator"), "{}", first_line(&writes));
|
||||
assert_eq!(acked, [1], "acked once, after it was answered");
|
||||
}
|
||||
|
||||
/// A refused ack at the start ends the start with the ack failure, and nothing is told.
|
||||
#[test]
|
||||
fn a_refused_quiet_ack_ends_the_start_and_tells_nothing() {
|
||||
use crate::chat_client::Form;
|
||||
let rig = Rig::new(vec![heard(1, "chatter", None, Form::Message), heard(2, "more chatter", None, Form::Message)], &[]);
|
||||
rig.chat.fail_ack.set(true);
|
||||
rig.clock.step.set(0);
|
||||
let (child, writes) = answering(&rig, empty_reply());
|
||||
let (outcome, acked) = carry_children(&rig, vec![child]);
|
||||
assert_eq!(failure_of(outcome), Some(Failure::Ack));
|
||||
assert!(acked.is_empty());
|
||||
assert!(writes.borrow().is_empty());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue