diff --git a/CLAUDE.md b/CLAUDE.md index 76cbe65..2836b23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,7 +73,7 @@ Separate binaries in one Cargo workspace. Each role holds as little authority as - `loopd` owns sessions and prompt assembly. It has no credentials and no network. Its only I/O is Unix sockets to `gatewayd`, `brokerd`, `inferproxy` and (from M5a) `memoryd`. -- `memoryd` (M5) owns memory: `memory/soul.md`, `memory.md`, `user.md`, `archive.md`, the FTS5 +- `memoryd` (M5) owns memory: `memory/soul.md`, `memory.md`, `user.md` (identity), `notes.md` (experience), the FTS5 index, and from M5b the sync to the fleet's Honcho. It runs memory tool calls `brokerd` has decided, and reaches Honcho and nothing else. - `brokerd` is the only place authority lives. It reads owner-written grants (it cannot write diff --git a/docs/decisions.md b/docs/decisions.md index 8a92a0b..30ec1ec 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -103,9 +103,8 @@ Later changes to the brief: | 2026-09-24 | P18, Roles and Network isolation: a sixth role, `memoryd`, owns `memory/` and `index/`, syncs turns to Honcho and runs memory tool calls; `loopd` has four sockets and no longer holds the memory files. | Owner's approval, 2026-09-24; see the row of that date under Decided. | | 2026-09-24 | P19, Memory 2 to 8: `recall` over a local FTS5 index (session logs, archive) and Honcho, `ask_memory`; Honcho writes only from clean sessions; per-workspace trust set by the owner; notes, the flush job and the embedding index go; memory text stays on the owner's hosts. | Owner's approval, 2026-09-24; see the row of that date under Decided. | | 2026-09-24 | P20, Milestones: M5 becomes M5a (local memory in `memoryd`), M5b (Honcho) and M5c (the scheduler and the tiering job); the embedding-index M5b goes. | Owner's approval, 2026-09-24; see the row of that date under Decided. | +| 2026-09-24 | P21, Memory 1, 2, 6, 7, State and Milestones: memory is identity (`soul.md`, `memory.md`, `user.md`, in the prompt, edited only by clean sessions) and experience (`notes.md` and session logs, recalled with a source tag: `identity`, `memory`, `memory-tainted`); writes need no approval; `note` in any session; an untrusted session's identity `add` becomes a note; a check on every write, a journal and undo; the policy of the memory tools is built in. | Owner's approval, 2026-09-24; see the rows of that date under Decided. | ## Proposed changes to the design brief -| # | Change | Reason | -|---|---|---| -| 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. | +None pending. diff --git a/docs/design.md b/docs/design.md index cda3730..c8d1dab 100644 --- a/docs/design.md +++ b/docs/design.md @@ -176,39 +176,49 @@ a multi-turn tool conversation and tool parsing had no failures in 20 trials. Memory is what carries continuity across epoch compaction, so it is required. It must not break the inference contract, and it must not become a way for injected text to persist. -1. **Memory files.** Three files, read once per epoch into the baseline, in this order after the - system prompt: `memory/soul.md`, the persona, written only by the owner (at most 4,000 - characters); `memory/memory.md`, the agent's core notes on its work (at most 2,200 characters); - and `memory/user.md`, its core notes on the owner (at most 1,375 characters). Each file is shown - with how full it is. The agent edits the last two with a `memory` tool: `add`, `replace` and - `remove` an entry, and `consolidate`, one edit that marks every entry of a file kept, - rewritten, moved or dropped. Moved entries are appended to `memory/archive.md` (append-only, - each entry with the date and session it was first approved in), where `recall` finds them; - nothing leaves `memory.md` silently. Every edit is `ask`, and the approval shows `consolidate` - as a diff. An approved edit takes effect at the next epoch, never mid-session. A write over a - file's limit is refused with a pointer to `consolidate`. +1. **Identity and experience.** Memory is two things. **Identity** is who the agent is and what + it holds true about its work and its owner: three files read once per epoch into the baseline, + in this order after the system prompt: `memory/soul.md`, the persona, written only by the owner + (at most 4,000 characters); `memory/memory.md`, the agent's core notes on its work (at most + 2,200 characters); and `memory/user.md`, its core notes on the owner (at most 1,375 + characters), each shown with how full it is. **Experience** is what it has seen or been told: + `memory/notes.md` (append-only) and the session logs, never in the prompt, found with + `recall`, each item labelled with its source and trust. Not everything remembered is believed. + The agent writes memory with a `memory` tool, without asking: `note` records something seen, + in any session; `add`, `replace`, `remove` and `consolidate` (one edit marking every entry of a + file kept, rewritten, moved or dropped) edit identity, and only a session that has not read + untrusted content may use them. In a session that has, an identity `add` becomes a `note` and + the other edits are refused. What leaves an identity file goes to `notes.md` as moved or + dropped; nothing is deleted. A write takes effect at the next epoch, never mid-session. A + write over a file's limit is refused with a pointer to `consolidate`. 2. **Everything else is retrieved, not injected.** A `recall(query, k)` tool returns size-capped - snippets with their source (a file and line, or a Honcho workspace). An `ask_memory` tool asks - Honcho's dialectic. Results are appended like any tool result, so the cache prefix is untouched. -3. **Local index.** SQLite FTS5 over the session logs and `memory/archive.md`, kept by `memoryd` + snippets, each with a source tag (`identity` for entries moved out of identity, `memory` for + trusted experience, `memory-tainted` for experience from sessions or workspaces that are not + trusted), where it came from, and its class. An `ask_memory` tool asks Honcho's dialectic. + Results are appended like any tool result, so the cache prefix is untouched. +3. **Local index.** SQLite FTS5 over the session logs and `memory/notes.md`, kept by `memoryd` and rebuildable with `bxctl reindex`. It needs no network and no model, so it still answers - when Honcho is down. `memoryd` counts `recall` hits per archive entry and when each was last - found. Retrieval sits behind a `Retriever` trait. + when Honcho is down. `memoryd` counts `recall` hits per note and when each was last found. Retrieval sits behind a `Retriever` trait. 4. **Honcho.** The fleet's memory service; it and the models it calls run on the owner's hosts. Each Boxmaker writes finished turns to its own workspace, and only turns from sessions that saw no untrusted content and are within a configured data class; `memoryd` enforces this itself. Entries moved by `consolidate` are written there too. Honcho's own extraction, summaries and consolidation replace a flush job of ours. What Honcho holds is derived from the session logs and - `memory/archive.md`, and can be rebuilt from them. + `memory/notes.md`, and can be rebuilt from them. 5. **Workspaces and trust.** `memoryd` can read other workspaces. The owner marks each one trusted or untrusted; untrusted is the default. Results from an untrusted workspace are marked untrusted and taint the session. Honcho's conclusions carry no per-entry provenance, so the filter is applied when writing and the label is per workspace. 6. **Provenance on recall.** Recalled text is presented as data with its source and data class, - never placed in the system prompt, and taints the session with that class. Entries from - `memory/archive.md` come back as approved memory. -7. **The tools go through `brokerd`** under ordinary grants and run in `memoryd`: `recall` and - `ask_memory` `auto`, every `memory` edit `ask`. + never placed in the system prompt, and taints the session with that class; a `memory-tainted` hit + marks the session untrusted. +7. **The tools go through `brokerd`** and run in `memoryd`. Their policy is built in, and no grant + file can change it: `recall`, `ask_memory` and `memory` are all `auto`. What protects the + prompt is which sessions may edit identity (item 1), plus a check on every write (no control + or invisible characters, no chat-template markup, a short list of instruction phrases), run + again when the prompt is built. Every write is journalled in `memory/edits/`, listed by + `bxctl memory log`, and the latest edit of a file can be undone. An owner setting stages + identity writes for review instead (off by default). 8. **No memory text leaves the owner's hosts.** Honcho and the models it calls run on the owner's hosts, and each is listed in `docs/egress.md`. @@ -217,7 +227,7 @@ the inference contract, and it must not become a way for injected text to persis Files are the source of truth. SQLite is allowed only for rebuildable indexes and queues. - `sessions//.jsonl` — append-only session log -- `memory/soul.md`, `memory/memory.md`, `memory/user.md`, `memory/archive.md` — owner-readable memory +- `memory/soul.md`, `memory/memory.md`, `memory/user.md`, `memory/notes.md`, `memory/edits/` — owner-readable memory - `index/` — FTS5, kept by `memoryd`; disposable and rebuildable from `memory/` and `sessions/` - `grants/*.toml` — owner-written - `audit/*.jsonl` — hash-chained diff --git a/docs/milestones.md b/docs/milestones.md index 3e645cb..001a045 100644 --- a/docs/milestones.md +++ b/docs/milestones.md @@ -13,9 +13,9 @@ One session per milestone. The design brief is `docs/design.md`. | M3b | `brokerd` runner | Rootless container runner, four tools (read file, write file, sandboxed shell, HTTP fetch with host allowlist), egress proxy | A container with no network grant cannot reach the network; fetch refused for hosts outside the grant | | M4a | `gatewayd` + Mattermost conversations | Bot connection over TLS, user-ID allowlist, threads as sessions, bursts as one turn, typing while working, catching up after downtime, the secret store, no listening port | A direct message answered on the real server; a non-allowlisted user gets no response | | M4b | Approvals over Mattermost | `gateway.sock`, approval posts, answers by `!approve`/`!deny` in the thread, approvers list, long approvals in parts | An `ask` call approved and refused from Mattermost; the audit log names the post | -| M5a | Local memory | `memoryd` without a network: the three memory files served to `loopd` once per epoch, the `memory` tool (add, replace, remove, `consolidate`) at `ask`, `memory/archive.md`, FTS5 `recall` over session logs and the archive, `recall` hit counts, `bxctl reindex` | A fact from one session is recalled in another; an entry moved by `consolidate` is found by `recall`; a memory edit changes the prompt only in the next session (next epoch once M5c adds epochs); the baseline with the owner's files stays under 3,000 tokens | -| M5b | Honcho and hardening | A sanitizer module for memory writes, from the prompt-injection research of 2026-09-24; `memoryd` syncs finished turns to its own Honcho workspace through `roncho`, `recall` and `ask_memory` over Honcho, other workspaces read with the owner's trust labels, moved entries written as conclusions | A turn from a tainted session never reaches Honcho; a result from an untrusted workspace taints the session; Honcho's contents are rebuilt from the logs | -| M5c | Scheduler and compaction | Heartbeat and cron on their own slot in isolated sessions; compaction at idle, starting a new epoch from the baseline and a summary; summaries of tool output for the tools that opt in (a web fetch, say), indexed with the output's label; the tiering job, which proposes what stays at which tier | A scheduled run leaves the main slot's cache intact; a fact from epoch 1 is recalled in epoch 2, and a memory edit reaches the next epoch; the tiering job's proposals reach the owner as ordinary approvals and change nothing on their own | +| M5a | Local memory | `memoryd` without a network: the three memory files served to `loopd` once per epoch, the `memory` tool (`note`, and add, replace, remove, `consolidate` for identity) without questions and its check, `memory/notes.md`, the journal and `bxctl memory log`/`undo`, FTS5 `recall` over session logs and notes with source tags, `recall` hit counts, `bxctl reindex` | A fact from one session is recalled in another; an entry moved by `consolidate` is found by `recall`; a write from a session that read untrusted content becomes an untrusted note, never identity; a memory edit changes the prompt only in the next session (next epoch once M5c adds epochs); the baseline with the owner's files stays under 3,000 tokens | +| M5b | Honcho and hardening | A sanitizer module for memory writes, from the prompt-injection research of 2026-09-24; `memoryd` syncs finished turns to its own Honcho workspace through `roncho`, `recall` and `ask_memory` over Honcho, other workspaces read with the owner's trust labels, moved entries and trusted notes written as conclusions | A turn from a tainted session never reaches Honcho; a result from an untrusted workspace taints the session; Honcho's contents are rebuilt from the logs | +| M5c | Scheduler and compaction | Heartbeat and cron on their own slot in isolated sessions; compaction at idle, starting a new epoch from the baseline and a summary; summaries of tool output for the tools that opt in (a web fetch, say), indexed with the output's label; the tiering job, which decides what stays at which tier | A scheduled run leaves the main slot's cache intact; a fact from epoch 1 is recalled in epoch 2, and a memory edit reaches the next epoch; the tiering job promotes only trusted notes into identity, and its writes are journalled like any other | | M6 | Subagents + cloud consult | Subagents on their slot returning summaries; consult tool gated by data-class grants with full payload log | A tainted session is refused; payload log matches what was sent | | M7 | Split deployment | Each role in its own rootless container; `loopd` with no network; socket volume permissions; egress proxy for tools; host ACL policy in `deploy/` | From inside the `loopd` container, requests to the internet, the tailnet and the host all fail; a tool container without a host grant cannot reach the tailnet |