Persona wake: the brake counts only Self runs the Mind started itself #4

Merged
eureka merged 7 commits from hands/wake-brake-unasked into main 2026-11-04 19:13:40 +00:00
2 changed files with 129 additions and 5 deletions

View file

@ -132,6 +132,8 @@ pub const ENV_DOCTRINE: &str = "MIND_PERSONA_DOCTRINE";
pub const DOCTRINE_MAX_BYTES: usize = 96 * 1024;
/// How many of her latest runs a turn's input lists.
const RECENT_RUNS: u32 = 5;
/// How many of her newest Self runs the started-today read takes: the query's page bound.
const SELF_RUNS_READ: u32 = 200;
/// The most bytes the responsibilities section of a turn's input carries (stewardships always whole).
const GOALS_MAX_BYTES: usize = 6 * 1024;
/// The room kept inside that bound for the rulings the shown items cite.
@ -2169,17 +2171,39 @@ fn recent_runs(mind: &dyn MindPort, instance: &Slug) -> Result<Vec<String>> {
Ok(lines)
}
/// Whether she has started a Self run today (UTC), one that failed to start included. Her newest `RECENT_RUNS` Self runs
/// are read, newest first, so a day holding more of them than that still shows today's first: the answer rests on the
/// read's order, not on the day's count.
/// Whether the Mind has started a Self run of her own today (UTC), one that failed to start included. A run that came from
/// something said in chat does not count: it claims a follow-up `start_self_run` labelled by the batch of deliveries that
/// prompted it (ruling `eureka-body:ruling:unasked-work-brake-ruled`). Her newest `SELF_RUNS_READ` Self runs are read, newest
/// first, so the answer rests on the read's order; a read full of today's chat-started runs cannot show whether an older
/// one is hers, so it answers yes and the wake question holds.
fn started_self_today(mind: &dyn MindPort, instance: &Slug, now: DateTime<Utc>) -> Result<bool> {
let turn = format!("{:?}", RunTurn::SelfRun);
let operator = format!("{:?}", RunOperator::Mind);
let runs = any_of(any_of(any_of(of_kind(PipelineKind::Run), "root", &[&instance.0]), "turn", &[&turn]), "operated_by", &[&operator]);
let selection = Selection { descending: true, limit: Some(RECENT_RUNS), ..runs };
let selection = Selection { descending: true, limit: Some(SELF_RUNS_READ), ..runs };
let PipelinePageItems::Headers(runs) = mind.query(&selection)?.items else { bail!("the mind answered documents to a header selection") };
let today = date(now);
Ok(runs.iter().any(|run| matches!(&run.facts, PipelineFacts::Run { started_on, .. } if *started_on == today)))
let full = runs.len() >= SELF_RUNS_READ as usize;
let mut reached_older_day = false;
for run in &runs {
let PipelineFacts::Run { started_on, claims, .. } = &run.facts else { continue };
if *started_on != today {
reached_older_day = true;
continue;
}
if !claims.iter().any(is_chat_started_claim) {
return Ok(true);
}
}
Ok(full && !reached_older_day)
}
/// Whether a claim is the follow-up of a Self run started from chat: `start_self_run` labels it `chat-` and the batch's
/// `s<first>-<last>` delivery sequences. A wake has no deliveries, so its batch is named by the Persona run, never `s<digits>`.
fn is_chat_started_claim(claim: &PipelineRef) -> bool {
let label = claim.id.0.rsplit(':').next().unwrap_or_default();
let Some(sequences) = label.strip_prefix("chat-s") else { return false };
matches!(sequences.split_once('-'), Some((first, last)) if !first.is_empty() && !last.is_empty() && first.bytes().chain(last.bytes()).all(|b| b.is_ascii_digit()))
}
/// A run's latest resolution, if it has one: its sequence, id and outcome. A run with none is open.

View file

@ -2901,6 +2901,106 @@ fn the_newest_self_run_holds_the_brake_beneath_a_full_read_of_older_days() {
assert!(told(&rig)[0].trim_end().ends_with(WAKE_QUESTION), "an oldest-first read would see only the older days");
}
fn starts_a_self_run() -> String {
json!({ "intent": "Work it", "say": [], "start_self_run": { "brief": "Work", "repo": REPO, "campaign": CAMPAIGN, "for_item": null }, "remember": [], "settle": [], "urgency_permille": null }).to_string()
}
/// A rig whose first session starts a Self run, from a chat message or from a quiet wake, then a quiet wake that is told.
fn wake_after_a_self_run_started_from(deliveries: Vec<ChatDelivery>) -> String {
let rig = Rig::new(deliveries, &[starts_a_self_run(), says(&[])]);
let Outcome::Served { self_run, failure, .. } = rig.go() else { panic!("served") };
assert_eq!((failure, self_run.is_some()), (None, true));
assert_eq!(failure_of(rig.go()), None);
told(&rig).last().cloned().expect("a wake was told")
}
#[test]
fn a_self_run_started_from_chat_does_not_hold_the_ask() {
let line = wake_after_a_self_run_started_from(vec![operator_says(1, "please start")]);
assert!(line.trim_end().ends_with(WAKE_ASK), "{line}");
}
#[test]
fn a_self_run_started_from_a_quiet_wake_holds_the_ask() {
let line = wake_after_a_self_run_started_from(vec![]);
assert!(line.trim_end().ends_with(WAKE_QUESTION), "{line}");
}
/// A Self run of hers claiming a follow-up of the given label, both admitted.
fn self_run_claiming(rig: &Rig, label: &str, follow_up_label: &str) {
let source = run_ref("persona-source");
if rig.mind.view(K::Run, &source.id.0).is_none() {
rig.mind.committed(vec![run("persona-source", RunTurn::PersonaTurn, RunOperator::Mind, &[])]);
rig.mind.committed(vec![close(source.clone(), recorded())]);
}
rig.mind.committed(vec![follow_up(follow_up_label, &source)]);
let id = rig.mind.ids(K::FollowUp).into_iter().find(|id| id.ends_with(follow_up_label)).expect("the follow-up");
rig.mind.committed(vec![run(label, RunTurn::SelfRun, RunOperator::Mind, &[reference(K::FollowUp, &id)])]);
rig.mind.committed(vec![close(run_ref(label), recorded())]);
}
#[test]
fn a_run_of_hers_beneath_chat_started_runs_holds_the_ask() {
let rig = Rig::new(vec![], &[says(&[])]);
self_run_claiming(&rig, "self-a-wake", "chat-mind-20261104T000000000Z-t1");
for n in 0..RECENT_RUNS + 2 {
self_run_claiming(&rig, &format!("self-b-chat-{n}"), &format!("chat-s{n}-{n}"));
}
assert_eq!(failure_of(rig.go()), None);
assert!(told(&rig)[0].trim_end().ends_with(WAKE_QUESTION), "an older wake-started run today still holds the ask");
}
fn chat_started_runs_today(rig: &Rig, count: u32) {
for n in 0..count {
self_run_claiming(rig, &format!("self-chat-{n:03}"), &format!("chat-s{n}-{n}"));
}
}
#[test]
fn a_read_full_of_chat_started_runs_today_holds_the_ask_since_an_older_run_may_be_hers() {
let rig = Rig::new(vec![], &[says(&[])]);
chat_started_runs_today(&rig, SELF_RUNS_READ);
assert_eq!(failure_of(rig.go()), None);
assert!(told(&rig)[0].trim_end().ends_with(WAKE_QUESTION));
}
#[test]
fn a_full_read_that_reaches_an_older_day_has_seen_every_run_today() {
let rig = Rig::new(vec![], &[says(&[])]);
self_runs_admitted_in_order(&rig, &[("self-0-old".into(), "2026-10-08")]);
chat_started_runs_today(&rig, SELF_RUNS_READ - 1);
assert_eq!(failure_of(rig.go()), None);
assert!(told(&rig)[0].trim_end().ends_with(WAKE_ASK));
}
#[test]
fn the_newest_runs_are_read_not_the_oldest() {
let rig = Rig::new(vec![], &[says(&[])]);
let old: Vec<(String, &str)> = (0..SELF_RUNS_READ + 5).map(|n| (format!("self-old-{n:03}"), "2026-10-08")).collect();
self_runs_admitted_in_order(&rig, &old);
self_runs_admitted_in_order(&rig, &[("self-new".into(), "2026-10-09")]);
assert_eq!(failure_of(rig.go()), None);
assert!(told(&rig)[0].trim_end().ends_with(WAKE_QUESTION));
}
#[test]
fn only_chat_started_runs_today_leave_the_ask() {
let rig = Rig::new(vec![], &[says(&[])]);
self_run_claiming(&rig, "self-chat", "chat-s12-14");
assert_eq!(failure_of(rig.go()), None);
assert!(told(&rig)[0].trim_end().ends_with(WAKE_ASK));
}
#[test]
fn a_claim_is_chat_started_only_when_the_batch_is_delivery_sequences() {
let claim = |id: &str| PipelineRef { kind: K::FollowUp, id: id.into() };
assert!(is_chat_started_claim(&claim("cm:follow_up:chat-s1-1")));
assert!(is_chat_started_claim(&claim("cm:follow_up:chat-s1650-1651")));
for other in ["cm:follow_up:chat-mind-20261104T1Z-t1", "cm:follow_up:chat-s-1", "cm:follow_up:chat-s1-", "cm:follow_up:chat-s1-x", "cm:follow_up:chat-s1", "cm:follow_up:brief-s1-1", ""] {
assert!(!is_chat_started_claim(&claim(other)), "{other}");
}
}
#[test]
fn a_self_run_today_that_is_not_hers_does_not_hold_the_brake() {
let rig = Rig::new(vec![], &[says(&[])]);