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

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.

  • 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)

/// 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, 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:

    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.