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

13 KiB

M5c-2 task 10: ask_memory in memoryd, both chats at once

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, each workspace's chat at once with its own limit Log row: the Task cell is M5c2/10-memoryd-ask-memory.

Goal

ask_memory { question, workspaces } (task 01) asks Honcho's dialectic about one or two workspaces (M5c spec, section 9). brokerd sends it to memoryd (tasks 02, 03), and loopd declares it (task 04). This task writes what memoryd does with it, as a new module crates/memoryd/src/honcho/ask.rs, modelled on task 08's search.rs. Task 12 wires it into memoryd's serving; until then nothing calls it.

  • ask(readers, args, wait, log) -> Asked:
    1. The names. args.asked() gives the names, own when none is given. Each is resolved with Readers::resolve (task 05). If a name is unknown, nothing is sent, not even for the other name: the answer is resolve's sentence for it (no workspace is named boxmaker9; ask own, untrusted, fleet, boxmaker1 or boxmaker2).
    2. A secret. If decide::has_secret(&args.question), nothing is sent: the answer is ask_memory: not asked; the question looks like it holds a secret.
    3. Both chats at once, one thread each: workspace_chat at reasoning level minimal, always (spec 9: a low chat recorded from rift answered with raw tool-call markup), with the question and nothing else. roncho's blocking workspace_chat refuses a target or filters; neither is set.
    4. Each chat has wait (150 s in memoryd, CHAT_WAIT, inside brokerd's 170 s), counted from one deadline taken when the chats start. A chat that failed, or has not answered by then, is unavailable. A workspace that does not exist answers a chat 404 (recorded, fixture 36), so it is unavailable too. A late thread holds no lock; its answer is dropped. Each unavailable workspace is one log line, memoryd: ask_memory: {name} unavailable: {error} (the error's kind and status, or no answer in {wait} ms), never the question or an answer.
  • answer(asked) -> Recalled:
    • each workspace that answered, in the order named, as an item in task 06's form: a header [n] {tag} · {name} · {class}, whose "from" is just the name as ask_memory knows it (own, untrusted, fleet, or the agent's name), and whose tag and class are the workspace's label (task 05); then the answer on one indented line;
    • the text is neutralised, and every control character and line or paragraph separator is a space, as task 07's snippet, but cut to 2,000 characters (ANSWER_CAP), not 600: a chat answers with a paragraph or two of reasoning, not a snippet. An answer that is empty once trimmed is (no answer);
    • then one {name}: unavailable line for each workspace that did not answer;
    • the label: the highest class of the answers shown, private at the least, and untrusted when any shown answer is (memory-tainted).
  • recall::one_line(text, cap): task 07's rule with a cap of the caller's. snippet becomes one_line(text, SNIPPET), so its callers do not change.
  • The fake Honcho gets a new version: a workspace chat answers fixture 19 (the chat recorded from rift at minimal), and Mode::Chat(text) answers a chat with the given text.

Every place that changes

This is the complete list.

Place What happens there
crates/memoryd/src/honcho/ask.rs new: a skeleton you copy, then fill 2 bodies
crates/memoryd/src/honcho/mod.rs pub mod ask; (edit H1)
crates/memoryd/src/recall.rs one_line, and snippet calls it (edit R1)
crates/memoryd/tests/honcho_ask.rs new, given
crates/memoryd/tests/support/honcho.rs new version, given
everything else no change: not serve.rs (task 12), not search.rs, no Cargo.toml

Files

  • Copy, from docs/plans/M5c2/files/ (the same path under it): the new test crates/memoryd/tests/honcho_ask.rs; a new version of crates/memoryd/tests/support/honcho.rs (it replaces the file in the tree; the given honcho.rs-task08 beside it is task 08's, not yours); and the skeleton crates/memoryd/src/honcho/ask.rs.
  • Modify: crates/memoryd/src/honcho/mod.rs, crates/memoryd/src/recall.rs, docs/implementer-log.md.
  • Do not touch: any other file, and in the skeleton anything but its two todo!() bodies.

Facts you need

  • The skeleton is written except two bodies: chat (one workspace's chat) and answer. ask (the names, the secret, the threads, the limit, the log) and the types are written; read them before you fill the bodies. ask calls chat on each thread.

  • roncho's calls and types (this table is complete; roncho's source, in the Cargo registry, is outside the repository and off limits):

    Item What it does
    Client::workspace_chat(&self, &DialecticOptions) -> Result<ChatResponse, roncho::blocking::Error> POST /v3/workspaces/{w}/chat, not streamed; refuses target or filters
    DialecticOptions { session_id, filters, scope, target, query: String, stream, reasoning_level: Option<ReasoningLevel>, response_format, include_evidence } #[derive(Default)]: DialecticOptions { query, reasoning_level: Some(..), ..Default::default() }
    ReasoningLevel::Minimal written "minimal" in the request
    ChatResponse::content(&self) -> &str the answer's text, "" when Honcho sent none

    The request chat sends is exactly {"query": question, "reasoning_level": "minimal"}; the given test checks it.

  • From earlier tasks: classify (M5c-1); Reader::chat() -> &Client, Label::tag(), Label::untrusted(), Label { name, class, .. } (task 05); Item, render, label, unavailable (task 06); Recalled (M5a) and the new one_line (edit R1).

  • The given test honcho_ask.rs (10 tests; about 3 s), against the fake Honcho:

    • the limits and the sentences;
    • the default: one chat, to own's workspace, with exactly {"query", "reasoning_level": "minimal"}; [1] memory · own · private, trusted;
    • untrusted and fleet memory-tainted, making the answer untrusted; a trusted read agent memory with its class; an untrusted one memory-tainted (T58);
    • both workspaces asked at once: both hanging, answered after one limit, not two;
    • one chat hanging: unavailable after the limit, the other answered;
    • a missing workspace (404) and a 503: unavailable, logged by kind and status, never the question;
    • an unknown name: nothing sent, resolve's sentence;
    • a question holding a secret: nothing sent, the secret sentence;
    • a forged header in an answer kept inside its item (T37);
    • the 2,000-character cap counted in characters, neutralising, and (no answer).

    A key and a chat-template tag in it are built from pieces; no assertion prints a tag.

  • The calls the bodies use: DialecticOptions, ReasoningLevel::Minimal, Client::workspace_chat, Result::map_err, classify, ChatResponse::content; one_line, str::trim, Item, render, unavailable, label, slice::join.

The edits, exactly

Nothing else in these files changes. Each quoted "before" block occurs exactly once in its file.

crates/memoryd/src/honcho/mod.rs

H1. Add one line. The line

pub mod client;

becomes

pub mod ask;
pub mod client;

crates/memoryd/src/recall.rs

R1. The lines

/// A hit's text as an item holds it, from the local index or from Honcho: neutralised, every
/// control character (a line break among them) and every Unicode line or paragraph separator a
/// space, then cut to its first `SNIPPET` characters, with no mark.
pub fn snippet(text: &str) -> String {
    neutralise(text)
        .chars()
        .map(|c| {
            if c.is_control() || c == '\u{2028}' || c == '\u{2029}' {
                ' '
            } else {
                c
            }
        })
        .take(SNIPPET)
        .collect()
}

become

/// A hit's text as an item holds it, from the local index or from Honcho: `one_line`, cut to
/// `SNIPPET` characters.
pub fn snippet(text: &str) -> String {
    one_line(text, SNIPPET)
}

/// Text from memory as an item holds it: neutralised, every control character (a line break
/// among them) and every Unicode line or paragraph separator a space, then cut to its first
/// `cap` characters, with no mark.
pub fn one_line(text: &str, cap: usize) -> String {
    neutralise(text)
        .chars()
        .map(|c| {
            if c.is_control() || c == '\u{2028}' || c == '\u{2029}' {
                ' '
            } else {
                c
            }
        })
        .take(cap)
        .collect()
}

Steps

  • 1. Copy the given tests. From the repository root, two commands:

    cp docs/plans/M5c2/files/crates/memoryd/tests/honcho_ask.rs crates/memoryd/tests/
    cp docs/plans/M5c2/files/crates/memoryd/tests/support/honcho.rs crates/memoryd/tests/support/
    

    Then git status --short lists crates/memoryd/tests/support/honcho.rs as modified and honcho_ask.rs as new.

  • 2. See them fail. cargo build --tests --keep-going -p memoryd, with a shell timeout of 600000 ms: one test target, honcho_ask, does not compile: E0432, unresolved import `memoryd::honcho::ask` . Every other target compiles.

  • 3. The skeleton and the edits. Copy the skeleton, then make edits H1 and R1:

    cp docs/plans/M5c2/files/crates/memoryd/src/honcho/ask.rs crates/memoryd/src/honcho/
    

    cargo check -p memoryd: no error, and 7 warnings, all from the unfilled bodies: unused imports (DialecticOptions and ReasoningLevel; classify; Item, label, render and unavailable; one_line) and unused variables (reader, question, asked).

  • 4. Fill the bodies, one at a time, from their comments, with cargo check -p memoryd after each: chat, then answer. Delete each instruction comment and keep the doc comment above the function. After the last, cargo check -p memoryd prints no warning.

  • 5. The tests. With a shell timeout of 600000 ms, every test passes: 10 in honcho_ask, and as before 4 in recall_form, 6 in recall, 11 in honcho_recalled and 12 in honcho_search:

    cargo test -p memoryd --test honcho_ask --test recall_form --test recall \
      --test honcho_recalled --test honcho_search
    
  • 6. Check. git diff --stat lists exactly crates/memoryd/src/honcho/mod.rs, crates/memoryd/src/recall.rs and crates/memoryd/tests/support/honcho.rs, and git status --short shows ask.rs and honcho_ask.rs as new.

  • 7. 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: 1444 after this task, 1434 before it; the 10 new ones are the tests of honcho_ask.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.
  • 8. Log and commit. Add your row to docs/implementer-log.md; its Task cell is M5c2/10-memoryd-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 1444 tests.
  • git diff HEAD~1 --stat lists only mod.rs, recall.rs, ask.rs, honcho_ask.rs, support/honcho.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, or is there more than once.
  • Step 2 or 3 shows a different result: other targets failing, other errors, or warnings about anything but the unfilled bodies.
  • cargo check reports an error after a body, and its comment does not tell you how to fix it. Quote the error, and stop.
  • After step 4 a test fails although each body follows its comment. Do not change a test, the fake or the written parts of the skeleton: stop, and give the failing tests' names and their messages.
  • The fix seems to need a change anywhere else: serve.rs, search.rs, read.rs, recalled.rs, a new dependency, a streamed chat. 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 1444.
  • A comment in the skeleton does not fit its signature. Quote it and stop.

What the given tests pin

Each of these changes to the finished code makes at least one given test fail (18 tried, all caught): reasoning at low, or none; an unknown name skipped instead of refused; the secret check skipped; the default not own; no limit on the wait; the chats asked one after another; a failed chat not reported unavailable; the cap 600, or the raw text uncapped and unflattened; an empty answer shown as empty; the tag, the trust or the class not the workspace's; the unavailable lines left out; a line break kept by one_line; snippet cut at 2,000; the limit 180 s.