Files
boxmaker/docs/plans/M5d2
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
..

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.