# 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`, turned into a result or a failed response below the `match`. `self.reading` is `Option`, `Some` with `[honcho]` (task 09); its `readers` is an `Arc`, 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::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 { 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.