Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
20 KiB
M5c-1 task 10: memoryd's Honcho clients, and making sure a workspace, its peers and a session exist
Branch: m5c1 (run git switch m5c1; git status --short must be empty, otherwise stop)
Commit subject: memoryd: a Honcho client per workspace, and its workspace, peers and sessions made sure of
Log row: the Task cell is M5c1/10-memoryd-honcho-client.
Goal
Tasks 11 and 13 send turns, conclusions and fleet messages to Honcho. This task gives them what they send through (M5c spec, sections 3 and 6):
- A client per workspace. One
ronchoblocking client for each of this agent's three workspaces ([honcho] trusted,untrusted,fleet), each with its own key when[secrets.honcho_trusted](and so on) is configured. Every client is built with.without_config_file(): without itronchoreads~/.config/roncho/roncho.tomland takes anapi_keyfrom it. Timeouts: connect 5 s, read ("no byte for") 30 s; chat, which M5c-2 uses, 180 s (the constant only, here). - Making sure things exist before anything is written: the workspace, the agent's peer, the
speakers' peers, then the session. Each is Honcho's get-or-create (
POST: 201 when made, 200 when it was there; both are success, and so is a 409). A session must be made before a message is added to it: Honcho makes a missing one on the fly, with no peers configured (recorded:20-messages-add-missing-session.json).- The fleet workspace is made with
reasoning,summaryanddreamallenabled: false; the other two with Honcho's defaults (no configuration sent). - The agent's peer is made with
observe_me: false; a speaker's peer with nothing. - A session holds the agent's peer (
observe_me: false, observe_others: false) and each speaker's (observe_me: true, observe_others: false). - What was made sure of is remembered in memory for the run. After a restart everything is made sure of again, which costs one request each and changes nothing.
- The fleet workspace is made with
- Which Honcho session a turn goes to (spec 3): the Boxmaker session's id; a speaker new to it
starts the next,
<id>-2,<id>-3, holding every speaker so far (a Honcho session's peers are fixed when it is made). The numbering comes from the ledger alone: the speakers of that session's decidedTurnactions, in any workspace, in the order of the decisions, each counted once, up to and including the turn. A restart numbers the same. - Errors by kind and status, never a body. A 422 echoes the whole request, memory text
included, and
roncho'sStatusandDecodeerrors carry up to 4 KiB of the response. So aHonchoErrorkeeps only a kind (a fixed word) and a status. - Deleting a conclusion (for task 13, which uses it): 204 is done, and 404 (deleted before) is done too.
Nothing calls this yet: task 11 builds the clients at start and sends turns. No request goes to
a real Honcho in any test: a fake Honcho on 127.0.0.1 answers from the responses the design
model recorded from rift in task 00, crates/memoryd/tests/fixtures/honcho/, which this task
copies in.
Interfaces
In crates/memoryd/src/honcho/client.rs (the module memoryd::honcho::client):
pub const CONNECT: Duration = Duration::from_secs(5);
pub const READ: Duration = Duration::from_secs(30);
pub const CHAT_READ: Duration = Duration::from_secs(180);
pub struct Timeouts { pub connect: Duration, pub read: Duration } // Timeouts::WRITES = (CONNECT, READ)
pub enum HonchoError {
Unreachable { kind: &'static str, status: Option<u16> },
Refused { kind: &'static str, status: Option<u16> },
KeyRefused { status: u16 },
}
impl HonchoError { pub fn kind(&self) -> &'static str; pub fn status(&self) -> Option<u16>; }
pub fn classify(error: &roncho::blocking::Error) -> HonchoError;
pub fn fleet_configuration() -> serde_json::Map<String, Value>;
pub fn session_peers(agent: &str, speakers: &[String]) -> HashMap<String, SessionPeerConfig>;
pub struct Clients { /* private */ }
impl Clients {
pub fn new(config: &HonchoConfig, env: &dyn Fn(&str) -> Option<OsString>, timeouts: Timeouts)
-> Result<(Clients, Vec<String>), String>;
pub fn client(&self, workspace: Workspace) -> &roncho::blocking::Client;
pub fn ensure_workspace(&mut self, workspace: Workspace) -> Result<(), HonchoError>;
pub fn ensure_peer(&mut self, workspace: Workspace, peer: &str) -> Result<(), HonchoError>;
pub fn ensure_session(&mut self, workspace: Workspace, session: &HonchoSession)
-> Result<(), HonchoError>;
pub fn delete_conclusion(&self, workspace: Workspace, id: &str) -> Result<(), HonchoError>;
}
In crates/memoryd/src/honcho/naming.rs (the module memoryd::honcho::naming):
pub struct HonchoSession { pub name: String, pub speakers: Vec<String> }
pub fn session_name(session: &SessionId, count: usize) -> String;
pub fn honcho_session<'a, I>(decisions: I, session: &SessionId, turn: u64) -> Option<HonchoSession>
where I: IntoIterator<Item = (&'a Item, &'a Action)>;
In crates/memoryd/src/honcho/ledger.rs, one new method (edit L1):
pub fn decisions(&self) -> impl Iterator<Item = (&Item, &Action)> + '_, every decided item and
its action (Skip included) in the order of the Decided records.
Clients::new returns the key files' plaintext warnings for the caller to log. Its error is one
line naming the key (bxcore::secrets' own text, with its pointer) or the workspace; never a
key's value.
The errors
classify is given whole in the skeleton. It maps roncho's error so:
roncho::blocking::Error |
HonchoError |
|---|---|
Connect, Timeout, Closed, Io |
Unreachable, kind connect, timeout, closed, io; no status |
Status with a status of 500 or more |
Unreachable, kind server, the status |
Status below 500 |
Refused, kind status, the status |
Unauthorized (401 or 403) |
KeyRefused, the status |
Redirect |
Refused, kind redirect, the status |
ActiveSessions |
Refused, kind active-sessions, 409 |
TooLarge, Decode, Stream, Config, any other |
Refused, kind too-large, decode, stream, config, other; no status |
Display (given): Honcho is unreachable (<kind>[, HTTP <status>]), Honcho refused the request (<kind>[, HTTP <status>]), Honcho refused the key (key-refused, HTTP <status>). For a create,
a Refused with status 409 is success (created); for a delete, one with 404 is.
The requests
Exactly what the given tests expect (Value bodies, compared as JSON):
| Step | Request | Body |
|---|---|---|
| a workspace | POST /v3/workspaces |
{"id":"<name>"}; the fleet: {"id":"<name>","configuration":{"reasoning":{"enabled":false},"summary":{"enabled":false},"dream":{"enabled":false}}} |
| the agent's peer | POST /v3/workspaces/<name>/peers |
{"id":"<agent>","configuration":{"observe_me":false}} |
| a speaker's peer | the same | {"id":"<speaker>"} |
| a session | POST /v3/workspaces/<name>/sessions |
{"id":"<session>","peers":{"<agent>":{"observe_me":false,"observe_others":false},"<speaker>":{"observe_me":true,"observe_others":false},…}} |
| delete a conclusion | DELETE /v3/workspaces/<name>/conclusions/<id> |
none |
ensure_session (given whole) makes sure of the workspace, the agent's peer, each speaker's peer,
and then the session, each only if not yet made sure of in this run. A failed step is not
remembered, so the next call tries it again.
Every place that changes
This is the complete list.
| Place | What happens there |
|---|---|
crates/memoryd/src/honcho/mod.rs |
pub mod client; and pub mod naming; (edit H1) |
crates/memoryd/src/honcho/ledger.rs |
decisions (edit L1) |
crates/memoryd/src/honcho/client.rs |
new: a skeleton you copy, then fill 11 bodies |
crates/memoryd/src/honcho/naming.rs |
new: a skeleton you copy, then fill 2 bodies |
crates/memoryd/tests/fixtures/honcho/ |
new: the recorded responses, copied |
everything else (serve.rs, turns.rs, config.rs, Cargo.toml, other crates) |
no change |
Files
- Copy, from
docs/plans/M5c1/files/(the same path under it):- new tests:
crates/memoryd/tests/honcho_client.rs,crates/memoryd/tests/honcho_keys.rs,crates/memoryd/tests/honcho_naming.rs - new test support:
crates/memoryd/tests/support/honcho.rs(the fake Honcho) - the directory
crates/memoryd/tests/fixtures/honcho/(29 JSON files and a README), whole - the skeletons
crates/memoryd/src/honcho/client.rsandcrates/memoryd/src/honcho/naming.rs
- new tests:
- Modify:
crates/memoryd/src/honcho/mod.rs,crates/memoryd/src/honcho/ledger.rs,docs/implementer-log.md. - Do not touch: any other source file, any other test or support file, the fixtures (never edit
one, and never record new ones: there is no Honcho to reach from here), any
Cargo.toml, and in the skeletons anything but the 13todo!()bodies.
Facts you need
-
roncho1.0.0 is already a dependency (roncho.workspace = true, the featuretls, which bringsblocking). Its blocking client is HTTP/1.1 overstd::net, one request per connection. The calls this task uses, all onroncho::blocking::Client:Call Signature, and what it gives Client::builder()-> ClientBuilder; then.without_config_file(),.base_url(url),.workspace_id(name),.connect_timeout(d),.read_timeout(d),.api_key(key)(eachself -> Self),.build() -> Result<Client, Error>workspace_idclient.workspace_id() -> &strget_or_create_workspaceclient.get_or_create_workspace(&WorkspaceCreate) -> Result<roncho::Workspace, Error>peerclient.peer(&PeerCreate) -> Result<roncho::models::Peer, Error>sessionclient.session(&SessionCreate) -> Result<roncho::models::Session, Error>delete_conclusionclient.delete_conclusion(id: &str) -> Result<(), Error>WorkspaceCreateroncho::WorkspaceCreate::new(id),.with_configuration(Map<String, Value>)PeerCreateroncho::PeerCreate::new(id),.with_configuration(Map<String, Value>)SessionCreateroncho::SessionCreate::new(id),.with_peers(HashMap<String, SessionPeerConfig>)SessionPeerConfigroncho::SessionPeerConfig::new().observe_me(bool).observe_others(bool)roncho'sClientandClientBuilderDebugshowapi_key_present, never the key; the client keeps the key in aZeroizing<String>. -
Keys.
HonchoConfig.keys(task 06) mapshoncho_trusted,honcho_untrusted,honcho_fleetto a checkedSecretSource.bxcore::secrets::load("memoryd", name, source, env) -> Result<Loaded, SecretError>reads it (Loaded { secret: Secret, warning: Option<String> }; a file source always has a warning).Secret::expose() -> &stris the one place the value is taken, to hand it toroncho.SecretError'sDisplaynames the key and ends withsee docs/runbook.md#secret-unavailable.envisstd::env::var_osinmemoryd; tests pass their own. -
serde_json.
serde_json::Map::new(),map.insert(String, Value), andserde_json::json!({ "enabled": false })(imported asjson). -
The fake Honcho (
tests/support/honcho.rs) answers by method and the shape of the path: the first create of a thing with its 201 fixture, a repeat with its 200 one; the first delete of a conclusion 204, a repeat 404; anything else 404. It can be set to answer every request with one status (a 422 echoing the request's body, as Honcho does) or to answer nothing, and it records every request with its headers and body. -
The given tests.
honcho_client.rs(13 tests): the timeouts; each client's workspace and timeouts (fromroncho'sDebug); the fleet's configuration equal to the recorded one; the four requests of a session, in order and exactly; nothing asked twice in a run, a new speaker's peer and session only, another workspace on its own; everything asked again after a restart (200 is success); the fleet's workspace and a session with the agent alone; 409 on create; errors by kind and status, not remembered, and a timeout (the fake answers nothing, the read timeout is 300 ms); nothing listening; no response text in an error;classifyfor every error; deleting a conclusion twice.honcho_keys.rs(3 tests, and 1 ignored that the third runs): each key sent as a bearer token to its own workspace only, and shown by no client, error or warning; a key that cannot be read; aroncho.tomlwith anapi_keyunderHOMEnot read (a child process of the test binary, sinceHOMEis the process's).honcho_naming.rs(5 tests): the names; each new speaker, in any workspace, starting the next session; a skipped or undecided turn; the order of decision; a restart;decisions.
-
Where the delete lives.
delete_conclusionis here, with its two recorded answers; task 13 calls it for an undo.
The edits, exactly
crates/memoryd/src/honcho/mod.rs
H1. The lines
pub mod config;
pub mod decide;
pub mod ledger;
become
pub mod client;
pub mod config;
pub mod decide;
pub mod ledger;
pub mod naming;
crates/memoryd/src/honcho/ledger.rs
L1. The lines
/// The items waiting to be sent, with their actions, in the order they were decided.
pub fn outbox(&self) -> Vec<(Item, Action)> {
become
/// Every decided item with its action, `Skip` included, in the order they were decided.
pub fn decisions(&self) -> impl Iterator<Item = (&Item, &Action)> + '_ {
self.order
.iter()
.filter_map(|item| Some((item, &self.entries.get(item)?.action)))
}
/// The items waiting to be sent, with their actions, in the order they were decided.
pub fn outbox(&self) -> Vec<(Item, Action)> {
Steps
-
1. Copy the given files. From the repository root:
mkdir -p crates/memoryd/tests/fixtures cp -r docs/plans/M5c1/files/crates/memoryd/tests/fixtures/honcho crates/memoryd/tests/fixtures/ cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_client.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_keys.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/honcho_naming.rs crates/memoryd/tests/ cp docs/plans/M5c1/files/crates/memoryd/tests/support/honcho.rs crates/memoryd/tests/support/ls crates/memoryd/tests/fixtures/honcho | wc -lprints30. -
2. See them fail.
cargo build --tests --keep-going -p memoryd, with a shell timeout of 600000 ms: three test targets do not compile.honcho_client, 2 errors:E0432(unresolved import `memoryd::honcho::client`) andE0432(the same formemoryd::honcho::naming).honcho_keys, 1 error: theE0432forclient.honcho_naming, 8 errors: theE0432fornaming, and sevenE0599(no method named `decisions` found for struct `memoryd::honcho::ledger::Ledger`). Every other target compiles. -
3. The modules. Make edits H1 and L1, then copy the two skeletons:
cp docs/plans/M5c1/files/crates/memoryd/src/honcho/client.rs crates/memoryd/src/honcho/ cp docs/plans/M5c1/files/crates/memoryd/src/honcho/naming.rs crates/memoryd/src/honcho/cargo check -p memoryd: no error, and 26 warnings, all from the unfilled bodies:unused import: `json`;unused imports: `PeerCreate` and `WorkspaceCreate`; twenty-twounused variable(workspacefive times,sessiontwice, andagent,config,count,decisions,env,id,key,name,peer,result,speakers,timeouts,turn,url,warningsonce each);fields `trusted`, `untrusted`, and `fleet` are never read; andvariants `Workspace` and `Peer` are never constructed. They go as you fill the bodies. -
4. Fill the thirteen bodies, one at a time, in the order of each file, each from its comment, and run
cargo check -p memorydafter each. Inclient.rs:kind,status,fleet_configuration,session_peers,created,client(the free function),key,Clients::client,ensure_workspace,ensure_peer,delete_conclusion. Innaming.rs:session_name,honcho_session. When a body is filled, delete its instruction comment (the//lines inside the body) and keep the function's doc comment (///) as it is. Change nothing else:classify,Display,Clients::newandensure_sessionare given. After the last,cargo check -p memorydprints no warning. -
5. The tests. With a shell timeout of 600000 ms, and every test passes (13, 3 and 5; one test is ignored: the child the third test of
honcho_keysruns):cargo test -p memoryd --test honcho_client --test honcho_keys --test honcho_naming -
6. Check. Three commands:
grep -c 'without_config_file' crates/memoryd/src/honcho/client.rsprints1.grep -n 'expose()' crates/memoryd/src -rprints exactly one line, inclient(the free function): the one place a key's value is taken.git diff --stat -- crates/bxcore crates/loopd crates/brokerd crates/gatewayd crates/bxctl Cargo.toml Cargo.lock crates/memoryd/Cargo.toml crates/memoryd/src/serve.rsprints nothing.
-
7. Run the gate.
cargo fmt --all, thenmake gate, with a shell timeout of 600000 ms. Expected last line:gate: ok, with 1278 tests: 1257 before this task, plus 21 new ones (13 inhoncho_client.rs, 3 inhoncho_keys.rs, 5 inhoncho_naming.rs). -
8. Log and commit. Add your row to
docs/implementer-log.md; its Task cell isM5c1/10-memoryd-honcho-client.git status --shortlistshoncho/mod.rs,honcho/ledger.rsand your log as modified, andhoncho/client.rs,honcho/naming.rs, the three test files,tests/support/honcho.rsandtests/fixtures/as new. Stage withgit add crates/memoryd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.Cargo.lockdoes not change in this task; thegit addis harmless.
Done when
make gateprintsgate: okwith 1278 tests.git diff HEAD~1 --statlists only the files of step 8.
Stop and report if
- A "before" block of H1 or L1 is not in its file exactly as shown, or is there more than once.
- Step 1 does not give 30 files, or step 2 or 3 shows a different result: other test targets, errors of another kind, or other warnings.
cargo checkreports an error after a body you filled, and the comment does not tell you how to fix it. Quote the error, and stop.- A comment in a skeleton does not fit the code under it, or disagrees with this task's tables. Quote both and stop.
- After step 4 a test fails although each body follows its comment. Do not change a test, the fake, a fixture or the skeletons' given code: stop, and give the failing tests' names and their messages.
- A test seems to need a fixture that is not there, or a request to a real Honcho. It does not; stop and say why you think so. Never record a response.
- The fix seems to need a change anywhere else:
serve.rs,turns.rs, aCargo.toml, another crate. It does not; stop and say why you think so. - A test outside the new files fails in the gate: check once whether it fails without your change, then stop and report both results.
- The gate passes with a count other than 1278.
What the given tests pin
Each of these changes to the finished code makes at least one given test fail (44 tried, one
cannot change behaviour): either timeout ignored or changed; roncho.toml read; each error
mapped to the other side (a 5xx refused, a 401 refused, a timeout, a connect failure or a closed
socket refused, a bad body unreachable); a 409 on create an error, or any refusal on create
taken as success; a 404 on delete an error; the fleet's settings on another workspace, missing
one, or on; the agent observed, or every peer unobserved; the workspace, a peer or a session not
remembered, or remembered for every workspace, or before it was made; the session made without
its workspace, its agent or its speakers first; the agent observed or left out of a session, the
speakers unobserved or observing others; a key sent to the wrong workspace or dropped; a key
file's warning dropped; a key that cannot be read taken as no key; the wrong client for a
workspace; an error shown without its status; a session named <id>-1; a speaker counted
twice; another session's speakers counted; the speakers of later turns counted; decisions
without the skipped items or in reverse.
After this task
Task 11 builds the Clients at start (after the ledger, logging the warnings, a key that cannot
be read stopping memoryd with secret-unavailable), and sends turns with ensure_session and
honcho_session. Task 13 uses delete_conclusion, and makes the fleet's sessions with no
speakers.