Files
boxmaker/docs/plans/M5d2
kyleandGrok 4.6 f426b46ae7 docs: M5d-2 tiers given test keeps TempDir as _dir
Setup.dir was only held so the directory outlived Store; clippy
dead_code failed the task 10 gate. The field is now _dir.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-03 02:26:48 -07:00
..

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:

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.