Files
boxmaker/docs/plans/M5c2/12-memoryd-serve-ask-memory.md
T
2026-09-28 21:27:10 -07:00

277 lines
12 KiB
Markdown

# M5c-2 task 12: `memoryd` serves `ask_memory`, never under the state lock
**Branch:** `m5c2`. Check it: `git branch --show-current` prints `m5c2` and `git status --short`
is empty; otherwise stop. Never switch branch.
**Commit subject:** `memoryd: ask_memory served beside recall and memory, outside the lock`
**Log row:** the Task cell is `M5c2/12-memoryd-serve-ask-memory`.
## Goal
The last piece of `ask_memory` (M5c spec, section 9). `brokerd` decides it and sends it to
`memoryd` (tasks 02, 03), `loopd` declares it (task 04), and task 10 wrote what `memoryd` does
with it (`honcho/ask.rs`: `ask`, then `answer`). This task makes `memoryd`'s `call` answer it.
- **`call`** gets a third arm, `ask_memory`, which **never takes the state lock**: its chats may
take 150 s, and nothing else may wait behind them. `memory` keeps the lock for the whole call,
and `recall` for its local part (task 09), as before.
- **Its arguments are checked again**, as `recall`'s are (its `recall::local` calls
`RecallArgs::check` first): `AskMemoryArgs` is decoded from the call's arguments (an error is
`ask_memory's arguments: {serde's error}`), then `check()`, whose error is `bxcore`'s sentence.
Either is a failed result, as for `recall`. `brokerd` has checked them already; `memoryd` does
not rely on that.
- **With `[honcho]`**: `ask::ask(&reading.readers, &args, ask::CHAT_WAIT, &self.log)`, then
`ask::answer`, which becomes a `ToolResponse::Result` with its text and its label, as
`recall`'s answer does. An unknown name or a question holding a secret is `answer`'s sentence,
a result too.
- **Without `[honcho]`**: a result, not an error, labelled `private` and trusted, which the model
reads and moves on from: `ask_memory: Honcho is not configured for this agent; recall searches
its own memory` (`ask::NOT_CONFIGURED`).
- **Another tool**: the sentence becomes `memoryd runs only recall, memory and ask_memory`. No
given test or fixture pins the old one.
- **No new fail-closed state.** A workspace that does not answer is an `unavailable` line in a
result, not a stop, and `memoryd` without `[honcho]` answers with a sentence. No runbook entry
and no pointer is needed. `bxctl` has no `ask_memory` command, and its usage does not change.
`serve.rs` is 456 lines after this task (433 after task 11).
## Every place that changes
This is the complete list.
| Place | What happens there |
|---|---|
| `crates/memoryd/src/honcho/ask.rs` | `NOT_CONFIGURED` (edit A1) |
| `crates/memoryd/src/serve.rs` | two `use` lines; `call`'s third arm and its sentence; `ask_memory` (edits E1 to E4) |
| `crates/memoryd/tests/honcho_ask_serve.rs` | new, given |
| everything else | **no change**: not `search.rs`, not the runbook, not `bxctl`, no `Cargo.toml` |
## Files
- Copy, from `docs/plans/M5c2/files/` (the same path under it): the new test
`crates/memoryd/tests/honcho_ask_serve.rs`.
- Modify: `crates/memoryd/src/honcho/ask.rs`, `crates/memoryd/src/serve.rs`,
`docs/implementer-log.md`.
- Do not touch: any other file.
## Facts you need
- **`serve.rs` today**: `call` matches the tool's name; `recall` is `self.recall(&call)` (task
09); `memory` is `self.memory(&self.lock(), &call)`; each returns `Result<Recalled, String>`,
turned into a result or a failed response below the `match`. `self.reading` is
`Option<search::Reading>`, `Some` with `[honcho]` (task 09); its `readers` is an
`Arc<Readers>`, and `&reading.readers` passes as `&Readers`.
- **From task 10**: `ask::ask(&Readers, &AskMemoryArgs, Duration, &Log) -> Asked`,
`ask::answer(Asked) -> Recalled`, `ask::CHAT_WAIT` (150 s). From task 01:
`bxcore::AskMemoryArgs` with `check(&self) -> Result<(), String>`, and
`bxcore::memory::ASK_MEMORY` (`"ask_memory"`).
- **The given test** `honcho_ask_serve.rs` (6 tests; well under a second), through `memoryd`'s
`brokerd` socket, against the fake Honcho:
- `own`'s answer, `[1] memory · own · private`, trusted; a `fleet` answer, `memory-tainted`,
the result untrusted (T58, end to end);
- while every chat hangs, a `memory` call is answered within 2 s and the `ask_memory` is still
waiting; then the fake Honcho is stopped, which ends the chat, and the answer is
`own: unavailable`. (So the test does not wait the 150 s.)
- without `[honcho]`, the sentence above, private and trusted;
- bad arguments: an empty question and three workspaces with `bxcore`'s sentences, an unknown
field with `ask_memory's arguments: unknown field `reasoning``, and nothing sent;
- an unknown name's sentence as the result, nothing sent;
- an unknown tool's new sentence.
- The items the edits use. This table is complete.
| Item | Where |
|---|---|
| `serde_json::from_str::<AskMemoryArgs>`, `AskMemoryArgs::check` | E4, as `recall`'s arm |
| `ask::ask`, `ask::answer`, `ask::CHAT_WAIT`, `ask::NOT_CONFIGURED` | E4 |
| `Recalled { text, class, untrusted }`, `DataClass::Private` | E4 |
## 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).
### `crates/memoryd/src/honcho/ask.rs`
**A1.** The line (its spaces are checked: the sentence is compared letter for letter)
```rust
/// The text of an answer with no content.
```
becomes
```rust
/// The answer without `[honcho]`: a result, not an error, so that the model reads it and moves on.
pub const NOT_CONFIGURED: &str =
"ask_memory: Honcho is not configured for this agent; recall searches its own memory";
/// The text of an answer with no content.
```
### `crates/memoryd/src/serve.rs`
**E1.** Add one line. The lines
```rust
use std::time::Duration;
use bxcore::{
```
become
```rust
use std::time::Duration;
use bxcore::AskMemoryArgs;
use bxcore::{
```
**E2.** Add one line. The line
```rust
use crate::edit::{self, Context};
```
becomes
```rust
use crate::edit::{self, Context};
use crate::honcho::ask;
```
**E3.** The lines (its spaces are checked: the new sentence is compared letter for letter)
```rust
/// A decided call from `brokerd`: `recall` or `memory`. `memory` runs under the one lock;
/// `recall` takes it for the local index only (`recall`). `Failed` carries the sentence for
/// the model.
fn call(&self, call: MemoryCall) -> ToolResponse {
let result = match call.request.tool.as_str() {
bxcore::memory::RECALL => self.recall(&call),
bxcore::memory::MEMORY => self.memory(&self.lock(), &call),
_ => {
return ToolResponse::Failed {
message: "memoryd runs only recall and memory".to_string(),
};
}
};
```
become
```rust
/// A decided call from `brokerd`: `recall`, `memory` or `ask_memory`. `memory` runs under
/// the one lock; `recall` takes it for the local index only (`recall`); `ask_memory` never
/// takes it. `Failed` carries the sentence for the model.
fn call(&self, call: MemoryCall) -> ToolResponse {
let result = match call.request.tool.as_str() {
bxcore::memory::RECALL => self.recall(&call),
bxcore::memory::MEMORY => self.memory(&self.lock(), &call),
bxcore::memory::ASK_MEMORY => self.ask_memory(&call),
_ => {
return ToolResponse::Failed {
message: "memoryd runs only recall, memory and ask_memory".to_string(),
};
}
};
```
**E4.** The line
```rust
/// `memory`: refused, staged for review, or applied and journalled; then what it appended to
```
becomes
```rust
/// `ask_memory` (M5c spec, section 9): the workspaces named, asked by Honcho's chat, never
/// under the lock. Its arguments are checked again, as `recall`'s are. Without `[honcho]`, a
/// sentence that says so, labelled private and trusted.
fn ask_memory(&self, call: &MemoryCall) -> Result<Recalled, String> {
let args: AskMemoryArgs = match serde_json::from_str(&call.request.arguments) {
Ok(args) => args,
Err(e) => return Err(format!("ask_memory's arguments: {e}")),
};
args.check()?;
let Some(reading) = &self.reading else {
return Ok(Recalled {
text: ask::NOT_CONFIGURED.to_string(),
class: DataClass::Private,
untrusted: false,
});
};
let asked = ask::ask(&reading.readers, &args, ask::CHAT_WAIT, &self.log);
Ok(ask::answer(asked))
}
/// `memory`: refused, staged for review, or applied and journalled; then what it appended to
```
## Steps
- [ ] **1. Copy the given test.** From the repository root, one command:
```sh
cp docs/plans/M5c2/files/crates/memoryd/tests/honcho_ask_serve.rs crates/memoryd/tests/
```
- [ ] **2. See it fail.** `cargo test --no-fail-fast -p memoryd --test honcho_ask_serve`, with a
shell timeout of 600000 ms: it compiles, and all 6 tests fail, since `memoryd` answers every
`ask_memory` with `memoryd runs only recall and memory` (the hang test after its 10 s
deadline for the chat, which is never sent).
- [ ] **3. The edits.** Make edits A1 and E1 to E4, 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: 6 in
`honcho_ask_serve`, and as before 10 in `honcho_ask`, 4 in `honcho_recall_serve`, 4 in
`serve_admin` and 8 in `serve`:
```sh
cargo test -p memoryd --test honcho_ask_serve --test honcho_ask --test honcho_recall_serve \
--test serve_admin --test serve
```
- [ ] **5. Check.** Two commands:
- `git diff --stat` lists exactly `crates/memoryd/src/honcho/ask.rs` and
`crates/memoryd/src/serve.rs`.
- `wc -l crates/memoryd/src/serve.rs` prints 456.
- [ ] **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: **1454** after this
task, 1448 before it; the 6 new ones are the tests of `honcho_ask_serve.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/12-memoryd-serve-ask-memory`. 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 1454 tests.
- `git diff HEAD~1 --stat` lists only `ask.rs`, `serve.rs`, `honcho_ask_serve.rs` 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: a compile error, or a test passing.
- `cargo check` in step 3 prints an error or a warning. Quote it, and stop.
- 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: `ask.rs` beyond A1, `search.rs`, the runbook,
`bxctl`, a new configuration key. It does not; stop and say why you think so.
- A test outside the given file 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 1454.
- 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 (9 tried, all
caught): `ask_memory` under the state lock; not routed at all; its arguments not checked, or a
decoding error without `ask_memory's arguments: `; without `[honcho]`, an error instead of a
result, a result labelled untrusted, or other words; the unknown tool's old sentence; the
answer's label dropped.