M5a spec: memory writes are not announced, only journalled; approval syntax noted in the open decision

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 13:43:55 -07:00
co-authored by Claude Opus 5.5
parent 29f9069a33
commit bb04467001
2 changed files with 16 additions and 27 deletions
+13 -24
View File
@@ -32,17 +32,16 @@ Facts this rests on, checked on 2026-09-24:
untrusted, and never in the prompt.
5. The baseline with the starter `soul.md` stays at 3,000 tokens or less, measured on the device.
6. A hit from a session that had seen untrusted content comes back marked untrusted.
7. The owner can see every write where it happened, and undo the latest one of a file.
7. The owner can list every write (`bxctl memory log`), and undo the latest one of a file.
## 2. Components
| Piece | Change |
|---|---|
| `memoryd` (new crate) | Owns `memory/` and `index/`. Serves the memory block to `loopd`, runs `recall` and `memory` calls for `brokerd`, indexes session logs, rebuilds the index for `bxctl`. No network. |
| `proto` | Eight new message kinds (section 7); the `memory` tool's argument structs; `TurnEvent::MemoryWritten`. |
| `loopd` | The baseline takes the memory block from `memoryd` instead of reading `memory/core.md`. `recall` and `memory` are core tools. After each finished turn it tells `memoryd` the session has new records. A successful write is announced as a turn event. |
| `proto` | Eight new message kinds (section 7); the `memory` tool's argument structs. |
| `loopd` | The baseline takes the memory block from `memoryd` instead of reading `memory/core.md`. `recall` and `memory` are core tools. After each finished turn it tells `memoryd` the session has new records. |
| `brokerd` | `recall` and `memory` are decided under built-in grants (both `auto`); a decided call goes to `memoryd` with the session's state, instead of a container. A result's label can only be made stricter by the runtime. |
| `gatewayd` | Posts a `MemoryWritten` event as one line in the thread. |
| `bxctl` | `bxctl reindex`; `bxctl memory log`, `undo`, and, when review is on, `pending`, `accept`, `reject`. |
| `config/soul.md` (new) | The generic starter persona. |
@@ -168,7 +167,7 @@ Arguments, one of (`file` is `"memory"` or `"user"`):
```
No write asks the owner anything (P21). Entries are named by their **full text**, never by number,
so the journal and the announcement read on their own, and an edit made against an old view of
so the journal reads on its own, and an edit made against an old view of
the file fails safely: every `old` must match an entry of the file as it is now, exactly.
**Where a write goes.** `brokerd` sends each call with the session's state (section 7):
@@ -213,12 +212,8 @@ true one.
appended. The answer to the model: `memory.md updated (1,510 of 2,200 characters); it takes
effect in the next session`, or the archive sentence above.
**Announcing.** Each successful write is announced as one line, built by `memoryd`:
`memory: added to user.md: "Prefers answers in UTC."`, `memory: consolidated memory.md: 2 moved
to the archive, 1 rewritten, 5 kept`, `memory: saved to the archive (untrusted): "…"`. Entries are
cut at 200 characters in this line. `loopd` emits it as `TurnEvent::MemoryWritten { text }`;
`gatewayd` posts it in the turn's thread and `bxctl chat` prints it. It is information, not a
question.
**Seeing writes.** Writes are not posted anywhere (owner, 2026-09-24): the journal is the
record, and `bxctl memory log` shows it.
**Undo.** `bxctl memory log [N]` prints the last N edits (default 10) from the journal: id, time,
session, action, file. `bxctl memory undo <id>` reverts an edit if it is the **latest** edit of
@@ -343,9 +338,6 @@ connection, the existing frames (`proto::frame`, `MAX_FRAME`). New message kinds
refusing, fails the new session with the runbook pointer `memory-unavailable`.
- Registry: `recall` and `memory` are core, with the schemas of section 5. The model sees
`clock`, `recall`, `memory`, `find_tool`, `call_tool`.
- A successful `memory` result carries the announcement line (section 5) at its end, after a
fixed marker line; `loopd` strips it before the result is logged or shown to the model, and
emits it as `TurnEvent::MemoryWritten`.
- After a finished turn: `session_updated`, best effort. A failure is one log line; the next
startup of `memoryd` catches up.
- `system.md` gains two sentences: keep `memory.md` and `user.md` short and current, and use
@@ -387,13 +379,13 @@ max_class = "private" # records above this class are not indexed
untrusted, where it can be recalled but never lands in the prompt by itself.
- Memory writes are the one tool with no approval anywhere. The trade is deliberate (owner,
2026-09-24): the risk it leaves is a clean session writing something false, which is the
agent's judgement of what the owner said; every such write is announced, journalled and can be
undone, and review can be turned on.
agent's judgement of what the owner said; every such write is journalled, listed by `bxctl
memory log`, and can be undone, and review can be turned on.
- Recalled text is a tool result: appended, labelled, and it taints the session like any other.
Tool results are not indexed, so a fetched page cannot come back through `recall`; what the
assistant said after reading one can, marked untrusted.
- The FTS5 query is built only from quoted terms; the model's text is never SQL.
- Entries shown by `bxctl` and in announcements are escaped (`proto::escape`), so an entry
- Entries shown by `bxctl` are escaped (`proto::escape`), so an entry
cannot render as markup or mention anyone.
## 13. Runbook
@@ -418,14 +410,12 @@ Offline, in `make gate`:
file naming either tool refused at load; the session's state carried in `memory_call`; the composite runtime; the
stricter label, with a runtime that returns a stricter and a laxer one.
- `loopd`: the baseline with a fake `memoryd`; the refusal and its pointer; `session_updated`
sent after a finished turn and not after a turn ending in tool calls; the announcement stripped
from the logged result and emitted as a turn event.
- `gatewayd`: a `MemoryWritten` event posted as one line.
sent after a finished turn and not after a turn ending in tool calls.
- `bxctl`: `reindex` and each `memory` command against a fake socket.
On the device (the owner's Mattermost, as for M4b): the baseline token count with the starter
soul; a fact told in one thread and recalled in another; an `add`, announced in its thread with no
question, that appears in a new thread and not in the thread that wrote it; a `consolidate` that
soul; a fact told in one thread and recalled in another; an `add`, made with no question and listed by
`bxctl memory log`, that appears in a new thread and not in the thread that wrote it; a `consolidate` that
moves an entry, which `recall` then finds; a write after a fetched page that lands in the archive,
untrusted; `bxctl memory undo` of the latest edit.
@@ -441,5 +431,4 @@ untrusted; `bxctl memory undo` of the latest edit.
4. Decided: the starter `soul.md` is generic (section 3).
5. Open: the phrase list of the check (section 5) is short on purpose. Add phrases you have seen
in the wild, or drop the list and keep only rules 1 to 3.
6. Open: the announcement goes to the turn's thread (section 5). A channel of its own for memory
writes, or silence with `bxctl memory log` only, are the alternatives.
6. Decided: writes are not announced; `bxctl memory log` lists them (section 5).