From 6d51fd190f05d604403784d00b31a7fc5e74cb5d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 3 Nov 2026 12:47:16 +0100 Subject: [PATCH] connector: the plan line dates a reading by the minute it was taken, not by an age render_plan printed 'read 2m ago', rendered once per Persona process and restated under every Self brief, so the line was false within minutes of a session that lives up to four hours and cannot refresh it. It now prints 'read at 2026-11-03T11:20Z', true at any later moment. Nothing else in the prompt changes. Co-Authored-By: Claude Opus 5.5 --- crates/mind-body/src/connector.rs | 34 ++++++++++++++++++++------ crates/mind-body/src/persona/tests.rs | 4 +-- crates/mind-body/src/self_run/tests.rs | 4 ++- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/crates/mind-body/src/connector.rs b/crates/mind-body/src/connector.rs index 37acb5d..e0846da 100644 --- a/crates/mind-body/src/connector.rs +++ b/crates/mind-body/src/connector.rs @@ -21,8 +21,14 @@ fn resets(at_unix_ms: u64, now_ms: u64) -> String { if at_unix_ms > now_ms { format!("resets at {at} (in {})", span(at_unix_ms - now_ms)) } else { format!("resets at {at} (already past)") } } +/// The minute a reading was taken, as a UTC time. An instant, not an age: the line stays true when +/// it is read again hours later in a session that cannot refresh it. +fn read_at(at_unix_ms: u64) -> String { + i64::try_from(at_unix_ms).ok().and_then(DateTime::::from_timestamp_millis).map_or_else(|| at_unix_ms.to_string(), |at| at.format("%Y-%m-%dT%H:%MZ").to_string()) +} + /// The plan windows as one line for a Mind: each window's percent used, its -/// status, when it resets and how old the reading is. Rendered from the readout +/// status, when it resets and the minute the reading was taken. Rendered from the readout /// and nothing else, so no number in it is ever a paraphrase; the guidance that /// follows it states the swarm's aim and that nothing enforces it. pub fn render_plan(windows: &[PlanQuota], now_ms: u64) -> String { @@ -34,13 +40,13 @@ pub fn render_plan(windows: &[PlanQuota], now_ms: u64) -> String { .iter() .map(|plan| match plan.utilization_permille { Some(permille) => format!( - "{} {}.{}% {}, {}, read {} ago", + "{} {}.{}% {}, {}, read at {}", plan.window, permille / 10, permille % 10, plan.status, resets(plan.resets_at_unix_ms, now_ms), - span(now_ms.saturating_sub(plan.observed_at_unix_ms)) + read_at(plan.observed_at_unix_ms) ), None => format!("{} expired, {}", plan.window, resets(plan.resets_at_unix_ms, now_ms)), }) @@ -60,17 +66,29 @@ mod tests { fn a_window_reads_as_a_time_and_spans_not_unix_milliseconds() { let now = 1_793_307_000_000 - (4 * 86_400 + 11 * 3600 + 30 * 60) * 1000; let line = render_plan(&[window(Some(423), 1_793_307_000_000, now - 385_639_000)], now); - assert!(line.starts_with("Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 4d 11h), read 4d 11h ago. "), "{line}"); + assert!(line.starts_with("Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 4d 11h), read at 2026-10-20T22:12Z. "), "{line}"); assert!(!line.contains("unix ms"), "{line}"); } #[test] fn spans_use_the_two_largest_units_and_a_passed_reset_says_so() { let reset = 1_793_307_000_000; - assert!(render_plan(&[window(Some(10), reset, reset - 5000)], reset - 5000).contains("(in 5s), read 0s ago")); - assert!(render_plan(&[window(Some(10), reset, reset - 720_000)], reset - 600_000).contains("(in 10m), read 2m ago")); - assert!(render_plan(&[window(Some(10), reset, reset)], reset).contains("(already past), read 0s ago"), "a reset at this very moment is past"); - assert!(render_plan(&[window(Some(10), reset, reset)], reset + 1000).contains("(already past), read 1s ago")); + assert!(render_plan(&[window(Some(10), reset, reset - 5000)], reset - 5000).contains("(in 5s), read at 2026-10-29T20:49Z")); + assert!(render_plan(&[window(Some(10), reset, reset - 720_000)], reset - 600_000).contains("(in 10m), read at 2026-10-29T20:38Z")); + assert!(render_plan(&[window(Some(10), reset, reset)], reset).contains("(already past), read at 2026-10-29T20:50Z"), "a reset at this very moment is past"); + assert!(render_plan(&[window(Some(10), reset, reset)], reset + 1000).contains("(already past), read at 2026-10-29T20:50Z")); assert!(render_plan(&[window(None, reset, reset)], reset - 12_000_000).contains("five_hour expired, resets at 2026-10-29T20:50:00Z (in 3h 20m)")); } + #[test] + fn the_reading_is_dated_by_the_minute_it_was_taken_and_the_line_does_not_age() { + let reset = 1_793_307_000_000; + let taken = 1_793_306_999_000; // 20:49:59Z: the minute is cut, not rounded + let first = render_plan(&[window(Some(10), reset, taken)], taken); + assert!(first.contains(", read at 2026-10-29T20:49Z. "), "{first}"); + let hours_later = render_plan(&[window(Some(10), reset, taken)], taken + 3 * 3_600_000); + assert!(hours_later.contains(", read at 2026-10-29T20:49Z. "), "{hours_later}"); + assert!(!first.contains(" ago") && !hours_later.contains(" ago"), "{first} {hours_later}"); + assert!(render_plan(&[window(Some(10), reset, 0)], reset).contains(", read at 1970-01-01T00:00Z. ")); + assert!(render_plan(&[window(Some(10), reset, u64::MAX)], reset).contains(&format!(", read at {}. ", u64::MAX)), "an instant out of range is printed as the number it is"); + } } diff --git a/crates/mind-body/src/persona/tests.rs b/crates/mind-body/src/persona/tests.rs index 4011e89..453dba1 100644 --- a/crates/mind-body/src/persona/tests.rs +++ b/crates/mind-body/src/persona/tests.rs @@ -3511,7 +3511,7 @@ Near what was said: - (2026-10-09, in #swarm, Did): "Closed the old bridge" -Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 20d 7h), read 0s ago. The swarm aims to have used about 90% of the weekly allowance at reset; nothing enforces it; you and the other Minds set the tempo. +Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 20d 7h), read at 2026-10-29T20:33Z. The swarm aims to have used about 90% of the weekly allowance at reset; nothing enforces it; you and the other Minds set the tempo. Tempo: not yet read. The swarm beat is the lower median of the votes cast: a Mind with no standing vote abstains and is not counted, and with no vote cast the beat is 900 s. A vote only needs changing when conditions change; leave it standing otherwise. You vote with tempo_vote_beat_s (60 to 86400 seconds per beat; omit the key to leave your standing vote as it is, and send null to withdraw it and abstain) and declare your own urgency with urgency_permille (0 to 1000, null for none); an urgency lasts until your next turn, so use it as etiquette, not as a habit. @@ -3538,7 +3538,7 @@ What you remember (from your own mind; `huginn view ` reads a record): - nothing is remembered yet. -Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 20d 7h), read 0s ago. The swarm aims to have used about 90% of the weekly allowance at reset; nothing enforces it; you and the other Minds set the tempo. +Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 20d 7h), read at 2026-10-29T20:33Z. The swarm aims to have used about 90% of the weekly allowance at reset; nothing enforces it; you and the other Minds set the tempo. Tempo: not yet read. The swarm beat is the lower median of the votes cast: a Mind with no standing vote abstains and is not counted, and with no vote cast the beat is 900 s. A vote only needs changing when conditions change; leave it standing otherwise. You vote with tempo_vote_beat_s (60 to 86400 seconds per beat; omit the key to leave your standing vote as it is, and send null to withdraw it and abstain) and declare your own urgency with urgency_permille (0 to 1000, null for none); an urgency lasts until your next turn, so use it as etiquette, not as a habit. diff --git a/crates/mind-body/src/self_run/tests.rs b/crates/mind-body/src/self_run/tests.rs index 3c2b76e..49d5659 100644 --- a/crates/mind-body/src/self_run/tests.rs +++ b/crates/mind-body/src/self_run/tests.rs @@ -357,7 +357,7 @@ fn the_start_message_is_the_first_thing_written_and_names_the_skill_the_instance let ran = go(vec![Out(result_line())]); assert!(matches!(&ran.events[0], Event::Wrote(_)), "the start precedes the first read: {:?}", ran.events[0]); let written = ran.written(); - for needle in ["Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 20d 7h), read 0s ago", "about 90% of the weekly allowance", "nothing enforces it"] { + for needle in ["Plan usage: five_hour 42.3% allowed, resets at 2026-10-29T20:50:00Z (in 20d 7h), read at 2026-10-29T20:33Z", "about 90% of the weekly allowance", "nothing enforces it"] { assert!(written[0].contains(needle), "the lease grant plan windows reach the child: {needle}: {}", written[0]); } for needle in ["Use the eureka skill.", "yggdrasil", "yggdrasil:run:selfrun-a", AGENT] { @@ -1376,6 +1376,8 @@ fn a_brief_derived_while_a_turn_runs_is_written_at_once_counted_at_its_echo_and_ assert_eq!(told.len(), 1, "{:?}", ran.written()); assert!(told[0].contains(&brief_ref("b1").id.0) && told[0].contains("Survey the noise functions."), "{}", told[0]); assert!(told[0].contains("resolve follow-up") && told[0].contains("Recorded"), "she is told how to close it: {}", told[0]); + // The plan line is restated under every brief for the run's life, so it names the instant of the reading, never an age that stops being true. + assert!(told[0].contains("read at 2026-10-29T20:33Z") && !told[0].contains(" ago"), "{}", told[0]); assert_eq!(ran.written().len(), 2, "the start message and one tell: {:?}", ran.written()); assert!(ran.out.contains("decision Write { tells: true, flush: None } prefix 10 ttl_s 0 idle_s working inbox true written 2 consumed 1\n"), "{}", ran.out); assert_eq!(ran.count(&Event::InputClosed), 0);