A room's earlier tail skips what cannot fit and counts rendered bytes; a repeat at another instant survives; refusal tests name the exact refusal
The bound counts the quoted text that enters the prompt, an oversize line no longer blanks the older lines that fit, and the dedupe key gains at_unix_ms, which the mouth stamps equal on a message and its context copy (Bifrost persona-matrix-mouth.mjs origin_server_ts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c842fafcaf
commit
a85a736585
2 changed files with 75 additions and 26 deletions
|
|
@ -82,7 +82,7 @@ pub const SANDBOX_DIR: &str = "sandbox";
|
|||
const INSTRUCTIONS_FILE: &str = "persona-instructions.md";
|
||||
/// A refused batch is told back; its third refusal ends the start, however many answers or session restarts lie between.
|
||||
const REFUSALS_PER_BATCH: u32 = 3;
|
||||
/// The most bytes of `earlier` text one line carries for one room: the room's tail, newest kept. Each delivery brings its own
|
||||
/// The most bytes of `earlier` text, as rendered into the line (JSON-quoted), one line carries for one room: the room's tail, newest kept. Each delivery brings its own
|
||||
/// context, so a batch repeats a room's tail per message; `earlier_shown` owns this bound because a line's size decides how soon the session hits its ceiling.
|
||||
const EARLIER_MAX_BYTES_PER_ROOM: usize = 16 * 1024;
|
||||
/// The environment variables the unit's `persona.env` sets for the persona flags.
|
||||
|
|
@ -1867,25 +1867,30 @@ 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, where it first
|
||||
/// comes in the batch, none the batch carries as a message, and of each room only the newest within `EARLIER_MAX_BYTES_PER_ROOM`.
|
||||
/// 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.
|
||||
fn earlier_shown(deliveries: &[ChatDelivery]) -> Vec<Vec<&ContextLine>> {
|
||||
let mut seen: BTreeSet<(&str, &str, &str)> = deliveries.iter().map(|delivery| (delivery.room_alias.as_str(), delivery.sender.as_str(), delivery.text.as_str())).collect();
|
||||
let mut shown: Vec<Vec<&ContextLine>> =
|
||||
deliveries.iter().map(|delivery| delivery.context.iter().filter(|line| seen.insert((delivery.room_alias.as_str(), line.sender.as_str(), line.text.as_str()))).collect()).collect();
|
||||
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();
|
||||
let mut shown: Vec<Vec<&ContextLine>> = deliveries
|
||||
.iter()
|
||||
.map(|delivery| delivery.context.iter().filter(|line| seen.insert((delivery.room_alias.as_str(), line.sender.as_str(), line.text.as_str(), line.at_unix_ms))).collect())
|
||||
.collect();
|
||||
let mut left: BTreeMap<&str, usize> = BTreeMap::new();
|
||||
for (delivery, lines) in deliveries.iter().zip(&mut shown).rev() {
|
||||
let room = left.entry(delivery.room_alias.as_str()).or_insert(EARLIER_MAX_BYTES_PER_ROOM);
|
||||
let kept = lines
|
||||
.iter()
|
||||
.rev()
|
||||
.take_while(|line| {
|
||||
let fits = line.text.len() <= *room;
|
||||
*room = if fits { *room - line.text.len() } else { 0 };
|
||||
fits
|
||||
})
|
||||
.count();
|
||||
lines.drain(..lines.len() - kept);
|
||||
let mut kept: Vec<&ContextLine> = Vec::new();
|
||||
for line in lines.iter().rev() {
|
||||
let size = quoted(&line.text).len();
|
||||
if size <= *room {
|
||||
*room -= size;
|
||||
kept.push(line);
|
||||
}
|
||||
}
|
||||
kept.reverse();
|
||||
*lines = kept;
|
||||
}
|
||||
shown
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1225,10 +1225,16 @@ fn a_reply_that_votes_and_declares_nothing_admits_no_pulse() {
|
|||
|
||||
#[test]
|
||||
fn a_vote_or_urgency_outside_its_bounds_fails_the_turn_without_a_pulse_or_an_ack() {
|
||||
for (beat, urgency) in [(json!(59), json!(null)), (json!(86_401), json!(null)), (json!(null), json!(1001)), (json!(null), json!(-1)), (json!("fast"), json!(null))] {
|
||||
for (beat, urgency, refusal) in [
|
||||
(json!(59), json!(null), Failure::OutOfBounds(VOTE_FAULT)),
|
||||
(json!(86_401), json!(null), Failure::OutOfBounds(VOTE_FAULT)),
|
||||
(json!(null), json!(1001), Failure::OutOfBounds(VOTE_FAULT)),
|
||||
(json!(null), json!(-1), Failure::Undecodable),
|
||||
(json!("fast"), json!(null), Failure::Undecodable),
|
||||
] {
|
||||
let rig = Rig::new(vec![operator_says(1, "go")], &thrice(voting(Some(beat.clone()), urgency.clone())));
|
||||
let Outcome::Served { failure, .. } = rig.go() else { panic!("served") };
|
||||
assert!(matches!(failure, Some(Failure::Undecodable | Failure::OutOfBounds(VOTE_FAULT))), "{beat} {urgency}: {failure:?}");
|
||||
assert_eq!(failure, Some(refusal), "{beat} {urgency}");
|
||||
assert!(rig.mind.ids(K::Pulse).is_empty() && rig.chat.acked.borrow().is_empty(), "{beat} {urgency}");
|
||||
}
|
||||
}
|
||||
|
|
@ -1711,16 +1717,16 @@ fn an_ask_not_shown_this_turn_is_a_decode_failure_and_nothing_is_sent() {
|
|||
let bad_settle = json!([{ "item": format!("{INSTANCE}:memory:nope"), "outcome": "done", "why": "x" }]);
|
||||
let bad_take = starting(json!(format!("{INSTANCE}:memory:nope")));
|
||||
let not_asked = |for_item: &str| reply(&[("swarm", "x")], json!([keeps("Idea", "an idea", &[])]), json!([]), starting(json!(for_item)));
|
||||
for text in [
|
||||
reply(&[("swarm", "x")], json!([]), bad_settle, json!(null)),
|
||||
reply(&[("swarm", "x")], json!([]), json!([]), bad_take),
|
||||
not_asked("new:0"),
|
||||
not_asked("new:1"),
|
||||
not_asked("new:00"),
|
||||
for (text, refusal) in [
|
||||
(reply(&[("swarm", "x")], json!([]), bad_settle, json!(null)), SETTLE_ITEM_FAULT),
|
||||
(reply(&[("swarm", "x")], json!([]), json!([]), bad_take), FOR_ITEM_FAULT),
|
||||
(not_asked("new:0"), FOR_ITEM_FAULT),
|
||||
(not_asked("new:1"), FOR_ITEM_FAULT),
|
||||
(not_asked("new:00"), FOR_ITEM_FAULT),
|
||||
] {
|
||||
let rig = Rig::new(vec![delivery(1, "swarm", "@op:chat", "hi")], &thrice(text.clone()));
|
||||
let failure = failure_of(rig.go());
|
||||
assert!(matches!(failure, Some(Failure::Undecodable | Failure::OutOfBounds(_))), "{text}: {failure:?}");
|
||||
assert_eq!(failure, Some(Failure::OutOfBounds(refusal)), "{text}");
|
||||
assert!(rig.sent_texts().is_empty() && rig.chat.acked.borrow().is_empty() && rig.mind.ids(K::Memory).is_empty());
|
||||
}
|
||||
}
|
||||
|
|
@ -3278,7 +3284,7 @@ fn the_same_text_in_another_room_is_another_line() {
|
|||
/// The bound keeps the tail: the newest lines whole up to exactly the bound, nothing older past the first that does not fit, and each room its own.
|
||||
#[test]
|
||||
fn a_rooms_earlier_lines_keep_the_newest_within_the_bound() {
|
||||
let half = EARLIER_MAX_BYTES_PER_ROOM / 2;
|
||||
let half = EARLIER_MAX_BYTES_PER_ROOM / 2 - 2; // the two quotes make each line exactly half
|
||||
let line = |mark: char| earlier("@aqua:chat", &mark.to_string().repeat(half));
|
||||
let messages = vec![
|
||||
with_context(delivery(1, "swarm", "@op:chat", "one"), vec![line('a'), earlier("@aqua:chat", "tiny")]),
|
||||
|
|
@ -3313,3 +3319,41 @@ fn thirty_four_messages_with_a_shared_tail_do_not_repeat_it() {
|
|||
assert_eq!(failure_of(rig.go()), None);
|
||||
assert_eq!(earlier_lines(&rig.model.requests.borrow()[0].input).len(), 36, "16 starts and 20 of tail, not 680");
|
||||
}
|
||||
|
||||
/// One line too big for the room never blanks it: it is skipped and the older lines that fit stay.
|
||||
#[test]
|
||||
fn an_oversize_line_is_skipped_and_the_older_lines_that_fit_stay() {
|
||||
let huge = earlier("@aqua:chat", &"h".repeat(EARLIER_MAX_BYTES_PER_ROOM));
|
||||
let tail = vec![earlier("@aqua:chat", "old"), earlier("@libby:chat", "older still"), huge, earlier("@aqua:chat", "newest")];
|
||||
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@op:chat", "one"), tail)], &[says(&[("swarm", "ok")])]);
|
||||
assert_eq!(failure_of(rig.go()), None);
|
||||
let shown = earlier_lines(&rig.model.requests.borrow()[0].input);
|
||||
assert_eq!(shown.len(), 3, "{shown:?}");
|
||||
assert!(shown[0].ends_with(r#"text="old""#) && shown[1].ends_with(r#"text="older still""#) && shown[2].ends_with(r#"text="newest""#), "{shown:?}");
|
||||
}
|
||||
|
||||
/// The bound counts what enters the prompt: a quote is two bytes there, so these two lines (10 000 raw bytes) do not both fit.
|
||||
#[test]
|
||||
fn the_earlier_bound_counts_the_quoted_bytes() {
|
||||
let quotes = |mark: &str| earlier(mark, &"\"".repeat(5000));
|
||||
let rig = Rig::new(vec![with_context(delivery(1, "swarm", "@op:chat", "one"), vec![quotes("@aqua:chat"), quotes("@libby:chat")])], &[says(&[("swarm", "ok")])]);
|
||||
assert_eq!(failure_of(rig.go()), None);
|
||||
let shown = earlier_lines(&rig.model.requests.borrow()[0].input);
|
||||
assert_eq!(shown.len(), 1, "{}", shown.len());
|
||||
assert!(shown[0].contains(r#"sender="@libby:chat""#), "the newest stays");
|
||||
}
|
||||
|
||||
/// The mouth stamps a message and its copy in a later context with one instant; the same words at another instant are said again.
|
||||
#[test]
|
||||
fn a_genuine_repeat_at_another_instant_is_another_line() {
|
||||
let said = |at: u64| ContextLine { sender: "@aqua:chat".into(), text: "ok".into(), at_unix_ms: at };
|
||||
let rig = Rig::new(
|
||||
vec![
|
||||
with_context(delivery(1, "swarm", "@op:chat", "one"), vec![said(10), said(20), said(10)]),
|
||||
with_context(delivery(2, "swarm", "@op:chat", "two"), vec![said(10), said(20), said(30)]),
|
||||
],
|
||||
&[says(&[("swarm", "ok")])],
|
||||
);
|
||||
assert_eq!(failure_of(rig.go()), None);
|
||||
assert_eq!(earlier_lines(&rig.model.requests.borrow()[0].input).len(), 3, "10, 20 and 30 once each");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue