# 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 `roncho` blocking 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 it `roncho` reads `~/.config/roncho/roncho.toml` and takes an `api_key` from 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`, `summary` and `dream` all `enabled: 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. - **Which Honcho session a turn goes to** (spec 3): the Boxmaker session's id; a speaker new to it starts the next, `-2`, `-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 decided `Turn` actions, 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`'s `Status` and `Decode` errors carry up to 4 KiB of the response. So a `HonchoError` keeps 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`): ```rust 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 }, Refused { kind: &'static str, status: Option }, KeyRefused { status: u16 }, } impl HonchoError { pub fn kind(&self) -> &'static str; pub fn status(&self) -> Option; } pub fn classify(error: &roncho::blocking::Error) -> HonchoError; pub fn fleet_configuration() -> serde_json::Map; pub fn session_peers(agent: &str, speakers: &[String]) -> HashMap; pub struct Clients { /* private */ } impl Clients { pub fn new(config: &HonchoConfig, env: &dyn Fn(&str) -> Option, timeouts: Timeouts) -> Result<(Clients, Vec), 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`): ```rust pub struct HonchoSession { pub name: String, pub speakers: Vec } pub fn session_name(session: &SessionId, count: usize) -> String; pub fn honcho_session<'a, I>(decisions: I, session: &SessionId, turn: u64) -> Option where I: IntoIterator; ``` In `crates/memoryd/src/honcho/ledger.rs`, one new method (edit L1): `pub fn decisions(&self) -> impl Iterator + '_`, 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 ([, HTTP ])`, `Honcho refused the request ([, HTTP ])`, `Honcho refused the key (key-refused, HTTP )`. 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":""}`; the fleet: `{"id":"","configuration":{"reasoning":{"enabled":false},"summary":{"enabled":false},"dream":{"enabled":false}}}` | | the agent's peer | `POST /v3/workspaces//peers` | `{"id":"","configuration":{"observe_me":false}}` | | a speaker's peer | the same | `{"id":""}` | | a session | `POST /v3/workspaces//sessions` | `{"id":"","peers":{"":{"observe_me":false,"observe_others":false},"":{"observe_me":true,"observe_others":false},…}}` | | delete a conclusion | `DELETE /v3/workspaces//conclusions/` | 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 (A copy command names `-taskNN` where a later task gives a newer version of that file: this task's version is the one named.) - 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), from `honcho.rs-task10` (this task's version; task 13 gives a later one) - the directory `crates/memoryd/tests/fixtures/honcho/` (29 JSON files and a README), whole - the skeletons `crates/memoryd/src/honcho/client.rs` and `crates/memoryd/src/honcho/naming.rs` - 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 13 `todo!()` bodies. ## Facts you need - **`roncho` 1.0.0** is already a dependency (`roncho.workspace = true`, the feature `tls`, which brings `blocking`). Its blocking client is HTTP/1.1 over `std::net`, one request per connection. The calls this task uses, all on `roncho::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)` (each `self -> Self`), `.build() -> Result` | | `workspace_id` | `client.workspace_id() -> &str` | | `get_or_create_workspace` | `client.get_or_create_workspace(&WorkspaceCreate) -> Result` | | `peer` | `client.peer(&PeerCreate) -> Result` | | `session` | `client.session(&SessionCreate) -> Result` | | `delete_conclusion` | `client.delete_conclusion(id: &str) -> Result<(), Error>` | | `WorkspaceCreate` | `roncho::WorkspaceCreate::new(id)`, `.with_configuration(Map)` | | `PeerCreate` | `roncho::PeerCreate::new(id)`, `.with_configuration(Map)` | | `SessionCreate` | `roncho::SessionCreate::new(id)`, `.with_peers(HashMap)` | | `SessionPeerConfig` | `roncho::SessionPeerConfig::new().observe_me(bool).observe_others(bool)` | `roncho`'s `Client` and `ClientBuilder` `Debug` show `api_key_present`, never the key; the client keeps the key in a `Zeroizing`. - **Keys.** `HonchoConfig.keys` (task 06) maps `honcho_trusted`, `honcho_untrusted`, `honcho_fleet` to a checked `SecretSource`. `bxcore::secrets::load("memoryd", name, source, env) -> Result` reads it (`Loaded { secret: Secret, warning: Option }`; a file source always has a warning). `Secret::expose() -> &str` is the one place the value is taken, to hand it to `roncho`. `SecretError`'s `Display` names the key and ends with `see docs/runbook.md#secret-unavailable`. `env` is `std::env::var_os` in `memoryd`; tests pass their own. - **serde_json.** `serde_json::Map::new()`, `map.insert(String, Value)`, and `serde_json::json!({ "enabled": false })` (imported as `json`). - **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 (from `roncho`'s `Debug`); 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; `classify` for 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; a `roncho.toml` with an `api_key` under `HOME` not read (a child process of the test binary, since `HOME` is 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_conclusion` is 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 ```rust pub mod config; pub mod decide; pub mod ledger; ``` become ```rust pub mod client; pub mod config; pub mod decide; pub mod ledger; pub mod naming; ``` ### `crates/memoryd/src/honcho/ledger.rs` **L1.** The lines ```rust /// The items waiting to be sent, with their actions, in the order they were decided. pub fn outbox(&self) -> Vec<(Item, Action)> { ``` become ```rust /// Every decided item with its action, `Skip` included, in the order they were decided. pub fn decisions(&self) -> impl Iterator + '_ { 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: ```sh 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-task10 crates/memoryd/tests/honcho_naming.rs cp docs/plans/M5c1/files/crates/memoryd/tests/support/honcho.rs-task10 crates/memoryd/tests/support/honcho.rs ``` `ls crates/memoryd/tests/fixtures/honcho | wc -l` prints `30`. - [ ] **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` ``) and `E0432` (the same for `memoryd::honcho::naming`). `honcho_keys`, 1 error: the `E0432` for `client`. `honcho_naming`, 8 errors: the `E0432` for `naming`, and seven `E0599` (``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: ```sh 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-two `unused variable` (`workspace` five times, `session` twice, and `agent`, `config`, `count`, `decisions`, `env`, `id`, `key`, `name`, `peer`, `result`, `speakers`, `timeouts`, `turn`, `url`, `warnings` once each); ``fields `trusted`, `untrusted`, and `fleet` are never read``; and ``variants `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 memoryd` after each. In `client.rs`: `kind`, `status`, `fleet_configuration`, `session_peers`, `created`, `client` (the free function), `key`, `Clients::client`, `ensure_workspace`, `ensure_peer`, `delete_conclusion`. In `naming.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::new` and `ensure_session` are given. After the last, `cargo check -p memoryd` prints 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_keys` runs): ```sh 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.rs` prints `1`. - `grep -n 'expose()' crates/memoryd/src -r` prints exactly one line, in `client` (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.rs` prints nothing. - [ ] **7. Run the gate.** `cargo fmt --all`, then `make 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 in `honcho_client.rs`, 3 in `honcho_keys.rs`, 5 in `honcho_naming.rs`). - [ ] **8. Log and commit.** Add your row to `docs/implementer-log.md`; its Task cell is `M5c1/10-memoryd-honcho-client`. `git status --short` lists `honcho/mod.rs`, `honcho/ledger.rs` and your log as modified, and `honcho/client.rs`, `honcho/naming.rs`, the three test files, `tests/support/honcho.rs` and `tests/fixtures/` as new. Stage with `git add crates/memoryd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. `Cargo.lock` does not change in this task; the `git add` is harmless. ## Done when - `make gate` prints `gate: ok` with 1278 tests. - `git diff HEAD~1 --stat` lists 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 check` reports 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`, a `Cargo.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 `-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.