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>
122 lines
9.0 KiB
Markdown
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.
|