Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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:- The names.
args.asked()gives the names,ownwhen none is given. Each is resolved withReaders::resolve(task 05). If a name is unknown, nothing is sent, not even for the other name: the answer isresolve's sentence for it (no workspace is named boxmaker9; ask own, untrusted, fleet, boxmaker1 or boxmaker2). - A secret. If
decide::has_secret(&args.question), nothing is sent: the answer isask_memory: not asked; the question looks like it holds a secret. - Both chats at once, one thread each:
workspace_chatat reasoning levelminimal, always (spec 9: alowchat recorded from rift answered with raw tool-call markup), with the question and nothing else.roncho's blockingworkspace_chatrefuses atargetorfilters; neither is set. - Each chat has
wait(150 s inmemoryd,CHAT_WAIT, insidebrokerd'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, orno answer in {wait} ms), never the question or an answer.
- The names.
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 asask_memoryknows 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'ssnippet, 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}: unavailableline for each workspace that did not answer; - the label: the highest class of the answers shown,
privateat the least, and untrusted when any shown answer is (memory-tainted).
- each workspace that answered, in the order named, as an item in task 06's form: a header
recall::one_line(text, cap): task 07's rule with a cap of the caller's.snippetbecomesone_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), andMode::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 testcrates/memoryd/tests/honcho_ask.rs; a new version ofcrates/memoryd/tests/support/honcho.rs(it replaces the file in the tree; the givenhoncho.rs-task08beside it is task 08's, not yours); and the skeletoncrates/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) andanswer.ask(the names, the secret, the threads, the limit, the log) and the types are written; read them before you fill the bodies.askcallschaton 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; refusestargetorfiltersDialecticOptions { 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::Minimalwritten "minimal"in the requestChatResponse::content(&self) -> &strthe answer's text, ""when Honcho sent noneThe request
chatsends 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 newone_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; untrustedandfleetmemory-tainted, making the answer untrusted; a trusted read agentmemorywith its class; an untrusted onememory-tainted(T58);- both workspaces asked at once: both hanging, answered after one limit, not two;
- one chat hanging:
unavailableafter 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 --shortlistscrates/memoryd/tests/support/honcho.rsas modified andhoncho_ask.rsas 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 (DialecticOptionsandReasoningLevel;classify;Item,label,renderandunavailable;one_line) and unused variables (reader,question,asked). -
4. Fill the bodies, one at a time, from their comments, with
cargo check -p memorydafter each:chat, thenanswer. Delete each instruction comment and keep the doc comment above the function. After the last,cargo check -p memorydprints no warning. -
5. The tests. With a shell timeout of 600000 ms, every test passes: 10 in
honcho_ask, and as before 4 inrecall_form, 6 inrecall, 11 inhoncho_recalledand 12 inhoncho_search:cargo test -p memoryd --test honcho_ask --test recall_form --test recall \ --test honcho_recalled --test honcho_search -
6. Check.
git diff --statlists exactlycrates/memoryd/src/honcho/mod.rs,crates/memoryd/src/recall.rsandcrates/memoryd/tests/support/honcho.rs, andgit status --shortshowsask.rsandhoncho_ask.rsas new. -
7. 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: 1444 after this task, 1434 before it; the 10 new ones are the tests ofhoncho_ask.rs. To count, add up theN passedof everytest result:line the gate'scargo test --workspaceprinted, and add- Never run the gate again only to count.
-
8. Log and commit. Add your row to
docs/implementer-log.md; its Task cell isM5c2/10-memoryd-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 1444 tests.git diff HEAD~1 --statlists onlymod.rs,recall.rs,ask.rs,honcho_ask.rs,support/honcho.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, 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 checkreports 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.