An earlier line this session has not shown prints once, in a thread or a room alike

rooms_shown kept the room as the unit, so a thread in a shown room lost its exchange, a first message
in a thread lost the room's tail, and a failed context fetch marked the room shown. Now one set of the
lines a process told, keyed by room, sender, instant and the head of the text (the mouth clips a
message's copy, so the whole text differs). Her own lines of this session do not come back. Who she
may name follows what printed.

Findings thread-in-a-shown-room-loses-the-exchange, people-bound-counts-unprinted-context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-11-02 19:20:58 +01:00
commit 3eee12f600
2 changed files with 192 additions and 39 deletions

View file

@ -867,7 +867,8 @@ fn one_process(
rooms,
conversation,
plan: render_plan(lease.plan_quota(), started_ms),
rooms_shown: BTreeSet::new(),
started_ms,
told: BTreeSet::new(),
start: first,
waiting: Vec::new(),
told_through: 0,
@ -973,6 +974,13 @@ struct Retell {
refusals: u32,
}
/// A rendered line: its text, what it lets a reply name, and the lines it tells.
struct Rendered {
input: String,
shown: Shown,
told: Vec<LineKey>,
}
/// The Persona's side of the session: her chat, in one conversation, told to the live session
/// by the Self's own driver. Its caches (what was told, what was echoed) are the process's;
/// the mouth's ack is the truth.
@ -987,8 +995,10 @@ struct PersonaInbox<'a, C: ControlSource, S: ControlSource> {
conversation: Conversation,
/// The plan windows the lease granted, as one line.
plan: String,
/// The rooms a line of this session has shown: a later message from another brings that room's tail once.
rooms_shown: BTreeSet<String>,
/// When this process began: a line of her own from then on she wrote in this session.
started_ms: u64,
/// Every message and `earlier` line a line of this process has told: an `earlier` line prints once, in a thread or a room alike.
told: BTreeSet<LineKey>,
/// The batch the first line carries; taken by it.
start: Vec<ChatDelivery>,
/// The deliveries derived at the last poll that no line has carried yet.
@ -1016,8 +1026,13 @@ struct PersonaInbox<'a, C: ControlSource, S: ControlSource> {
}
impl<C: ControlSource, S: ControlSource> PersonaInbox<'_, C, S> {
/// The `earlier` lines a batch brings now.
fn earlier<'d>(&self, deliveries: &'d [ChatDelivery]) -> Vec<Vec<&'d ContextLine>> {
earlier_shown(deliveries, &self.told, &self.config.own_sender, self.started_ms)
}
/// A session's first line: her whole machine. The gate was read by the caller.
fn whole_line(&self, deliveries: &[ChatDelivery], note: Option<&str>) -> Result<(String, Shown)> {
fn whole_line(&self, deliveries: &[ChatDelivery], note: Option<&str>) -> Result<Rendered> {
let organs = self.organs;
let instance = organs.mind.instance().clone();
let tempo = render_tempo(organs.tempo, &instance);
@ -1028,26 +1043,28 @@ impl<C: ControlSource, S: ControlSource> PersonaInbox<'_, C, S> {
let cue = recall::Cue { senders, texts: deliveries.iter().rev().map(|delivery| delivery.text.as_str()).collect(), names: names.clone(), listed: &goals.listed, now: organs.clock.now() };
let recalled = recall::select(organs.mind, &cue)?;
let shown: Vec<&str> = goals.people.iter().map(String::as_str).chain(recalled.entries().flat_map(|entry| entry.people.iter().map(String::as_str))).collect();
let people = people_she_may_name(self.config, deliveries, &shown);
let input = render_input(self.config, deliveries, note, &history, &goals.text, &recall::render(&recalled), &format!("{}\n\n{tempo}", self.plan), &people);
Ok((input, Shown { people, open: goals.open }))
let earlier = self.earlier(deliveries);
let people = people_she_may_name(self.config, deliveries, &earlier, &shown);
let input = render_input(self.config, &message_lines(self.config, deliveries, &earlier), note, &history, &goals.text, &recall::render(&recalled), &format!("{}\n\n{tempo}", self.plan), &people);
Ok(Rendered { input, shown: Shown { people, open: goals.open }, told: lines_told(deliveries, &earlier) })
}
/// A later line: the messages, a refused batch's note before them, the notices of her ended runs, and nothing else read from her mind.
/// What a reply to it may name is what the session showed, and whoever is on this line.
/// The organ's notices of her ended runs come first: they are not messages and answer to no delivery.
fn tell_line(&self, deliveries: &[ChatDelivery], note: Option<&str>, notices: &str) -> (String, Shown) {
let lines = format!("{notices}{}", message_lines(self.config, deliveries, &self.rooms_shown));
fn tell_line(&self, deliveries: &[ChatDelivery], note: Option<&str>, notices: &str) -> Rendered {
let earlier = self.earlier(deliveries);
let lines = format!("{notices}{}", message_lines(self.config, deliveries, &earlier));
let input = note.map_or_else(|| lines.clone(), |note| format!("{note}\n\n{lines}"));
(input, Shown { people: people_she_may_name(self.config, deliveries, &[]), open: Vec::new() })
Rendered { input, shown: Shown { people: people_she_may_name(self.config, deliveries, &earlier, &[]), open: Vec::new() }, told: lines_told(deliveries, &earlier) }
}
/// Notes what a rendered line carries, as it goes to the child.
fn write(&mut self, batch: Vec<ChatDelivery>, (input, shown): (String, Shown), refusals: u32) -> String {
fn write(&mut self, batch: Vec<ChatDelivery>, Rendered { input, shown, told }: Rendered, refusals: u32) -> String {
if let Some(through) = batch.iter().map(|delivery| delivery.seq).max() {
self.told_through = self.told_through.max(through);
}
self.rooms_shown.extend(batch.iter().map(|delivery| delivery.room_alias.clone()));
self.told.extend(told);
self.written = Some(Written { batch, shown, refusals });
input
}
@ -1621,9 +1638,9 @@ fn other_senders<'a>(config: &Config, deliveries: &'a [ChatDelivery]) -> Vec<&'a
}
/// Whom a memory may be about, each within `Short`'s bound, once, in this order: the batch's other
/// senders, the senders of the earlier lines shown beside them, the Minds mentioned, the operators,
/// senders, the senders of the earlier lines this line prints, the Minds mentioned, the operators,
/// the swarm's Minds, then everyone a memory shown this turn names. Never her own id.
fn people_she_may_name(config: &Config, deliveries: &[ChatDelivery], shown: &[&str]) -> Vec<String> {
fn people_she_may_name(config: &Config, deliveries: &[ChatDelivery], earlier: &[Vec<&ContextLine>], shown: &[&str]) -> Vec<String> {
let mut people: Vec<String> = Vec::new();
let mut add = |id: &str| {
if id != config.own_sender && id.len() <= SHORT_MAX_BYTES && !id.is_empty() && !people.iter().any(|known| known == id) {
@ -1631,7 +1648,7 @@ fn people_she_may_name(config: &Config, deliveries: &[ChatDelivery], shown: &[&s
}
};
other_senders(config, deliveries).into_iter().for_each(&mut add);
deliveries.iter().flat_map(|delivery| delivery.context.iter().map(|line| line.sender.as_str())).for_each(&mut add);
earlier.iter().flatten().map(|line| line.sender.as_str()).for_each(&mut add);
deliveries.iter().flat_map(|delivery| delivery.mentions.iter().map(String::as_str)).for_each(&mut add);
config.operators.iter().map(String::as_str).for_each(&mut add);
config.minds.iter().map(String::as_str).for_each(&mut add);
@ -1929,16 +1946,41 @@ const WAKE_QUESTION: &str = "No new messages. Where is your machine least perfec
/// Last text of a line with deliveries, after the messages.
const TRAFFIC_QUESTION: &str = "Answer what needs you. Then the question of every turn: where is your machine least perfect right now, by your values, and does anything here change your move? Put that in `intent` first.";
/// The `earlier` lines a batch's line shows, one list per delivery: each distinct line of a room once (the mouth stamps a
/// message and its copy in a later context with one `at_unix_ms`, so a genuine repeat at another instant is another line),
/// where it first comes in the batch, none the batch carries as a message, and of each room the newest lines whose rendered
/// (quoted) text fits `EARLIER_MAX_BYTES_PER_ROOM`. A line too big for what is left is skipped, never a reason to drop the
/// older lines that fit. A room this session has already shown brings none: its tail comes once.
fn earlier_shown<'a>(deliveries: &'a [ChatDelivery], rooms_shown: &BTreeSet<String>) -> Vec<Vec<&'a ContextLine>> {
let mut seen: BTreeSet<(&str, &str, &str, u64)> = deliveries.iter().map(|delivery| (delivery.room_alias.as_str(), delivery.sender.as_str(), delivery.text.as_str(), delivery.at_unix_ms)).collect();
/// How much of a line's text its key holds: the mouth clips a message's copy in a later context at 1000 bytes and the message
/// itself at 4000, so a long message and its copy agree only on their head. 200 chars are at most 800 bytes.
const LINE_KEY_TEXT_CHARS: usize = 200;
/// A line of a room, as a session knows it: room, sender, instant and the head of its text. The mouth stamps a message and its copy
/// in a later context with one `at_unix_ms`, so a genuine repeat at another instant is another line.
type LineKey = (String, String, u64, String);
fn line_key(room: &str, sender: &str, at_unix_ms: u64, text: &str) -> LineKey {
(room.to_owned(), sender.to_owned(), at_unix_ms, text.chars().take(LINE_KEY_TEXT_CHARS).collect())
}
fn message_key(delivery: &ChatDelivery) -> LineKey {
line_key(&delivery.room_alias, &delivery.sender, delivery.at_unix_ms, &delivery.text)
}
/// What a line tells: its messages and the `earlier` lines it prints. A line the budget skipped is not among them.
fn lines_told(deliveries: &[ChatDelivery], earlier: &[Vec<&ContextLine>]) -> Vec<LineKey> {
let printed = deliveries.iter().zip(earlier).flat_map(|(delivery, lines)| lines.iter().map(|line| line_key(&delivery.room_alias, &line.sender, line.at_unix_ms, &line.text)));
deliveries.iter().map(message_key).chain(printed).collect()
}
/// The `earlier` lines a batch's line shows, one list per delivery: the one rule is that a line this session has not shown prints
/// once, whether the mouth read it from a thread or the room. So it prints where it first comes in the batch, never if the
/// batch carries it as a message or a line of this process told it, and never if it is her own from this session. Of each room
/// the newest lines whose rendered (quoted) text fits `EARLIER_MAX_BYTES_PER_ROOM`: a line too big for what is left is skipped,
/// never a reason to drop the older lines that fit, and it can still come with a later message.
fn earlier_shown<'a>(deliveries: &'a [ChatDelivery], told: &BTreeSet<LineKey>, own_sender: &str, started_ms: u64) -> Vec<Vec<&'a ContextLine>> {
let mut seen = told.clone();
seen.extend(deliveries.iter().map(message_key));
let mut shown: Vec<Vec<&ContextLine>> = deliveries
.iter()
.map(|delivery| delivery.context.iter().filter(|_| !rooms_shown.contains(&delivery.room_alias)).filter(|line| seen.insert((delivery.room_alias.as_str(), line.sender.as_str(), line.text.as_str(), line.at_unix_ms))).collect())
.map(|delivery| {
delivery.context.iter().filter(|line| line.sender != own_sender || line.at_unix_ms < started_ms).filter(|line| seen.insert(line_key(&delivery.room_alias, &line.sender, line.at_unix_ms, &line.text))).collect()
})
.collect();
let mut left: BTreeMap<&str, usize> = BTreeMap::new();
for (delivery, lines) in deliveries.iter().zip(&mut shown).rev() {
@ -1961,7 +2003,7 @@ fn earlier_shown<'a>(deliveries: &'a [ChatDelivery], rooms_shown: &BTreeSet<Stri
/// remembers, the usage and tempo, the rooms and people she may name, the messages (each a framed
/// line whose role the organ derived from the sender id and not from anything the sender wrote),
/// and the question.
fn render_input(config: &Config, deliveries: &[ChatDelivery], note: Option<&str>, history: &[String], goals: &str, remembered: &str, steer: &str, people: &[String]) -> String {
fn render_input(config: &Config, messages: &str, note: Option<&str>, history: &[String], goals: &str, remembered: &str, steer: &str, people: &[String]) -> String {
let mut out = String::new();
out.push_str(TURN_INSTRUCTION);
if let Some(note) = note {
@ -1982,13 +2024,13 @@ fn render_input(config: &Config, deliveries: &[ChatDelivery], note: Option<&str>
out.push_str(&format!("People you may name in a memory's about: {}.\n\n", people.join(", ")));
out.push_str(MESSAGES_HEADER);
out.push('\n');
if deliveries.is_empty() {
if messages.is_empty() {
out.push('\n');
out.push_str(WAKE_QUESTION);
out.push('\n');
return out;
}
out.push_str(&message_lines(config, deliveries, &BTreeSet::new()));
out.push_str(messages);
out.push('\n');
out.push_str(TRAFFIC_QUESTION);
out.push('\n');
@ -1996,10 +2038,10 @@ fn render_input(config: &Config, deliveries: &[ChatDelivery], note: Option<&str>
}
/// A batch as framed lines: each message, after the `earlier` lines it brings.
fn message_lines(config: &Config, deliveries: &[ChatDelivery], rooms_shown: &BTreeSet<String>) -> String {
fn message_lines(config: &Config, deliveries: &[ChatDelivery], earlier: &[Vec<&ContextLine>]) -> String {
let mut out = String::new();
let names = sender_names(deliveries);
for (delivery, shown) in deliveries.iter().zip(earlier_shown(deliveries, rooms_shown)) {
for (delivery, shown) in deliveries.iter().zip(earlier) {
for earlier in shown {
out.push_str(&message_line(config, "earlier", &delivery.room_alias, &earlier.sender, None, &Addressees { to_you: false, others: vec![] }, earlier.at_unix_ms, &earlier.text));
}

View file

@ -1157,7 +1157,9 @@ fn input_for(mentions: &[&str], display: &str) -> String {
aqua.sender_display = display.into();
let mut said = delivery(2, "swarm", "@operator:chat", "a reply");
said.mentions = mentions.iter().map(|m| (*m).to_string()).collect();
render_input(&config(), &[aqua, said], None, &[], "", "", "usage", &[])
let c = config();
let batch = [aqua, said];
render_input(&c, &message_lines(&c, &batch, &earlier_shown(&batch, &BTreeSet::new(), &c.own_sender, u64::MAX)), None, &[], "", "", "usage", &[])
}
fn message_two(input: &str) -> &str {
@ -1940,11 +1942,12 @@ fn the_people_she_may_name_come_in_order_once_each_never_her_own_never_past_a_sh
let too_long = format!("@{}:chat", "z".repeat(220));
let batch = [delivery(1, "swarm", "@eureka:chat", "me"), said, delivery(3, "swarm", &too_long, "long")];
let shown = ["@named-in-memory:chat", "@op:chat", too_long.as_str()];
let printed: Vec<Vec<&ContextLine>> = batch.iter().map(|delivery| delivery.context.iter().collect()).collect();
assert_eq!(
people_she_may_name(&c, &batch, &shown),
people_she_may_name(&c, &batch, &printed, &shown),
["@aqua:chat", "@earlier:chat", "@mentioned:chat", "@op:chat", "@libby:chat", "@named-in-memory:chat"].map(String::from)
);
assert_eq!(people_she_may_name(&c, &[], &[]), ["@op:chat", "@libby:chat"].map(String::from), "on a wake: the operators and the swarm's Minds");
assert_eq!(people_she_may_name(&c, &[], &[], &[]), ["@op:chat", "@libby:chat"].map(String::from), "on a wake: the operators and the swarm's Minds");
}
fn keeps_at(sort: &str, text: &str, about: &[&str], salience: u16) -> serde_json::Value {
@ -3268,7 +3271,7 @@ fn a_rooms_earlier_lines_keep_the_newest_within_the_bound() {
/// The bound holds a room's ordinary tail: 20 lines of the 400 bytes the live line of 2026-11-02 averaged.
#[test]
fn a_rooms_ordinary_tail_fits_the_bound() {
let tail: Vec<ContextLine> = (0..20).map(|index| earlier("@aqua:chat", &format!("{index:0>400}"))).collect();
let tail: Vec<ContextLine> = (0..20).map(|index| ContextLine { at_unix_ms: 1_700_000_000_000 + index, ..earlier("@aqua:chat", &format!("{index:0>400}")) }).collect();
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@op:chat", "one"), tail)], &[says(&[("swarm", "ok")])]);
assert_eq!(failure_of(rig.go()), None);
assert_eq!(earlier_lines(&rig.model.requests.borrow()[0].input).len(), 20);
@ -3575,7 +3578,11 @@ fn a_tell_is_only_the_message_at_live_shape() {
a_live_shape_mind(&rig);
rig.clock.step.set(0);
let body = format!("hello {}", "m".repeat(494));
let second = with_context(delivery(2, "swarm", libby, &body), live_context(1));
// The mouth's next tail: one line older gone, and the message she was told last, as its copy.
let mut tail = live_context(1);
tail.pop();
tail.push(earlier(libby, "hello"));
let second = with_context(delivery(2, "swarm", libby, &body), tail);
one_line_each(&rig, vec![second]);
assert_eq!(failure_of(rig.go()), None);
let (whole, tell) = (input_of(&rig, 0), input_of(&rig, 1));
@ -3659,23 +3666,127 @@ fn a_wake_first_line_says_messages_are_data_before_a_tell_comes_bare() {
assert!(lines[1].starts_with("message ") && !lines[1].contains(MESSAGES_HEADER), "{}", lines[1]);
}
/// A line the session has not shown comes once, before the message that brings it: from another room, or from the same room.
#[test]
fn a_room_this_session_has_not_shown_brings_its_tail_once() {
fn an_earlier_line_the_session_has_not_shown_prints_once() {
let tail = |text: &str| vec![earlier("@op:chat", text)];
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@libby:chat", "one"), tail("SWARM TAIL"))], &[says(&[("swarm", "a")]), says(&[("swarm", "b")]), says(&[("swarm", "c")]), says(&[("swarm", "d")])]);
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@libby:chat", "one"), tail("SWARM TAIL"))], &[says(&[("swarm", "a")]), says(&[("swarm", "b")]), says(&[("swarm", "c")]), says(&[("swarm", "d")]), says(&[("swarm", "e")])]);
rig.clock.step.set(0);
one_line_each(&rig, vec![
with_context(delivery(2, "swarm", "@libby:chat", "two"), tail("SWARM MORE")),
with_context(delivery(2, "swarm", "@libby:chat", "two"), tail("SWARM TAIL")),
with_context(delivery(3, "usage", "@libby:chat", "three"), tail("USAGE TAIL")),
with_context(delivery(4, "usage", "@libby:chat", "four"), tail("USAGE MORE")),
with_context(delivery(4, "usage", "@libby:chat", "four"), tail("USAGE TAIL")),
with_context(delivery(5, "swarm", "@libby:chat", "five"), tail("NEVER DELIVERED")),
]);
assert_eq!(failure_of(rig.go()), None);
let lines = told(&rig);
assert_eq!(lines.len(), 4, "{lines:?}");
assert_eq!(lines.len(), 5, "{lines:?}");
assert!(lines[0].contains(r#"text="SWARM TAIL""#), "{}", lines[0]);
assert_eq!(lines[1].lines().count(), 1, "the first line showed this room: {}", lines[1]);
assert_eq!(lines[1].lines().count(), 1, "the first line showed it: {}", lines[1]);
assert!(lines[2].starts_with("earlier ") && lines[2].contains(r#"text="USAGE TAIL""#) && lines[2].lines().count() == 2, "a new room's tail, before its message: {}", lines[2]);
assert_eq!(lines[3].lines().count(), 1, "once: {}", lines[3]);
assert!(lines[4].starts_with("earlier ") && lines[4].contains(r#"text="NEVER DELIVERED""#) && lines[4].lines().count() == 2, "a room already shown still brings a line it has not shown: {}", lines[4]);
}
/// Bifrost's `contextFor`: a message in a thread carries the thread, root first, and a message in the room carries the room outside threads.
#[test]
fn a_thread_in_a_room_the_session_has_shown_brings_its_exchange_once() {
let at = |text: &str, ms: u64| ContextLine { sender: "@third:chat".into(), text: text.into(), at_unix_ms: 1_700_000_000_000 + ms };
let (root, reply_in_it, room_line) = (at("THREAD ROOT", 10), at("THREAD REPLY", 20), at("ROOM LINE", 5));
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@libby:chat", "one"), vec![room_line.clone()])], &[says(&[("swarm", "a")]), says(&[("swarm", "b")]), says(&[("swarm", "c")])]);
rig.clock.step.set(0);
one_line_each(&rig, vec![
with_context(delivery(2, "swarm", "@op:chat", "in the thread"), vec![root.clone(), reply_in_it.clone()]),
with_context(delivery(3, "swarm", "@op:chat", "more in it"), vec![root, reply_in_it]),
]);
assert_eq!(failure_of(rig.go()), None);
let lines = told(&rig);
assert_eq!(lines.len(), 3, "{lines:?}");
let thread = &lines[1];
assert_eq!(earlier_lines(thread).len(), 2, "{thread}");
assert!(thread.find("THREAD ROOT").unwrap() < thread.find("THREAD REPLY").unwrap() && thread.find("THREAD REPLY").unwrap() < thread.find("in the thread").unwrap(), "the exchange, root first, before the message: {thread}");
assert_eq!(lines[2].lines().count(), 1, "once: {}", lines[2]);
}
/// A first message from a new room that sits in a thread shows the thread and not the room: the room's own tail still comes.
#[test]
fn a_first_message_in_a_thread_leaves_the_rooms_tail_to_come() {
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@op:chat", "in the thread"), vec![earlier("@third:chat", "THREAD ROOT")])], &[says(&[("swarm", "a")]), says(&[("swarm", "b")])]);
rig.clock.step.set(0);
one_line_each(&rig, vec![with_context(delivery(2, "swarm", "@op:chat", "in the room"), vec![earlier("@third:chat", "ROOM TAIL")])]);
assert_eq!(failure_of(rig.go()), None);
let lines = told(&rig);
assert!(lines[0].contains("THREAD ROOT") && !lines[0].contains("ROOM TAIL"), "{}", lines[0]);
assert!(lines[1].starts_with("earlier ") && lines[1].contains("ROOM TAIL"), "{}", lines[1]);
}
/// The mouth answers a failed context fetch with an empty context: that shows nothing, so the next message that has one brings the tail.
#[test]
fn a_failed_context_fetch_loses_nothing() {
let rig = Rig::new(vec![delivery(1, "swarm", "@op:chat", "one")], &[says(&[("swarm", "a")]), says(&[("swarm", "b")])]);
rig.clock.step.set(0);
one_line_each(&rig, vec![with_context(delivery(2, "swarm", "@op:chat", "two"), vec![earlier("@third:chat", "THE TAIL")])]);
assert_eq!(failure_of(rig.go()), None);
let lines = told(&rig);
assert!(lines[1].starts_with("earlier ") && lines[1].contains("THE TAIL"), "{}", lines[1]);
}
/// The mouth clips a context line at 1000 bytes and a message at 4000: a long message and its copy are one line.
#[test]
fn a_long_message_is_not_told_again_as_its_clipped_copy() {
let long = format!("HEAD {}", "x".repeat(3000));
let copy = ContextLine { sender: "@op:chat".into(), text: long.chars().take(1000).collect(), at_unix_ms: 1_700_000_000_000 };
assert_ne!(copy.text, long);
let rig = Rig::new(vec![delivery(1, "swarm", "@op:chat", &long)], &[says(&[("swarm", "a")]), says(&[("swarm", "b")])]);
rig.clock.step.set(0);
one_line_each(&rig, vec![with_context(delivery(2, "swarm", "@libby:chat", "two"), vec![copy])]);
assert_eq!(failure_of(rig.go()), None);
assert_eq!(told(&rig)[1].lines().count(), 1, "{}", told(&rig)[1]);
}
/// Hers from before this process she reads, as `reply-sees-full-exchange` asks; hers from this session she wrote, and it does not come back.
#[test]
fn her_own_lines_of_this_session_do_not_return_and_her_older_ones_do() {
let hers = |text: &str, at: u64| ContextLine { sender: "@eureka:chat".into(), text: text.into(), at_unix_ms: at };
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@op:chat", "one"), vec![hers("BEFORE THE SESSION", 1)])], &[says(&[("swarm", "a")]), says(&[("swarm", "b")])]);
rig.clock.step.set(0);
let now = unix_ms(rig.clock.now());
one_line_each(&rig, vec![with_context(delivery(2, "swarm", "@op:chat", "two"), vec![hers("BEFORE THE SESSION", 1), hers("WRITTEN IN IT", now), hers("BEFORE TOO", 2)])]);
assert_eq!(failure_of(rig.go()), None);
let lines = told(&rig);
assert!(lines[0].contains("BEFORE THE SESSION"), "{}", lines[0]);
assert!(!lines[1].contains("WRITTEN IN IT") && lines[1].contains("BEFORE TOO") && !lines[1].contains("BEFORE THE SESSION"), "{}", lines[1]);
}
/// What a process told dies with it: a later line of the next process, not only its first, brings the line again.
#[test]
fn what_an_earlier_process_told_is_told_again_by_the_next() {
let tailed = |seq, text| with_context(delivery(seq, "swarm", "@libby:chat", text), vec![earlier("@op:chat", "THE TAIL")]);
// A direct message ends the first process at idle; the second answers it and the public line starts a third, whose first line has no tail, and whose next does.
let rig = Rig::new(vec![tailed(1, "one"), direct(2, "!dm:chat", "@op:chat", "aside"), delivery(3, "swarm", "@libby:chat", "three")], &[says(&[("swarm", "a")]), says(&[("!dm:chat", "b")]), says(&[("swarm", "c")]), says(&[("swarm", "d")])]);
let (deliveries, now, echoes) = (rig.chat.deliveries.clone(), rig.clock.now.clone(), Cell::new(0));
let next = tailed(4, "four");
*rig.model.after_echo.borrow_mut() = Some(Box::new(move || {
echoes.set(echoes.get() + 1);
if echoes.get() == 3 {
deliveries.borrow_mut().push(next.clone());
now.set(now.get() + 6_000);
}
}));
assert_eq!(failure_of(rig.go()), None);
let lines = told(&rig);
assert_eq!(rig.model.spawns.borrow().len(), 3, "{lines:?}");
assert!(lines[3].starts_with("earlier ") && lines[3].contains("THE TAIL"), "{}", lines[3]);
}
/// A sender whose line is in a context she was not shown is no one she may name: the bound follows what printed.
#[test]
fn a_sender_on_no_printed_line_is_not_nameable() {
let over_budget = ContextLine { sender: "@big:chat".into(), text: "x".repeat(EARLIER_MAX_BYTES_PER_ROOM + 1), at_unix_ms: 1_700_000_000_000 };
let names_big = reply(&[("swarm", "a")], json!([keeps("Did", "Noted", &["@big:chat"])]), json!([]), json!(null));
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@libby:chat", "one"), vec![over_budget])], &[names_big.clone(), names_big.clone(), names_big]);
rig.clock.step.set(1000);
assert_eq!(failure_of(rig.go()), Some(Failure::OutOfBounds(ABOUT_FAULT)));
}
#[test]