review: a memory write wakes the Honcho worker
A write, an undo or an accepted write decided into the ledger waited out the worker's 30 s idle sleep. Mirror::journalled now says whether it added a line, and memoryd wakes the worker when it did. The Wake lock is a leaf, taken under the index lock as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -225,9 +225,16 @@ impl Mirror {
|
|||||||
/// Decide the journalled edit `id` (the latest when `None`), called under the index lock
|
/// Decide the journalled edit `id` (the latest when `None`), called under the index lock
|
||||||
/// right after it was journalled, so the ledger has the journal's order; the ledger's lock is
|
/// right after it was journalled, so the ledger has the journal's order; the ledger's lock is
|
||||||
/// taken inside the index lock, and before an undo is decided against it. Its parts are
|
/// taken inside the index lock, and before an undo is decided against it. Its parts are
|
||||||
/// `Edit { id, part }` from 0, numbering what is decided. An edit already decided, or asking nothing, adds no line. A
|
/// `Edit { id, part }` from 0, numbering what is decided. An edit already decided, or asking
|
||||||
/// failure is logged; the edit itself stands.
|
/// nothing, adds no line. A failure is logged; the edit itself stands. Whether any line was
|
||||||
pub fn journalled(&self, store: &Store, id: Option<&str>, now: Timestamp, log: &dyn Fn(&str)) {
|
/// added, so that the worker is woken only when there is something new.
|
||||||
|
pub fn journalled(
|
||||||
|
&self,
|
||||||
|
store: &Store,
|
||||||
|
id: Option<&str>,
|
||||||
|
now: Timestamp,
|
||||||
|
log: &dyn Fn(&str),
|
||||||
|
) -> bool {
|
||||||
let records = match journal::records(store) {
|
let records = match journal::records(store) {
|
||||||
Ok(records) => records,
|
Ok(records) => records,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -235,7 +242,7 @@ impl Mirror {
|
|||||||
"memoryd: {e}; the edit is not mirrored in Honcho; {}",
|
"memoryd: {e}; the edit is not mirrored in Honcho; {}",
|
||||||
ledger::RUNBOOK
|
ledger::RUNBOOK
|
||||||
));
|
));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let found = match id {
|
let found = match id {
|
||||||
@@ -243,7 +250,7 @@ impl Mirror {
|
|||||||
None => records.last(),
|
None => records.last(),
|
||||||
};
|
};
|
||||||
let Some(record) = found else {
|
let Some(record) = found else {
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
let session = session_of(record, &records);
|
let session = session_of(record, &records);
|
||||||
let mut book = self.ledger.lock().unwrap_or_else(|p| p.into_inner());
|
let mut book = self.ledger.lock().unwrap_or_else(|p| p.into_inner());
|
||||||
@@ -251,9 +258,10 @@ impl Mirror {
|
|||||||
if record.undoes.is_some() {
|
if record.undoes.is_some() {
|
||||||
actions.retain(|a| brought_back(&book, a));
|
actions.retain(|a| brought_back(&book, a));
|
||||||
}
|
}
|
||||||
|
let mut added = false;
|
||||||
for (part, action) in actions.into_iter().enumerate() {
|
for (part, action) in actions.into_iter().enumerate() {
|
||||||
let Ok(part) = u32::try_from(part) else {
|
let Ok(part) = u32::try_from(part) else {
|
||||||
return;
|
return added;
|
||||||
};
|
};
|
||||||
let item = Item::Edit {
|
let item = Item::Edit {
|
||||||
id: record.id.clone(),
|
id: record.id.clone(),
|
||||||
@@ -268,8 +276,10 @@ impl Mirror {
|
|||||||
action,
|
action,
|
||||||
}) {
|
}) {
|
||||||
log(&format!("memoryd: {e}; {}", ledger::RUNBOOK));
|
log(&format!("memoryd: {e}; {}", ledger::RUNBOOK));
|
||||||
return;
|
return added;
|
||||||
}
|
}
|
||||||
|
added = true;
|
||||||
}
|
}
|
||||||
|
added
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,9 +228,12 @@ impl Memoryd {
|
|||||||
|
|
||||||
/// With `[honcho]`: decide journalled edit `id` (the latest when `None`) into the ledger. Called
|
/// With `[honcho]`: decide journalled edit `id` (the latest when `None`) into the ledger. Called
|
||||||
/// under the index lock, so edits are decided in the journal's order (M5c spec, section 4).
|
/// under the index lock, so edits are decided in the journal's order (M5c spec, section 4).
|
||||||
|
/// What it decided is sent at once: the worker is woken.
|
||||||
fn mirror(&self, id: Option<&str>) {
|
fn mirror(&self, id: Option<&str>) {
|
||||||
if let Some(mirror) = &self.mirror {
|
if let Some(mirror) = &self.mirror
|
||||||
mirror.journalled(&self.store, id, Timestamp::now(), &*self.log);
|
&& mirror.journalled(&self.store, id, Timestamp::now(), &*self.log)
|
||||||
|
{
|
||||||
|
self.wake.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
//! A memory write wakes the Honcho worker (M5c spec, section 4: a write reaches Honcho "at once";
|
||||||
|
//! review of M5c-1, finding 3): with the worker asleep for its 30 s idle wait, the memory tool's
|
||||||
|
//! conclusion and the owner's undo's deletion both reach the fake Honcho within 5 s.
|
||||||
|
|
||||||
|
#[path = "support/honcho.rs"]
|
||||||
|
mod honcho;
|
||||||
|
#[path = "support/tmp.rs"]
|
||||||
|
mod tmp;
|
||||||
|
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use bxcore::{
|
||||||
|
CallId, DataClass, Envelope, MemoryAdmin, MemoryCall, MemoryCommand, Message, SessionId,
|
||||||
|
ToolRequest, read_frame, write_frame,
|
||||||
|
};
|
||||||
|
use honcho::FakeHoncho;
|
||||||
|
use memoryd::config::Config;
|
||||||
|
use memoryd::honcho::worker::Tuning;
|
||||||
|
use memoryd::serve;
|
||||||
|
use tmp::TempDir;
|
||||||
|
|
||||||
|
fn ask(socket: &Path, msg: Message) -> Message {
|
||||||
|
let mut stream = UnixStream::connect(socket).unwrap();
|
||||||
|
stream
|
||||||
|
.set_read_timeout(Some(Duration::from_secs(10)))
|
||||||
|
.unwrap();
|
||||||
|
let env = Envelope {
|
||||||
|
v: 1,
|
||||||
|
id: 1,
|
||||||
|
r#final: true,
|
||||||
|
msg,
|
||||||
|
};
|
||||||
|
write_frame(&mut stream, &env).unwrap();
|
||||||
|
read_frame(&mut stream).unwrap().msg
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many requests of `method` to a path ending `suffix` the fake has had.
|
||||||
|
fn count(fake: &FakeHoncho, method: &str, suffix: &str) -> usize {
|
||||||
|
fake.requests()
|
||||||
|
.iter()
|
||||||
|
.filter(|r| {
|
||||||
|
r.method == method && r.path.contains("/conclusions") && r.path.ends_with(suffix)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits up to 5 s for `done`; true if it came.
|
||||||
|
fn within_5s(done: impl Fn() -> bool) -> bool {
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
if done() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_memory_write_and_an_undo_wake_the_worker() {
|
||||||
|
// The worker `serve` starts waits this long when idle: far past every deadline here.
|
||||||
|
assert!(Tuning::DEFAULT.idle >= Duration::from_secs(30));
|
||||||
|
let fake = FakeHoncho::start();
|
||||||
|
let dir = TempDir::new("wake-serve");
|
||||||
|
let soul = dir.write("etc/soul.md", "You are Boxmaker.\n");
|
||||||
|
let text = format!(
|
||||||
|
"[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n[honcho]\nurl = \"{}\"\nagent = \"boxmaker0\"\nowner = \"kyle\"\ntrusted = \"boxmaker0-trusted\"\nuntrusted = \"boxmaker0-untrusted\"\nfleet = \"fleet\"\n",
|
||||||
|
dir.path().display(),
|
||||||
|
soul.display(),
|
||||||
|
fake.url
|
||||||
|
);
|
||||||
|
let config = Config::parse(&text).unwrap();
|
||||||
|
let lines = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let sink = Arc::clone(&lines);
|
||||||
|
let log: serve::Log = Arc::new(move |l: &str| sink.lock().unwrap().push(l.to_string()));
|
||||||
|
let running = serve::start(config.clone(), log).unwrap();
|
||||||
|
std::thread::spawn(move || running.run());
|
||||||
|
// Let the worker find nothing to send and fall asleep.
|
||||||
|
std::thread::sleep(Duration::from_millis(200));
|
||||||
|
|
||||||
|
let call = Message::MemoryCall(MemoryCall {
|
||||||
|
request: ToolRequest {
|
||||||
|
session: SessionId::new("mm-a").unwrap(),
|
||||||
|
call: CallId(1),
|
||||||
|
tool: "memory".to_string(),
|
||||||
|
arguments: r#"{"action":"add","file":"user","text":"Prefers UTC."}"#.to_string(),
|
||||||
|
},
|
||||||
|
taint: DataClass::Private,
|
||||||
|
untrusted: false,
|
||||||
|
});
|
||||||
|
assert!(matches!(
|
||||||
|
ask(&config.broker_socket(), call),
|
||||||
|
Message::ToolResponse(_)
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
within_5s(|| count(&fake, "POST", "/conclusions") == 1),
|
||||||
|
"the conclusion waited: {:?} {:?}",
|
||||||
|
fake.requests(),
|
||||||
|
lines.lock().unwrap()
|
||||||
|
);
|
||||||
|
std::thread::sleep(Duration::from_millis(200));
|
||||||
|
|
||||||
|
let log = ask(
|
||||||
|
&config.admin_socket(),
|
||||||
|
Message::MemoryAdmin(MemoryAdmin {
|
||||||
|
command: MemoryCommand::Log,
|
||||||
|
arg: String::new(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let Message::MemoryAdminReply(reply) = log else {
|
||||||
|
panic!("{log:?}")
|
||||||
|
};
|
||||||
|
let id = reply.text.split(" ").next().unwrap().to_string();
|
||||||
|
let undone = ask(
|
||||||
|
&config.admin_socket(),
|
||||||
|
Message::MemoryAdmin(MemoryAdmin {
|
||||||
|
command: MemoryCommand::Undo,
|
||||||
|
arg: id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert!(matches!(undone, Message::MemoryAdminReply(_)), "{undone:?}");
|
||||||
|
assert!(
|
||||||
|
within_5s(|| count(&fake, "DELETE", "") == 1),
|
||||||
|
"the deletion waited: {:?} {:?}",
|
||||||
|
fake.requests(),
|
||||||
|
lines.lock().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user