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