Files
boxmaker/docs/plans/M5d1/README.md
T
kyleandGrok 4.6 c3b05cf003 docs: M5d-1 checklist fixes before hand-over
Given tests: Action::Turn (not To), send_turn of epoch 1, clippy on
compact_summary, jsonl-without-baseline, live turn after compact sends
the carry. send_turn maps Damaged to Local("log") as read_turn did.
Gate counts 1484 → 1508. Mutations recorded in the README.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-02 22:36:51 -07:00

120 lines
7.7 KiB
Markdown

# M5d-1 implementation plan: epochs and compaction
> 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:** a live session can start a new epoch. `loopd` writes `N.jsonl` / `N.baseline.json` for
`N > 0`, turn numbers and `untrusted()` span every epoch, compaction runs after a turn whose last
`Usage` is at or above a configured fraction of `expect.n_ctx`, the summary call uses the
background slot, and `memoryd`'s Honcho walker reads every epoch file of a session.
**Architecture:** `loopd` gains a `[compact]` table and a `compact` module (when to compact, the
summary call, writing the next epoch). `Session::open` loads the highest complete epoch and the
carried summary. `messages` for epoch `n > 0` inserts that carry as a `ChatMessage::User` that is
never a `LogRecord::User`. Honcho concatenates epoch logs in order so `Item::Turn` numbers stay
monotonic. Heartbeat, cron, `ToolSummary`, and tiering are M5d-2.
**Tech stack:** Rust 1.98.1, edition 2024; no new crate.
**Spec:** `docs/specs/2026-10-03-m5d-scheduler.md`, sections 3, 5 (`[compact]` without using
`summarise`), 7 (the M5d-1 tests), and 8 (`session-compact-failed`).
## The owner's decisions (2026-10-03)
1. Compaction runs after a turn, when that turn's `cache_n + prompt_n` is at or above a configured
fraction of `expect.n_ctx` (default 0.85). A quiet session is not a reason to compact.
2. If a live request is `exceed_context_size_error`, compact and retry that user message once in
the new epoch.
3. The summary call is on the background slot, with no tools, thinking on. A context-full summary
falls back to the epoch's `User` texts, newest last, cut to `summary_chars`.
4. The carried summary is not a `LogRecord::User`, so it is not a Honcho turn.
## What the code already has, and what it lacks
- `LogRecord::EpochEnd { time, next, summary }` is in `bxcore` and unused. `messages` already
ignores it. This plan writes it.
- `Session::open` reads only `0.baseline.json` and `0.jsonl`. `turns()` and `untrusted()` look at
that file only. `next_call` is recovered from that file only.
- `run_turn` always sends `slots.main`. Live turns stay on the session's live slot (main, from
`SessionStart`). Only the summary call uses `slots.background`. Changing live turns to the
session's slot is M5d-2.
- `memoryd`'s indexer already walks every `<epoch>.jsonl`. Honcho's `log_path` is still
`sessions/<id>/0.jsonl`.
- `Runtime` has no `MemorySource`. Compaction asks `memoryd` for a fresh block from `channel.rs`
(and `compact::finish_turn`), so given `turn` tests that build a `Runtime` do not change.
- `[compact] summarise` is stored from this plan so a spec-complete `loopd.toml` loads; nothing
in M5d-1 reads the list. M5d-2 opts in `http_fetch`.
## 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). The carry heading is built
from pieces and named in words.
- Every path a given test creates is removed by a guard (T44); every process a test starts has a
deadline.
- The fail-closed state `session-compact-failed` has a runbook entry, added by the task that adds
its pointer (T49).
- Each task gives its baseline and its gate count with what the count includes (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).
- Commit trailer from the driver.
## Tasks
Every task below has been written, replayed from its text and checked; 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 **1456** (straylight, rustc 1.98.1 / clippy 0.1.98, 48a229ceae;
master `3726ae5` after the M5d plans). End-state `make gate` on the reference is **1508**.
| # | Delivers | Crates | Gate after | Tests added | Mutations tried, caught |
|---|---|---|---|---|---|
| 01 | `[compact]` on `loopd.toml`: `threshold` 0.85, `summary_chars` 4000, `summarise` default `["http_fetch"]` (stored, unused); `Config::compact_prompt` is `compact.md` beside `system.md` | loopd | 1464 | 8 | 5, 5 |
| 02 | `Session::open` loads the highest epoch that has both a baseline and a log, the carry from the previous `EpochEnd`, `turns` / `untrusted` / `next_call` over every epoch; persist writes the current epoch | loopd | 1473 | 9 | 5, 4 (`log_path` after `open` does not move the already-open `File`) |
| 03 | `messages_with_carry`: epoch `n > 0` is the new system message, then one `ChatMessage::User` (fixed heading plus carry, not a log record), then the new epoch's records; `run_turn` uses `session.messages()` | loopd | 1478 | 5 | 2, 2 |
| 04 | Honcho concatenates every `<epoch>.jsonl` in epoch order; `turn_records` of turn 3 is the epoch-1 user; `send_turn` reads the concatenated log | memoryd | 1484 | 6 | 3, 3 |
| 05 | `loopd::compact`: `due`, `fallback_summary`; starter `config/compact.md` | loopd | 1490 | 6 | 3, 3 |
| 06 | Summary call on the background slot, no tools, thinking on; tag-neutralised and capped; context-full uses the fallback; `session-compact-failed` pointer and runbook entry | loopd | 1496 | 6 | 3, 3 |
| 07 | Writing the next epoch: `EpochEnd`, fresh baseline from `memoryd`, `SessionStart` on the live slot; resume if `EpochEnd` is already the last record | loopd | 1502 | 6 | 2, 2 |
| 08 | After a turn: compact when `due`; on `SessionFull`, compact and retry the user message once; compaction holds the session busy | loopd | 1508 | 6 | 2, 2 (`finish_turn` after `drop(held)` is wiring in `channel.rs`; the given tests call `finish_turn` directly) |
After task 08, the design model runs the M5d-1 device check (spec 7) on straylight: a `bxctl`
session writes a distinctive fact into `memory.md`, continues until compaction with
`[compact] threshold` lowered, and the next turn's baseline contains the fact; `recall` of a
phrase from epoch 0 still hits; `TurnDone.untrusted` stays true across the epoch change after a
tainted recall.
## Known limits
These are known, accepted for M5d-1, and not bugs to fix in a task:
- **Live turns still use `slots.main`.** `SessionStart.slot` is recorded and copied onto the new
epoch; `run_turn` does not read it yet. Isolated sessions on the background slot are M5d-2.
- **`[compact] summarise` is unused.** Unknown tool names are not a config error until M5d-2.
- **No idle path exists.** Compaction is only called after a turn or on `SessionFull`. There is
no timer to test.
- **A crash after `EpochEnd` and before the next files exist** reopens epoch 0 with `EpochEnd` at
the end of the log. The next compact resumes without writing a second `EpochEnd` (task 07).
- **A `SessionFull` retry logs the user message twice**, once in the old epoch (Honcho skips it:
no reply) and once in the new epoch. Turn numbers stay monotonic.
## 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; scratch
files in the worktree's `.state/`; the reference moved to straylight as a bundle before the run
(T18). Task files name their log row (T47).
## Running it
The reference (`m5d1-ref`, tasks 01 to 08) is moved to straylight as a bundle and deleted here
before the run (T18). Then, from the repository root, with the OpenCode TUI closed:
```sh
git switch -c m5d1 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/M5d1 01
```
After task 08, the gate count is 1508. Then the device check above.