Files
boxmaker/docs/plans/M5c2/05-memoryd-honcho-readers.md
2026-09-28 19:30:58 -07:00

411 lines
18 KiB
Markdown

# M5c-2 task 05: `memoryd`'s readers, the Honcho workspaces it reads
**Branch:** `m5c2`. Check it: `git branch --show-current` prints `m5c2` and `git status --short`
is empty; otherwise stop. Never switch branch.
**Commit subject:** `memoryd: the Honcho workspaces it reads, with their clients and labels`
**Log row:** the Task cell is `M5c2/05-memoryd-honcho-readers`.
## Goal
`memoryd` writes to three Honcho workspaces through the `Clients` of M5c-1 (the agent's trusted
and untrusted ones and the fleet's), which move into the sync worker's thread. M5c-2 also reads
(M5c spec, sections 3, 8 and 9): `recall` searches other agents' workspaces and the fleet's, and
`ask_memory` asks one or two workspaces by name. Reading needs clients of its own. This task
builds them at start and gives later tasks the types they use; nothing searches or asks yet
(tasks 08 to 10).
- **`Readers`** (new module `crates/memoryd/src/honcho/read.rs`, given whole): a `Reader` for
- each `[[honcho.read]]` workspace, in the order written, with that entry's `trust` and
`class` and the key `honcho_read_<agent>`;
- the fleet's workspace (key `honcho_fleet`), always untrusted and `private`;
- the agent's own trusted workspace (key `honcho_trusted`), trusted, `private`, and its own
untrusted one (key `honcho_untrusted`), untrusted, `private`. These two are for `ask_memory`
only: `recall` never searches the agent's own workspaces (spec, section 8).
- **Two clients per reader.** A `roncho` client has one read timeout. Searches wait 5 s
(`SEARCH_READ`) and chat 150 s (`CHAT_READ`, which this task changes from 180 s: spec 9 gives
chat 150 s, inside `brokerd`'s 170 s); both connect within 5 s (`CONNECT`).
- **The label** of each reader's results: the name each hit shows (the agent's name, `fleet`,
`own` or `untrusted`), the trust, and the class. `Label::tag()` is `memory` for a trusted
workspace and `memory-tainted` otherwise, as a local `recall` hit's tag.
- **`recall_sources()`**: the read workspaces, then the fleet's. **`resolve(name)`**: the reader
`ask_memory` means by `own`, `untrusted`, `fleet` or a read agent's name; any other name is
the sentence the model is shown, which lists the names it may use.
- **Reserved names.** Since `ask_memory` names this agent's own workspaces `own`, `untrusted`
and `fleet`, a `[[honcho.read]]` agent with one of those names is now refused at start
(`honcho-config-invalid`). Otherwise a read agent called `fleet` would shadow the fleet in
`ask_memory`.
- **At start**, `start::open` builds the readers beside the writers' `Clients` and keeps them in
`Opened::readers`. A read key that cannot be read stops `memoryd`, as a written workspace's
does. The three written workspaces' keys are read by both; each key file's warning is logged
once. `serve.rs` does not change: nothing uses the readers until task 09, which takes
`opened.readers` before the worker's thread takes the rest of `Opened`.
## Every place that changes
This is the complete list.
| Place | What happens there |
|---|---|
| `crates/memoryd/src/honcho/read.rs` | new, given whole: copy it |
| `crates/memoryd/src/honcho/mod.rs` | `pub mod read;` (edit H1) |
| `crates/memoryd/src/honcho/client.rs` | `CHAT_READ` is 150 s; `client` and `key` become `pub(crate)`, for `read.rs` (edits C1 to C3) |
| `crates/memoryd/src/honcho/config.rs` | `RESERVED`, and the check that uses it (edits G1, G2) |
| `crates/memoryd/src/honcho/start.rs` | `Opened::readers`, built in `open` (edits O1 to O3) |
| `docs/runbook.md`, `honcho-config-invalid` | the reserved names among the reasons (edit D1) |
| `crates/memoryd/tests/honcho_read.rs` | new, given |
| `crates/memoryd/tests/honcho_client.rs`, `honcho_start.rs` | new versions, given |
| everything else | **no change**: not `serve.rs`, not `recall.rs`, not `worker.rs`, no `Cargo.toml` |
## Files
- Copy, from `docs/plans/M5c2/files/` (the same path under it): the new test
`crates/memoryd/tests/honcho_read.rs`; **new versions** of (they replace the files in the tree)
`crates/memoryd/tests/honcho_client.rs` and `crates/memoryd/tests/honcho_start.rs`; and the
new source `crates/memoryd/src/honcho/read.rs`, which is complete: it has no `todo!()`, and you
do not change it.
- Modify: `crates/memoryd/src/honcho/mod.rs`, `crates/memoryd/src/honcho/client.rs`,
`crates/memoryd/src/honcho/config.rs`, `crates/memoryd/src/honcho/start.rs`,
`docs/runbook.md`, `docs/implementer-log.md`.
- Do not touch: any other file.
## Facts you need
- **`read.rs` needs** from `client.rs`: `CONNECT`, `CHAT_READ`, `Timeouts`, and the two helpers
`client(url, workspace, key, timeouts) -> Result<Client, String>` and
`key(config, name, env, warnings) -> Result<Option<secrets::Secret>, String>`, which are
private today. Edits C2 and C3 make them `pub(crate)`: visible in `memoryd`, not outside it.
From `config.rs` it needs `FLEET_KEY`, `TRUSTED_KEY`, `UNTRUSTED_KEY`, `read_key`, `Trust` and
`HonchoConfig`, all public already, and from `bxcore` `DataClass` and
`bxcore::memory::OWN_WORKSPACE` (`"own"`, task 01).
- **What `Readers` offers** (for tasks 08 to 10; read `read.rs` for the whole):
```rust
pub const SEARCH_READ: Duration; // 5 s
pub const UNTRUSTED: &str; // "untrusted"
pub const FLEET: &str; // "fleet"
pub struct ReadTimeouts { pub connect: Duration, pub search: Duration, pub chat: Duration }
pub enum Source { Agent(String), Fleet, Own, OwnUntrusted }
pub struct Label { pub name: String, pub trust: Trust, pub class: DataClass }
impl Label { pub fn tag(&self) -> &'static str; pub fn untrusted(&self) -> bool }
pub struct Reader { pub source: Source, pub label: Label, /* two clients */ }
impl Reader { pub fn search(&self) -> &Client; pub fn chat(&self) -> &Client }
impl Readers {
pub fn new(config: &HonchoConfig, env: &dyn Fn(&str) -> Option<OsString>,
timeouts: ReadTimeouts) -> Result<(Readers, Vec<String>), String>;
pub fn recall_sources(&self) -> Vec<&Reader>;
pub fn resolve(&self, name: &str) -> Result<&Reader, String>;
}
```
- **`roncho::blocking::Client`** is `Clone`, `Send` and `Sync` (its documentation says so, and
a given test checks it at compile time), and its `Debug` shows the timeouts and whether a key
is present, never the key. This task's table of `roncho`'s API is complete; `roncho`'s source
(the Cargo registry, `~/.cargo/registry`) is outside the repository and off limits.
- **Why the warnings are filtered.** `Clients::new` and `Readers::new` each read the keys of
the three written workspaces, and each returns a key file's plaintext warning. Edit O3 logs
the writers' warnings, then only those of the readers' that are not among them.
- **The given tests.**
- `honcho_read.rs` (8 tests), against the fake Honcho: the client and the readers are
`Send + Sync`; the timeouts, read from each client's `Debug`; each reader searches its own
workspace with its own key (the fake records each request's `Authorization` header), and no
header without a key; no key in a reader's `Debug`, an error, or the error of a key that
cannot be read; `recall_sources` never holds the agent's own workspaces; each source's
label and tag; `resolve` for every name, and the exact sentence for an unknown one; a read
agent named `own`, `untrusted` or `fleet` refused by `config::check`, with its sentence.
- `honcho_client.rs`: one line changes, `CHAT_READ` is 150 s.
- `honcho_start.rs`: a new test, `a_read_key_that_cannot_be_read_stops_memoryd`, and one new
assertion in `a_valid_section_with_keys_starts_memoryd_as_before`: the fleet key file's
warning is printed once.
- The items the edits use. This table is complete.
| Item | Where |
|---|---|
| `Readers::new`, `ReadTimeouts::DEFAULT` (from `read.rs`) | O3 |
| `StartError::Secret(String)` | O3, as `Clients::new`'s error beside it |
| `Vec::contains`, `Iterator::filter`, `Iterator::chain` | O3 |
| `Arc::new` | O3 |
| `[&str; 3]::contains(&&str)`, `String::as_str` | G2: `RESERVED.contains(&entry.agent.as_str())` |
## The edits, exactly
Nothing else in these files changes. Each quoted "before" block occurs exactly once in its file
when its turn comes (make them in order). Keep the fields in the order shown.
### `crates/memoryd/src/honcho/mod.rs`
**H1.** Add one line. The line
```rust
pub mod naming;
```
becomes
```rust
pub mod naming;
pub mod read;
```
### `crates/memoryd/src/honcho/client.rs`
**C1.** The lines
```rust
/// A chat's read: a non-streamed chat sends nothing until it is done (M5c-2's `ask_memory`).
pub const CHAT_READ: Duration = Duration::from_secs(180);
```
become
```rust
/// A chat's read: a non-streamed chat sends nothing until it is done (M5c spec, section 9).
pub const CHAT_READ: Duration = Duration::from_secs(150);
```
**C2.** The lines
```rust
/// One workspace's client: no `roncho.toml` read, the timeouts given, the key when there is one.
fn client(
```
become
```rust
/// One workspace's client: no `roncho.toml` read, the timeouts given, the key when there is one.
pub(crate) fn client(
```
**C3.** The lines
```rust
/// The key named `name` in `[secrets]`, if one is configured, read now.
fn key(
```
become
```rust
/// The key named `name` in `[secrets]`, if one is configured, read now.
pub(crate) fn key(
```
### `crates/memoryd/src/honcho/config.rs`
**G1.** The lines
```rust
/// The names of the keys in `[secrets.<name>]`, one per workspace. A read workspace's key is
```
become
```rust
/// The names `ask_memory` gives this agent's own workspaces and the fleet's (M5c spec, section 9),
/// which no `[[honcho.read]]` agent may have.
pub const RESERVED: [&str; 3] = ["own", "untrusted", "fleet"];
/// The names of the keys in `[secrets.<name>]`, one per workspace. A read workspace's key is
```
**G2.** In `read_workspace`, the lines (its spaces are checked: the sentence is one string split
across two lines with a `\` at the end of the first, and the given test compares it letter for
letter)
```rust
"[[honcho.read]] agent {:?} is this agent ([honcho] agent)",
entry.agent
));
}
```
become
```rust
"[[honcho.read]] agent {:?} is this agent ([honcho] agent)",
entry.agent
));
}
if RESERVED.contains(&entry.agent.as_str()) {
return Err(format!(
"[[honcho.read]] agent {:?} is a name ask_memory gives this agent's own workspaces: \
own, untrusted and fleet cannot be agents' names",
entry.agent
));
}
```
### `crates/memoryd/src/honcho/start.rs`
**O1.** Add one line. The line
```rust
use crate::honcho::mirror::Mirror;
```
becomes
```rust
use crate::honcho::mirror::Mirror;
use crate::honcho::read::{ReadTimeouts, Readers};
```
**O2.** The lines
```rust
pub struct Opened {
pub sync: Arc<Mutex<TurnSync>>,
clients: Clients,
```
become
```rust
pub struct Opened {
pub sync: Arc<Mutex<TurnSync>>,
/// The workspaces `recall` and `ask_memory` read (M5c spec, sections 8 and 9).
pub readers: Arc<Readers>,
clients: Clients,
```
**O3.** In `open`, the lines
```rust
for warning in warnings {
(log)(&warning);
}
Ok(Opened {
sync: Arc::new(Mutex::new(sync)),
clients,
```
become
```rust
let (readers, read_warnings) =
Readers::new(honcho, &env, ReadTimeouts::DEFAULT).map_err(StartError::Secret)?;
// The readers load the keys of the three written workspaces again: each warning once.
let read_warnings: Vec<String> = read_warnings
.into_iter()
.filter(|w| !warnings.contains(w))
.collect();
for warning in warnings.into_iter().chain(read_warnings) {
(log)(&warning);
}
Ok(Opened {
sync: Arc::new(Mutex::new(sync)),
readers: Arc::new(readers),
clients,
```
### `docs/runbook.md`
**D1.** In the entry `honcho-config-invalid`, the lines (its spaces are checked: the entry is
read by people, and `scripts/check-runbook.sh` reads the file)
```text
- A `[[honcho.read]]` names this agent, names an agent twice, or names one of this agent's three
workspaces; or its `trust` is not `trusted` or `untrusted`, or its `class` not `public`,
`private` or `secret` (all lower case).
```
become
```text
- A `[[honcho.read]]` names this agent, names an agent twice, names an agent `own`, `untrusted`
or `fleet` (the names `ask_memory` gives this agent's own workspaces), or names one of this
agent's three workspaces; or its `trust` is not `trusted` or `untrusted`, or its `class` not
`public`, `private` or `secret` (all lower case).
```
## Steps
- [ ] **1. Copy the given tests.** From the repository root, three commands:
```sh
cp docs/plans/M5c2/files/crates/memoryd/tests/honcho_read.rs crates/memoryd/tests/
cp docs/plans/M5c2/files/crates/memoryd/tests/honcho_client.rs crates/memoryd/tests/
cp docs/plans/M5c2/files/crates/memoryd/tests/honcho_start.rs crates/memoryd/tests/
```
Then `git status --short` lists `honcho_client.rs` and `honcho_start.rs` as modified and
`honcho_read.rs` as new, all under `crates/memoryd/tests/`.
- [ ] **2. See them fail.** Two commands, each with a shell timeout of 600000 ms:
- `cargo build --tests --keep-going -p memoryd`: one test target, `honcho_read`, does not
compile: `E0432` for ``unresolved import `memoryd::honcho::read` `` and for
``unresolved import `memoryd::honcho::config::RESERVED` ``. Every other target compiles.
- `cargo test --no-fail-fast -p memoryd --test honcho_client --test honcho_start`: two tests
fail: `the_timeouts` in `honcho_client` (`CHAT_READ` is 180 s) and
`a_read_key_that_cannot_be_read_stops_memoryd` in `honcho_start` (`memoryd` does not read
that key yet, so it starts, and the test stops it after 10 s).
- [ ] **3. The new module and the edits.** Copy the module:
```sh
cp docs/plans/M5c2/files/crates/memoryd/src/honcho/read.rs crates/memoryd/src/honcho/
```
Then make edits H1, C1 to C3, G1, G2, O1 to O3 and D1, in order. `cargo check -p memoryd`
prints no error and no warning.
- [ ] **4. The tests.** With a shell timeout of 600000 ms, every test passes: 13 in
`honcho_client`, 8 in `honcho_config`, 3 in `honcho_keys` (and 1 ignored, as before), 8 in
`honcho_read`, 5 in `honcho_start`:
```sh
cargo test -p memoryd --test honcho_read --test honcho_client --test honcho_start \
--test honcho_config --test honcho_keys
```
- [ ] **5. Check.** Two commands:
- `git diff --stat -- crates/memoryd/src` lists exactly `client.rs`, `config.rs`, `mod.rs` and
`start.rs` under `crates/memoryd/src/honcho/` (the new `read.rs` is untracked, so `git diff`
does not list it), and `git status --short crates/memoryd/src` shows `read.rs` as new.
- `git diff --stat -- crates/memoryd/src/serve.rs crates/memoryd/src/recall.rs Cargo.toml Cargo.lock`
prints nothing.
- [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, with a shell timeout of 600000 ms.
Expected last line: `gate: ok`. The test count is the passed total of `cargo test --workspace`
plus one, for the ignored `end_to_end` test that the gate runs on its own: **1403** after this
task, 1394 before it; the 9 new ones are the 8 tests of `honcho_read.rs` and the new test in
`honcho_start.rs`. To count, add up the `N passed` of every `test result:` line the gate's
`cargo test --workspace` printed, and add 1. Never run the gate again only to count.
- [ ] **7. Log and commit.** Add your row to `docs/implementer-log.md`; its Task cell is
`M5c2/05-memoryd-honcho-readers`. `git status --short` lists the four edited sources, the two
replaced tests, `docs/runbook.md` and your log as modified, and `read.rs` and `honcho_read.rs`
as new. Stage with `git add crates/memoryd docs/runbook.md 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 1403 tests.
- `git diff HEAD~1 --stat` lists only the four edited sources, `read.rs`, the three test files of
step 1, `docs/runbook.md` and `docs/implementer-log.md`.
## Stop and report if
- The branch check at the top fails.
- A "before" block is not in its file exactly as shown when its turn comes, or is there more
than once.
- Step 2 shows a different result: another target failing, other errors, other tests failing.
- `cargo check` in step 3 prints an error or a warning. Quote it, and stop. Do not change
`read.rs` to make it compile.
- A test fails in step 4. Do not change a test: stop, and give the failing tests' names and
their messages.
- The fix seems to need a change anywhere else: `serve.rs`, `recall.rs`, `worker.rs`, a change
to `read.rs`, a new dependency. It does not; stop and say why you think so.
- A test outside the given files fails in the gate: check once whether it fails without your
change (`git stash -u`, run it, `git stash pop`), then stop and report both results.
- The gate passes with a count other than 1403.
- A comment in an edit does not fit the code under it. Quote it and stop.
## What the given tests pin
Each of these changes to the finished code makes at least one given test fail (24 tried, all
caught): the search clients given chat's timeout, or chat's the search one; either timeout
constant changed; a read workspace given the fleet's key, the fleet the trusted key, or the own
untrusted workspace the trusted key; the own reader on the untrusted workspace; a key not sent;
a read workspace's trust or class ignored; the fleet trusted, or the own workspace untrusted;
the tags swapped, or `untrusted()` inverted; `recall_sources` holding the own workspace, or not
the fleet's; `untrusted` resolved to the trusted workspace; read agents not resolved, or
resolved by workspace instead of name; the sentence without the read agents' names; the
reserved names allowed; the readers' errors ignored at start; the key file's warning logged
twice.