memoryd: ask_memory served beside recall and memory, outside the lock

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-29 10:01:39 -07:00
co-authored by ornith-1.5-35b-a3b
parent 165145edb5
commit e0f0057ac8
4 changed files with 264 additions and 4 deletions
+4
View File
@@ -27,6 +27,10 @@ pub const ANSWER_CAP: usize = 2_000;
/// The answer when the question holds a known-prefix secret.
pub const SECRET: &str = "ask_memory: not asked; the question looks like it holds a secret";
/// 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.
pub const NO_ANSWER: &str = "(no answer)";
+27 -4
View File
@@ -7,6 +7,7 @@ use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
use bxcore::AskMemoryArgs;
use bxcore::{
DataClass, Empty, Envelope, ErrorCode, MemoryArgs, MemoryBlockReply, MemoryCall, Message,
PROTOCOL_VERSION, RecallArgs, SessionId, SessionUpdated, Timestamp, ToolResponse, WireError,
@@ -16,6 +17,7 @@ use bxcore::{
use crate::config::Config;
use crate::edit::{self, Context};
use crate::files::Store;
use crate::honcho::ask;
use crate::honcho::ledger::{self, LedgerError};
use crate::honcho::mirror::Mirror;
use crate::honcho::search;
@@ -318,16 +320,17 @@ impl Memoryd {
Message::Ok(Empty {})
}
/// 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.
/// 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 and memory".to_string(),
message: "memoryd runs only recall, memory and ask_memory".to_string(),
};
}
};
@@ -362,6 +365,26 @@ impl Memoryd {
Ok(reading.recall(local, &args, &self.log))
}
/// `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
/// `notes.md` is indexed. Its answer is labelled private and trusted: it is `memoryd`'s own
/// sentence. A refusal and a flag are logged with the rule's name, never with the text.