# roncho: what Boxmaker's `memoryd` needs from it **For:** the agent that develops `roncho` (`git.wntrmute.dev/kyle/roncho`, `~/src/roncho` on titan). **From:** Boxmaker's design model, 2026-09-25, for milestone M5b. **Status:** requirements, for the owner to hand over. Boxmaker's M5b spec is written against the release this produces. `roncho` was being reworked while this was written: the divergences in 3.4 were last checked at `a9708f1` and are described by behaviour, not by line. Check each one before fixing it. ## 1. Why Boxmaker is the owner's personal agent harness (Rust, `~/src/boxmaker`). Its memory daemon, `memoryd`, will sync finished conversation turns to the owner's Honcho on rift, write some entries as conclusions, search Honcho, and ask its dialectic. The owner wants it to do that through `roncho`, not through a second client. Today `roncho` cannot be used from Boxmaker: - It is async only (tokio, reqwest, hyper, futures). Boxmaker has a standing rule: blocking I/O with threads, no async runtime, and `memoryd` is the smallest-authority daemon of the set. - Some of its request and response shapes disagree with the live server. sift's Go client, checked against rift on 2026-09-22, found that workspace search takes `{"query", "limit"}` and returns a bare JSON array, not a `Page` (`~/src/sift/internal/fleetctx/fleetctx.go`, `~/src/sift/docs/P2.md`). - It defaults to `https://api.honcho.dev`. For Boxmaker, memory text must never leave the owner's hosts, so a default that points at a cloud service is a hazard, not a convenience. Boxmaker's policy (which turns are synced, trust labels, taint) stays in `memoryd`. `roncho` is the transport and the types: correct, small, and predictable. ## 2. The deployment it must work against | Fact | Value | Source | |---|---|---| | Honcho API | `http://rift.scylla-hammerhead.ts.net:8000`, plain HTTP over the tailnet | `imladris/hw/orion/default.nix`; `sift/docs/PLAN.md` | | Version | 3.2.0, image `3.2.0-pr1092-7e57fe2` (roncho's SPEC targets 3.2.1) | `imladris/docs/metrics-plan.md` | | Authentication | apparently none: sift sends no `Authorization` header and succeeds. **Not confirmed.** | `sift/internal/fleetctx/fleetctx.go` | | Models | deriver, summaries and dialectic on dixie's `ornith-1.5-9b-uncensored`; dialectic answers take 30 to 50 s | `imladris/hw/dixie/default.nix`; `sift/docs/PLAN.md` | | Workspaces in use | `hermes-fleet` and others (other agents'); each Boxmaker will have its own | `imladris/hw/orion/default.nix` | **Never write to a workspace you did not create for testing.** `hermes-fleet` and the other existing workspaces hold other agents' live memory. Write tests go to a throwaway workspace (for example `roncho-test-`), deleted afterwards. ## 3. Requirements MUST: Boxmaker cannot use `roncho` without it. SHOULD: Boxmaker can work around it, at a cost. ### 3.1 A blocking client with a small dependency graph - **R1 (MUST)** A blocking API: every operation in 3.3 as a plain function call that returns when the response is read. No async runtime is started, required or depended on. - **R2 (MUST)** Selected by Cargo features so a consumer can build **only** the blocking client: `roncho = { …, default-features = false, features = ["blocking"] }` (the name is yours). With that, the dependency graph contains none of `tokio`, `reqwest`, `hyper`, `h2`, `futures`, `async-stream`, `bytes`, `webpki-roots`. The async client may stay as the default feature. - **R3 (MUST)** HTTP/1.1 over plain TCP for `http://` URLs, written on `std::net` or with a small blocking crate. Boxmaker already has two such clients (`crates/loopd/src/http.rs`, `crates/gatewayd/src/http.rs`): one request per connection, `Connection: close`, chunked and `Content-Length` bodies. - **R4 (SHOULD)** `https://` behind an optional feature, using `rustls` 0.23 with the `ring` provider and the platform's certificates (`rustls-native-certs` 0.8). Not `webpki-roots`, not `aws-lc-rs`. rift needs no TLS today, so this may come after the rest. - **R5 (MUST)** Every crate in the blocking graph (with and without R4) is on crates.io, is licensed under one of MIT, Apache-2.0, ISC, BSD-3-Clause, Unicode-3.0 or Zlib, and does not duplicate a different major version of a crate Boxmaker already locks. Boxmaker locks today: `serde` 1.0.229, `serde_json` 1.0.151, `rustls` 0.23.45, `ring` 0.17.14, `rustls-native-certs` 0.8.4, `thiserror` 2.0.21, `syn` 3, `toml` 1.1.6, `zeroize` 1.9.0. Boxmaker's `cargo deny` runs with `multiple-versions = "deny"`. - **R6 (SHOULD)** Keep the blocking graph small: beyond `serde` and `serde_json` (and R4's TLS crates), add as few crates as you can. Prefer timestamps as RFC 3339 strings, ids as strings and URLs parsed by hand over `chrono`, `uuid` and `url` in the blocking models. If you keep any of them, use `default-features = false` (for `chrono`, no `clock`, which pulls platform time-zone crates) and say why in the README. `thiserror` is acceptable: Boxmaker already locks it. - **R7 (MUST)** Builds and passes its tests with Rust 1.95 (Boxmaker's `rust-version`). Set `rust-version` in `Cargo.toml`. - **R8 (MUST)** A git tag per release that Boxmaker can pin (`rev` or `tag`), and a changelog entry saying what changed in the blocking API. Boxmaker will add the git source to its `deny.toml` allow-list for this one URL; publishing to crates.io instead is fine too. ### 3.2 Configuration and safety - **R9 (MUST)** No default base URL in the blocking client. Building a client without one is an error. (Keep the async default if you like; the blocking path must not have one.) - **R10 (MUST)** The API key is optional. When set, it is sent as `Authorization: Bearer`; when not, no `Authorization` header at all. Building without a key is not an error. The key never appears in an error, a `Debug` output or a log line. - **R11 (MUST)** The workspace is set explicitly per client. A client is cheap to clone and one client serves one workspace, as today. A client is `Send + Sync`. - **R12 (MUST)** The blocking client takes its configuration only from the caller. It reads no environment variables: not `HONCHO_*` (today `build()` falls back to `HONCHO_WORKSPACE_ID`, `HONCHO_API_KEY` and `HONCHO_BASE_URL`), not proxy settings (`HTTP_PROXY`, `ALL_PROXY` and so on). It does not follow redirects: a 3xx is returned as an error. Memory text is sent only to the configured host. - **R13 (MUST)** Timeouts are set by the caller, per client: a connect timeout, and a read timeout that means "no bytes for N seconds", not a total deadline. A dialectic answer takes 30 to 50 s on the current hardware, so the caller will set a long one there. No hidden retries: either implement `max_retries` for idempotent calls with a documented policy, or remove it. The blocking client defaults to no retries. - **R14 (MUST)** A cap on the size of a response body, set by the caller, with a sensible default (for example 8 MiB). A body over the cap is an error, and reading stops at the cap. - **R15 (MUST)** No background threads, no global state, no logging of request or response content, no telemetry, no update checks. ### 3.3 Operations the blocking API provides Each operation is verified against rift (3.4) before it is released. Names are suggestions. | # | Operation | Honcho endpoint (as roncho has it today; verify) | Boxmaker's use | |---|---|---|---| | O1 | get or create the workspace | `POST /v3/workspaces` (in the async client as `get_or_create_workspace`) | first start | | O2 | get or create a peer, with metadata and configuration | `POST peers` | the owner, the agent, other agents met in channels | | O3 | get or create a session, with its peers and each peer's `observe_me` / `observe_others` | `POST sessions` | one Honcho session per Boxmaker session | | O4 | add messages in a batch, each with `peer_id`, `content`, `metadata` (a JSON object) and `created_at` | `POST sessions/{s}/messages` | the sync of finished turns; the metadata holds Boxmaker's session id, record number and labels | | O5 | list a session's messages, paged, with filters | `POST sessions/{s}/messages/list` | checking what a sync already sent | | O6 | delete a session | `DELETE sessions/{s}` | rebuilding | | O7 | delete the workspace, and report the "active sessions" conflict (409) as its own error | `DELETE /v3/workspaces/{id}` (the async `delete_workspace` returns it as a generic error) | rebuilding | | O8 | create conclusions in a batch (1 to 100), `content`, `observer_id`, `observed_id`, optional `session_id` | `POST conclusions` | entries moved out of identity; trusted notes | | O9 | list, query and delete conclusions | `POST conclusions/list`, `POST conclusions/query`, `DELETE conclusions/{id}` | rebuilding; checks | | O10 | search the workspace, with a result limit | `POST search` (rift: `{"query", "limit"}` and a bare array) | `recall` | | O11 | search a peer's messages and a session's messages, with a limit | `POST peers/{p}/search`, `POST sessions/{s}/search` | `recall`, narrowed | | O12 | the dialectic, non-streaming: query, target, session, reasoning level, whether to include evidence | `POST peers/{p}/chat`, `POST chat` | `ask_memory` | | O13 | a cheap probe that the server is up and speaks the expected version | whatever Honcho 3.2 offers; if nothing, a cheap read (O1's get) | `memoryd`'s startup and health | - **R16 (MUST)** O1 to O13 in the blocking API, with typed requests and responses. Fields the server may add later are ignored when decoding; fields Boxmaker relies on are required. - **R17 (SHOULD)** O12 streaming, with the read timeout of R13 applying between chunks, so a caller can tell a slow answer from a dead one. The SSE must be parsed by lines across network chunks (see R20). - **R18 (MUST)** One error type for the blocking client that lets the caller tell apart: cannot connect, timed out (no bytes), HTTP status with the first 4 KiB of the body, response too large, response not decodable, invalid configuration, and the workspace-delete conflict. Implements `std::error::Error` and `Display`; never contains the API key. ### 3.4 Correctness against rift - **R19 (MUST)** Every O-operation is exercised against rift 3.2.0 in a throwaway workspace (reads may use `hermes-fleet`, writes never), and the result recorded in a compatibility document in the repository: endpoint, method, request shape, response shape, the date, and the server's version. Where Honcho's docs and rift disagree, rift wins and the document says so. - **R20 (MUST)** Fix these divergences, found by reading the async client; they apply to the blocking client too, and any fix to shared models or request code applies to both: - **Search shapes (O10, O11).** rift takes `{"query", "limit"}` at the top level and answers with a bare array (sift, above). roncho's workspace search sends `{"query"}` with no limit, and its peer and session searches send `{"filters": {"query"}}`; all three decode the answer as a `Page`. There is no way to set a limit. - **Paging of messages (O5).** `list_messages` builds the `page`, `size` and `reverse` query and never sends it, so a paged read can keep getting the first page. - **Message fields.** `Message.role` is required when decoding; sift's verified field list has no `role`. Make every field Boxmaker does not need optional, and check which fields rift sends. - **List methods.** Peer and session lists use `GET`, while Honcho 3's other lists (messages, conclusions, workspaces) are `POST …/list`. Settle on what rift does, and the same for `peers/{p}/context`. - **Streaming.** The SSE reader splits each network chunk into lines on its own, so a line split across two chunks is misparsed. Buffer until a newline. - **Build errors.** A missing workspace is reported as `MissingApiKey`, and a missing key is an error at all (R10). - **Retries.** `max_retries` is stored and never read (R13). - **Workspace delete.** The "active sessions" conflict is not told apart from other failures (O7, R18). - **R21 (MUST)** Tests for the blocking client against a fake server in the test process (`std::net::TcpListener`), covering each O-operation's request bytes and response decoding, and also: a refused connection, a server that sends nothing (read timeout), a server that stops mid-body, a redirect (refused), an oversized body (capped), a non-JSON body, a 409 on workspace delete, and that no `Authorization` header is sent without a key. Every wait in a test has a limit. - **R22 (SHOULD)** A live suite, ignored by default, run with an environment variable naming the server, that performs R19's checks and deletes its workspace at the end. ### 3.5 Semantics to find out and write down Boxmaker's design depends on these answers. For each, check Honcho 3.2's documentation and rift, and record what you found (or "not found") in the compatibility document. Do not guess. - **Q1** Is authentication off on rift? If it can be on, are keys scoped per workspace, and is there a read-only key? - **Q2** Can a single message be deleted or edited in 3.2.0? What does deleting a session remove from conclusions, representations and summaries derived from it? - **Q3** The deriver: what triggers it (per message, per batch, per session), how long until conclusions appear under normal load, how to see whether a message has been processed, and which peer, session or workspace configuration keys turn derivation off for a peer. - **Q4** Message metadata: can search (O10, O11) and the dialectic (O12) be filtered by metadata (so that labelled messages can be excluded), and what is the filter grammar? - **Q5** What does a conclusion's `source_ids` refer to? Can a conclusion be traced to the messages it came from? - **Q6** When does workspace deletion return 409 ("active sessions"), and how does a caller make a session inactive? - **Q7** Is there a health or version endpoint (O13)? - **Q8** Does the `3.2.0-pr1092` image differ from upstream 3.2.0 or 3.2.1 in any endpoint above? ## 4. Out of scope for roncho - Which turns are synced, trust per workspace, taint, sanitising text: Boxmaker's `memoryd`. - Queues and progress tracking of a sync: `memoryd` keeps its own. - A CLI. Boxmaker does not need one. ## 5. Done when - A tagged release exists whose blocking feature meets R1 to R18, with the R20 fixes. - `cargo tree -e normal --no-default-features -F blocking` (with your feature name) shows no crate from R2's list, and every crate meets R5. - The compatibility document covers O1 to O13 and Q1 to Q8, dated, with rift's version. - The fake-server tests of R21 pass with Rust 1.95; the live suite of R22 passes against rift. - The owner is told the tag, and anything in this document that could not be done, and why. ## 6. Addendum: status at `v0.1.0`, and API keys (2026-09-25, later) Boxmaker's review of `v0.1.0` (`1e58d25`) and of the fixes on top of it, which were still uncommitted when checked. Everything marked "checked" was run by Boxmaker's design model on titan with Rust 1.95, `--offline`, in a scratch target directory; "reported" is the `roncho` agent's word, not yet checked here. ### 6.1 Fixed since `v0.1.0` | Item | State | |---|---| | The blocking-only library did not build (`src/error.rs` imported `reqwest`, `src/models/chat.rs` imported `futures`) | checked: `cargo build --no-default-features -F blocking` builds | | `ClientBuilder` derived `Debug` and printed the key | checked: the only `Debug` derive left in `src/blocking/client.rs` is on `Probe` | | Streaming never sent `"stream": true`, and decoded UTF-8 per socket read | checked: `"stream"` is sent; the UTF-8 test reported | | Request bodies not asserted; the live suite passed when unset; 200/201 and the 202 body contradicted each other in the compatibility table | reported | | Tests of the blocking client | checked: `--lib --test blocking_test` with `-F blocking`: 3 and 9 pass, 1 ignored; with `-F tls`: 4 and 9 pass; the default suite passes | ### 6.2 Still to do (MUST) - **A1** `cargo test --no-default-features -F blocking`, the command quoted as passing, does not compile: the async test files (`smoke_test.rs`, `client_test.rs` and the others that `smoke_test.rs` includes) use `roncho::Honcho` and `futures` without a feature gate. Gate each with `#![cfg(feature = "async")]`, and run both `-F blocking` and `-F tls` (default features off) wherever the tests are run, so the next break is caught. - **A2** Commit, push, and tag a new release from a commit that is on `origin`. The tag's message must name the compatibility document by the name it has at that commit (`v0.1.0` names `docs/rift-2026-09-25.md`, which the working tree deletes). - **A3** `query_conclusions`: rift refuses a query without the observer and observed filters. Make them required in the blocking request type, or refuse before sending. - **A4** `create_conclusions`: refuse a batch of 0 or more than 100 before sending. - **A5** Blocking chat drops `response_format` without a word. Send it, or remove it from what the blocking options accept. ### 6.3 Still to do (SHOULD) - **A6** Version requirements: `rustls = "=0.23.45"` and `rustls-native-certs = "=0.8.4"` are exact pins, so every patch release Boxmaker takes would need a `roncho` release. Use the ordinary caret form (`"0.23.45"`, `"0.8.4"`). Cargo still unifies compatible versions, so Boxmaker's no-duplicates rule holds. - **A7** `dial` tries only the first address a name resolves to. Try each in turn, with the connect timeout per address: a tailnet name can resolve to IPv6 and IPv4, and one may be unreachable. Say in the README that name resolution itself is not bounded by the timeout. - **A8** `Message` requires `session_id`, `workspace_id`, `created_at` and `token_count`. Keep required only what Boxmaker reads (`id`, `content`, `peer_id`, `session_id`, `created_at`, `metadata`); make the rest optional, so a server that omits one does not fail a whole search. - **A9** Mark the blocking `Error` `#[non_exhaustive]`, so the variants of 6.4 are not a breaking change. - **A10** O14, a new operation: the workspace's queue status (`GET /v3/workspaces/{id}/queue/status`, from Q3), so Boxmaker can tell when derivation has caught up after a sync or a rebuild. - **A11** Tests: the body cap on chunked and close-delimited bodies (only `Content-Length` is covered); the request of workspace chat and of the probe; a limit on the fake servers' `accept`. - **A12** Live suite: workspace chat, streaming and `include_evidence`; the time until a conclusion appears for a new message (Q3), measured a few times; and whether `observe_me: false` and `reasoning.enabled: false` stop derivation (Q3), since Boxmaker may use them. - **A13** The async client's share of R20: the 409 on workspace delete as its own error, the key optional, and the retry policy written down. Boxmaker uses only the blocking client, so this is for `roncho`'s other users. - **A14** `SPEC.md` and `AGENTS.md` still say "async throughout" and list `chrono` and `uuid`; the README's roadmap still lists a blocking API as out of scope. ### 6.4 API keys (new: MUST unless marked) Rift will turn authentication on. Q1 found that keys are scoped (to a workspace, a peer or a session) and that key creation is disabled on rift today (`405 "Feature is disabled"`). - **R23** A 401 or 403 is its own error variant (for example `Unauthorized { status }`), not a generic status, so `memoryd` can say "the key for workspace X was refused" and point to a runbook entry. - **R24** A key set explicitly but empty is a configuration error, not "no key". With keys on, silently sending no `Authorization` header hides a mistake. An absent key still means no header (R10). - **R25** Each client carries its own key, since keys are per workspace: Boxmaker will build one client per workspace (its trusted and untrusted workspaces, and each fleet workspace it reads), each with that workspace's key. Say so in the README. - **R26 (SHOULD)** Hold the key in memory that is wiped when the client is dropped (`zeroize`, which Boxmaker already locks at 1.9.0), and never copy it into an error or `Debug` output (R10). - **R27 (SHOULD)** The live suite takes a key from the environment (for example `RONCHO_LIVE_KEY`), so it can run once rift has authentication, and checks a refused key (R23). When rift's authentication is on, re-answer Q1 there: what a workspace key may do in another workspace, and whether a peer-scoped key is read-only as the docs say. ### 6.5 Accepted as `roncho` has them - The features `blocking` and `tls` (implying `blocking`), with async on by default; Boxmaker uses `default-features = false`. - Defaults for the timeouts (10 s connect, 120 s idle read) and the body cap (8 MiB), which the caller overrides; the write timeout reusing the read timeout. - The probe as `GET /health` returning a status. It cannot report a version, since Honcho 3.2 has no version route. - Streaming as an `Iterator`; models shared with the async client, timestamps as strings. - The compatibility document not naming rift or its address, if `roncho` is to be public. Boxmaker keeps those facts in its own documents. The document should still give the date and the Honcho version and image as far as they are known.