From 3eee12f600a500ea2074f301d179b4df5c3277ff Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 2 Nov 2026 19:20:58 +0100 Subject: [PATCH] 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 --- crates/mind-body/src/persona.rs | 98 +++++++++++++------ crates/mind-body/src/persona/tests.rs | 133 +++++++++++++++++++++++--- 2 files changed, 192 insertions(+), 39 deletions(-) diff --git a/crates/mind-body/src/persona.rs b/crates/mind-body/src/persona.rs index 0b71800..09b987c 100644 --- a/crates/mind-body/src/persona.rs +++ b/crates/mind-body/src/persona.rs @@ -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, +} + /// 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, + /// 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, /// The batch the first line carries; taken by it. start: Vec, /// 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 PersonaInbox<'_, C, S> { + /// The `earlier` lines a batch brings now. + fn earlier<'d>(&self, deliveries: &'d [ChatDelivery]) -> Vec> { + 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 { let organs = self.organs; let instance = organs.mind.instance().clone(); let tempo = render_tempo(organs.tempo, &instance); @@ -1028,26 +1043,28 @@ impl 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, (input, shown): (String, Shown), refusals: u32) -> String { + fn write(&mut self, batch: Vec, 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 { +fn people_she_may_name(config: &Config, deliveries: &[ChatDelivery], earlier: &[Vec<&ContextLine>], shown: &[&str]) -> Vec { let mut people: Vec = 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) -> Vec> { - 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 { + 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, own_sender: &str, started_ms: u64) -> Vec> { + let mut seen = told.clone(); + seen.extend(deliveries.iter().map(message_key)); let mut shown: Vec> = 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, 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 { +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)); } diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 215ee0c..4950519 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -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> = 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 = (0..20).map(|index| earlier("@aqua:chat", &format!("{index:0>400}"))).collect(); + let tail: Vec = (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]