Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
13 KiB
M5c-2 task 09: recall asks Honcho, outside 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: recall asks Honcho after the local index, outside the state lock
Log row: the Task cell is M5c2/09-memoryd-recall-honcho.
Goal
Task 08 wrote recall's Honcho part (honcho/search.rs): search asks every source at once,
each with its own limit, and answer makes the answer from the local index's items and
Honcho's. This task makes memoryd's recall use it (M5c spec, section 8).
-
With
[honcho], arecallcall:- takes the state lock, runs
recall::local(task 07: the search, the recall log, the local items and their label), and releases the lock; - then, with no lock held, runs the Honcho part and makes the answer:
search::Reading::recall, a new method that issearchthenanswer.
So a source that hangs for its 5 s holds up no
memorycall, no memory block and no otherrecall: before this task,callheld the lock for the whole of any call. - takes the state lock, runs
-
Without
[honcho],recallis exactly as before:recall::recallunder the lock. -
memorycalls are unchanged: under the lock for the whole call. -
At start, with
[honcho],startkeeps whatrecallreads,search::Reading { readers, owner, wait }: the readers task 05 built (opened.readers, taken before the worker's thread takes the rest ofOpened), the owner's peer from[honcho](honcho.owner), andsearch::SOURCE_WAIT(5 s).
serve.rs is near the 500-line limit (495 lines after this task), so Reading and its method go
into search.rs, not serve.rs.
Every place that changes
This is the complete list.
| Place | What happens there |
|---|---|
crates/memoryd/src/honcho/search.rs |
Reading and Reading::recall (edit S1) |
crates/memoryd/src/serve.rs |
a use line; Memoryd::reading; start builds it; call and recall (edits E1 to E7) |
crates/memoryd/tests/honcho_recall_serve.rs |
new, given |
| everything else | no change: not recall.rs, not start.rs, not the worker, no Cargo.toml |
Files
- Copy, from
docs/plans/M5c2/files/(the same path under it): the new testcrates/memoryd/tests/honcho_recall_serve.rs. - Modify:
crates/memoryd/src/honcho/search.rs,crates/memoryd/src/serve.rs,docs/implementer-log.md. - Do not touch: any other file.
Facts you need
serve.rstoday.startopens the Honcho side (worker::open, returningOpened) whenconfig.honcho_config()?isSome(honcho), and later movesopenedinto the worker's thread (worker::spawn).Opened::readersis anArc<Readers>(task 05).calltakes the state lock (self.lock()) first, then runsrecallormemorywith it.- Taking the lock for a block only:
let local = { let state = self.lock(); … };releases the lock at the block's closing brace, whenstateis dropped. The Honcho part runs after it. let … else:let Some(reading) = &self.reading else { …; return …; };runs theelseblock (which must return) when there is no[honcho].- What
recallanswers with[honcho](task 08'sanswer): the local items, then Honcho's up tokin all, numbered after them, then any{source}: unavailableline;(no matches for "…")when there is nothing. A failed or late source is logged bysearch, never with the query. - The given test
honcho_recall_serve.rs(4 tests; about 5 s), throughmemoryd'sbrokerdsocket, against the fake Honcho (task 08's version):- a local hit, then
boxmaker1's (labelled trusted, among them a conclusion about the owner's peer,kyle) and the fleet's; the answer untrusted because a fleet hit is shown (T58); the agent's own workspaces never searched; with the fleet missing, no fleet hit and a trusted answer; - while the fleet hangs, a
memorycall answered within 2 s and therecallstill waiting; then therecallanswered withfleet: unavailable, and the line logged; - nothing found anywhere:
(no matches for "zebra"); - without
[honcho]: the local answer exactly as before, and nothing logged.
- a local hit, then
- The items the edits use. This table is complete.
Item Where search::search,search::answer,search::SOURCE_WAIT(task 08)S1, E4 recall::local,recall::recall(task 07)E7 Arc::cloneE4
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). Keep the fields in the order shown.
crates/memoryd/src/honcho/search.rs
S1. The line
/// What the Honcho part found.
becomes
/// What `recall` reads in Honcho: the sources, the owner's peer, and each source's limit
/// (`SOURCE_WAIT` in `memoryd`).
pub struct Reading {
pub readers: Arc<Readers>,
pub owner: String,
pub wait: Duration,
}
impl Reading {
/// `recall`'s answer from the local index's items and Honcho's: `search`, then `answer`.
pub fn recall(&self, local: Local, args: &RecallArgs, log: &Log) -> Recalled {
let searched = search(&self.readers, &self.owner, args, self.wait, log);
answer(local, &args.query, args.k, searched)
}
}
/// What the Honcho part found.
crates/memoryd/src/serve.rs
E1. Add one line. The line
use crate::honcho::report;
becomes
use crate::honcho::report;
use crate::honcho::search;
E2. In struct Memoryd, the lines
mirror: Option<Mirror>,
log: Log,
}
become
mirror: Option<Mirror>,
/// With `[honcho]`: what `recall` reads beside the local index, outside `state`.
reading: Option<search::Reading>,
log: Log,
}
E3. In start, the lines
let opened = match config.honcho_config()? {
Some(honcho) => Some(worker::open(&config, &honcho, &log)?),
None => None,
};
become
let (opened, owner) = match config.honcho_config()? {
Some(honcho) => (
Some(worker::open(&config, &honcho, &log)?),
honcho.owner.clone(),
),
None => (None, String::new()),
};
E4. In start, the lines
let (honcho, mirror) = match opened {
Some(opened) => {
let sync = Arc::clone(&opened.sync);
let mirror = opened.mirror();
worker::spawn(opened, &config, Arc::clone(&wake), Arc::clone(&log))?;
(Some(sync), Some(mirror))
}
None => (None, None),
};
become
let (honcho, mirror, reading) = match opened {
Some(opened) => {
let sync = Arc::clone(&opened.sync);
let mirror = opened.mirror();
let reading = search::Reading {
readers: Arc::clone(&opened.readers),
owner,
wait: search::SOURCE_WAIT,
};
worker::spawn(opened, &config, Arc::clone(&wake), Arc::clone(&log))?;
(Some(sync), Some(mirror), Some(reading))
}
None => (None, None, None),
};
E5. In start, the lines
wake,
mirror,
log,
}),
become
wake,
mirror,
reading,
log,
}),
E6. The lines
/// A decided call from `brokerd`: `recall` or `memory`, under the one lock. `Failed` carries the
/// sentence for the model.
fn call(&self, call: MemoryCall) -> ToolResponse {
let state = self.lock();
let result = match call.request.tool.as_str() {
bxcore::memory::RECALL => self.recall(&state, &call),
bxcore::memory::MEMORY => self.memory(&state, &call),
become
/// 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),
E7. The lines
/// `recall`: search, labelled by what it found.
fn recall(&self, state: &State, call: &MemoryCall) -> Result<Recalled, String> {
let args: RecallArgs = match serde_json::from_str(&call.request.arguments) {
Ok(args) => args,
Err(e) => return Err(format!("recall's arguments: {e}")),
};
recall::recall(
&state.index,
&self.store,
&args,
call.request.session.as_str(),
Timestamp::now(),
)
}
become
/// `recall`: search, labelled by what it found. Without `[honcho]`, the local index under the
/// lock, as before. With it, the local index under the lock, then, with the lock released,
/// Honcho's sources (M5c spec, section 8): a source that hangs holds up no other request.
fn recall(&self, call: &MemoryCall) -> Result<Recalled, String> {
let args: RecallArgs = match serde_json::from_str(&call.request.arguments) {
Ok(args) => args,
Err(e) => return Err(format!("recall's arguments: {e}")),
};
let session = call.request.session.as_str();
let Some(reading) = &self.reading else {
let state = self.lock();
return recall::recall(&state.index, &self.store, &args, session, Timestamp::now());
};
let local = {
let state = self.lock();
recall::local(&state.index, &self.store, &args, session, Timestamp::now())?
};
Ok(reading.recall(local, &args, &self.log))
}
Steps
-
1. Copy the given test. From the repository root, one command:
cp docs/plans/M5c2/files/crates/memoryd/tests/honcho_recall_serve.rs crates/memoryd/tests/ -
2. See it fail.
cargo test --no-fail-fast -p memoryd --test honcho_recall_serve, with a shell timeout of 600000 ms: it compiles, and two tests fail:a_fleet_hit_makes_the_answer_untrusted(no Honcho hit is shown yet) anda_hanging_source_holds_up_no_memory_call(the fleet is never searched:the fleet was never searched). The other two pass already. -
3. The edits. Make edits S1 and E1 to E7, in order.
cargo check -p memorydprints no error and no warning. -
4. The tests. With a shell timeout of 600000 ms, every test passes: 4 in
honcho_recall_serve, and as before 12 inhoncho_search, 6 inhoncho_serve, 8 inserveand 5 inserve_sanitize:cargo test -p memoryd --test honcho_recall_serve --test honcho_serve --test serve \ --test serve_sanitize --test honcho_search -
5. Check. Two commands:
git diff --statlists exactlycrates/memoryd/src/honcho/search.rsandcrates/memoryd/src/serve.rs.wc -l crates/memoryd/src/serve.rsprints 495.
-
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: 1434 after this task, 1430 before it; the 4 new ones are the tests ofhoncho_recall_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/09-memoryd-recall-honcho. 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 1434 tests.git diff HEAD~1 --statlists onlysearch.rs,serve.rs,honcho_recall_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 other tests failing.
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:
recall.rs,start.rs,read.rs, the worker, a configuration key for the limit. It does not; stop and say why you think so. serve.rspasses 500 lines, or the gate's line check fails.- 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 1434.
- 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 (8 tried, all
caught): the state lock held across the Honcho part; Honcho not asked though configured; no
Reading built at start; the agent's peer passed as the owner's; the path without [honcho]
going through the Honcho answer; Reading::recall dropping Honcho's label; a zero limit; the
local items left out of the answer.