memoryd: recall's Honcho part, every source at once with its own limit
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -12,6 +12,7 @@ pub mod naming;
|
|||||||
pub mod read;
|
pub mod read;
|
||||||
pub mod recalled;
|
pub mod recalled;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
pub mod search;
|
||||||
pub mod start;
|
pub mod start;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
//! `recall`'s Honcho part (M5c spec, section 8): every source `Readers::recall_sources` names,
|
||||||
|
//! searched at once, each on its own thread with its own limit, then its answer made from the
|
||||||
|
//! local index's items followed by Honcho's. A query that holds a secret asks nothing. Runs
|
||||||
|
//! outside `memoryd`'s state lock; a source still running at its limit is left to finish, and
|
||||||
|
//! what it finds is dropped.
|
||||||
|
|
||||||
|
use std::sync::{Arc, mpsc};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use bxcore::RecallArgs;
|
||||||
|
use roncho::{Conclusion, ConclusionQuery, MessageSearch};
|
||||||
|
use serde_json::{Map, json};
|
||||||
|
|
||||||
|
use crate::honcho::client::{HonchoError, classify};
|
||||||
|
use crate::honcho::decide::has_secret;
|
||||||
|
use crate::honcho::read::{Reader, Readers, Source};
|
||||||
|
use crate::honcho::recalled::{Item, interleave, items, label, render, unavailable};
|
||||||
|
use crate::recall::{Local, Recalled};
|
||||||
|
use crate::serve::Log;
|
||||||
|
|
||||||
|
/// How long each source has, counted from when the Honcho part starts (M5c spec, section 8).
|
||||||
|
pub const SOURCE_WAIT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// The line in place of Honcho's items when the query holds a known-prefix secret.
|
||||||
|
pub const SECRET: &str = "honcho: not asked; the query looks like it holds a secret";
|
||||||
|
|
||||||
|
/// What the Honcho part found.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Searched {
|
||||||
|
/// The query holds a secret: no source was asked.
|
||||||
|
Secret,
|
||||||
|
/// Each source's kept items, in `recall_sources` order (none for a source that did not
|
||||||
|
/// answer), and the names of those that did not, in the same order.
|
||||||
|
Asked {
|
||||||
|
found: Vec<Vec<Item>>,
|
||||||
|
unavailable: Vec<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The conclusions about `peer` that `peer` drew, as `memoryd` writes them: the observer and the
|
||||||
|
/// observed are the same peer (M5c spec, section 8).
|
||||||
|
fn conclusions(
|
||||||
|
reader: &Reader,
|
||||||
|
peer: &str,
|
||||||
|
query: &str,
|
||||||
|
k: u32,
|
||||||
|
) -> Result<Vec<Conclusion>, HonchoError> {
|
||||||
|
let mut filters = Map::new();
|
||||||
|
filters.insert("observer_id".to_string(), json!(peer));
|
||||||
|
filters.insert("observed_id".to_string(), json!(peer));
|
||||||
|
let asked = ConclusionQuery::new(query).top_k(k).filters(filters);
|
||||||
|
reader
|
||||||
|
.search()
|
||||||
|
.query_conclusions(&asked)
|
||||||
|
.map_err(|e| classify(&e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One source, its calls one after another: the workspace's messages, then for another agent's
|
||||||
|
/// workspace the conclusions about that agent's peer and about the owner's. Its kept items, or
|
||||||
|
/// the first call's failure.
|
||||||
|
pub fn source(reader: &Reader, owner: &str, query: &str, k: u32) -> Result<Vec<Item>, HonchoError> {
|
||||||
|
let messages = reader
|
||||||
|
.search()
|
||||||
|
.search_workspace(&MessageSearch {
|
||||||
|
limit: Some(k),
|
||||||
|
..MessageSearch::new(query)
|
||||||
|
})
|
||||||
|
.map_err(|e| classify(&e))?;
|
||||||
|
let (about_agent, about_owner) = match &reader.source {
|
||||||
|
Source::Agent(peer) => (
|
||||||
|
conclusions(reader, peer, query, k)?,
|
||||||
|
conclusions(reader, owner, query, k)?,
|
||||||
|
),
|
||||||
|
_ => (Vec::new(), Vec::new()),
|
||||||
|
};
|
||||||
|
Ok(items(
|
||||||
|
&reader.label,
|
||||||
|
query,
|
||||||
|
&messages,
|
||||||
|
&about_agent,
|
||||||
|
&about_owner,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask every source of `readers.recall_sources()` at once for `args`, each on a thread of its
|
||||||
|
/// own, and wait for each at most `wait` from now. A source that failed, or that has not answered
|
||||||
|
/// by then, is unavailable; each is logged by its name and the error's kind and status, never the
|
||||||
|
/// query. `Secret`, with nothing sent, when the query holds a known-prefix secret.
|
||||||
|
pub fn search(
|
||||||
|
readers: &Arc<Readers>,
|
||||||
|
owner: &str,
|
||||||
|
args: &RecallArgs,
|
||||||
|
wait: Duration,
|
||||||
|
log: &Log,
|
||||||
|
) -> Searched {
|
||||||
|
if has_secret(&args.query) {
|
||||||
|
return Searched::Secret;
|
||||||
|
}
|
||||||
|
let deadline = Instant::now() + wait;
|
||||||
|
let k = u32::try_from(args.k).unwrap_or(u32::MAX);
|
||||||
|
let names: Vec<String> = readers
|
||||||
|
.recall_sources()
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.label.name.clone())
|
||||||
|
.collect();
|
||||||
|
let (tx, rx) = mpsc::channel::<(usize, Result<Vec<Item>, HonchoError>)>();
|
||||||
|
let mut answers: Vec<Option<Result<Vec<Item>, HonchoError>>> = vec![None; names.len()];
|
||||||
|
for (i, slot) in answers.iter_mut().enumerate() {
|
||||||
|
let (readers, sender) = (Arc::clone(readers), tx.clone());
|
||||||
|
let (owner, query) = (owner.to_string(), args.query.clone());
|
||||||
|
let spawned = std::thread::Builder::new()
|
||||||
|
.name("honcho-recall".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
if let Some(reader) = readers.recall_sources().get(i) {
|
||||||
|
let _ = sender.send((i, source(reader, &owner, &query, k)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if spawned.is_err() {
|
||||||
|
*slot = Some(Err(HonchoError::Unreachable {
|
||||||
|
kind: "thread",
|
||||||
|
status: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(tx);
|
||||||
|
while answers.iter().any(Option::is_none) {
|
||||||
|
let left = deadline.saturating_duration_since(Instant::now());
|
||||||
|
match rx.recv_timeout(left) {
|
||||||
|
Ok((i, answer)) => {
|
||||||
|
if let Some(slot) = answers.get_mut(i) {
|
||||||
|
*slot = Some(answer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut found = Vec::with_capacity(names.len());
|
||||||
|
let mut missing = Vec::new();
|
||||||
|
for (name, answer) in names.into_iter().zip(answers) {
|
||||||
|
match answer {
|
||||||
|
Some(Ok(kept)) => found.push(kept),
|
||||||
|
Some(Err(e)) => {
|
||||||
|
(log)(&format!("memoryd: recall: {name} unavailable: {e}"));
|
||||||
|
found.push(Vec::new());
|
||||||
|
missing.push(name);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
(log)(&format!(
|
||||||
|
"memoryd: recall: {name} unavailable: no answer in {} ms",
|
||||||
|
wait.as_millis()
|
||||||
|
));
|
||||||
|
found.push(Vec::new());
|
||||||
|
missing.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Searched::Asked {
|
||||||
|
found,
|
||||||
|
unavailable: missing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `recall`'s answer from the local index's items and what Honcho found: the local items, then
|
||||||
|
/// Honcho's up to `k` in all, numbered after them; `(no matches for "…")` when there is neither;
|
||||||
|
/// then the `unavailable` lines, or the secret line. The label is the stricter of the two parts'.
|
||||||
|
pub fn answer(local: Local, query: &str, k: u64, searched: Searched) -> Recalled {
|
||||||
|
let k = usize::try_from(k).unwrap_or(usize::MAX);
|
||||||
|
let (shown, tail) = match searched {
|
||||||
|
Searched::Secret => (Vec::new(), SECRET.to_string()),
|
||||||
|
Searched::Asked {
|
||||||
|
found,
|
||||||
|
unavailable: missing,
|
||||||
|
} => {
|
||||||
|
let names: Vec<&str> = missing.iter().map(|s| s.as_str()).collect();
|
||||||
|
(interleave(local.items.len(), k, found), unavailable(&names))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (class, untrusted) = label(&shown);
|
||||||
|
let first = local.items.len() + 1;
|
||||||
|
let mut parts: Vec<String> = local.items;
|
||||||
|
if !shown.is_empty() {
|
||||||
|
parts.push(render(first, &shown));
|
||||||
|
}
|
||||||
|
if parts.is_empty() {
|
||||||
|
parts.push(format!("(no matches for \"{query}\")"));
|
||||||
|
}
|
||||||
|
if !tail.is_empty() {
|
||||||
|
parts.push(tail);
|
||||||
|
}
|
||||||
|
Recalled {
|
||||||
|
text: parts.join("\n"),
|
||||||
|
class: local.class.max(class),
|
||||||
|
untrusted: local.untrusted || untrusted,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
//! `recall`'s Honcho part (M5c spec, section 8), against a fake Honcho that answers from the
|
||||||
|
//! responses recorded from rift: every read workspace and the fleet's searched at once, never the
|
||||||
|
//! agent's own; a source that hangs or fails is one `unavailable` line after its limit while the
|
||||||
|
//! others answer; a missing workspace is no hits; a query holding a secret asks nothing; and the
|
||||||
|
//! answer made from the local items and Honcho's, with the stricter label. Do not edit.
|
||||||
|
|
||||||
|
#[path = "support/honcho.rs"]
|
||||||
|
mod honcho;
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use bxcore::{DataClass, RecallArgs};
|
||||||
|
use honcho::{FakeHoncho, Mode};
|
||||||
|
use memoryd::honcho::config::{HonchoConfig, ReadWorkspace, Trust};
|
||||||
|
use memoryd::honcho::read::{ReadTimeouts, Readers};
|
||||||
|
use memoryd::honcho::search::{SECRET, SOURCE_WAIT, Searched, answer, search};
|
||||||
|
use memoryd::recall::Local;
|
||||||
|
use memoryd::serve::Log;
|
||||||
|
|
||||||
|
const FAST: ReadTimeouts = ReadTimeouts {
|
||||||
|
connect: Duration::from_secs(5),
|
||||||
|
search: Duration::from_secs(5),
|
||||||
|
chat: Duration::from_secs(5),
|
||||||
|
};
|
||||||
|
/// The limit the tests give each source: long, so that a loaded machine never makes a source
|
||||||
|
/// that answers late; a search where every source answers returns at once.
|
||||||
|
const WAIT: Duration = Duration::from_secs(10);
|
||||||
|
/// The limit when a source hangs: the others still answer well inside it.
|
||||||
|
const HANG: Duration = Duration::from_millis(1_500);
|
||||||
|
|
||||||
|
fn read(agent: &str, trust: Trust) -> ReadWorkspace {
|
||||||
|
ReadWorkspace {
|
||||||
|
agent: agent.to_string(),
|
||||||
|
workspace: format!("{agent}-trusted"),
|
||||||
|
trust,
|
||||||
|
class: DataClass::Private,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `boxmaker0`, owned by `kyle`, reading `boxmaker1` (labelled trusted) and `boxmaker2`.
|
||||||
|
fn readers(fake: &FakeHoncho, read: Vec<ReadWorkspace>) -> Arc<Readers> {
|
||||||
|
let config = HonchoConfig {
|
||||||
|
url: fake.url.clone(),
|
||||||
|
agent: "boxmaker0".to_string(),
|
||||||
|
owner: "kyle".to_string(),
|
||||||
|
trusted: "boxmaker0-trusted".to_string(),
|
||||||
|
untrusted: "boxmaker0-untrusted".to_string(),
|
||||||
|
fleet: "fleet".to_string(),
|
||||||
|
read,
|
||||||
|
keys: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
let env = |_: &str| -> Option<OsString> { None };
|
||||||
|
Arc::new(Readers::new(&config, &env, FAST).unwrap().0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two(fake: &FakeHoncho) -> Arc<Readers> {
|
||||||
|
readers(
|
||||||
|
fake,
|
||||||
|
vec![
|
||||||
|
read("boxmaker1", Trust::Trusted),
|
||||||
|
read("boxmaker2", Trust::Untrusted),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A log that keeps its lines.
|
||||||
|
fn log() -> (Log, Arc<Mutex<Vec<String>>>) {
|
||||||
|
let lines = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let sink = Arc::clone(&lines);
|
||||||
|
let log: Log = Arc::new(move |line: &str| sink.lock().unwrap().push(line.to_string()));
|
||||||
|
(log, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn args(query: &str, k: u64) -> RecallArgs {
|
||||||
|
RecallArgs {
|
||||||
|
query: query.to_string(),
|
||||||
|
k,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nothing_local() -> Local {
|
||||||
|
Local {
|
||||||
|
items: Vec::new(),
|
||||||
|
class: DataClass::Private,
|
||||||
|
untrusted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn asked(searched: &Searched) -> (Vec<Vec<String>>, Vec<String>) {
|
||||||
|
match searched {
|
||||||
|
Searched::Asked { found, unavailable } => (
|
||||||
|
found
|
||||||
|
.iter()
|
||||||
|
.map(|items| items.iter().map(|i| i.text.clone()).collect())
|
||||||
|
.collect(),
|
||||||
|
unavailable.clone(),
|
||||||
|
),
|
||||||
|
Searched::Secret => panic!("not asked"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_limit() {
|
||||||
|
assert_eq!(SOURCE_WAIT, Duration::from_secs(5));
|
||||||
|
assert_eq!(
|
||||||
|
SECRET,
|
||||||
|
"honcho: not asked; the query looks like it holds a secret"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read agent's workspace: its messages, the conclusions about its peer and about the owner's;
|
||||||
|
/// the fleet's: its messages. Each kept only with a word of the query, once.
|
||||||
|
#[test]
|
||||||
|
fn every_source_is_searched_with_its_calls() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let (log, lines) = log();
|
||||||
|
let searched = search(&two(&fake), "kyle", &args("tea", 7), WAIT, &log);
|
||||||
|
let (found, unavailable) = asked(&searched);
|
||||||
|
let tea = [
|
||||||
|
"I prefer tea to coffee in the afternoon.",
|
||||||
|
"Noted: tea in the afternoon.",
|
||||||
|
];
|
||||||
|
let with_owner = [tea[0], tea[1], "Kyle prefers tea in the afternoon."];
|
||||||
|
assert_eq!(found, [&with_owner[..], &with_owner[..], &tea[..]]);
|
||||||
|
assert_eq!(unavailable, Vec::<String>::new());
|
||||||
|
assert!(lines.lock().unwrap().is_empty());
|
||||||
|
let mut seen: Vec<(String, serde_json::Value)> = fake
|
||||||
|
.requests()
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| (r.path, r.body.unwrap_or_default()))
|
||||||
|
.collect();
|
||||||
|
seen.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.to_string().cmp(&b.1.to_string())));
|
||||||
|
let query = |peer: &str| {
|
||||||
|
serde_json::json!({ "query": "tea", "top_k": 7,
|
||||||
|
"filters": { "observer_id": peer, "observed_id": peer } })
|
||||||
|
};
|
||||||
|
let search = serde_json::json!({ "query": "tea", "limit": 7 });
|
||||||
|
let w = "/v3/workspaces";
|
||||||
|
assert_eq!(
|
||||||
|
seen,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
format!("{w}/boxmaker1-trusted/conclusions/query"),
|
||||||
|
query("boxmaker1")
|
||||||
|
),
|
||||||
|
(
|
||||||
|
format!("{w}/boxmaker1-trusted/conclusions/query"),
|
||||||
|
query("kyle")
|
||||||
|
),
|
||||||
|
(format!("{w}/boxmaker1-trusted/search"), search.clone()),
|
||||||
|
(
|
||||||
|
format!("{w}/boxmaker2-trusted/conclusions/query"),
|
||||||
|
query("boxmaker2")
|
||||||
|
),
|
||||||
|
(
|
||||||
|
format!("{w}/boxmaker2-trusted/conclusions/query"),
|
||||||
|
query("kyle")
|
||||||
|
),
|
||||||
|
(format!("{w}/boxmaker2-trusted/search"), search.clone()),
|
||||||
|
(format!("{w}/fleet/search"), search),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The agent's own workspaces hold what the local index holds: `recall` never searches them.
|
||||||
|
#[test]
|
||||||
|
fn the_agents_own_workspaces_are_never_searched() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let (log, _) = log();
|
||||||
|
search(&two(&fake), "kyle", &args("tea", 5), WAIT, &log);
|
||||||
|
let requests = fake.requests();
|
||||||
|
assert_eq!(requests.len(), 7);
|
||||||
|
for request in requests {
|
||||||
|
assert!(!request.path.contains("boxmaker0"), "{}", request.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The answer shows each source's hits with its tag, numbered after the local items, one from
|
||||||
|
/// each source in turn.
|
||||||
|
#[test]
|
||||||
|
fn the_answer_shows_each_sources_hits_with_its_tag() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let (log, _) = log();
|
||||||
|
let searched = search(&two(&fake), "kyle", &args("tea", 4), WAIT, &log);
|
||||||
|
let local = Local {
|
||||||
|
items: vec![
|
||||||
|
"[1] memory · session mm-a, 2026-09-20 14:02 UTC, the owner said · private\n tea time"
|
||||||
|
.to_string(),
|
||||||
|
],
|
||||||
|
class: DataClass::Private,
|
||||||
|
untrusted: false,
|
||||||
|
};
|
||||||
|
let got = answer(local, "tea", 4, searched);
|
||||||
|
let headers: Vec<&str> = got
|
||||||
|
.text
|
||||||
|
.lines()
|
||||||
|
.filter(|l| l.starts_with('['))
|
||||||
|
.map(|l| l.split(" · ").next().unwrap())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
headers,
|
||||||
|
[
|
||||||
|
"[1] memory",
|
||||||
|
"[2] memory",
|
||||||
|
"[3] memory-tainted",
|
||||||
|
"[4] memory-tainted"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
got.text.contains("\n[2] memory · boxmaker1: kyle said, "),
|
||||||
|
"{}",
|
||||||
|
got.text
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
got.text
|
||||||
|
.contains("\n[4] memory-tainted · fleet: kyle said, "),
|
||||||
|
"{}",
|
||||||
|
got.text
|
||||||
|
);
|
||||||
|
assert_eq!((got.class, got.untrusted), (DataClass::Private, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// T58: a fleet hit makes the answer untrusted; a hit from a workspace the owner labelled trusted
|
||||||
|
/// does not.
|
||||||
|
#[test]
|
||||||
|
fn a_fleet_hit_taints_the_answer_and_a_trusted_workspaces_does_not() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let (log, _) = log();
|
||||||
|
let one = readers(&fake, vec![read("boxmaker1", Trust::Trusted)]);
|
||||||
|
fake.set_workspace("fleet", Mode::Missing);
|
||||||
|
let got = answer(
|
||||||
|
nothing_local(),
|
||||||
|
"tea",
|
||||||
|
5,
|
||||||
|
search(&one, "kyle", &args("tea", 5), WAIT, &log),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
got.text.contains("[1] memory · boxmaker1: "),
|
||||||
|
"{}",
|
||||||
|
got.text
|
||||||
|
);
|
||||||
|
assert_eq!((got.class, got.untrusted), (DataClass::Private, false));
|
||||||
|
fake.set_workspace("fleet", Mode::Fixtures);
|
||||||
|
fake.set_workspace("boxmaker1-trusted", Mode::Missing);
|
||||||
|
let got = answer(
|
||||||
|
nothing_local(),
|
||||||
|
"tea",
|
||||||
|
5,
|
||||||
|
search(&one, "kyle", &args("tea", 5), WAIT, &log),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
got.text.starts_with("[1] memory-tainted · fleet: "),
|
||||||
|
"{}",
|
||||||
|
got.text
|
||||||
|
);
|
||||||
|
assert_eq!((got.class, got.untrusted), (DataClass::Private, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A source that does not answer is one `unavailable` line after its limit; the others answer.
|
||||||
|
#[test]
|
||||||
|
fn a_hanging_source_is_unavailable_and_the_others_still_answer() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
fake.set_workspace("boxmaker2-trusted", Mode::Hang);
|
||||||
|
let (log, lines) = log();
|
||||||
|
let started = Instant::now();
|
||||||
|
let searched = search(&two(&fake), "kyle", &args("tea", 10), HANG, &log);
|
||||||
|
let took = started.elapsed();
|
||||||
|
assert!(took >= HANG, "{took:?}");
|
||||||
|
assert!(took < Duration::from_secs(6), "{took:?}");
|
||||||
|
let (found, unavailable) = asked(&searched);
|
||||||
|
assert_eq!(unavailable, ["boxmaker2"]);
|
||||||
|
assert_eq!(found.iter().map(Vec::len).collect::<Vec<_>>(), [3, 0, 2]);
|
||||||
|
assert_eq!(
|
||||||
|
*lines.lock().unwrap(),
|
||||||
|
["memoryd: recall: boxmaker2 unavailable: no answer in 1500 ms"]
|
||||||
|
);
|
||||||
|
let got = answer(nothing_local(), "tea", 10, searched);
|
||||||
|
assert!(
|
||||||
|
got.text.ends_with("\nboxmaker2: unavailable"),
|
||||||
|
"{}",
|
||||||
|
got.text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sources are asked at once: when every one hangs, the answer comes after one limit, not
|
||||||
|
/// one per source.
|
||||||
|
#[test]
|
||||||
|
fn every_source_is_asked_at_once() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
fake.set(Mode::Hang);
|
||||||
|
let (log, _) = log();
|
||||||
|
let started = Instant::now();
|
||||||
|
let (found, unavailable) = asked(&search(&two(&fake), "kyle", &args("tea", 5), HANG, &log));
|
||||||
|
let took = started.elapsed();
|
||||||
|
assert!(took < Duration::from_millis(4_500), "{took:?}");
|
||||||
|
assert_eq!(unavailable, ["boxmaker1", "boxmaker2", "fleet"]);
|
||||||
|
assert!(found.iter().all(Vec::is_empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A source whose call fails is unavailable, logged by the error's kind and status only.
|
||||||
|
#[test]
|
||||||
|
fn a_failing_source_is_unavailable_and_logged_without_the_query() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
fake.set_workspace("fleet", Mode::Status(503));
|
||||||
|
fake.set_workspace("boxmaker1-trusted", Mode::Status(422));
|
||||||
|
let (log, lines) = log();
|
||||||
|
let searched = search(&two(&fake), "kyle", &args("tea", 5), WAIT, &log);
|
||||||
|
let (found, unavailable) = asked(&searched);
|
||||||
|
assert_eq!(unavailable, ["boxmaker1", "fleet"]);
|
||||||
|
assert_eq!(found[1].len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
*lines.lock().unwrap(),
|
||||||
|
[
|
||||||
|
"memoryd: recall: boxmaker1 unavailable: Honcho refused the request (status, HTTP 422)",
|
||||||
|
"memoryd: recall: fleet unavailable: Honcho is unreachable (server, HTTP 503)",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let got = answer(nothing_local(), "tea", 5, searched);
|
||||||
|
assert!(
|
||||||
|
got.text
|
||||||
|
.ends_with("\nboxmaker1: unavailable\nfleet: unavailable"),
|
||||||
|
"{}",
|
||||||
|
got.text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Honcho answers a search or a conclusion query in a workspace that does not exist with 200 and
|
||||||
|
/// no hits: the source answered, with nothing.
|
||||||
|
#[test]
|
||||||
|
fn a_missing_workspace_is_no_hits() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
fake.set_workspace("boxmaker1-trusted", Mode::Missing);
|
||||||
|
let (log, lines) = log();
|
||||||
|
let (found, unavailable) = asked(&search(&two(&fake), "kyle", &args("tea", 5), WAIT, &log));
|
||||||
|
assert_eq!(found[0], Vec::<String>::new());
|
||||||
|
assert_eq!(unavailable, Vec::<String>::new());
|
||||||
|
assert!(lines.lock().unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A query that holds a known-prefix secret is sent to no source (M5c spec, section 8).
|
||||||
|
#[test]
|
||||||
|
fn a_query_holding_a_secret_asks_nothing() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let (log, _) = log();
|
||||||
|
let token = format!("{}{}", ["gh", "p_"].concat(), "A1b2".repeat(9));
|
||||||
|
let searched = search(
|
||||||
|
&two(&fake),
|
||||||
|
"kyle",
|
||||||
|
&args(&format!("tea {token}"), 5),
|
||||||
|
WAIT,
|
||||||
|
&log,
|
||||||
|
);
|
||||||
|
assert_eq!(searched, Searched::Secret);
|
||||||
|
assert_eq!(fake.requests().len(), 0);
|
||||||
|
let local = Local {
|
||||||
|
items: vec!["[1] memory · x · private\n tea".to_string()],
|
||||||
|
class: DataClass::Private,
|
||||||
|
untrusted: false,
|
||||||
|
};
|
||||||
|
let got = answer(local, "tea", 5, Searched::Secret);
|
||||||
|
assert_eq!(
|
||||||
|
got.text,
|
||||||
|
format!("[1] memory · x · private\n tea\n{SECRET}")
|
||||||
|
);
|
||||||
|
assert_eq!((got.class, got.untrusted), (DataClass::Private, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Neither the local index nor Honcho has anything: the sentence local `recall` gives, then any
|
||||||
|
/// `unavailable` lines.
|
||||||
|
#[test]
|
||||||
|
fn when_nothing_is_found_the_answer_says_so() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let (log, _) = log();
|
||||||
|
let searched = search(&two(&fake), "kyle", &args("zebra", 5), WAIT, &log);
|
||||||
|
let got = answer(nothing_local(), "zebra", 5, searched);
|
||||||
|
assert_eq!(got.text, "(no matches for \"zebra\")");
|
||||||
|
assert_eq!((got.class, got.untrusted), (DataClass::Private, false));
|
||||||
|
let got = answer(
|
||||||
|
nothing_local(),
|
||||||
|
"zebra",
|
||||||
|
5,
|
||||||
|
Searched::Asked {
|
||||||
|
found: vec![Vec::new()],
|
||||||
|
unavailable: vec!["fleet".to_string()],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(got.text, "(no matches for \"zebra\")\nfleet: unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local index first: when its hits fill `k`, no Honcho hit is shown and none labels the
|
||||||
|
/// answer. The label is the stricter of the two parts'.
|
||||||
|
#[test]
|
||||||
|
fn local_hits_come_first_and_the_labels_join() {
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let (log, _) = log();
|
||||||
|
let local = Local {
|
||||||
|
items: vec![
|
||||||
|
"[1] memory · a · secret\n tea".to_string(),
|
||||||
|
"[2] memory · b · private\n tea".to_string(),
|
||||||
|
],
|
||||||
|
class: DataClass::Secret,
|
||||||
|
untrusted: false,
|
||||||
|
};
|
||||||
|
let searched = search(&two(&fake), "kyle", &args("tea", 2), WAIT, &log);
|
||||||
|
let got = answer(local.clone(), "tea", 2, searched.clone());
|
||||||
|
assert_eq!(got.text, local.items.join("\n"));
|
||||||
|
assert_eq!((got.class, got.untrusted), (DataClass::Secret, false));
|
||||||
|
let got = answer(local, "tea", 3, searched);
|
||||||
|
assert_eq!(got.text.lines().filter(|l| l.starts_with('[')).count(), 3);
|
||||||
|
assert!(
|
||||||
|
got.text.contains("\n[3] memory · boxmaker1: "),
|
||||||
|
"{}",
|
||||||
|
got.text
|
||||||
|
);
|
||||||
|
assert_eq!((got.class, got.untrusted), (DataClass::Secret, false));
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
//! A fake Honcho for tests, on 127.0.0.1 and a port of its own: it answers each request from the
|
//! A fake Honcho for tests, on 127.0.0.1 and a port of its own: it answers each request from the
|
||||||
//! responses recorded from rift (`tests/fixtures/honcho/`), by method and path (a batch of
|
//! responses recorded from rift (`tests/fixtures/honcho/`), by method and path (a batch of
|
||||||
//! messages or conclusions answered with as many as it asked for, up to the two recorded), and
|
//! messages or conclusions answered with as many as it asked for, up to the two recorded), and
|
||||||
//! records every request, body included. It can be set to answer every request with a status instead, or to
|
//! records every request, body included. It can be set to answer every request with a status
|
||||||
//! answer nothing. Stopped, and its held connections released, when dropped; every wait has a
|
//! instead, to answer nothing, or to answer as a workspace that does not exist, for every
|
||||||
//! deadline. Do not edit.
|
//! workspace or for one. Stopped, and its held connections released, when dropped; every wait has
|
||||||
|
//! a deadline. Do not edit.
|
||||||
|
|
||||||
#![allow(dead_code)] // each test file uses a different part of this module
|
#![allow(dead_code)] // each test file uses a different part of this module
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -46,10 +47,15 @@ pub enum Mode {
|
|||||||
Status(u16),
|
Status(u16),
|
||||||
/// Read the request, then answer nothing until the fake is dropped (at most 30 s).
|
/// Read the request, then answer nothing until the fake is dropped (at most 30 s).
|
||||||
Hang,
|
Hang,
|
||||||
|
/// As a workspace that does not exist: a search or a conclusion query answers 200 with no
|
||||||
|
/// hits (fixtures 34, 35), chat 404 (36).
|
||||||
|
Missing,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Shared {
|
struct Shared {
|
||||||
mode: Mutex<Mode>,
|
mode: Mutex<Mode>,
|
||||||
|
/// A mode for one workspace's requests, by its name, instead of `mode`.
|
||||||
|
workspaces: Mutex<HashMap<String, Mode>>,
|
||||||
requests: Mutex<Vec<Request>>,
|
requests: Mutex<Vec<Request>>,
|
||||||
/// What was made, by method and path and id: a second create answers 200, a second delete
|
/// What was made, by method and path and id: a second create answers 200, a second delete
|
||||||
/// 404.
|
/// 404.
|
||||||
@@ -85,6 +91,7 @@ impl FakeHoncho {
|
|||||||
let port = listener.local_addr().unwrap().port();
|
let port = listener.local_addr().unwrap().port();
|
||||||
let shared = Arc::new(Shared {
|
let shared = Arc::new(Shared {
|
||||||
mode: Mutex::new(Mode::Fixtures),
|
mode: Mutex::new(Mode::Fixtures),
|
||||||
|
workspaces: Mutex::new(HashMap::new()),
|
||||||
requests: Mutex::new(Vec::new()),
|
requests: Mutex::new(Vec::new()),
|
||||||
made: Mutex::new(HashSet::new()),
|
made: Mutex::new(HashSet::new()),
|
||||||
stop: AtomicBool::new(false),
|
stop: AtomicBool::new(false),
|
||||||
@@ -112,6 +119,15 @@ impl FakeHoncho {
|
|||||||
*self.shared.mode.lock().unwrap() = mode;
|
*self.shared.mode.lock().unwrap() = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Answer `workspace`'s requests in `mode`, whatever `set` says for the others.
|
||||||
|
pub fn set_workspace(&self, workspace: &str, mode: Mode) {
|
||||||
|
self.shared
|
||||||
|
.workspaces
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(workspace.to_string(), mode);
|
||||||
|
}
|
||||||
|
|
||||||
/// Every request read so far, in order.
|
/// Every request read so far, in order.
|
||||||
pub fn requests(&self) -> Vec<Request> {
|
pub fn requests(&self) -> Vec<Request> {
|
||||||
self.shared.requests.lock().unwrap().clone()
|
self.shared.requests.lock().unwrap().clone()
|
||||||
@@ -222,6 +238,21 @@ fn answer(request: &Request, made: &Mutex<HashSet<String>>) -> (u16, String) {
|
|||||||
request,
|
request,
|
||||||
"conclusions",
|
"conclusions",
|
||||||
),
|
),
|
||||||
|
// A search answers the three recorded messages about tea (fixture 30), whatever it asked.
|
||||||
|
("POST", ["v3", "workspaces", _, "search"]) => fixture("30-workspace-search-limit.json"),
|
||||||
|
// A conclusion query answers the recorded one about the owner (32) when it asks about
|
||||||
|
// `kyle`, else the one about an agent (33).
|
||||||
|
("POST", ["v3", "workspaces", _, "conclusions", "query"]) => {
|
||||||
|
let about = request
|
||||||
|
.body
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|b| b["filters"]["observed_id"].as_str().map(str::to_string));
|
||||||
|
if about.as_deref() == Some("kyle") {
|
||||||
|
fixture("32-conclusions-query-owner.json")
|
||||||
|
} else {
|
||||||
|
fixture("33-conclusions-query-agent.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
("DELETE", ["v3", "workspaces", _, "conclusions", _]) if again => {
|
("DELETE", ["v3", "workspaces", _, "conclusions", _]) if again => {
|
||||||
fixture("16-conclusion-delete-again.json")
|
fixture("16-conclusion-delete-again.json")
|
||||||
}
|
}
|
||||||
@@ -251,9 +282,20 @@ fn serve(mut stream: TcpStream, shared: &Shared) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
shared.requests.lock().unwrap().push(request.clone());
|
shared.requests.lock().unwrap().push(request.clone());
|
||||||
let mode = *shared.mode.lock().unwrap();
|
let workspace = request.path.split('/').nth(3).unwrap_or("").to_string();
|
||||||
|
let mode = match shared.workspaces.lock().unwrap().get(&workspace) {
|
||||||
|
Some(mode) => *mode,
|
||||||
|
None => *shared.mode.lock().unwrap(),
|
||||||
|
};
|
||||||
let (status, body) = match mode {
|
let (status, body) = match mode {
|
||||||
Mode::Fixtures => answer(&request, &shared.made),
|
Mode::Fixtures => answer(&request, &shared.made),
|
||||||
|
Mode::Missing if request.path.ends_with("/search") => {
|
||||||
|
fixture("34-workspace-search-missing-workspace.json")
|
||||||
|
}
|
||||||
|
Mode::Missing if request.path.ends_with("/conclusions/query") => {
|
||||||
|
fixture("35-conclusions-query-missing-workspace.json")
|
||||||
|
}
|
||||||
|
Mode::Missing => fixture("36-workspace-chat-missing-workspace.json"),
|
||||||
Mode::Status(422) => {
|
Mode::Status(422) => {
|
||||||
let input = request.body.clone().unwrap_or(Value::Null);
|
let input = request.body.clone().unwrap_or(Value::Null);
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
|
|||||||
|
|
||||||
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|
||||||
|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| M5c2/08-memoryd-honcho-search | 2026-09-29 | done | 1 | pass | none | Copied the given test `crates/memoryd/tests/honcho_search.rs` (new), a new version of `crates/memoryd/tests/support/honcho.rs` (from `honcho.rs-task08`, replacing the tree's), and the skeleton `crates/memoryd/src/honcho/search.rs` (fill 2 bodies). Step 2 (`cargo build --tests --keep-going -p memoryd`) showed exactly the one named failure: `honcho_search` with one E0432 `unresolved import memoryd::honcho::search`; every other target compiled. Made H1 (`pub mod search;` after `pub mod report;` in `honcho/mod.rs`). Filled its two bodies from their comments, deleting each instruction comment and keeping the doc comment, each from its comment and `cargo check -p memoryd` after: `source` (`search_workspace` with `limit: Some(k)` via `MessageSearch { limit: Some(k), ..MessageSearch::new(query) }`, error `classify(&e)` then `?`; match `&reader.source`, `Source::Agent(peer)` asks `conclusions(reader, peer, query, k)?` then `conclusions(reader, owner, query, k)?` in that order, any other source both `Vec::new()` with no conclusion asked; `items(&reader.label, query, &messages, &about_agent, &about_owner)`); `answer` (`k = usize::try_from(k).unwrap_or(usize::MAX)`; `Secret` -> `(Vec::new(), SECRET.to_string())`, `Asked { found, unavailable: missing }` (field bound `missing`) -> `interleave(local.items.len(), k, found)` and `unavailable(&names)` with `names` the missing names as `Vec<&str>``; `(class, untrusted) = label(&shown)`; `first = local.items.len() + 1`; `parts = local.items`, push `render(first, &shown)` when shown non-empty, `(no matches for "...")` when parts still empty, then `tail` when non-empty; `Recalled { text: parts.join("\n"), class: local.class.max(class), untrusted: local.untrusted || untrusted }`). `cargo check -p memoryd` clean, no warning. `cargo test` passes: 13 in `honcho_client`, 3 in `honcho_keys` (1 ignored), 8 in `honcho_read`, 12 in `honcho_search`, 12 in `honcho_worker`. `git diff --stat` lists exactly `mod.rs` and `support/honcho.rs`; `git status --short` shows `search.rs` and `honcho_search.rs` as new. `make gate` printed `gate: ok` with 1430 tests (1429 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +12 over the 1418 before is exactly the tests of `honcho_search.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c2/07-memoryd-recall-form | 2026-09-29 | done | 1 | pass | one | Copied the two given tests `crates/memoryd/tests/recall_form.rs` (new) and `crates/memoryd/tests/recall.rs` (new version, replacing the tree's). Step 2 (`cargo build --tests --keep-going -p memoryd`) showed exactly the one named failure: `recall_form` with one E0432 `unresolved imports memoryd::recall::Local, item, local, snippet`; every other target compiled. `cargo test --no-fail-fast` on `recall` failed the two named tests `hits_are_tagged_and_labelled` and `long_hits_are_cut`. Made the edits exactly as the task gives: R1 (`use bxcore::sanitize::neutralise;` before the `DataClass` use); R2 (the `Local` struct, `snippet` and `item` inserted after `Recalled`, before the `terms` doc); R3 (`recall` now calls `local` and joins its items or answers `(no matches for "…")`, the body up to `// 1. args.check()?` becomes `local`); R4 (the comment for `local`'s step 4); R5 (`local`'s body builds `items` with `item(&hit.header(i + 1), &snippet(&hit.text))` and returns `Local { items, class, untrusted }`). H1 (dropped `use bxcore::sanitize::neutralise;`); H2 (`use crate::recall::{item, snippet, when}`); H3 (`Item::render` builds the header then calls `item(&header, &self.text)`); H4 (`clean` now calls `snippet`). `cargo check -p memoryd` clean after all edits, no warning. `cargo test` passes: 4 in `recall_form`, 6 in `recall`, 11 in `honcho_recalled`, 2 in `forgery`. `git diff --stat -- crates/memoryd/src` lists only `recall.rs` and `honcho/recalled.rs`; `cargo fmt --all` reordered the `use crate::recall::{when, SNIPPET}` line the task's H2 quoted, so I anchored H2 on the actual `{when, SNIPPET}` line. `make gate` printed `gate: ok` with 1418 tests (1417 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +4 over the 1414 before is exactly the tests of `recall_form.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
| M5c2/07-memoryd-recall-form | 2026-09-29 | done | 1 | pass | one | Copied the two given tests `crates/memoryd/tests/recall_form.rs` (new) and `crates/memoryd/tests/recall.rs` (new version, replacing the tree's). Step 2 (`cargo build --tests --keep-going -p memoryd`) showed exactly the one named failure: `recall_form` with one E0432 `unresolved imports memoryd::recall::Local, item, local, snippet`; every other target compiled. `cargo test --no-fail-fast` on `recall` failed the two named tests `hits_are_tagged_and_labelled` and `long_hits_are_cut`. Made the edits exactly as the task gives: R1 (`use bxcore::sanitize::neutralise;` before the `DataClass` use); R2 (the `Local` struct, `snippet` and `item` inserted after `Recalled`, before the `terms` doc); R3 (`recall` now calls `local` and joins its items or answers `(no matches for "…")`, the body up to `// 1. args.check()?` becomes `local`); R4 (the comment for `local`'s step 4); R5 (`local`'s body builds `items` with `item(&hit.header(i + 1), &snippet(&hit.text))` and returns `Local { items, class, untrusted }`). H1 (dropped `use bxcore::sanitize::neutralise;`); H2 (`use crate::recall::{item, snippet, when}`); H3 (`Item::render` builds the header then calls `item(&header, &self.text)`); H4 (`clean` now calls `snippet`). `cargo check -p memoryd` clean after all edits, no warning. `cargo test` passes: 4 in `recall_form`, 6 in `recall`, 11 in `honcho_recalled`, 2 in `forgery`. `git diff --stat -- crates/memoryd/src` lists only `recall.rs` and `honcho/recalled.rs`; `cargo fmt --all` reordered the `use crate::recall::{when, SNIPPET}` line the task's H2 quoted, so I anchored H2 on the actual `{when, SNIPPET}` line. `make gate` printed `gate: ok` with 1418 tests (1417 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +4 over the 1414 before is exactly the tests of `recall_form.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c2/06-memoryd-honcho-recalled | 2026-09-29 | done | 1 | pass | one | Copied the given test `crates/memoryd/tests/honcho_recalled.rs` and the skeleton `crates/memoryd/src/honcho/recalled.rs` (fill 3 bodies). Made the edits exactly as the task gives: H1 (`pub mod recalled;` after `pub mod read;` in `honcho/mod.rs`); R1 (`when` `pub(crate)` in `recall.rs`). Filled the three bodies from their comments, deleting each instruction comment and keeping the doc comment, `cargo check -p memoryd` after each: `items` (`words(query)`; chain the messages as `(content, message_from(label,m))`, then the agent's conclusions, then the owner's, each as `(content, conclusion_from(label,c))`; keep only when `relevant(&query, content)`, dedupe by the cleaned text `clean(content)` against what is already kept, else `Item { tag: label.tag(), from, class: label.class, untrusted: label.untrusted(), text }`); `interleave` (`room = k.saturating_sub(local)`, round-robin over the sources' `into_iter`, taking the next of each that has one until `room` is filled or a round takes none, so local hits filling `k` leave none); `label` (`fold` from `DataClass::Private` with `DataClass::max`, `untrusted` when any item's is true). After the last, `cargo check` printed no warning. `cargo test -p memoryd --test honcho_recalled --test recall` passes: 11 and 6. `git diff --stat` lists exactly `mod.rs` (1 line) and `recall.rs` (1 line); the two new files are `recalled.rs` and `honcho_recalled.rs`. `make gate` printed `gate: ok` with 1414 tests (1413 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +11 over the 1403 before is exactly the tests of `honcho_recalled.rs`). No new dependency, so `Cargo.lock` unchanged. Deviation: the edit/write layer reordered the `use crate::index::{...}` import on line 10 of `recall.rs` to case-insensitive order (`{class_name, class_of, Index}`) and my follow-up edits to that line did not persist, so I restored the rustfmt-canonical order (`{Index, class_name, class_of}`, which matches HEAD) with an in-place perl edit and confirmed `cargo fmt --all` leaves it. | ornith-1.5-35b-a3b |
|
| M5c2/06-memoryd-honcho-recalled | 2026-09-29 | done | 1 | pass | one | Copied the given test `crates/memoryd/tests/honcho_recalled.rs` and the skeleton `crates/memoryd/src/honcho/recalled.rs` (fill 3 bodies). Made the edits exactly as the task gives: H1 (`pub mod recalled;` after `pub mod read;` in `honcho/mod.rs`); R1 (`when` `pub(crate)` in `recall.rs`). Filled the three bodies from their comments, deleting each instruction comment and keeping the doc comment, `cargo check -p memoryd` after each: `items` (`words(query)`; chain the messages as `(content, message_from(label,m))`, then the agent's conclusions, then the owner's, each as `(content, conclusion_from(label,c))`; keep only when `relevant(&query, content)`, dedupe by the cleaned text `clean(content)` against what is already kept, else `Item { tag: label.tag(), from, class: label.class, untrusted: label.untrusted(), text }`); `interleave` (`room = k.saturating_sub(local)`, round-robin over the sources' `into_iter`, taking the next of each that has one until `room` is filled or a round takes none, so local hits filling `k` leave none); `label` (`fold` from `DataClass::Private` with `DataClass::max`, `untrusted` when any item's is true). After the last, `cargo check` printed no warning. `cargo test -p memoryd --test honcho_recalled --test recall` passes: 11 and 6. `git diff --stat` lists exactly `mod.rs` (1 line) and `recall.rs` (1 line); the two new files are `recalled.rs` and `honcho_recalled.rs`. `make gate` printed `gate: ok` with 1414 tests (1413 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +11 over the 1403 before is exactly the tests of `honcho_recalled.rs`). No new dependency, so `Cargo.lock` unchanged. Deviation: the edit/write layer reordered the `use crate::index::{...}` import on line 10 of `recall.rs` to case-insensitive order (`{class_name, class_of, Index}`) and my follow-up edits to that line did not persist, so I restored the rustfmt-canonical order (`{Index, class_name, class_of}`, which matches HEAD) with an in-place perl edit and confirmed `cargo fmt --all` leaves it. | ornith-1.5-35b-a3b |
|
||||||
| M5c2/05-memoryd-honcho-readers | 2026-09-29 | done | 1 | pass | none | Copied the given test `crates/memoryd/tests/honcho_read.rs` (new), new versions of `crates/memoryd/tests/honcho_client.rs` and `honcho_start.rs` (they replace the tree's), and the new source `crates/memoryd/src/honcho/read.rs` (complete, no `todo!`). Step 2 (`cargo build --tests --keep-going -p memoryd`) showed exactly the two named failures: `honcho_read` with one E0432 `unresolved import memoryd::honcho::config::RESERVED` and one E0432 `unresolved import memoryd::honcho::read`; every other target compiled. `cargo test --no-fail-fast` on `honcho_client` and `honcho_start` failed the two named tests: `the_timeouts` (`CHAT_READ` is 180 s) and `a_read_key_that_cannot_be_read_stops_memoryd` (memoryd starts instead of stopping). Made the edits exactly as the task gives: H1 (`pub mod read;` after `pub mod naming;` in `honcho/mod.rs`); C1 (`CHAT_READ` = 150 s, doc now `M5c spec, section 9`); C2 (`client` `pub(crate)`); C3 (`key` `pub(crate)`); G1 (`RESERVED` `["own", "untrusted", "fleet"]` before the key names, doc naming section 9); G2 (the reserved-names check in `read_workspace` after the this-agent check, its sentence split with `\` as shown); O1 (`use ...read::{ReadTimeouts, Readers}`); O2 (`Opened::readers` `Arc<Readers>` after `sync`, before `clients`); O3 (`Readers::new` beside `Clients::new`, `StartError::Secret`, the readers' warnings filtered to those not among the writers' then logged once); D1 (the reserved names among the `honcho-config-invalid` reasons in `docs/runbook.md`). `cargo check -p memoryd` clean, no warning. `cargo test` passes: 8 in `honcho_read`, 13 in `honcho_client`, 8 in `honcho_config`, 3 in `honcho_keys` (1 ignored), 5 in `honcho_start`. `git diff --stat -- crates/memoryd/src` lists only `client.rs`, `config.rs`, `mod.rs` and `start.rs`; `git diff --stat -- crates/memoryd/src/serve.rs crates/memoryd/src/recall.rs crates/memoryd/src/worker.rs Cargo.toml Cargo.lock` prints nothing. `make gate` printed `gate: ok` with 1403 tests (1402 in `cargo test --workspace`, plus the 1 ignored `end_to_end` the gate runs separately; the +9 over the 1394 before is exactly the 8 `honcho_read` tests and the 1 new `honcho_start` test). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
| M5c2/05-memoryd-honcho-readers | 2026-09-29 | done | 1 | pass | none | Copied the given test `crates/memoryd/tests/honcho_read.rs` (new), new versions of `crates/memoryd/tests/honcho_client.rs` and `honcho_start.rs` (they replace the tree's), and the new source `crates/memoryd/src/honcho/read.rs` (complete, no `todo!`). Step 2 (`cargo build --tests --keep-going -p memoryd`) showed exactly the two named failures: `honcho_read` with one E0432 `unresolved import memoryd::honcho::config::RESERVED` and one E0432 `unresolved import memoryd::honcho::read`; every other target compiled. `cargo test --no-fail-fast` on `honcho_client` and `honcho_start` failed the two named tests: `the_timeouts` (`CHAT_READ` is 180 s) and `a_read_key_that_cannot_be_read_stops_memoryd` (memoryd starts instead of stopping). Made the edits exactly as the task gives: H1 (`pub mod read;` after `pub mod naming;` in `honcho/mod.rs`); C1 (`CHAT_READ` = 150 s, doc now `M5c spec, section 9`); C2 (`client` `pub(crate)`); C3 (`key` `pub(crate)`); G1 (`RESERVED` `["own", "untrusted", "fleet"]` before the key names, doc naming section 9); G2 (the reserved-names check in `read_workspace` after the this-agent check, its sentence split with `\` as shown); O1 (`use ...read::{ReadTimeouts, Readers}`); O2 (`Opened::readers` `Arc<Readers>` after `sync`, before `clients`); O3 (`Readers::new` beside `Clients::new`, `StartError::Secret`, the readers' warnings filtered to those not among the writers' then logged once); D1 (the reserved names among the `honcho-config-invalid` reasons in `docs/runbook.md`). `cargo check -p memoryd` clean, no warning. `cargo test` passes: 8 in `honcho_read`, 13 in `honcho_client`, 8 in `honcho_config`, 3 in `honcho_keys` (1 ignored), 5 in `honcho_start`. `git diff --stat -- crates/memoryd/src` lists only `client.rs`, `config.rs`, `mod.rs` and `start.rs`; `git diff --stat -- crates/memoryd/src/serve.rs crates/memoryd/src/recall.rs crates/memoryd/src/worker.rs Cargo.toml Cargo.lock` prints nothing. `make gate` printed `gate: ok` with 1403 tests (1402 in `cargo test --workspace`, plus the 1 ignored `end_to_end` the gate runs separately; the +9 over the 1394 before is exactly the 8 `honcho_read` tests and the 1 new `honcho_start` test). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
|
||||||
|
|||||||
Reference in New Issue
Block a user