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>
133 lines
7.8 KiB
Markdown
133 lines
7.8 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 | 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
|
|
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.
|