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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user