Files
kyleandClaude Opus 5.5 3d266df9de review: bxctl memory sync shows what the Honcho side waits on
A shared status, kept by the worker and the walk and read by the report under
its own leaf lock, holds the workspaces whose key was refused, when an outage
of Honcho began, and when brokerd stopped answering. The report adds a line
for each while it lasts, and is unchanged when all is well. The runbook's
Honcho entries and broker-state-unavailable now say what sync shows.

Tuning and Wake move to honcho/wake.rs (re-exported from worker) to keep
worker.rs under 500 lines. honcho_sync.rs's serve test now allows the outage
line: the write wakes the worker, which finds Honcho unreachable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:11:31 -07:00

245 lines
7.2 KiB
Rust

//! What `bxctl memory sync` prints (M5c spec, section 5): the ledger's counts, and each failed
//! item, oldest first, by its label, its kind and its status, exactly; the same after a restart;
//! and `memoryd` answering it, with `[honcho]` and without. Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::time::Duration;
use bxcore::{
CallId, DataClass, Envelope, MemoryAdmin, MemoryCall, MemoryCommand, Message, SessionId,
Timestamp, ToolRequest, read_frame, write_frame,
};
use memoryd::config::Config;
use memoryd::honcho::ledger::{Action, Failure, Item, Ledger, Record, Workspace, label};
use memoryd::honcho::report::{NOT_CONFIGURED, report};
use memoryd::serve;
use tmp::TempDir;
fn at(minute: u32) -> Timestamp {
Timestamp::parse(&format!("2026-09-28T10:{minute:02}:00.000Z")).unwrap()
}
fn turn(n: u64) -> Item {
Item::Turn {
session: SessionId::new("mm-a").unwrap(),
turn: n,
}
}
fn edit(id: &str) -> Item {
Item::Edit {
id: id.to_string(),
part: 0,
}
}
fn to_trusted() -> Action {
Action::Turn {
workspace: Workspace::Trusted,
speaker: "kyle".to_string(),
}
}
/// A ledger with every state: 2 sent, 1 waiting, 2 failed (the later-decided one failing first),
/// 1 skipped.
fn mixed(dir: &TempDir) -> Ledger {
let mut l = Ledger::open(&dir.path().join("honcho.jsonl")).unwrap();
l.append(Record::Began { time: at(0) }).unwrap();
let decided = |item: Item, action: Action| Record::Decided {
time: at(0),
item,
action,
};
let records = vec![
decided(turn(1), to_trusted()),
decided(turn(2), to_trusted()),
decided(
edit("2026-09-28-0001"),
Action::Conclude {
workspace: Workspace::Trusted,
peer: "kyle".to_string(),
session: SessionId::new("mm-a").unwrap(),
text: "a private fact".to_string(),
},
),
decided(
turn(3),
Action::Skip {
why: "the turn has no single speaker".to_string(),
},
),
decided(turn(4), to_trusted()),
decided(turn(5), to_trusted()),
decided(turn(6), to_trusted()),
Record::Sent {
time: at(1),
item: turn(1),
ids: vec!["m1".to_string()],
},
Record::Failed {
time: at(2),
item: turn(4),
status: None,
kind: "log".to_string(),
},
Record::Failed {
time: at(3),
item: edit("2026-09-28-0001"),
status: Some(422),
kind: "status".to_string(),
},
Record::Sent {
time: at(4),
item: turn(2),
ids: vec!["m2".to_string()],
},
];
for r in records {
l.append(r).unwrap();
}
l
}
const MIXED: &str = "honcho sync: 7 decided: 2 sent, 2 waiting, 2 failed, 1 skipped
failed, oldest first:
2026-09-28T10:02:00.000Z turn 4 of session mm-a log
2026-09-28T10:03:00.000Z part 0 of edit 2026-09-28-0001 status (HTTP 422)";
#[test]
fn the_report_is_exactly_this() {
let dir = TempDir::new("sync-report");
let l = mixed(&dir);
assert_eq!(report(&l), MIXED);
assert!(!report(&l).contains("private"), "no text is printed");
let empty = Ledger::open(&dir.path().join("none.jsonl")).unwrap();
assert_eq!(
report(&empty),
"honcho sync: 0 decided: 0 sent, 0 waiting, 0 failed, 0 skipped"
);
assert_eq!(
NOT_CONFIGURED,
"honcho sync: not configured (memoryd.toml has no [honcho])"
);
}
#[test]
fn the_failures_are_oldest_first_and_survive_a_restart() {
let dir = TempDir::new("sync-failures");
let l = mixed(&dir);
let expected = vec![
Failure {
item: turn(4),
time: at(2),
status: None,
kind: "log".to_string(),
},
Failure {
item: edit("2026-09-28-0001"),
time: at(3),
status: Some(422),
kind: "status".to_string(),
},
];
assert_eq!(l.failures(), expected);
let again = Ledger::open(&dir.path().join("honcho.jsonl")).unwrap();
assert_eq!(again.failures(), expected);
assert_eq!(report(&again), MIXED);
assert_eq!(label(&turn(4)), "turn 4 of session mm-a");
assert_eq!(label(&edit("e")), "part 0 of edit e");
}
const SECTION: &str = "[honcho]
url = \"http://127.0.0.1:9\"
agent = \"boxmaker0\"
owner = \"kyle\"
trusted = \"boxmaker0-trusted\"
untrusted = \"boxmaker0-untrusted\"
fleet = \"fleet\"
";
fn ask(socket: &std::path::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: 5,
r#final: true,
msg,
};
write_frame(&mut stream, &env).unwrap();
read_frame(&mut stream).unwrap().msg
}
fn sync(config: &Config) -> String {
let msg = Message::MemoryAdmin(MemoryAdmin {
command: MemoryCommand::Sync,
arg: String::new(),
});
match ask(&config.admin_socket(), msg) {
Message::MemoryAdminReply(reply) => reply.text,
other => panic!("{other:?}"),
}
}
fn serve(dir: &TempDir, honcho: bool) -> Config {
let soul = dir.write("etc/soul.md", "You are Boxmaker.\n");
let text = format!(
"[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n{}",
dir.path().display(),
soul.display(),
if honcho { SECTION } else { "" }
);
let config = Config::parse(&text).unwrap();
let log: serve::Log = Arc::new(|_: &str| {});
let running = serve::start(config.clone(), log).unwrap();
std::thread::spawn(move || running.run());
config
}
#[test]
fn memoryd_answers_with_the_report() {
let dir = TempDir::new("sync-serve");
let config = serve(&dir, true);
assert_eq!(
sync(&config),
"honcho sync: 0 decided: 0 sent, 0 waiting, 0 failed, 0 skipped"
);
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,
});
ask(&config.broker_socket(), call);
// Honcho is not reachable here: the conclusion waits. The write wakes the worker, so once it
// has tried, the report also shows the outage (review of M5c-1, findings 3 and 4).
let shown = sync(&config);
let mut lines = shown.lines();
assert_eq!(
lines.next(),
Some("honcho sync: 1 decided: 0 sent, 1 waiting, 0 failed, 0 skipped")
);
if let Some(outage) = lines.next() {
assert!(outage.starts_with("honcho unreachable since "), "{shown}");
assert!(outage.ends_with(": sends wait and are retried"), "{shown}");
}
assert_eq!(lines.next(), None, "{shown}");
}
#[test]
fn without_honcho_it_is_not_configured() {
let dir = TempDir::new("sync-off");
let config = serve(&dir, false);
assert_eq!(sync(&config), NOT_CONFIGURED);
}