P21 and the M5a spec: identity and experience; the archive becomes notes, and note is a memory action

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 13:49:28 -07:00
co-authored by Claude Opus 5.5
parent 20bb2ff6fc
commit 64a5931354
2 changed files with 56 additions and 42 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ Newest first. A decision that changes `docs/design.md` lands in the same commit
| Date | Decision | Reason | | Date | Decision | Reason |
|---|---|---| |---|---|---|
| 2026-09-24 | M5a's check on memory writes stays short and fixed: control characters, invisible characters, chat-template markup, six phrases. A research thread on prompt-injection defences feeds a sanitizer module for memory writes (and possibly other persisted text), built as hardening in M5b. Memory writes are not posted to the chat; `bxctl memory log` lists them. | Owner. The archive routing of untrusted sessions' writes is the main defence; a pattern list is defence in depth and should rest on research, not guesses. | | 2026-09-24 | M5a's check on memory writes stays short and fixed: control characters, invisible characters, chat-template markup, six phrases. A research thread on prompt-injection defences feeds a sanitizer module for memory writes (and possibly other persisted text), built as hardening in M5b. Memory writes are not posted to the chat; `bxctl memory log` lists them. | Owner. The archive routing of untrusted sessions' writes is the main defence; a pattern list is defence in depth and should rest on research, not guesses. |
| 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 | Memory writes need no approval, as in Hermes (the owner's `~/.hermes` has no `write_approval`). What a session that has seen untrusted content wants remembered becomes a note, labelled untrusted, not identity in the prompt (refined the same day: identity and experience are separate kinds of memory). 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 | 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 | 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. | | 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. |
@@ -108,4 +108,4 @@ Later changes to the brief:
| # | Change | Reason | | # | 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 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. | | P21 | Memory 1 to 8, State and Milestones: memory is **identity** and **experience** (owner, 2026-09-24). Identity is who the agent is and what it holds true about its work and owner: `soul.md` (the owner's), `memory.md`, `user.md`, in every prompt, written only by the owner or by sessions that have not read untrusted content. Experience is what it has seen or been told: `memory/notes.md` (replacing `archive.md`) and the session logs, never in the prompt, found with `recall`, each item labelled with its source and trust; not everything remembered is believed. Memory writes are not approved. The `memory` tool gains `note`, which records something seen in any session, labelled by the session's state. In a session that has read untrusted content an identity `add` becomes a `note` and the other identity edits are refused. Every write is checked (no control or invisible characters, no chat-template tokens, no instruction phrases from a short fixed list), and identity entries are checked again when the block is built. Every write is recorded in the edit journal and listed by `bxctl memory log` (not posted to the chat), and the latest edit of a file can be undone with `bxctl`. `consolidate` and `remove` put what they take out into `notes.md` as moved or dropped; nothing is deleted. An owner setting stages identity 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. `recall` tells identity ("you knew") from notes ("you noted, from an untrusted session"). Milestones: M5a's row loses `ask` and gains notes; M5c's tiering job may promote only trusted notes into identity, and its writes are journalled like any other. | Owner, 2026-09-24, pointing to Hermes: writes without questions, a scan, a snapshot frozen per session, size limits; and memory is two things, who the agent is and what it has seen. Turning an untrusted session's writes into notes covers what a pattern scan cannot, a plausible fact planted by a page, without losing what was seen. |
+54 -40
View File
@@ -28,8 +28,8 @@ Facts this rests on, checked on 2026-09-24:
2. An entry moved out of `memory.md` by `consolidate` is found by `recall`. 2. An entry moved out of `memory.md` by `consolidate` is found by `recall`.
3. A memory write reaches the prompt only in the next session; a session already running keeps 3. A memory write reaches the prompt only in the next session; a session already running keeps
the bytes it started with. No write asks the owner anything. the bytes it started with. No write asks the owner anything.
4. A write from a session that has seen untrusted content lands in the archive, labelled 4. What a session that has seen untrusted content wants to remember becomes a note, labelled
untrusted, and never in the prompt. untrusted: remembered and found by `recall`, never in the prompt.
5. The baseline with the starter `soul.md` stays at 3,000 tokens or less, measured on the device. 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. 6. A hit from a session that had seen untrusted content comes back marked untrusted.
7. The owner can list every write (`bxctl memory log`), 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.
@@ -57,7 +57,7 @@ any of them with an editor; `soul.md` only ever that way.
| `soul.md` | the owner | 4,000 characters | a copy of the starter (`[memory] soul_starter`, default `soul.md` next to the config file) | | `soul.md` | the owner | 4,000 characters | a copy of the starter (`[memory] soul_starter`, default `soul.md` next to the config file) |
| `memory.md` | `memoryd`, applying the agent's writes | 2,200 characters | empty | | `memory.md` | `memoryd`, applying the agent's writes | 2,200 characters | empty |
| `user.md` | `memoryd`, applying the agent's writes | 1,375 characters | empty | | `user.md` | `memoryd`, applying the agent's writes | 1,375 characters | empty |
| `archive.md` | `memoryd`, appending moved and dropped entries, and writes from untrusted sessions | none | empty | | `notes.md` | `memoryd`, appending notes (things seen) and entries moved or dropped from the files above | none | empty |
| `edits/YYYY-MM.jsonl` | `memoryd`, appending one line per applied edit | none | absent | | `edits/YYYY-MM.jsonl` | `memoryd`, appending one line per applied edit | none | absent |
| `pending.jsonl` | `memoryd`, when review is on (section 5) | none | absent | | `pending.jsonl` | `memoryd`, when review is on (section 5) | none | absent |
| `recalls/YYYY-MM.jsonl` | `memoryd`, appending one line per `recall` | none | absent | | `recalls/YYYY-MM.jsonl` | `memoryd`, appending one line per `recall` | none | absent |
@@ -69,23 +69,31 @@ counted with `chars()`. Limits are on the file's text.
(as Hermes does). An entry is trimmed, is not empty, and may span lines, but may not contain a (as Hermes does). An entry is trimmed, is not empty, and may span lines, but may not contain a
line holding only `§`. Two identical entries in one file are refused. line holding only `§`. Two identical entries in one file are refused.
**`archive.md`.** Append-only. Each entry starts with one header line saying how it got there: **Two kinds of memory** (owner, 2026-09-24). **Identity** is who the agent is and what it holds
true about its work and its owner: `soul.md`, `memory.md`, `user.md`, in every prompt, written
only by the owner or by sessions that have not read untrusted content. **Experience** is what it
has seen or been told: `notes.md` and the session logs, never in the prompt, found with `recall`,
each item with its own label. Not everything remembered is believed, but it is remembered.
**`notes.md`.** Append-only. Each entry starts with one header line saying how it got there:
```text ```text
§ noted 2026-09-24T21:02Z in session mm-dh45qg… · untrusted
<the entry>
§ moved 2026-09-24T20:14Z from memory.md in session mm-y73sxj3popgwumyccptb56p1ah § moved 2026-09-24T20:14Z from memory.md in session mm-y73sxj3popgwumyccptb56p1ah
<the entry> <the entry>
§ dropped 2026-09-24T20:14Z from memory.md in session mm-y73sxj3popgwumyccptb56p1ah § dropped 2026-09-24T20:14Z from memory.md in session mm-y73sxj3popgwumyccptb56p1ah
<the entry> <the entry>
§ written 2026-09-24T21:02Z for user.md in session mm-dh45qg… · untrusted
<the entry>
``` ```
Nothing a write removes is lost: moved and dropped entries are archived, and the journal keeps A `noted` entry is labelled by the session that wrote it: untrusted if it had read untrusted
content, trusted otherwise; its class is the session's. `moved` and `dropped` entries were
identity, so they are trusted. Nothing a write removes is lost: moved and dropped entries go to `notes.md`, and the journal keeps
every before and after. every before and after.
**The edit journal** (`edits/`) records every applied edit: its id (`YYYY-MM-DD-NNNN`), time, **The edit journal** (`edits/`) records every applied edit: its id (`YYYY-MM-DD-NNNN`), time,
session, call, action, file, where it went (`memory.md`, `user.md` or the archive), the entries session, call, action, file, where it went (`memory.md`, `user.md` or `notes.md`), the entries
before and after, and whether the session had seen untrusted content. It is the history `archive.md` alone cannot give (when before and after, and whether the session had seen untrusted content. It is the history `notes.md` alone cannot give (when
an entry first appeared), and the record an owner reads when a memory looks wrong. **The recall an entry first appeared), and the record an owner reads when a memory looks wrong. **The recall
log** (`recalls/`) records time, session, query, and the source of every hit, and the queries that log** (`recalls/`) records time, session, query, and the source of every hit, and the queries that
found nothing: hit counts for M5c's tiering job are worked out from it, so they survive a reindex. found nothing: hit counts for M5c's tiering job are worked out from it, so they survive a reindex.
@@ -132,11 +140,13 @@ its source and label, and each snippet at most 600 characters:
```text ```text
[1] session mm-8ass9m…, 2026-09-20 14:02 UTC, the owner said · private [1] session mm-8ass9m…, 2026-09-20 14:02 UTC, the owner said · private
…the launch window moved to Thursday… …the launch window moved to Thursday…
[2] archive.md, moved 2026-09-24 from memory.md · private [2] you knew this: moved 2026-09-24 from memory.md · private
Owner prefers answers in UTC. Owner prefers answers in UTC.
[3] session chat-1789…, 2026-09-21 09:15 UTC, the assistant said · private · untrusted [3] session chat-1789…, 2026-09-21 09:15 UTC, the assistant said · private · untrusted
… …
[4] session mm-dh45qg…, 2026-09-22 18:17 UTC, the assistant called · private [4] you noted this, 2026-09-22 18:20 UTC, in a session that had read untrusted content · private · untrusted
example.org says the release moved to March.
[5] session mm-dh45qg…, 2026-09-22 18:17 UTC, the assistant called · private
http_fetch {"url":"https://example.org/notes"} http_fetch {"url":"https://example.org/notes"}
``` ```
@@ -151,12 +161,14 @@ stricter of each (section 8).
### `memory` ### `memory`
Arguments, one of (`file` is `"memory"` or `"user"`): Arguments, one of (`file` is `"memory"` or `"user"`). `note` records something seen or told
(experience); the others edit identity:
```json ```json
{"action": "add", "file": "memory", "text": "…"} {"action": "add", "file": "memory", "text": "…"}
{"action": "replace", "file": "memory", "old": "<an entry, exactly>", "text": "…"} {"action": "replace", "file": "memory", "old": "<an entry, exactly>", "text": "…"}
{"action": "remove", "file": "user", "old": "<an entry, exactly>"} {"action": "remove", "file": "user", "old": "<an entry, exactly>"}
{"action": "note", "text": "…"}
{"action": "consolidate", "file": "memory", "entries": [ {"action": "consolidate", "file": "memory", "entries": [
{"old": "<an entry>", "keep": true}, {"old": "<an entry>", "keep": true},
{"old": "<an entry>", "text": "<its rewrite>"}, {"old": "<an entry>", "text": "<its rewrite>"},
@@ -172,25 +184,27 @@ the file fails safely: every `old` must match an entry of the file as it is now,
**Where a write goes.** `brokerd` sends each call with the session's state (section 7): **Where a write goes.** `brokerd` sends each call with the session's state (section 7):
- In a session that has **not** seen untrusted content, the write applies to `memory.md` or - `note` is appended to `notes.md` in any session, labelled by the session (above). The answer:
`user.md`. `noted (untrusted)` or `noted`.
- In a session that **has**, only `add` is possible, and the entry is appended to `archive.md` - In a session that has **not** seen untrusted content, the identity actions apply to `memory.md`
with the header `§ written … · untrusted`, never to the file named. `recall` finds it, labelled or `user.md`.
untrusted; it never reaches the prompt on its own. The answer says so: `saved to the archive, - In a session that **has**, an identity `add` becomes a `note`, and the answer says so: `saved as
not memory.md: this session has read untrusted content`. `replace`, `remove` and `consolidate` a note (untrusted): this session has read untrusted content`. It is remembered and `recall`
are refused with `this session has read untrusted content; it can add to the archive but not finds it; it never reaches the prompt on its own. `replace`, `remove` and `consolidate` are
change memory.md`. The owner can move an archived entry into a file by editing it. refused with `this session has read untrusted content; it can take notes but not change
memory.md`. The owner can move a note into a file by editing it.
**Rules**, checked by `brokerd` when parsing (shape) and by `memoryd` when applying (content): **Rules**, checked by `brokerd` when parsing (shape) and by `memoryd` when applying (content):
- `add`: the new file must fit its limit; a duplicate is refused. - `add`: the new file must fit its limit; a duplicate is refused.
- `replace`, `remove`: `old` must match exactly one entry. A removed entry is archived as dropped. - `replace`, `remove`: `old` must match exactly one entry. A removed entry goes to `notes.md` as dropped.
- `note`: 1 to 1,000 characters; the check applies.
- `consolidate`: every current entry of the file must appear exactly once as an `old`, each with - `consolidate`: every current entry of the file must appear exactly once as an `old`, each with
exactly one of `keep`, `text`, `move`, `drop`; entries with only `text` are new. The resulting exactly one of `keep`, `text`, `move`, `drop`; entries with only `text` are new. The resulting
file must fit its limit. Moved and dropped entries are appended to `archive.md` in the order file must fit its limit. Moved and dropped entries are appended to `notes.md` in the order
given, with their headers. given, with their headers.
- An edit that would go over the limit is refused with: `memory.md would be 2,350 of 2,200 - An edit that would go over the limit is refused with: `memory.md would be 2,350 of 2,200
characters; use consolidate to move entries to the archive`. That sentence is fixed apart from characters; use consolidate to move entries to notes`. That sentence is fixed apart from
the numbers and the file. the numbers and the file.
**The check.** Every new or rewritten entry is refused, with a fixed sentence naming which rule, **The check.** Every new or rewritten entry is refused, with a fixed sentence naming which rule,
@@ -204,13 +218,13 @@ if it contains:
`ignore all previous`, `disregard previous instructions`, `disregard all previous`, `ignore all previous`, `disregard previous instructions`, `disregard all previous`,
`new instructions:`, `system prompt:`. `new instructions:`, `system prompt:`.
The list is short and fixed on purpose: it is defence in depth, as Hermes' scan is. The archive The list is short and fixed on purpose: it is defence in depth, as Hermes' scan is. Turning an untrusted
routing above is the real defence, because a pattern cannot tell a plausible planted fact from a session's writes into notes is the real defence, because a pattern cannot tell a plausible planted fact from a
true one. true one.
**Applying.** The file is written to a temporary file and renamed, then the journal line is **Applying.** The file is written to a temporary file and renamed, then the journal line is
appended. The answer to the model: `memory.md updated (1,510 of 2,200 characters); it takes 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. effect in the next session`, or the note sentences above.
**Seeing writes.** Writes are not posted anywhere (owner, 2026-09-24): the journal is the **Seeing writes.** Writes are not posted anywhere (owner, 2026-09-24): the journal is the
record, and `bxctl memory log` shows it. record, and `bxctl memory log` shows it.
@@ -218,34 +232,33 @@ record, and `bxctl memory log` shows it.
**Undo.** `bxctl memory log [N]` prints the last N edits (default 10) from the journal: id, time, **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 session, action, file. `bxctl memory undo <id>` reverts an edit if it is the **latest** edit of
its file and the file still holds exactly what that edit left: the file gets its entries back from its file and the file still holds exactly what that edit left: the file gets its entries back from
the journal, entries the edit archived stay in the archive (append-only), and the undo is itself the journal, entries the edit moved to `notes.md` stay there (append-only), and the undo is itself
journalled. Anything else is refused with the reason, so an undo never overwrites a later change. journalled. Anything else is refused with the reason, so an undo never overwrites a later change.
**Review (off by default).** With `[memory] review = true`, a write that would change `memory.md` **Review (off by default).** With `[memory] review = true`, a write that would change `memory.md`
or `user.md` is appended to `pending.jsonl` instead, and the answer says it is waiting for the or `user.md` is appended to `pending.jsonl` instead, and the answer says it is waiting for the
owner. `bxctl memory pending` lists them; `bxctl memory accept <id>` applies one (with the same owner. `bxctl memory pending` lists them; `bxctl memory accept <id>` applies one (with the same
checks, against the file as it is then); `bxctl memory reject <id>` discards it (journalled). checks, against the file as it is then); `bxctl memory reject <id>` discards it (journalled).
Writes to the archive are never staged. Notes are never staged.
## 6. The index ## 6. The index
`{home}/index/memory.sqlite3`, directory 0700, file 0600, owned by `memoryd`. Disposable: deleting `{home}/index/memory.sqlite3`, directory 0700, file 0600, owned by `memoryd`. Disposable: deleting
it and restarting `memoryd`, or `bxctl reindex`, rebuilds it from `sessions/` and `archive.md`. it and restarting `memoryd`, or `bxctl reindex`, rebuilds it from `sessions/` and `notes.md`.
**What is indexed.** From session logs: the `content` of `User` records, of `Assistant` records **What is indexed.** From session logs: the `content` of `User` records, of `Assistant` records
that have content, and each tool call the assistant made, as its tool name and arguments that have content, and each tool call the assistant made, as its tool name and arguments
(`read_file {"path":"/srv/notes/q3.txt"}`), so `recall` can answer "did we read that, and when". (`read_file {"path":"/srv/notes/q3.txt"}`), so `recall` can answer "did we read that, and when".
Not tool results (the bulk of every log, and the way injected text arrives) and not Not tool results (the bulk of every log, and the way injected text arrives) and not
`reasoning_content`. A summary of a tool's output is M5c's (owner, 2026-09-24): it needs a model `reasoning_content`. A summary of a tool's output is M5c's (owner, 2026-09-24): it needs a model
to write it. From `archive.md`: every entry. to write it. From `notes.md`: every entry.
`memory.md` and `user.md` are not indexed: they are already in every prompt. `memory.md` and `user.md` are not indexed: they are already in every prompt.
**Labels.** Walking a session log in order, `memoryd` keeps the session's label so far: the **Labels.** Walking a session log in order, `memoryd` keeps the session's label so far: the
highest `class` of any `ToolResult` before this record (at least `private`), and whether any was highest `class` of any `ToolResult` before this record (at least `private`), and whether any was
untrusted. Each indexed record gets the label in force when it was written. A record whose class untrusted. Each indexed record gets the label in force when it was written. A record whose class
is above `[index] max_class` (default `private`) is not indexed at all. Archive entries are is above `[index] max_class` (default `private`) is not indexed at all. `notes.md` entries carry
labelled `private`, and untrusted when their header says so; moved and dropped entries came from the label their header gives (section 3).
`memory.md` or `user.md`, which only clean sessions write, so they are trusted.
**Keeping up.** The index stores, per session log, how many records it has read. After each **Keeping up.** The index stores, per session log, how many records it has read. After each
finished turn `loopd` sends `memoryd` `session_updated` with the session id (section 7), and finished turn `loopd` sends `memoryd` `session_updated` with the session id (section 7), and
@@ -281,7 +294,7 @@ connection, the existing frames (`proto::frame`, `MAX_FRAME`). New message kinds
| `session_updated` | `{"session": SessionId}` | | `session_updated` | `{"session": SessionId}` |
| `memory_call` | `{"request": ToolRequest, "taint": DataClass, "untrusted": bool}`: the decided call, with the session's state when `brokerd` decided it | | `memory_call` | `{"request": ToolRequest, "taint": DataClass, "untrusted": bool}`: the decided call, with the session's state when `brokerd` decided it |
| `reindex` | `{}` | | `reindex` | `{}` |
| `reindexed` | `{"logs": u64, "records": u64, "archive": u64}` | | `reindexed` | `{"logs": u64, "records": u64, "notes": u64}` |
| `memory_admin` | `{"command": "log" \| "undo" \| "pending" \| "accept" \| "reject", "arg": string}` | | `memory_admin` | `{"command": "log" \| "undo" \| "pending" \| "accept" \| "reject", "arg": string}` |
| `memory_admin_reply` | `{"text": string}` | | `memory_admin_reply` | `{"text": string}` |
@@ -347,7 +360,7 @@ connection, the existing frames (`proto::frame`, `MAX_FRAME`). New message kinds
## 10. `bxctl` ## 10. `bxctl`
- `bxctl reindex [--admin-socket PATH]`: sends `reindex`, waits, and prints `reindexed: 42 logs, - `bxctl reindex [--admin-socket PATH]`: sends `reindex`, waits, and prints `reindexed: 42 logs,
1,208 records, 7 archive entries`. `memoryd` rebuilds into a new file and renames it over the 1,208 records, 7 notes`. `memoryd` rebuilds into a new file and renames it over the
old one, so `recall` keeps working meanwhile. old one, so `recall` keeps working meanwhile.
- `bxctl memory log [N]`, `bxctl memory undo <id>`, and with review on, `bxctl memory pending`, - `bxctl memory log [N]`, `bxctl memory undo <id>`, and with review on, `bxctl memory pending`,
`accept <id>`, `reject <id>` (section 5). Entries are printed escaped, as approvals are. `accept <id>`, `reject <id>` (section 5). Entries are printed escaped, as approvals are.
@@ -375,8 +388,8 @@ max_class = "private" # records above this class are not indexed
- `memoryd` has no network in M5a and never writes `sessions/`, grants or the audit log. - `memoryd` has no network in M5a and never writes `sessions/`, grants or the audit log.
- What reaches the prompt: `soul.md`, the owner's file; and `memory.md` and `user.md`, which only - What reaches the prompt: `soul.md`, the owner's file; and `memory.md` and `user.md`, which only
sessions that have not read untrusted content write, through the check, frozen per session. sessions that have not read untrusted content write, through the check, frozen per session.
Everything a session that read untrusted content writes goes to the archive, labelled Everything a session that read untrusted content wants remembered becomes a note, labelled
untrusted, where it can be recalled but never lands in the prompt by itself. untrusted, which 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, - 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 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 journalled, listed by `bxctl agent's judgement of what the owner said; every such write is journalled, listed by `bxctl
@@ -402,7 +415,8 @@ Offline, in `make gate`:
- `memoryd`: entries parsing and rendering; each edit's rules, including a stale `old`, a - `memoryd`: entries parsing and rendering; each edit's rules, including a stale `old`, a
duplicate, the limit sentence, and `consolidate` missing an entry or naming one twice; every duplicate, the limit sentence, and `consolidate` missing an entry or naming one twice; every
rule of the check, on write and on building the block; routing by the session's state (an rule of the check, on write and on building the block; routing by the session's state (an
untrusted `add` to the archive, the other actions refused); the archive headers and journal untrusted `add` becoming a note, the other identity actions refused; `note` in both kinds of
session); the `notes.md` headers and labels, and the journal
lines; undo of the latest edit, and its refusal when the file changed; review on and off; the block; the quoted-terms query builder against FTS5 operators lines; undo of the latest edit, and its refusal when the file changed; review on and off; the block; the quoted-terms query builder against FTS5 operators
(`NEAR`, `*`, `^`, quotes, column filters); labels walked from a log; `max_class`; catching up (`NEAR`, `*`, `^`, quotes, column filters); labels walked from a log; `max_class`; catching up
from `progress`; a reindex equal to the incremental index; the recall log. from `progress`; a reindex equal to the incremental index; the recall log.
@@ -416,7 +430,7 @@ Offline, in `make gate`:
On the device (the owner's Mattermost, as for M4b): the baseline token count with the starter 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`, made with no question and listed by 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 `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, moves an entry, which `recall` then finds; a write after a fetched page that becomes an untrusted note,
untrusted; `bxctl memory undo` of the latest edit. untrusted; `bxctl memory undo` of the latest edit.
## 15. For the owner's review ## 15. For the owner's review
@@ -426,7 +440,7 @@ untrusted; `bxctl memory undo` of the latest edit.
summariser, labelled like the output they summarise. summariser, labelled like the output they summarise.
2. Decided: memory writes need no approval (P21); the policy of `recall` and `memory` is built in 2. Decided: memory writes need no approval (P21); the policy of `recall` and `memory` is built in
and no grant file can change it (section 8). A session that read untrusted content writes only and no grant file can change it (section 8). A session that read untrusted content writes only
to the archive (section 5). to `notes.md`, as notes (section 5). Identity and experience are separate (section 3).
3. Decided: entries are named by their full text, not by number (section 5). 3. Decided: entries are named by their full text, not by number (section 5).
4. Decided: the starter `soul.md` is generic (section 3). 4. Decided: the starter `soul.md` is generic (section 3).
5. Decided: M5a starts with the check as written, the short fixed phrase list and rules 1 to 3 5. Decided: M5a starts with the check as written, the short fixed phrase list and rules 1 to 3