plan line: absolute instants, so a figure rendered once stays true for the session (squash of eureka-body/plan-read-honest f0566fd..7ac4098; Soul s5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
337c60dbc0
commit
723e01743c
4 changed files with 34 additions and 31 deletions
|
|
@ -4,28 +4,24 @@
|
||||||
use chrono::{DateTime, SecondsFormat, Utc};
|
use chrono::{DateTime, SecondsFormat, Utc};
|
||||||
use model_connector::PlanQuota;
|
use model_connector::PlanQuota;
|
||||||
|
|
||||||
/// A span in the two largest units that fit: `45s`, `12m`, `3h 20m`, `4d 11h`.
|
/// When a window resets: the UTC time alone. An instant is true when read again hours later; "in 4d 11h"
|
||||||
fn span(ms: u64) -> String {
|
/// or "already past" is not.
|
||||||
let seconds = ms / 1000;
|
fn resets(at_unix_ms: u64) -> String {
|
||||||
match seconds {
|
let at = i64::try_from(at_unix_ms).ok().and_then(DateTime::<Utc>::from_timestamp_millis).map_or_else(|| at_unix_ms.to_string(), |at| at.to_rfc3339_opts(SecondsFormat::Secs, true));
|
||||||
0..=59 => format!("{seconds}s"),
|
format!("resets at {at}")
|
||||||
60..=3599 => format!("{}m", seconds / 60),
|
|
||||||
3600..=86399 => format!("{}h {}m", seconds / 3600, seconds % 3600 / 60),
|
|
||||||
_ => format!("{}d {}h", seconds / 86400, seconds % 86400 / 3600),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When a window resets: the UTC time, and how far off it is from `now_ms`.
|
/// The minute a reading was taken, as a UTC time. An instant, not an age: the line stays true when
|
||||||
fn resets(at_unix_ms: u64, now_ms: u64) -> String {
|
/// it is read again hours later in a session that cannot refresh it.
|
||||||
let at = i64::try_from(at_unix_ms).ok().and_then(DateTime::<Utc>::from_timestamp_millis).map_or_else(|| at_unix_ms.to_string(), |at| at.to_rfc3339_opts(SecondsFormat::Secs, true));
|
fn read_at(at_unix_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)") }
|
i64::try_from(at_unix_ms).ok().and_then(DateTime::<Utc>::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
|
/// 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
|
/// 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.
|
/// follows it states the swarm's aim and that nothing enforces it.
|
||||||
pub fn render_plan(windows: &[PlanQuota], now_ms: u64) -> String {
|
pub fn render_plan(windows: &[PlanQuota], _now_ms: u64) -> String {
|
||||||
let guidance = "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.";
|
let guidance = "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.";
|
||||||
if windows.is_empty() {
|
if windows.is_empty() {
|
||||||
return format!("Plan usage: unobserved. {guidance}");
|
return format!("Plan usage: unobserved. {guidance}");
|
||||||
|
|
@ -34,15 +30,15 @@ pub fn render_plan(windows: &[PlanQuota], now_ms: u64) -> String {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|plan| match plan.utilization_permille {
|
.map(|plan| match plan.utilization_permille {
|
||||||
Some(permille) => format!(
|
Some(permille) => format!(
|
||||||
"{} {}.{}% {}, {}, read {} ago",
|
"{} {}.{}% {}, {}, read at {}",
|
||||||
plan.window,
|
plan.window,
|
||||||
permille / 10,
|
permille / 10,
|
||||||
permille % 10,
|
permille % 10,
|
||||||
plan.status,
|
plan.status,
|
||||||
resets(plan.resets_at_unix_ms, now_ms),
|
resets(plan.resets_at_unix_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)),
|
None => format!("{} expired, {}", plan.window, resets(plan.resets_at_unix_ms)),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
format!("Plan usage: {}. {guidance}", rendered.join("; "))
|
format!("Plan usage: {}. {guidance}", rendered.join("; "))
|
||||||
|
|
@ -57,20 +53,25 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_window_reads_as_a_time_and_spans_not_unix_milliseconds() {
|
fn a_window_reads_as_a_time_not_unix_milliseconds() {
|
||||||
let now = 1_793_307_000_000 - (4 * 86_400 + 11 * 3600 + 30 * 60) * 1000;
|
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);
|
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, read at 2026-10-20T22:12Z. "), "{line}");
|
||||||
assert!(!line.contains("unix ms"), "{line}");
|
assert!(!line.contains("unix ms"), "{line}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spans_use_the_two_largest_units_and_a_passed_reset_says_so() {
|
fn the_reset_is_an_instant_whatever_the_moment_of_rendering() { let reset = 1_793_307_000_000; let at = |now| render_plan(&[window(Some(10), reset, reset - 5000)], now); let before = at(reset - 5000); assert!(before.contains("resets at 2026-10-29T20:50:00Z, read at 2026-10-29T20:49Z"), "{before}"); for now in [reset - 12_000_000, reset, reset + 1000] { assert_eq!(at(now), before, "the line does not depend on when it is rendered"); } assert!(!before.contains("(in ") && !before.contains("already past"), "{before}"); assert_eq!(render_plan(&[window(None, reset, reset)], reset - 12_000_000).split(". ").next().unwrap(), "Plan usage: five_hour expired, resets at 2026-10-29T20:50:00Z"); }
|
||||||
|
#[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 reset = 1_793_307_000_000;
|
||||||
assert!(render_plan(&[window(Some(10), reset, reset - 5000)], reset - 5000).contains("(in 5s), read 0s ago"));
|
let taken = 1_793_306_999_000; // 20:49:59Z: the minute is cut, not rounded
|
||||||
assert!(render_plan(&[window(Some(10), reset, reset - 720_000)], reset - 600_000).contains("(in 10m), read 2m ago"));
|
let first = render_plan(&[window(Some(10), reset, taken)], taken);
|
||||||
assert!(render_plan(&[window(Some(10), reset, reset)], reset).contains("(already past), read 0s ago"), "a reset at this very moment is past");
|
assert!(first.contains(", read at 2026-10-29T20:49Z. "), "{first}");
|
||||||
assert!(render_plan(&[window(Some(10), reset, reset)], reset + 1000).contains("(already past), read 1s ago"));
|
let hours_later = render_plan(&[window(Some(10), reset, taken)], taken + 3 * 3_600_000);
|
||||||
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)"));
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3585,7 +3585,7 @@ Near what was said:
|
||||||
- (2026-10-09, in #swarm, Did): "Closed the old bridge"
|
- (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, 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.
|
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.
|
||||||
|
|
||||||
|
|
@ -3612,7 +3612,7 @@ What you remember (from your own mind; `huginn view <id>` reads a record):
|
||||||
- nothing is remembered yet.
|
- 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, 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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())]);
|
let ran = go(vec![Out(result_line())]);
|
||||||
assert!(matches!(&ran.events[0], Event::Wrote(_)), "the start precedes the first read: {:?}", ran.events[0]);
|
assert!(matches!(&ran.events[0], Event::Wrote(_)), "the start precedes the first read: {:?}", ran.events[0]);
|
||||||
let written = ran.written();
|
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, 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]);
|
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] {
|
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_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(&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]);
|
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_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!(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);
|
assert_eq!(ran.count(&Event::InputClosed), 0);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ mind_body::chat_rudp: struct RudpChat pub-fields[]
|
||||||
mind_body::cli: fn instance_and_endpoint (args : & [String] , env_endpoint : Option < String > , usage : & str) -> Result < (Slug , SocketAddr) >
|
mind_body::cli: fn instance_and_endpoint (args : & [String] , env_endpoint : Option < String > , usage : & str) -> Result < (Slug , SocketAddr) >
|
||||||
mind_body::cli: fn operator_name (sudo_user : Option < String > , user : Option < String >) -> String
|
mind_body::cli: fn operator_name (sudo_user : Option < String > , user : Option < String >) -> String
|
||||||
mind_body::cli: fn run (args : & [String] , root : & Path , set_by : & str , now : DateTime < Utc > , out : & mut impl Write , stop_units : & mut dyn FnMut (StopScope < '_ >) -> Result < () > ,) -> Result < () >
|
mind_body::cli: fn run (args : & [String] , root : & Path , set_by : & str , now : DateTime < Utc > , out : & mut impl Write , stop_units : & mut dyn FnMut (StopScope < '_ >) -> Result < () > ,) -> Result < () >
|
||||||
mind_body::connector: fn render_plan (windows : & [PlanQuota] , now_ms : u64) -> String
|
mind_body::connector: fn render_plan (windows : & [PlanQuota] , _now_ms : u64) -> String
|
||||||
mind_body::control: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum HeldReason { BrakeAbsent , BrakeHeld , Undecodable , }
|
mind_body::control: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum HeldReason { BrakeAbsent , BrakeHeld , Undecodable , }
|
||||||
mind_body::control: # [derive (Debug , PartialEq , Eq)] pub enum Effective { Held { reason : HeldReason } , Released (Grant) , }
|
mind_body::control: # [derive (Debug , PartialEq , Eq)] pub enum Effective { Held { reason : HeldReason } , Released (Grant) , }
|
||||||
mind_body::control: # [derive (Debug , PartialEq , Eq)] pub enum SwarmEffective { Held (HeldReason) , Released , }
|
mind_body::control: # [derive (Debug , PartialEq , Eq)] pub enum SwarmEffective { Held (HeldReason) , Released , }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue