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 <noreply@x.ai>
This commit is contained in:
2026-10-03 02:42:06 -07:00
co-authored by Grok 4.6
parent f426b46ae7
commit 72e87076bc
4 changed files with 21 additions and 19 deletions
@@ -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,
+5 -6
View File
@@ -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 {
+3
View File
@@ -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<String, String>`.** 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
+12 -12
View File
@@ -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/<name>.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/<name>.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