From 72e87076bcc49da154fd2f3d1d6f2320d073efbf Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 3 Oct 2026 02:42:06 -0700 Subject: [PATCH] docs: M5d-2 checklist: retain, Tally, mutation counts Task 08 filters pending after the records walk so a ToolResult before its ToolSummary is not summarised twice. Task 10 names Tally to satisfy clippy type_complexity. Task 06 does not return from tick when every_s is 0. README records 12/12 mutations caught. Co-Authored-By: Grok 4.6 --- .../M5d2/06-loopd-scheduler-heartbeat.md | 2 +- docs/plans/M5d2/08-loopd-tool-summary.md | 11 ++++----- docs/plans/M5d2/10-memoryd-tiers.md | 3 +++ docs/plans/M5d2/README.md | 24 +++++++++---------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md b/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md index 3b16fdf..ae7f6d5 100644 --- a/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md +++ b/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md @@ -43,7 +43,7 @@ Tests call `tick(ctx, now)` with injected time. No sleeps (T27). - **`Context::hold`.** Same insert as `handle`: if the id is already in `busy`, `Err(Busy)`; else insert and return `Held`. `Held` must be `pub` so the given test can keep it. `Drop` already removes the id. Do not return `Result<_, ()>`: clippy `result_unit_err` is denied. -- **Due.** `every_s == 0` → no heartbeat work. Else `now.unix_millis().saturating_sub(last.unix_millis()) >= every_s.saturating_mul(1000)` +- **Due.** `every_s == 0` → no heartbeat work (do not `return` from `tick`; task 07 runs jobs after this). Else `now.unix_millis().saturating_sub(last.unix_millis()) >= every_s.saturating_mul(1000)` where `last` is `last_heartbeat.unwrap_or(started)`. `07:00` to `07:01` with `every_s = 60` is due. `07:01` then `07:01:30` is not. - **Prompt first, then hold.** If `read_to_string(cfg.heartbeat_prompt())` fails, do not hold, diff --git a/docs/plans/M5d2/08-loopd-tool-summary.md b/docs/plans/M5d2/08-loopd-tool-summary.md index 19959ae..ca2232b 100644 --- a/docs/plans/M5d2/08-loopd-tool-summary.md +++ b/docs/plans/M5d2/08-loopd-tool-summary.md @@ -41,9 +41,10 @@ The starter `config/summarise.md` is copied like `config/heartbeat.md`. ## Facts you need - **Which results.** Walk `session.records()` once. From each `Assistant`, remember - `tool_calls` by `id → name`. From each `ToolSummary`, remember `call: CallId`. Then for - each `ToolResult` whose `tool_call_id` maps to a name in `cfg.compact.summarise` and whose - `call` is not already summarised, queue it. Do not append while iterating `records()`. + `tool_calls` by `id → name`. From each `ToolSummary`, remember `call: CallId`. Queue each + opted-in `ToolResult` during the walk; **after** the walk, drop any whose `call` is in + `done`. A `ToolResult` can appear before its `ToolSummary` in the same epoch, so an in-walk + `done.contains` still queues it. Do not append while iterating `records()`. - **The call.** `messages_with_carry(session.baseline(), session.records(), session.carry())`, then one `ChatMessage::User` whose content is `format!("{}\n\n{}", prompt.trim_end(), crate::compact::cap_chars(result_content, cfg.r#loop.tool_result_cap))` @@ -129,9 +130,6 @@ pub fn missing( untrusted, .. } => { - if done.contains(&call.0) { - continue; - } let Some(name) = names.get(tool_call_id) else { continue; }; @@ -149,6 +147,7 @@ pub fn missing( _ => {} } } + pending.retain(|(call, ..)| !done.contains(&call.0)); for (call, tool_call_id, content, class, untrusted) in pending { let mut messages = messages_with_carry(session.baseline(), session.records(), session.carry()); messages.push(ChatMessage::User { diff --git a/docs/plans/M5d2/10-memoryd-tiers.md b/docs/plans/M5d2/10-memoryd-tiers.md index e82be82..fe8597e 100644 --- a/docs/plans/M5d2/10-memoryd-tiers.md +++ b/docs/plans/M5d2/10-memoryd-tiers.md @@ -88,6 +88,9 @@ listing. The starter `config/jobs/tier.toml.example` documents a weekly job; not `deny_unknown_fields` **off** so `epoch` and `query` do not fail.) - **Identity.** `store.entries(MemoryFile::Memory)` then `User`. Empty file: no identity lines for that file. +- **Counts map.** A nested `HashMap<(String, i64), (u64, String)>` trips clippy + `type_complexity` (`-D warnings`). Name a small `Tally { n: u64, last: String }` and + return `HashMap<(String, i64), Tally>`. Do not `#[allow]`. - **`list` returns `Result`.** Trailing newline on the last block is fine; `bxctl` adds another when printing `MemoryAdminReply.text` (`writeln!`). The given bxctl test includes a trailing newline in the reply text and expects it plus `writeln!`'s diff --git a/docs/plans/M5d2/README.md b/docs/plans/M5d2/README.md index 4722590..5e6faff 100644 --- a/docs/plans/M5d2/README.md +++ b/docs/plans/M5d2/README.md @@ -58,18 +58,18 @@ Every task below has been written to be replayed from its text. The gate count i total of `cargo test --workspace` plus one, for the ignored `end_to_end` test the gate runs on its own. Before task 01 it is **1508** (straylight, rustc 1.98.1 / clippy 0.1.98; M5d-1 task 08). -| # | Delivers | Crates | Gate after | Tests added | -|---|---|---|---|---| -| 01 | `run_turn` sends `session.slot()`; a session created on the background slot has `id_slot` of that slot; `mm-*` stays main | loopd | 1512 | 4 | -| 02 | `LogRecord::ToolSummary`; `messages()` ignores it; `types` names it; JSON round-trip | bxcore, loopd | 1518 | 6 | -| 03 | `[heartbeat]` (`every_s` 0, `prompt` `heartbeat.md`); unknown `[compact] summarise` names are a config error; starter `heartbeat.md` | loopd | 1526 | 8 | -| 04 | Five-field UTC cron matcher: `*`, `n`, `n-m`, `*/n`; day-of-week 0 is Sunday | loopd | 1537 | 11 | -| 05 | `{home}/jobs/.toml`, `runs.jsonl`, `jobs-invalid` pointer and runbook | loopd | 1545 | 8 | -| 06 | Heartbeat tick: time line, standing session `heartbeat`, skip if busy, unreadable prompt logged once | loopd | 1551 | 6 | -| 07 | Cron tick: new session per run, skip if previous live, leftover `started` becomes `failed` | loopd | 1557 | 6 | -| 08 | After a turn, opted-in `ToolResult` without a `ToolSummary` gets a background summary appended with the result's labels | loopd | 1563 | 6 | -| 09 | `memoryd` indexes `ToolSummary` as kind `summary`; `recall` finds it and taints when untrusted | memoryd | 1568 | 5 | -| 10 | `MemoryCommand::Tiers`, `bxctl memory tiers`, starter `config/jobs/tier.toml.example` | bxcore, memoryd, bxctl | 1574 | 6 | +| # | Delivers | Crates | Gate after | Tests added | Mutations tried, caught | +|---|---|---|---|---|---| +| 01 | `run_turn` sends `session.slot()`; a session created on the background slot has `id_slot` of that slot; `mm-*` stays main | loopd | 1512 | 4 | 1, 1 | +| 02 | `LogRecord::ToolSummary`; `messages()` ignores it; `types` names it; JSON round-trip | bxcore, loopd | 1518 | 6 | 1, 1 | +| 03 | `[heartbeat]` (`every_s` 0, `prompt` `heartbeat.md`); unknown `[compact] summarise` names are a config error; starter `heartbeat.md` | loopd | 1526 | 8 | 1, 1 | +| 04 | Five-field UTC cron matcher: `*`, `n`, `n-m`, `*/n`; day-of-week 0 is Sunday | loopd | 1537 | 11 | 1, 1 | +| 05 | `{home}/jobs/.toml`, `runs.jsonl`, `jobs-invalid` pointer and runbook | loopd | 1545 | 8 | 1, 1 | +| 06 | Heartbeat tick: time line, standing session `heartbeat`, skip if busy, unreadable prompt logged once | loopd | 1551 | 6 | 1, 1 | +| 07 | Cron tick: new session per run, skip if previous live, leftover `started` becomes `failed` | loopd | 1557 | 6 | 1, 1 | +| 08 | After a turn, opted-in `ToolResult` without a `ToolSummary` gets a background summary appended with the result's labels | loopd | 1563 | 6 | 2, 2 | +| 09 | `memoryd` indexes `ToolSummary` as kind `summary`; `recall` finds it and taints when untrusted | memoryd | 1568 | 5 | 1, 1 | +| 10 | `MemoryCommand::Tiers`, `bxctl memory tiers`, starter `config/jobs/tier.toml.example` | bxcore, memoryd, bxctl | 1574 | 6 | 1, 1 | After task 10, the design model runs the M5d-2 device check (spec 7) on straylight: a heartbeat turn on the background slot leaves a following main-slot turn's `cache_n` near its previous