Files
kyleandClaude Opus 5.5 f389a9a43a M5a plan: tasks 21 and 22, from the device check
21: the ledger raises a session's state by the stricter of the grant's
label and the result's own, so a clean session that recalls a
memory-tainted note becomes untrusted and cannot write it into
memory.md (found on straylight, 2026-09-25). 22: the answers to a note
name notes.md, and for an untrusted add the file that was not changed.
The new given tests are ledger_label.rs and new versions of memoryd's
edit.rs and serve.rs; the old copies are kept as -task06 and -task10. On
a scratch tree at the end of task 20: the named tests fail and then pass,
5 and 4 mutations are caught, and the gate passes with 944 tests.

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

10 KiB

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
21 21-session-follows-result.md a session's state follows the result's own label (device finding) ledger_label.rs reference; 5 mutations caught
22 22-plain-note-answers.md the answers to a note say where it went (device finding) edit.rs, serve.rs updated reference; 4 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; at the end of task 21, 944, and task 22 adds none. 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.
  • 2026-09-25, the device check on straylight: a clean session that recalled a memory-tainted note stayed trusted, because the ledger raised the state by the grant's label alone, and it wrote the note into memory.md. The spec said "as today" and the reference did the same, so task 21 fixes both (0e5b1d2, section 8). And the model twice told the owner "Added to memory.md" for a write that had become a note; task 22 makes the answers say where it went (section 3). Task 22 hands over new versions of two given tests: task 06's edit.rs is now edit.rs-task06 and task 10's serve.rs is now serve.rs-task10. Resume from task 21.

Running it

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 device check's follow-ups:
OPENCODE_MODEL=titan/ornith-1.5-35b-a3b tools/run-plan.sh docs/plans/M5a 21

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.