Files
boxmaker/docs/plans/M5d2/README.md
T
kyleandGrok 4.6 df987c3058 docs: M5d-2 checklist: given-test lifetimes and cron AND
session_slot and the scheduler tests held a temporary JSON Value across
statements (E0716). Cron now pins DOM and DOW as AND, which the given tests
had not. Gate counts from task 04 on move up by one.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-03 01:31:07 -07:00

133 lines
7.7 KiB
Markdown

# M5d-2 implementation plan: the scheduler
> For the implementer: each task file is one closed task for one fresh session. Read `AGENTS.md`,
> then the task file, and do exactly that task. The driver is `tools/run-plan.sh`.
**Goal:** heartbeat and cron run as isolated sessions on the background slot; live turns use the
session's slot; opted-in tool results get a `ToolSummary` that `messages()` ignores and `recall`
indexes; the tiering listing is an ordinary admin command. Answers stay in the session log.
Nothing is posted to Mattermost.
**Architecture:** `run_turn` reads `session.slot()`. `loopd` gains `[heartbeat]`, a cron matcher,
job-file loading, a scheduler thread that shares `Context` and the busy set, and a no-tools
summary call for `http_fetch`. `bxcore` gains `LogRecord::ToolSummary` and `MemoryCommand::Tiers`.
`memoryd` indexes summaries and answers `Tiers` from `recalls/`. `bxctl memory tiers` prints that
listing. No new crate.
**Tech stack:** Rust 1.98.1, edition 2024; no cron crate; no new dependency.
**Spec:** `docs/specs/2026-10-03-m5d-scheduler.md`, sections 4, 5, 6, 7 (the M5d-2 tests), and 8
(`jobs-invalid`, `heartbeat-prompt-unreadable`). M5d-1 (epochs and compaction) is already on the
branch this plan starts from.
## The owner's decisions (2026-10-03)
1. Heartbeat and cron answers stay in the session log. They are not posted to Mattermost.
2. Five-field UTC crontab, `{home}/jobs/<name>.toml`, a new session each run, a tick skipped
when that job is still live.
3. Tool summaries on the background slot; a `ToolSummary` log record with the result's labels,
FTS-indexed; `http_fetch` opts in via `[compact] summarise`.
## What the code already has, and what it lacks
- M5d-1 writes epochs, compact after a turn, Honcho over every epoch file. Live turns still send
`slots.main`. `[compact] summarise` is stored and unused. Unknown names are not a config error.
- `LogRecord` has no `ToolSummary`. `messages()` names every variant. `support/turn.rs` `types`
is exhaustive.
- `loopd serve` binds the channel and blocks in `channel::serve`. There is no scheduler thread.
- `MemoryCommand` has no `Tiers`. `bxctl memory` has no `tiers`.
- `SessionId` is `[a-z0-9-]`, max 64. Job ids `job-<name>-<yyyymmdd>t<hhmm>` must fit.
## Global constraints
- Roles depend on `bxcore` only. No `unsafe`, no async runtime, no new crate.
- No file the implementer reads holds a chat-template tag whole (T33).
- Every path a given test creates is removed by a guard (T44); no test depends on timing (T27):
the scheduler is `tick(now)`, never a sleep.
- Fail-closed states `jobs-invalid` and `heartbeat-prompt-unreadable` have runbook entries,
added by the task that adds their pointer (T49).
- Each task gives its baseline and its gate count (T50, T53); the branch line is a check, never
`git switch` (T51).
- `Cargo.lock` is in the `git add` line (T24). Gate timeout 600000 ms (T40).
- A later task that changes a shared given file ships its own copy (T19): task 02 replaces
`crates/loopd/tests/support/turn.rs` so `types` names `ToolSummary`.
## Tasks
Every task below has been written to be replayed from its text. The gate count is the passed
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 |
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
total; a `jobs/tier.toml` for the check promotes only a trusted note.
## Review Focus
These are implied by the spec and pinned in the named task. A reviewer who only reads the
happy-path tests will miss them.
- A live `mm-*` turn still sends `slots.main` after heartbeat used the background slot (task 01).
- Inserting a `ToolSummary` between a `ToolResult` and a later `User` does not break prefix
extension (task 02).
- An unknown `[compact] summarise` name is a config error; an empty list is not (task 03).
- DOM and DOW are AND; `@daily` and a sixth field are errors (task 04).
- A leftover `started` whose session is currently held is not closed as `failed` (tasks 06, 07).
- `finish_turn` summarises before it compacts, so the `ToolResult` is still in the current epoch
(task 08).
- A secret `ToolSummary` is skipped when the index is private (task 09).
- Untrusted notes appear in `Tiers` and are marked; they cannot be omitted (task 10).
## Known limits
These are known, accepted for M5d-2, and not bugs to fix in a task:
- **No Mattermost delivery.** Heartbeat and cron answers are in `sessions/<id>/` only. The owner
reads them with `bxctl chat --session` or by opening the log.
- **DOM and DOW are AND.** Both restricted (`0 7 1 * 0`) matches only a Sunday that is also the
1st. Standard Vixie cron would OR them. The spec does not ask for OR.
- **Tool-summary inference runs inside `finish_turn`, before compaction, while the session is
still held.** That is how compaction waits for the summary, and how `missing` still sees the
current epoch's `ToolResult`. A second live turn on that session waits. The summary request
itself uses the background slot and omits `tools`. A later split that infers without the hold
is out of scope.
- **Job files are checked at start.** A file that becomes invalid after `loopd` is running is
skipped that tick and logged; it does not stop the process.
- **Identity entries in `Tiers` list with 0 hits** unless a `recalls/` line names that exact
index row. Notes carry counts from `recalls/`. That is enough for the tiering prompt.
## Before hand-over
Work through `docs/plans/CHECKLIST.md` with a reference in a scratch worktree with its own build
directory, replayed from its task text; mutations; `/tmp` compared around a gate run; the
reference moved to straylight as a bundle before the run (T18). Task files name their log row
(T47).
## Running it
The reference (`m5d2-ref`, tasks 01 to 10) is moved to straylight as a bundle and deleted here
before the run (T18). Then, from the repository root, with the OpenCode TUI closed, after M5d-1
has landed on master:
```sh
git switch -c m5d2 master
OPENCODE_MODEL=llama.cpp/ornith-1.5-35b-a3b RESUME=1 \
SLOTS_URL='http://straylight:11434/slots?model=ornith-1.5-35b-a3b' \
tools/run-plan.sh docs/plans/M5d2 01
```
After task 10, the gate count is 1574. Then the device check above.