A turn without complete speaker information is not sent; every peer in the fleet workspace is a member, so fleet notes are messages that one search finds, with a retraction message for an undone note (Honcho cannot delete a message); the fleet workspace derives nothing (reasoning, summaries and dreaming off); API keys per workspace are first-class, with a runbook entry for a refused key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
21 KiB
M5c design: Honcho
Status: draft for the owner's review, 2026-09-27; section 15's answers folded in. 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
secretsession reaches no workspace; - a result from an untrusted workspace taints the session that reads it;
recallstill 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):
- Write path (M5c-1): the
brokerdsession-state socket, the speaker on each turn, the ledger, the sync worker, mirrored memory writes, configuration,bxctl memory sync, runbook. - Read path (M5c-2):
recallover the fleet, theask_memorytool, 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 | untrusted notes, as messages from each agent's 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; - every peer in the fleet workspace is a member of the fleet, whether or not this agent's configuration names it (owner);
- 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 conclusionsmemorydwrites (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 speaker, or incomplete speaker information (a burst joined from several people) | nowhere (owner) |
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 message in the fleet workspace, from the agent's peer |
| 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 |
bxctl memory undo of an untrusted note already in the fleet |
a retraction message beside it, retracted: <the note>, from the agent's peer; the original stays, since Honcho cannot delete a message (owner) |
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
- A turn ends.
loopdalready sendsSessionUpdatedtomemoryd. It gains the turn's number in the session (M5c-1 wire change, section 11). - Decide.
memorydasksbrokerdfor the session's state (section 7) and reads the turn's records inloopd's log. The turn is untrusted if either says so (brokerd'suntrusted, or anyToolResultso far labelled untrusted); its class isbrokerd'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. - If
brokerdcannot answer, nothing is decided. The turn is decided at the nextSessionUpdatedof 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). - Memory writes are decided when journalled: the table of section 4, recorded in the same ledger.
- Send. One worker thread in
memorydsends 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, or, for an untrusted note, adds the message to the fleet workspace (or its retraction). Each send is recorded in the ledger with Honcho's ids. - 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). - 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 synclists 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().ronchootherwise reads~/.config/roncho/roncho.tomland takes anapi_keyfrom 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'sStatusandDecodeerrors carry up to 4 KiB of the response, which can be memory text:memorydlogs only the error's kind and status, never itsDisplay.- On creation,
memorydconfigures each workspace: the trusted and untrusted ones with reasoning, summaries and dreaming on (Honcho's defaults); the fleet one withreasoning,summaryanddreamallenabled = false, and every agent's peer in it unobserved, so no model reads the fleet's notes to derive anything (Honcho enqueues summaries before it checks reasoning, and schedules dreams by their own setting, so all three are set). - 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 | message search over the whole workspace | memory-tainted; the writing peer'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 mirrored memory writes in another agent's
trusted workspace are conclusions: so for those recall also queries conclusions
(POST /conclusions/query, which needs the observer and the observed; for a conclusion memoryd
wrote, both are the same peer). The fleet workspace holds messages, so one search finds the notes
of every peer in it, including agents this one was never told about (owner); a retracted note is
found beside its retraction.
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) orlow;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:
[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"
API keys (owner: the design must support them): each workspace this agent writes or reads has
its key, honcho_trusted, honcho_untrusted, honcho_fleet and honcho_read_<agent>, from
bxcore::secrets (M5b), and memoryd sends it as a bearer token on every call to that workspace
and never logs it. A workspace without a key configured is called without one; rift has auth off
today (2026-09-27), and turning it on needs only the keys. A refused key (401 or 403) stops the
sends to that workspace, is logged once with the workspace's name (runbook honcho-key-refused),
and is shown by bxctl memory sync; the others go on. 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, or not a valid peer name, is not synced (owner).
12. Runbook
New entries, given as fragments: honcho-unreachable, honcho-send-refused, honcho-key-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
brokerdor by the log → untrusted;secret→ none; no single speaker → none; a key in the text → none;brokerddown → 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: aroncho.tomlwith anapi_keyin the test'sHOMEis not read.- Keys: a configured key is sent as a bearer token to its workspace only, and appears in no
log line or error; without a key, no
Authorizationheader; a 401 stops that workspace's sends only, once logged. - The fleet: an untrusted note is a message from the agent's peer; its undo is a retraction
message; the fleet workspace is created with reasoning, summaries and dreaming off; a note
from a peer this agent's configuration does not name is found by
recall. recall: the fleet sources with their tags and classes; a source that times out is oneunavailableline 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
brokerdanswers 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
secretsession's in neither; "remember that …" appears as a conclusion at once, andbxctl memory undoremoves it; rift stopped (or unreachable) → the outbox waits and drains once it is back, sending nothing twice;bxctl memory syncshows each state. - M5c-2:
recallreturns a fleet note written by a second agent (a secondmemorydwith its own name), taggedmemory-tainted, and the session turns untrusted;ask_memoryanswers from the agent's own trusted workspace within its limit; with rift down,recallanswers from the local index withunavailablelines.
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, andrecall's sources.docs/milestones.md, M5c: its check "Honcho's contents are rebuilt from the logs" goes.
15. The owner's answers (2026-09-27)
- The speaker: a turn with no speaker, or incomplete speaker information, is not sent (sections 4 and 11).
- The fleet: every peer in the fleet workspace is a member of the fleet. Fleet notes are therefore messages, which one search finds whoever wrote them; an undone note gets a retraction message, since Honcho cannot delete a message (sections 3, 4, 8).
- For
roncho's agent, accepted (found 2026-09-27, none blocking): make the config file opt-in (it supplies anapi_keytoday; the README's "reads no environment variables" is wrong); evidence decoding requiresobserver_idandobserved_id, which rift's schema lacks; a message'sconfiguration(reasoning per message) is not exposed; the tagv1.0.0namesb4e7864onoriginand1f5ea42on crates.io. - API keys: the design supports them per workspace (section 10); rift has auth off today.