diff --git a/docs/specs/2026-09-24-m5a-local-memory.md b/docs/specs/2026-09-24-m5a-local-memory.md index 34115b6..6f12e7c 100644 --- a/docs/specs/2026-09-24-m5a-local-memory.md +++ b/docs/specs/2026-09-24-m5a-local-memory.md @@ -30,7 +30,9 @@ Facts this rests on, checked on 2026-09-24: the bytes it started with. No write asks the owner anything. 4. What a session that has seen untrusted content wants to remember becomes a note, labelled 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; + so does the baseline with every memory file at its limit. (Measured while building the + reference, 2026-09-24: 881 and 2,432 tokens with Ornith's tokenizer.) 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. @@ -78,7 +80,7 @@ each item with its own label. Not everything remembered is believed, but it is r **`notes.md`.** Append-only. Each entry starts with one header line saying how it got there: ```text -§ noted 2026-09-24T21:02Z in session mm-dh45qg… · untrusted +§ noted 2026-09-24T21:02Z in session mm-dh45qg… · private · untrusted § moved 2026-09-24T20:14Z from memory.md in session mm-y73sxj3popgwumyccptb56p1ah @@ -345,24 +347,36 @@ connection, the existing frames (`proto::frame`, `MAX_FRAME`). New message kinds is `recall` or `memory` is a grant problem at load (`grants-invalid`), with the fixed sentence `the policy of recall and memory is built in; remove this grant`. No grant file can turn memory off or put questions in front of it. What protects the prompt is where untrusted sessions' - writes go (section 5), which `memoryd` decides from the state `brokerd` sends. + writes go (section 5), which `memoryd` decides from the state `brokerd` sends. The built-ins + are kept apart from the owner's grants (`GrantSet::grants` lists only the owner's; matching sees + both), and grant ids starting `builtin-` are reserved. An invalid `grants/` still denies every + call, the memory tools included: that is the fail-closed state `grants-invalid` describes. - **Where a decided call runs.** A composite runtime sends `Recall` and `Memory` specs to `memoryd`'s `brokerd` socket as a `memory_call`, with the session's taint and untrusted flag as `brokerd` holds them when it decides, and everything else to the configured runtime - (Podman, or the refusing one). `[memory] socket` in `brokerd.toml` names the socket; without it, - memory calls fail with a fixed sentence and a runbook pointer (`memoryd-unreachable`). -- **Labels only get stricter.** `RunOutput` gains an optional label. `runner::run` answers with - the stricter of the grant's label and the runtime's: the higher class, and untrusted if either - is. Container runtimes return none, so nothing changes for them. The audit record and the - session's taint use the combined label, as today. + (Podman, or the refusing one). The socket is `{home}/run/broker-memory/memory.sock`, or + `[memory] socket` in `brokerd.toml`; the section is optional, since the memory tools are always + on. When `memoryd` does not answer in 30 seconds, the call fails with a fixed sentence and a + runbook pointer (`memoryd-unreachable`). The session's state reaches the runtime through the + `Decision` (which `decide` already receives it for) and the `RunSpec`. The Podman runtime + refuses a memory tool with a fixed sentence rather than start a container for it. +- **Labels only get stricter.** `Runtime` gains `run_labelled`, returning an optional label with + the output; its default calls `run` and returns none, so container runtimes and the given test + runtimes are unchanged. `runner::run` answers with the stricter of the grant's label and the + runtime's: the higher class, and untrusted if either is. The audit record and the session's + taint use the combined label, as today. - **Size.** `policy.rs` (498 lines) and `ledger.rs` (487) are at the limit; the plan splits them before anything is added. ## 9. `loopd` - `[memory] socket` in `loopd.toml` (default `{home}/run/loop-memory/memory.sock`). -- Baseline: `system.md`, a blank line, then the block from `memoryd`. `memoryd` unreachable, or - refusing, fails the new session with the runbook pointer `memory-unavailable`. +- Baseline: `system.md`, a blank line, then the block from `memoryd` + (`Baseline::assemble_with`; `Baseline::assemble` stays, with no memory, for tests and the + self-test). `memoryd` unreachable, or refusing, fails the new session with the runbook pointer + `memory-unavailable`. `Context` takes the memory source explicitly (a `MemorySource` trait: + `memoryd`'s client in `loopd serve`, `NoMemory` in tests), so `loopd serve` cannot leave it out. + `make verify-device` starts `memoryd` beside `loopd serve`. - Registry: `recall` and `memory` are core, with the schemas of section 5. The model sees `clock`, `recall`, `memory`, `find_tool`, `call_tool`. - After a finished turn: `session_updated`, best effort. A failure is one log line; the next @@ -373,11 +387,13 @@ connection, the existing frames (`proto::frame`, `MAX_FRAME`). New message kinds ## 10. `bxctl` -- `bxctl reindex [--admin-socket PATH]`: sends `reindex`, waits, and prints `reindexed: 42 logs, +- `bxctl reindex [--memory-socket PATH]`: sends `reindex`, waits, and prints `reindexed: 42 logs, 1,208 records, 7 notes`. `memoryd` rebuilds into a new file and renames it over the old one, so `recall` keeps working meanwhile. - `bxctl memory log [N]`, `bxctl memory undo `, and with review on, `bxctl memory pending`, - `accept `, `reject ` (section 5). Entries are printed escaped, as approvals are. + `accept `, `reject ` (section 5), each with `[--memory-socket PATH]` (default + `{home}/run/owner-memory/admin.sock`; `--admin-socket` stays `brokerd`'s). Entries are printed + escaped, as approvals are. ## 11. Configuration