docs: M5d-1 and M5d-2 implementation plans for Ornith
Eight closed tasks for epochs and compaction, then ten for the scheduler, ToolSummary, and tiers. Given tests first. Gate 1456 → 1507 → 1572. Co-Authored-By: Grok 4.6 <noreply@x.ai>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# 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 **1507** (talos, rustc 1.98.1; 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 | 1511 | 4 |
|
||||
| 02 | `LogRecord::ToolSummary`; `messages()` ignores it; `types` names it; JSON round-trip | bxcore, loopd | 1517 | 6 |
|
||||
| 03 | `[heartbeat]` (`every_s` 0, `prompt` `heartbeat.md`); unknown `[compact] summarise` names are a config error; starter `heartbeat.md` | loopd | 1525 | 8 |
|
||||
| 04 | Five-field UTC cron matcher: `*`, `n`, `n-m`, `*/n`; day-of-week 0 is Sunday | loopd | 1535 | 10 |
|
||||
| 05 | `{home}/jobs/<name>.toml`, `runs.jsonl`, `jobs-invalid` pointer and runbook | loopd | 1543 | 8 |
|
||||
| 06 | Heartbeat tick: time line, standing session `heartbeat`, skip if busy, unreadable prompt logged once | loopd | 1549 | 6 |
|
||||
| 07 | Cron tick: new session per run, skip if previous live, leftover `started` becomes `failed` | loopd | 1555 | 6 |
|
||||
| 08 | After a turn, opted-in `ToolResult` without a `ToolSummary` gets a background summary appended with the result's labels | loopd | 1561 | 6 |
|
||||
| 09 | `memoryd` indexes `ToolSummary` as kind `summary`; `recall` finds it and taints when untrusted | memoryd | 1566 | 5 |
|
||||
| 10 | `MemoryCommand::Tiers`, `bxctl memory tiers`, starter `config/jobs/tier.toml.example` | bxcore, memoryd, bxctl | 1572 | 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=straylight/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 1572. Then the device check above.
|
||||
Reference in New Issue
Block a user