M5c spec: Honcho, draft for the owner's review
Designed with the owner on 2026-09-27: per agent a trusted and an untrusted workspace plus one fleet workspace for untrusted notes; private ceiling, secret sessions never synced; no rebuild (contra-facts, or deleting our own conclusion); the owner and other people observed, the agent not; every trusted memory write mirrored as a conclusion; a decision ledger that doubles as the outbox; recall over the local index and the fleet; ask_memory by workspace chat. Grounded in roncho 1.0.0 and Honcho 3.2.0 (source read at v3.2.0) and rift's live metrics. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
# M5c design: Honcho
|
||||
|
||||
Status: draft for the owner's review, 2026-09-27. Designed with the owner the same day (the
|
||||
decisions are marked "owner"). Supersedes P23 where they differ (section 14).
|
||||
|
||||
## 1. What M5c proves
|
||||
|
||||
The agent's experience outlives a session and is shared across the fleet, without anything a
|
||||
session read from outside reaching memory the fleet treats as trusted. Finished turns go to
|
||||
Honcho, whose own extraction replaces a flush job of ours; every trusted memory write is mirrored
|
||||
there at once; `recall` searches the fleet as well as the local index; a new `ask_memory` tool asks
|
||||
Honcho's dialectic.
|
||||
|
||||
The milestone is checked by:
|
||||
|
||||
- a turn from a session that read untrusted content never reaches a trusted workspace, and a turn
|
||||
from a `secret` session reaches no workspace;
|
||||
- a result from an untrusted workspace taints the session that reads it;
|
||||
- `recall` still answers from the local index when Honcho is down, and says what it could not
|
||||
reach;
|
||||
- a memory write the agent is asked to keep ("remember that …") is in Honcho at once, and undoing
|
||||
it removes it.
|
||||
|
||||
Honcho is not rebuilt from the logs (owner, 2026-09-27). It accumulates; what it holds wrongly is
|
||||
corrected by writing a contrary fact, which Honcho's dreaming reconciles, or, for a conclusion
|
||||
`memoryd` wrote itself, by deleting that conclusion.
|
||||
|
||||
## 2. Two halves
|
||||
|
||||
One spec, two plans, each run and device-checked on its own (owner):
|
||||
|
||||
1. **Write path (M5c-1)**: the `brokerd` session-state socket, the speaker on each turn, the
|
||||
ledger, the sync worker, mirrored memory writes, configuration, `bxctl memory sync`, runbook.
|
||||
2. **Read path (M5c-2)**: `recall` over the fleet, the `ask_memory` tool, trust labels on other
|
||||
agents' workspaces.
|
||||
|
||||
Projects in memory stay out of M5c (open row in `docs/decisions.md`).
|
||||
|
||||
## 3. Workspaces and peers
|
||||
|
||||
**Workspaces** (owner, 2026-09-27). Each agent writes to three:
|
||||
|
||||
| Workspace | Holds | Read by | Trust | Class |
|
||||
|---|---|---|---|---|
|
||||
| its **trusted** one | turns from clean sessions; conclusions mirroring its trusted memory writes | the agent (through `ask_memory`); other agents, as the owner labels it | trusted | `private` |
|
||||
| its **untrusted** one | turns from sessions that read untrusted content | the agent only (through `ask_memory`) | untrusted, fixed | `private` |
|
||||
| the **fleet** one, shared by every agent | conclusions from untrusted notes, each agent under its own peer | every agent | untrusted, fixed in code | `private` |
|
||||
|
||||
A workspace's class is also its ceiling for writes: nothing above `private` is written to Honcho
|
||||
(owner). A `public` turn is written as `private`, since `public` records where a result came
|
||||
from, not a permission. A session that reached `secret` by a turn's end is never synced, to any
|
||||
workspace (owner).
|
||||
|
||||
**Peers** (owner):
|
||||
|
||||
- the owner: one peer, the same name in every workspace of the fleet (`[honcho] owner`); observed;
|
||||
- other people who speak in a channel the agent answers: a peer each, named from their
|
||||
Mattermost username (a `.` becomes `_`: Honcho allows `[a-zA-Z0-9_-]`); observed;
|
||||
- the agent: a peer under its own name (`[honcho] agent`), **not observed** (`observe_me = false`).
|
||||
Its replies are written as messages and serve as context when Honcho reasons about the people
|
||||
it talks to, but Honcho draws no conclusions about the agent from them: they are the agent's
|
||||
own output and can restate what the session read. What the agent knows enters Honcho only as
|
||||
conclusions `memoryd` writes (section 5).
|
||||
|
||||
A Honcho session's peers, and who observes whom, are fixed when it is created (`roncho`'s blocking
|
||||
client cannot change them). A Boxmaker session maps to one Honcho session per workspace, named after
|
||||
it (in the fleet workspace too, where it holds only the agent's peer and the session's untrusted
|
||||
notes); when a speaker who is not yet in it speaks, `memoryd` starts the next Honcho session for it,
|
||||
the same name with `-2`, `-3` and so on, holding every peer so far.
|
||||
|
||||
## 4. What goes where
|
||||
|
||||
**Turns.** A finished turn is two messages: the speaker's message (the turn's text as `loopd`
|
||||
logged it) from the speaker's peer, and the agent's final reply from the agent's peer (P23).
|
||||
Tool calls, tool results and reasoning are not written. A text over 25,000 characters (Honcho's
|
||||
limit) is written as several messages of at most 25,000, in order.
|
||||
|
||||
| The turn's session at its end | Goes to |
|
||||
|---|---|
|
||||
| clean, class `public` or `private` | the trusted workspace |
|
||||
| untrusted (read untrusted content), class `public` or `private` | the untrusted workspace |
|
||||
| class `secret` | nowhere |
|
||||
| no single speaker (a burst joined from several people) | nowhere |
|
||||
| the text holds a known-prefix secret (`sanitize::refuse`'s secret rules) | nowhere |
|
||||
|
||||
**Memory writes.** Every write is mirrored as a conclusion the moment it is journalled (owner),
|
||||
so "remember this" reaches Honcho at once, through the memory tool and its checks, never by
|
||||
spotting the phrase:
|
||||
|
||||
| Journalled edit | In Honcho |
|
||||
|---|---|
|
||||
| identity `add` (memory.md or user.md) | a conclusion in the trusted workspace |
|
||||
| identity `replace` | the old entry's conclusion deleted, the new one created |
|
||||
| an entry dropped from identity (`remove`, or dropped by `consolidate`) | its conclusion deleted |
|
||||
| an entry moved out of identity by `consolidate` | nothing: its conclusion is already there |
|
||||
| a trusted note | a conclusion in the trusted workspace |
|
||||
| an untrusted note | a conclusion in the fleet workspace |
|
||||
| a flagged note (a sanitizer flag rule matched) | nothing: it stays local (owner) |
|
||||
| `bxctl memory undo` of an edit whose conclusions were written | those conclusions deleted; any it had deleted, written again |
|
||||
|
||||
A conclusion's observer and observed are the same peer, which is Honcho's own picture of that
|
||||
peer: a `user.md` entry is the owner's (it is about the owner); a `memory.md` entry or a note is
|
||||
the agent's. Each carries the Honcho session of the Boxmaker session in which the write was made
|
||||
(Honcho leaves session-less conclusions out of session-filtered queries, and its automatic
|
||||
dreaming skips a peer whose newest conclusion has none). `memoryd` never deletes a Honcho session:
|
||||
that would delete the conclusions stamped with it.
|
||||
|
||||
The session logs and `memory/` stay the source of truth for everything local; Honcho is not
|
||||
rebuilt from them.
|
||||
|
||||
## 5. The write path
|
||||
|
||||
1. **A turn ends.** `loopd` already sends `SessionUpdated` to `memoryd`. It gains the turn's
|
||||
number in the session (M5c-1 wire change, section 11).
|
||||
2. **Decide.** `memoryd` asks `brokerd` for the session's state (section 7) and reads the turn's
|
||||
records in `loopd`'s log. The turn is untrusted if either says so (`brokerd`'s `untrusted`, or
|
||||
any `ToolResult` so far labelled untrusted); its class is `brokerd`'s. It decides by the table
|
||||
of section 4 and appends the decision to the ledger **at once**, so a Honcho outage later
|
||||
cannot change where the turn goes.
|
||||
3. **If `brokerd` cannot answer**, nothing is decided. The turn is decided at the next
|
||||
`SessionUpdated` of any session, or by the worker's retry, from the session's state then; a
|
||||
late decision can only move a turn towards untrusted or unsynced (P23).
|
||||
4. **Memory writes** are decided when journalled: the table of section 4, recorded in the same
|
||||
ledger.
|
||||
5. **Send.** One worker thread in `memoryd` sends what is decided and not yet sent, oldest first:
|
||||
for a turn it creates the Honcho workspace, peers and session on first use (section 6), then
|
||||
adds the two messages as one batch; for a memory write it creates or deletes the conclusion.
|
||||
Each send is recorded in the ledger with Honcho's ids.
|
||||
6. **Honcho down** (a connection error, a timeout, a 5xx): the worker waits and tries again,
|
||||
doubling the wait from 5 s to 10 minutes. It logs the outage once, when it starts, and once
|
||||
when it ends, not on every try (runbook `honcho-unreachable`).
|
||||
7. **Honcho refuses** (a 4xx other than 409 on create, which means "exists"): the entry is marked
|
||||
failed in the ledger, logged with the status and without the text (runbook
|
||||
`honcho-send-refused`), and the worker moves on. `bxctl memory sync` lists failed entries.
|
||||
|
||||
**The ledger**, `memory/honcho.jsonl`, is append-only JSON lines, one per event: `decided` (the
|
||||
item, the workspace or `none` and why), `sent` (the item and Honcho's ids), `failed` (the item and
|
||||
the status). What is decided and not sent is the outbox; there is no separate queue. An item is
|
||||
never sent twice: the worker checks for its `sent` line first, and on start `memoryd` reads the
|
||||
ledger through. Items are a turn (`session`, `turn`) or an edit (its journal id).
|
||||
|
||||
Everything written to Honcho is neutralised (`sanitize::neutralise`); a reply is the model's own
|
||||
output and was logged as it came, so it is neutralised here, when it leaves.
|
||||
|
||||
**The guarantee.** Nothing reaches a trusted workspace unless `brokerd` and `loopd`'s labels both
|
||||
say the session was clean, and `brokerd` says its class was at most `private`, when the turn
|
||||
ended.
|
||||
|
||||
## 6. Honcho, through `roncho`
|
||||
|
||||
`memoryd` uses `roncho` 1.0.0 from crates.io with `default-features = false, features =
|
||||
["blocking"]` (`["tls"]` when the URL is HTTPS). It adds one crate to the lock file; `cargo deny`
|
||||
passes (checked 2026-09-27).
|
||||
|
||||
- **Every client is built with `.without_config_file()`.** `roncho` otherwise reads
|
||||
`~/.config/roncho/roncho.toml` and takes an `api_key` from it when none is set.
|
||||
- One client per workspace (a key is per workspace), shared by the worker and the read path.
|
||||
- Timeouts: connect 5 s; read ("no bytes") 30 s for writes and searches, 180 s for chat (rift's
|
||||
dialectic took up to 120 s, and a non-streamed chat sends nothing until it is done).
|
||||
- `roncho`'s `Status` and `Decode` errors carry up to 4 KiB of the response, which can be memory
|
||||
text: `memoryd` logs only the error's kind and status, never its `Display`.
|
||||
- On creation, `memoryd` configures each workspace: the trusted and untrusted ones with reasoning,
|
||||
summaries and dreaming on (Honcho's defaults); the fleet one holds conclusions only.
|
||||
- Honcho ids allow `[a-zA-Z0-9_-]`, at most 512 characters. Boxmaker session ids fit; names from
|
||||
configuration are checked at start.
|
||||
|
||||
## 7. `brokerd`'s session state
|
||||
|
||||
A new socket, `run/memory-broker/state.sock`, served by `brokerd`, connected to only by
|
||||
`memoryd` (directory mode 0700, owned by the user both run as, as the other sockets). One message,
|
||||
`SessionState { session }`, answered with `SessionStateReply { class, untrusted }`. It changes
|
||||
nothing. A session `brokerd` has not seen answers `private`, trusted: it has made no tool call.
|
||||
|
||||
## 8. `recall`
|
||||
|
||||
One tool, as today, searching (owner):
|
||||
|
||||
| Source | Searched with | Tag on each hit | Class |
|
||||
|---|---|---|---|
|
||||
| the local index | FTS5, as today | as today (`identity`, `memory`, `memory-tainted`) | as recorded |
|
||||
| each other agent's trusted workspace in `[[honcho.read]]` | message search, and a conclusion query for that agent's peer and the owner's | `memory` if the owner labelled it trusted, else `memory-tainted`; the agent's name | the workspace's `class` |
|
||||
| the fleet workspace | a conclusion query for this agent's peer and each agent's in `[[honcho.read]]` | `memory-tainted`; the writing agent's name | `private` |
|
||||
|
||||
The agent's own workspaces are not searched: the local index holds every turn and note of this
|
||||
agent with exact labels, and would return the same text twice. Each source runs on its own thread
|
||||
with its own limit (the local index as today; 5 s for each Honcho source); `recall` answers with
|
||||
what arrived, and a source that did not answer is one line, `<source>: unavailable`. Hits are
|
||||
ranked within their source and interleaved to `k`, the local index first. Text from Honcho is
|
||||
neutralised on arrival and size-capped like every recall snippet. Any `memory-tainted` hit marks
|
||||
the session untrusted, and every hit raises the session's class to its label, as local hits do.
|
||||
|
||||
Honcho's search endpoints find messages only, and the fleet workspace holds only conclusions, as
|
||||
do the mirrored memory writes in another agent's trusted workspace: so `recall` also queries
|
||||
conclusions (`POST /conclusions/query`, which needs the observer and the observed; for a
|
||||
conclusion `memoryd` wrote, both are the same peer). `roncho`'s blocking client cannot list a
|
||||
workspace's peers, so the agents whose fleet notes an agent reads are itself and those in
|
||||
`[[honcho.read]]`.
|
||||
|
||||
## 9. `ask_memory`
|
||||
|
||||
A new core tool, `ask_memory { question, workspaces?, reasoning? }`:
|
||||
|
||||
- `workspaces`: names from `[honcho]`: `own` (default), `untrusted`, `fleet`, or another agent's
|
||||
name from `[[honcho.read]]`;
|
||||
- `reasoning`: `minimal` (default: one search, then the answer) or `low`;
|
||||
- `question`: at most 2,000 characters.
|
||||
|
||||
One workspace chat (`POST /workspaces/{w}/chat`) per named workspace, in parallel, at most two at a
|
||||
time; workspace chat searches every conclusion and message of the workspace without the model
|
||||
choosing a peer (it averaged 13 s on rift, over only three calls; peer chat, 32 s over 318). Each
|
||||
has 150 s. The answer lists each workspace's reply, neutralised and capped, tagged like a `recall`
|
||||
hit (`memory` for the agent's own trusted workspace and for others the owner labelled trusted,
|
||||
`memory-tainted` otherwise), or `<workspace>: unavailable`. Labels taint the session as `recall`'s
|
||||
do. `brokerd`'s wait for this tool is 170 s and `loopd`'s wait on `brokerd` for it 180 s (section
|
||||
11).
|
||||
|
||||
Adding a tool changes the `tools` array: a new epoch for every session when M5c-2 is deployed,
|
||||
and the baseline is measured again against the 3,000-token budget.
|
||||
|
||||
## 10. Configuration and secrets
|
||||
|
||||
In `memoryd.toml`; without a `[honcho]` section, `memoryd` behaves exactly as after M5b:
|
||||
|
||||
```toml
|
||||
[honcho]
|
||||
url = "http://rift.scylla-hammerhead.ts.net:8000"
|
||||
agent = "boxmaker0"
|
||||
owner = "kyle"
|
||||
trusted = "boxmaker0-trusted"
|
||||
untrusted = "boxmaker0-untrusted"
|
||||
fleet = "fleet"
|
||||
|
||||
[secrets.honcho_trusted] # optional, one per workspace: honcho_trusted, honcho_untrusted,
|
||||
file = "…" # honcho_fleet, honcho_read_<agent>; sent only when configured
|
||||
|
||||
[[honcho.read]]
|
||||
agent = "boxmaker1"
|
||||
workspace = "boxmaker1-trusted"
|
||||
trust = "untrusted" # the default; "trusted" must be written out
|
||||
class = "private"
|
||||
```
|
||||
|
||||
Keys come from `bxcore::secrets` (M5b). An invalid `[honcho]` section stops `memoryd` at start
|
||||
with the reason (runbook `honcho-config-invalid`). `docs/egress.md` gains rift: `memoryd` →
|
||||
the configured URL, for writes, searches and chat.
|
||||
|
||||
## 11. Links between the roles
|
||||
|
||||
M5c's changes to the table of the M5b spec, section 10:
|
||||
|
||||
| Link | Serves | Connects | Path or address | Carries | When the server is down |
|
||||
|---|---|---|---|---|---|
|
||||
| Session state (new) | `brokerd` | `memoryd` | `run/memory-broker/state.sock` | `SessionState` → `SessionStateReply` | turns wait undecided (`broker-state-unavailable`) |
|
||||
| Honcho (new) | Honcho | `memoryd` | `[honcho] url` | `roncho` calls | sends wait (`honcho-unreachable`); `recall` answers from the local index |
|
||||
| Memory for turns | `memoryd` | `loopd` | as before | `SessionUpdated` gains `turn` | as before |
|
||||
| Turns | `loopd` | `gatewayd`, `bxctl chat` | as before | `Turn` gains `speaker` | as before |
|
||||
|
||||
Wire changes (fixtures change): `Turn` gains `speaker: Option<String>`, which `gatewayd` sets to
|
||||
the Mattermost username when every post of the burst is from one person, and `bxctl chat` sets
|
||||
to the owner's name from its configuration; `loopd` logs it on the `User` record (a new optional
|
||||
field: older logs read as `None`); `SessionUpdated` gains `turn: u64`. A turn whose speaker is
|
||||
`None` is not synced.
|
||||
|
||||
## 12. Runbook
|
||||
|
||||
New entries, given as fragments: `honcho-unreachable`, `honcho-send-refused`,
|
||||
`broker-state-unavailable`, `honcho-config-invalid`. Each names what `bxctl memory sync` shows.
|
||||
|
||||
## 13. Testing
|
||||
|
||||
Given tests, first, against a fake Honcho server (a small HTTP server in `memoryd`'s test support
|
||||
that records requests and answers from fixtures taken from rift's OpenAPI document, like
|
||||
`loopd`'s fake llama server):
|
||||
|
||||
- **Decisions**: clean → trusted; untrusted by `brokerd` or by the log → untrusted; `secret` →
|
||||
none; no single speaker → none; a key in the text → none; `brokerd` down → undecided, then
|
||||
decided from the later state, never towards trusted.
|
||||
- **The ledger**: a restart sends nothing twice; an outage sends everything once it ends, in
|
||||
order; a refused entry is marked failed and the rest go on.
|
||||
- **Memory writes**: each row of section 4's table, including undo.
|
||||
- **Peers and sessions**: the agent's peer not observed; a new speaker starts the next Honcho
|
||||
session; names fit Honcho's pattern.
|
||||
- **`roncho`**: a `roncho.toml` with an `api_key` in the test's `HOME` is not read.
|
||||
- **`recall`**: the fleet sources with their tags and classes; a source that times out is one
|
||||
`unavailable` line and the rest still answer; the agent's own workspaces are not searched.
|
||||
- **`ask_memory`**: two at a time; the per-workspace limit; labels; unknown workspace names.
|
||||
- **No `[honcho]` section**: M5b's behaviour, every M5b test unchanged.
|
||||
- **Attack tests (T37)**: a turn from a session that fetched a page never reaches the trusted
|
||||
workspace, even when `brokerd` answers late; a fleet hit taints the reading session.
|
||||
|
||||
**On the device** (straylight, rift, the owner's Mattermost), for each half:
|
||||
|
||||
- M5c-1: a clean turn appears in the trusted workspace, an untrusted one in the untrusted
|
||||
workspace, a `secret` session's in neither; "remember that …" appears as a conclusion at once,
|
||||
and `bxctl memory undo` removes it; rift stopped (or unreachable) → the outbox waits and drains
|
||||
once it is back, sending nothing twice; `bxctl memory sync` shows each state.
|
||||
- M5c-2: `recall` returns a fleet note written by a second agent (a second `memoryd` with its own
|
||||
name), tagged `memory-tainted`, and the session turns untrusted; `ask_memory` answers from the
|
||||
agent's own trusted workspace within its limit; with rift down, `recall` answers from the local
|
||||
index with `unavailable` lines.
|
||||
|
||||
## 14. Changes to the brief and decisions
|
||||
|
||||
With this spec, as their own commits:
|
||||
|
||||
- `docs/design.md`, Memory items 4 and 5: two workspaces per agent and the fleet workspace; no
|
||||
rebuild ("corrected by writing a contrary fact"); mirrored memory writes; the peers.
|
||||
- `docs/decisions.md`: P23 applied as amended here (the fleet workspace for untrusted notes; no
|
||||
rebuild); rows for the class ceiling, the peers, mirroring, the ledger, and `recall`'s sources.
|
||||
- `docs/milestones.md`, M5c: its check "Honcho's contents are rebuilt from the logs" goes.
|
||||
|
||||
## 15. For the owner's review
|
||||
|
||||
1. **The speaker on each turn** (section 11) is new: Honcho needs a peer per message, and today's
|
||||
turn text carries no name. A burst joined from several people is not synced at all, rather
|
||||
than guessed at.
|
||||
2. **Which fleet notes an agent reads** (section 8): its own and those of the agents in
|
||||
`[[honcho.read]]`, since the client cannot list the fleet's peers. An agent the owner has not
|
||||
listed contributes to the fleet but is not read.
|
||||
3. **For `roncho`'s agent** (found 2026-09-27, none blocking): make the config file opt-in (it
|
||||
supplies an `api_key` today; the README's "reads no environment variables" is wrong); evidence
|
||||
decoding requires `observer_id` and `observed_id`, which rift's schema lacks; a message's
|
||||
`configuration` (reasoning per message) is not exposed; the tag `v1.0.0` names `b4e7864` on
|
||||
`origin` and `1f5ea42` on crates.io.
|
||||
4. **Rift**: auth is still off; the keys in section 10 are ready for it.
|
||||
Reference in New Issue
Block a user