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
+3 -3
View File
@@ -6,7 +6,7 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit
| Date | Decision | Reason |
|---|---|---|
| 2026-09-24 | Memory writes need no approval, as in Hermes (the owner's `~/.hermes` has no `write_approval`). Writes from a session that has seen untrusted content go to `memory/archive.md`, labelled untrusted, not into the prompt. Every write is scanned, announced in its thread, journalled and can be undone; nothing is deleted. Brief change P21 proposed; the M5a spec is revised to match. Session-scoped approvals for other tools become an open decision. | Owner: an agent harness should act without a stream of questions, and reading and writing memory is what an agent is. The only risk the approval guarded is injected text reaching every future prompt, and that needs a session that read untrusted content; `brokerd` knows which sessions those are. |
| 2026-09-24 | Memory writes need no approval, as in Hermes (the owner's `~/.hermes` has no `write_approval`). Writes from a session that has seen untrusted content go to `memory/archive.md`, labelled untrusted, not into the prompt. Every write is scanned, journalled (listed by `bxctl memory log`, not posted anywhere) and can be undone; nothing is deleted. Brief change P21 proposed; the M5a spec is revised to match. Session-scoped approvals for other tools become an open decision. | Owner: an agent harness should act without a stream of questions, and reading and writing memory is what an agent is. The only risk the approval guarded is injected text reaching every future prompt, and that needs a session that read untrusted content; `brokerd` knows which sessions those are. |
| 2026-09-24 | M5a spec review: the policy of `recall` (`auto`) and `memory` (`ask`) is built into `brokerd`, and grant files cannot name them; tool calls are indexed by name and arguments, tool results are not; summaries of tool output, where a tool opts in, come with M5c's summariser. | Owner: an agent that cannot write memories is no better than a plain chat window, and a grant file must not be able to turn edits off or make them unattended. A web fetch's summary is worth keeping; most tools' output is not. |
| 2026-09-24 | Compaction (new epochs) comes with the scheduler in M5c, not in M5a. Until then a session has one epoch, and M5a's rules say "the next session" where the brief says "the next epoch". `soul.md` starts from a generic file in the repository; `memory.md` and `user.md` start empty. M5a spec drafted: `docs/specs/2026-09-24-m5a-local-memory.md`. | Owner. `loopd` has no epochs yet (every session is epoch 0; a full context is `SessionFull`); compaction is an idle-time job on the background slot, which is what M5c builds. Over Mattermost each thread is a session, so a new thread already picks up approved edits. |
| 2026-09-24 | M5 direction, before a spec. Memory is three local files and Honcho. The files: `memory/soul.md` (owner-written), `memory/memory.md` and `memory/user.md` (agent-edited, `ask`), read once per epoch, as Hermes does. Honcho, the fleet's memory service on rift (v3.2.0; models on dixie), reached by a new role, `memoryd`, which also owns the memory files and the SQLite index (owner, same day): each Boxmaker writes to its own workspace, only turns from sessions that saw no untrusted content, and can read other workspaces such as `hermes-fleet`, each with an owner-set trust level (untrusted by default). Local full-text search over the session logs stays. `consolidate` moves entries out of `memory.md` into `memory/archive.md` (searched by `recall`) and, from M5b, into Honcho. From M5c a scheduled job on the background slot proposes which entries stay at which tier, from counts of `recall` hits that `memoryd` keeps from M5a; its proposals are ordinary `ask` edits. The owner can mark any workspace trusted or untrusted; which ones is configuration, not design (the owner trusts the Hermes agents' workspace today). M5 splits into M5a (files and search, no network), M5b (`memoryd`, Honcho through the owner's `roncho` client) and M5c (the scheduler). Brief changes P17 to P20 proposed. | Owner, 2026-09-24: Honcho lets several Boxmakers share memory across the fleet; the Hermes agents do Boxmaker's job today and may be trusted. `brokerd` never holds conversation history and `loopd` has no network, so syncing turns needs a role of its own. Honcho's derivation replaces the brief's notes, pre-compaction flush and embedding index. |
@@ -67,7 +67,7 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit
| How an approval whose arguments do not fit one Mattermost post is shown. Settled on 2026-09-18: what is being approved is never truncated, in any channel, because the unseen tail is where injected text would put its payload; `bxctl` prints arguments in full. Leading option for Mattermost: a thread of numbered chunks split on character boundaries of the escaped text, where only a reply or reaction on the last post counts and that post repeats the id, the chunk count and the hash of the whole; above some number of chunks, the post points to `bxctl approvals` and approval is local only. The post size limit and rate limits must be checked against the Mattermost API reference first. | M4 |
| Secret store: settled 2026-09-23 (systemd credentials, environment, owner-only file; P15 proposed). Where `brokerd` reads tool secrets from, and whether `SecretStore` moves out of `gatewayd` into a shared place, is decided when a tool first needs a secret. | when a tool needs a secret |
| Whether cloud-led sessions are ever allowed, and for which data classes. | M6 |
| Session-scoped and standing approvals for tools other than memory: answering an approval with, say, `!approve 42 session` to allow the same pattern for the rest of the session, or "always" to write a grant. A temporary grant is new authority (the grants are the owner's files, and `brokerd` cannot write them), so it needs its own design. Owner's point, 2026-09-24: a harness should not ask a stream of questions. | after M5 |
| Session-scoped and standing approvals for tools other than memory: answering an approval with `!approve 42 session` (the same pattern for the rest of the session) or `!approve 42 always` (standing), words after the id as `!deny 42 <reason>` has (owner's syntax, 2026-09-24). To settle: what "session" covers (this call's tool and path or host, or the whole grant it matched); and where "always" writes, since `brokerd` cannot write grants (a proposed grant for the owner, or a separate directory of chat-approved grants). A temporary grant is new authority (the grants are the owner's files, and `brokerd` cannot write them), so it needs its own design. Owner's point, 2026-09-24: a harness should not ask a stream of questions. | after M5 |
| Projects in memory. Honcho can separate projects by a workspace per project (hard walls, shared naturally by every agent on the project, but Honcho's picture of the owner splits per workspace) or by scopes inside one workspace (one picture, recall filtered by project; semantics not yet read). Either way a project is a `memoryd` setting: its channels and threads, its workspace or scope, `write` and `trust`. A shared project workspace is written by several agents, so its trust is the owner's call per project. Test both on rift before choosing. Owner's idea, 2026-09-24. | M5b or a later M5d |
## Applied to the design brief
@@ -107,4 +107,4 @@ Later changes to the brief:
| # | Change | Reason |
|---|---|---|
| P21 | Memory 1 and 7: memory edits are not approved. `memory` writes are automatic; in a session that has seen untrusted content they are appended to `memory/archive.md`, labelled untrusted, and never reach `memory.md` or `user.md` (only `add` is possible there). Every write is checked (no control or invisible characters, no chat-template tokens, no instruction phrases from a short fixed list), and entries are checked again when the block is built. Every write is announced in its thread, recorded in the edit journal, and the latest edit of a file can be undone with `bxctl`. `consolidate` archives what it drops. An owner setting stages writes for review instead of applying them (off by default). The policy of `recall` and `memory` is built into `brokerd` and no grant file can change it. `soul.md` stays the owner's alone. | Owner, 2026-09-24, pointing to Hermes: writes without questions, a scan, a snapshot frozen per session, size limits. The archive routing covers what a pattern scan cannot: a plausible fact planted by a page. |
| P21 | Memory 1 and 7: memory edits are not approved. `memory` writes are automatic; in a session that has seen untrusted content they are appended to `memory/archive.md`, labelled untrusted, and never reach `memory.md` or `user.md` (only `add` is possible there). Every write is checked (no control or invisible characters, no chat-template tokens, no instruction phrases from a short fixed list), and entries are checked again when the block is built. Every write is recorded in the edit journal, listed by `bxctl memory log` (not posted to the chat), and the latest edit of a file can be undone with `bxctl`. `consolidate` archives what it drops. An owner setting stages writes for review instead of applying them (off by default). The policy of `recall` and `memory` is built into `brokerd` and no grant file can change it. `soul.md` stays the owner's alone. | Owner, 2026-09-24, pointing to Hermes: writes without questions, a scan, a snapshot frozen per session, size limits. The archive routing covers what a pattern scan cannot: a plausible fact planted by a page. |
+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).