Files
boxmaker/docs/plans/M5a/README.md
T
kyleandClaude Opus 5.5 1dca52118d M5a plan: run tasks 16 to 20 on titan's own Ornith
Titan serves ornith-1.5-35b-a3b itself, so OpenCode there uses
titan/ornith-1.5-35b-a3b. The harness's device checks still use straylight.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 16:41:44 -07:00

122 lines
9.0 KiB
Markdown

# M5a implementation plan: local memory
> **For the implementing model:** do not work from this file. The owner gives you one task file at
> a time (`01-…` to `20-…`). This file is the index for the owner and the reviewer.
**Goal:** a new role, `memoryd`, owns the agent's memory: identity (`soul.md`, `memory.md`,
`user.md`, in every new session's prompt) and experience (`notes.md` and the session logs, found
with `recall`). The agent writes memory with a `memory` tool without asking; a session that has
read untrusted content can only take notes. `brokerd` decides both tools under built-in grants and
hands them to `memoryd`; `loopd` takes the memory block from `memoryd` for each new session.
**Architecture:** `brokerd`'s two largest files are split first (01). `proto` gains the tools'
arguments and eight message kinds (02). `memoryd` is built bottom up: its crate, configuration and
sockets (03), entries and the check (04), the files and the block (05), notes and the edits (06),
the journal, undo and review (07), the index (08), `recall` (09), and serving (10). `brokerd`
gains the two tools and their built-in grants (11), then sends decided memory calls to `memoryd`
with the session's state, keeping the stricter label (12). `loopd` takes the block and declares
the two core tools (13); `bxctl` gains `reindex` and `memory` (14). Last, the starter `soul.md`
and the device checks with `memoryd` running (15).
**Spec:** `docs/specs/2026-09-24-m5a-local-memory.md` (approved; P17 to P21 applied to the brief).
Runbook: task 03 adds `memoryd-start-failed`, task 12 `memoryd-unreachable`, task 13 replaces
`core-memory-unreadable` with `memory-unavailable` and adds `memory-index-damaged`.
**New dependency:** `rusqlite` 0.40.2 with `bundled` (task 03; `deny.toml` gains Zlib for
`foldhash`). Nothing else.
## Global constraints
- Everything in `AGENTS.md`, including "Lessons from earlier reviews".
- Branch `m5a`. One task, one fresh OpenCode session, one commit. Run `cargo fmt --all` before the
gate. Review happens once, after task 15.
- **New files** come as skeletons: types, constants and the glue written, `todo!()` bodies with the
code to write in the comment above. **Existing files** are changed by the exact edits in the
task, never replaced whole.
- Some tasks hand over **new versions of given test files**, because the change is in what the
test must see. Copy them over the old ones as the task says; they are still not to be edited.
- Only task 15 needs straylight (`make verify-device`). Everything else runs offline against
temporary directories and fakes.
## Tasks
Every task had a reference implementation, built in this order from master with the gate at every
step and replayed on its own (tip T26), its given tests run against it, and mutations of it caught
by them (tip T28); then the reference was moved off this machine (tip T18). Every skeleton was
checked to compile at its task's end state and to fail its tests, and every comment above a
`todo!()` was read beside its signature (tip T29). The three largest functions were split before
the skeletons were cut (tip T25).
| # | File | Delivers | Tests | Check |
|---|---|---|---|---|
| 01 | `01-brokerd-split.md` | `policy/` and `ledger/` as modules; no behaviour change | the existing ones | reference |
| 02 | `02-proto-memory.md` | the tools' arguments, eight message kinds | `memory_args.rs`, `memory_wire.rs`, 8 fixtures | reference; 5 mutations caught |
| 03 | `03-memoryd-crate.md` | the crate, configuration, `listen`, `rusqlite` | `config.rs`, `socket.rs` | reference; `cargo deny` |
| 04 | `04-memoryd-entries-check.md` | entries; the check given complete | `entries.rs`, `check.rs` | reference; 8 mutations caught |
| 05 | `05-memoryd-files.md` | the files, the block | `files.rs` | reference; 6 mutations caught |
| 06 | `06-memoryd-edits.md` | notes, the five actions, routing by trust | `edit.rs`, `notes.rs` | reference; 11 mutations caught |
| 07 | `07-memoryd-journal.md` | the journal, undo, review | `journal.rs` | reference; 5 mutations caught |
| 08 | `08-memoryd-index.md` | the FTS5 index, labels, catch-up, rebuild | `index.rs`, `support/logs.rs` | reference; 8 mutations caught |
| 09 | `09-memoryd-recall.md` | `recall`, source tags, the recall log | `recall.rs` | reference; 8 mutations caught |
| 10 | `10-memoryd-serve.md` | the three sockets; `memoryd serve` | `serve.rs` | reference; 10 mutations caught |
| 11 | `11-brokerd-memory-tools.md` | the two tools, built-in grants | `memory_tools.rs`; `grants.rs` updated | reference; 6 mutations caught |
| 12 | `12-brokerd-memory-port.md` | calls to `memoryd`, stricter labels | `memory_port.rs`; `rig.rs` updated | reference; 8 mutations caught |
| 13 | `13-loopd-memory.md` | the block, the core tools, `session_updated` | `memory.rs`; `baseline.rs`, `pointers.rs`, `channel.rs` updated | reference; 6 mutations caught |
| 14 | `14-bxctl-memory.md` | `bxctl reindex`, `bxctl memory` | `cli_memory.rs` | reference; 5 mutations caught |
| 15 | `15-soul-and-device.md` | the starter soul; device checks with `memoryd` | `device.rs` updated, `support/memoryd.rs` | reference; `make verify-device` on straylight |
| 16 | `16-macos-gate.md` | the gate passes on macOS: tests use /tmp, `gatewayd` finds its uid without `/proc` | `shell.rs` updated | reference; gate on titan and talos; 1 mutation caught (7 tests) |
| 17 | `17-memoryd-no-slicing.md` | the day and the month with `get`, not a slice (review nit) | the existing ones | reference |
| 18 | `18-repeated-code.md` | repeated error lines written once (review nit) | the existing ones | reference; 4 mutations caught |
| 19 | `19-no-forged-headers.md` | a note cannot forge a `notes.md` header (review finding) | `forgery.rs`; `memory_args.rs`, `notes.rs` updated | reference; 4 mutations caught |
| 20 | `20-review-ids.md` | a staged write never reuses an id (review finding) | `review_ids.rs` | reference; 2 mutations caught |
At the end of task 15: 936 tests (854 before task 01); tasks 16 to 18 add no test and
keep 936; at the end of task 19, 939; at the end of task 20, 941. Each task's count was checked by running
the gate at its end state on its own.
## Changes during the run
- 2026-09-24, task 04, first stop: tasks 01 to 03 committed, each on its first session. Task
04's session read the given test and the skeleton, which held chat-template tags literally,
because the check refuses them. Ornith took them for its own template. Its reasoning ended
mid-thought, it lost track of the file, and it made no change. The tags were then built from
pieces in `check.rs` and the four given tests that held them, and the spec describes them in
words (e40906f).
- 2026-09-24, task 04, second stop: this time Ornith copied the files and saw the tests fail, then
checked its planned code against the markup test cases in its reasoning, spelling out the tags as
it went. The tag it wrote ended its reasoning; what followed became its answer, and the turn
ended with no tool call. Building the tags from pieces keeps them out of files, not out of the
model's thoughts. So `check.rs` is now given complete (the reference's, tags from pieces), and
task 04 fills only `entries.rs` and says not to work through the markup cases. The three later
tests that used a tag only as something the check refuses (`edit.rs`, `files.rs`, `serve.rs`)
now use an instruction phrase or an invisible character. They pass against the reference and
are rustfmt-clean. The attempt is saved in `.state/runs/M5a/04-attempt-2/`. Resume from task
04. The measurements behind this are in `docs/research/2026-09-24-chat-template-tokens.md`,
added after the run.
- 2026-09-24, after the review: tasks 17 and 18 clean up the review's nits. Task 19 closes a hole
the review found in the spec and the reference alike: a note's text could hold a line shaped like
a `notes.md` header, so an untrusted session could plant a `moved from memory.md` entry that
`recall` showed as identity. The spec was changed (`4ae827a`, section 3). Since task 19 hands
over new versions of two given tests, task 02's `memory_args.rs` is now `memory_args.rs-task02`
and task 06's `notes.rs` is now `notes.rs-task06`. Task 20 fixes review ids: counting only the
waiting writes gave a write staged after an accepted one the accepted one's id, and the write
was lost.
- 2026-09-24, later: the work moved to titan (macOS) while talos's disk is replaced, and there
the gate failed for reasons older than M5a: socket paths under macOS's long `$TMPDIR`,
`gatewayd` reading `/proc/self`, and `/tmp` being `/private/tmp`. Task 16 makes it pass on
both; the review's tasks moved to 17 to 20. Resume from task 16.
## Running it
```sh
git switch m5a
OPENCODE_MODEL=straylight/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M5a
# the review's follow-ups, on titan, which serves Ornith itself (127.0.0.1:8081):
OPENCODE_MODEL=titan/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M5a 16
```
The model the implementer runs on is separate from the one the harness serves: `make
verify-device` and the device checks still use straylight's `llama-server`.
Keep the OpenCode TUI closed while it runs.