Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
12 KiB
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.
callgets a third arm,ask_memory, which never takes the state lock: its chats may take 150 s, and nothing else may wait behind them.memorykeeps the lock for the whole call, andrecallfor its local part (task 09), as before.- Its arguments are checked again, as
recall's are (itsrecall::localcallsRecallArgs::checkfirst):AskMemoryArgsis decoded from the call's arguments (an error isask_memory's arguments: {serde's error}), thencheck(), whose error isbxcore's sentence. Either is a failed result, as forrecall.brokerdhas checked them already;memoryddoes not rely on that. - With
[honcho]:ask::ask(&reading.readers, &args, ask::CHAT_WAIT, &self.log), thenask::answer, which becomes aToolResponse::Resultwith its text and its label, asrecall's answer does. An unknown name or a question holding a secret isanswer's sentence, a result too. - Without
[honcho]: a result, not an error, labelledprivateand 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
unavailableline in a result, not a stop, andmemorydwithout[honcho]answers with a sentence. No runbook entry and no pointer is needed.bxctlhas noask_memorycommand, 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 testcrates/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.rstoday:callmatches the tool's name;recallisself.recall(&call)(task 09);memoryisself.memory(&self.lock(), &call); each returnsResult<Recalled, String>, turned into a result or a failed response below thematch.self.readingisOption<search::Reading>,Somewith[honcho](task 09); itsreadersis anArc<Readers>, and&reading.readerspasses 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::AskMemoryArgswithcheck(&self) -> Result<(), String>, andbxcore::memory::ASK_MEMORY("ask_memory"). - The given test
honcho_ask_serve.rs(6 tests; well under a second), throughmemoryd'sbrokerdsocket, against the fake Honcho:own's answer,[1] memory · own · private, trusted; afleetanswer,memory-tainted, the result untrusted (T58, end to end);- while every chat hangs, a
memorycall is answered within 2 s and theask_memoryis still waiting; then the fake Honcho is stopped, which ends the chat, and the answer isown: 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 withask_memory's arguments: unknown fieldreasoning``, 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::checkE4, as recall's armask::ask,ask::answer,ask::CHAT_WAIT,ask::NOT_CONFIGUREDE4 Recalled { text, class, untrusted },DataClass::PrivateE4
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)
/// The text of an answer with no content.
becomes
/// 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
use std::time::Duration;
use bxcore::{
become
use std::time::Duration;
use bxcore::AskMemoryArgs;
use bxcore::{
E2. Add one line. The line
use crate::edit::{self, Context};
becomes
use crate::edit::{self, Context};
use crate::honcho::ask;
E3. The lines (its spaces are checked: the new sentence is compared letter for letter)
/// 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
/// 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
/// `memory`: refused, staged for review, or applied and journalled; then what it appended to
becomes
/// `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:
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, sincememorydanswers everyask_memorywithmemoryd 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 memorydprints 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 inhoncho_ask, 4 inhoncho_recall_serve, 4 inserve_adminand 8 inserve: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 --statlists exactlycrates/memoryd/src/honcho/ask.rsandcrates/memoryd/src/serve.rs.wc -l crates/memoryd/src/serve.rsprints 456.
-
6. Run the gate.
cargo fmt --all, thenmake gate, with a shell timeout of 600000 ms. Expected last line:gate: ok. The test count is the passed total ofcargo test --workspaceplus one, for the ignoredend_to_endtest that the gate runs on its own: 1454 after this task, 1448 before it; the 6 new ones are the tests ofhoncho_ask_serve.rs. To count, add up theN passedof everytest result:line the gate'scargo test --workspaceprinted, 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 isM5c2/12-memoryd-serve-ask-memory. Stage withgit add crates/memoryd docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.Cargo.lockdoes not change in this task; thegit addis harmless.
Done when
make gateprintsgate: okwith 1454 tests.git diff HEAD~1 --statlists onlyask.rs,serve.rs,honcho_ask_serve.rsanddocs/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 checkin 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.rsbeyond 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.