bxctl memory sync: the Honcho ledger's counts and failed items

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-28 13:22:26 -07:00
co-authored by ornith-1.5-35b-a3b
parent fb05b26462
commit fb2afa1979
11 changed files with 445 additions and 12 deletions
+2 -1
View File
@@ -34,8 +34,9 @@ usage: bxctl <command> [options]
bxctl reindex [--memory-socket PATH]
Rebuild memoryd's index from the session logs and notes.
bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> [--memory-socket PATH]
bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> | sync [--memory-socket PATH]
List memory edits, undo the latest edit of a file, or answer writes waiting for review.
sync shows what memoryd sends to Honcho: counts, and each failed item by its label.
bxctl sanitize check [TEXT]
Show which sanitizer rules TEXT, or standard input, breaks; writes nothing. Refuse shows
+4 -3
View File
@@ -1,5 +1,5 @@
//! The owner's memory commands over `memoryd`'s owner socket: `reindex`, and `memory log`,
//! `undo`, `pending`, `accept`, `reject` (M5a spec, section 10).
//! `undo`, `pending`, `accept`, `reject` (M5a spec, section 10), and `sync` (M5c spec, section 5).
use std::io::Write;
use std::os::unix::net::UnixStream;
@@ -39,8 +39,8 @@ pub fn parse_reindex(args: &[String], home: &Path) -> Result<Command, UsageError
})
}
/// `bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id>`, each with an optional
/// `--memory-socket PATH`.
/// `bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> | sync`, each with an
/// optional `--memory-socket PATH`.
pub fn parse_memory(args: &[String], home: &Path) -> Result<Command, UsageError> {
let (socket, rest) = socket_flag(args, home)?;
let rest: Vec<&str> = rest.iter().map(String::as_str).collect();
@@ -56,6 +56,7 @@ pub fn parse_memory(args: &[String], home: &Path) -> Result<Command, UsageError>
["undo", id] => Ask::Admin(MemoryCommand::Undo, id.to_string()),
["accept", id] => Ask::Admin(MemoryCommand::Accept, id.to_string()),
["reject", id] => Ask::Admin(MemoryCommand::Reject, id.to_string()),
["sync"] => Ask::Admin(MemoryCommand::Sync, String::new()),
_ => return Err(UsageError),
};
Ok(Command::Memory { socket, ask })
+108
View File
@@ -0,0 +1,108 @@
//! `bxctl memory sync` (M5c spec, section 5): parsed, sent to `memoryd` as the `sync` command,
//! its answer printed as it comes, and `memoryd` down reported as for every memory command. Do
//! not edit.
#[path = "support/temp.rs"]
mod temp;
use std::os::unix::net::UnixListener;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use bxcore::{
Envelope, MemoryAdmin, MemoryAdminReply, MemoryCommand, Message, read_frame, write_frame,
};
use bxctl::cli::{Command, USAGE, UsageError, parse};
use bxctl::memory::{Ask, run};
fn args(line: &str) -> Vec<String> {
line.split_whitespace().map(str::to_string).collect()
}
/// A fake `memoryd` answering one request with `text`; hands back what it was sent.
fn fake(socket: &Path, text: &str) -> mpsc::Receiver<Message> {
let listener = UnixListener::bind(socket).unwrap();
let (tx, rx) = mpsc::channel();
let answer = Message::MemoryAdminReply(MemoryAdminReply {
text: text.to_string(),
});
std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let env = read_frame(&mut stream).unwrap();
tx.send(env.msg).unwrap();
let reply = Envelope {
v: 1,
id: env.id,
r#final: true,
msg: answer,
};
write_frame(&mut stream, &reply).unwrap();
});
rx
}
#[test]
fn sync_parses() {
let home = PathBuf::from("/srv/bx");
assert_eq!(
parse(&args("memory sync"), &home),
Ok(Command::Memory {
socket: PathBuf::from("/srv/bx/run/owner-memory/admin.sock"),
ask: Ask::Admin(MemoryCommand::Sync, String::new()),
})
);
assert_eq!(
parse(&args("memory sync --memory-socket /r/m.sock"), &home),
Ok(Command::Memory {
socket: PathBuf::from("/r/m.sock"),
ask: Ask::Admin(MemoryCommand::Sync, String::new()),
})
);
assert_eq!(parse(&args("memory sync now"), &home), Err(UsageError));
assert!(
USAGE.contains("| reject <id> | sync [--memory-socket PATH]\n"),
"{USAGE}"
);
}
#[test]
fn the_command_is_named_sync_on_the_wire() {
assert_eq!(
serde_json::to_string(&MemoryCommand::Sync).unwrap(),
"\"sync\""
);
}
#[test]
fn the_answer_is_printed_as_it_comes() {
let dir = temp::TempDir::new("bx-mem-sync");
let socket = dir.join("admin.sock");
let report = "honcho sync: 3 decided: 1 sent, 1 waiting, 1 failed, 0 skipped\n\
failed, oldest first:\n \
2026-09-28T10:00:00.000Z turn 3 of session mm-a status (HTTP 422)";
let seen = fake(&socket, report);
let mut out = Vec::new();
let ask = Ask::Admin(MemoryCommand::Sync, String::new());
run(&socket, &ask, Duration::from_secs(5), &mut out).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), format!("{report}\n"));
assert_eq!(
seen.recv_timeout(Duration::from_secs(5)).unwrap(),
Message::MemoryAdmin(MemoryAdmin {
command: MemoryCommand::Sync,
arg: String::new(),
})
);
}
#[test]
fn memoryd_down_is_reported_as_for_every_memory_command() {
let dir = temp::TempDir::new("bx-mem-sync-down");
let ask = Ask::Admin(MemoryCommand::Sync, String::new());
let socket = dir.join("nobody.sock");
let err = run(&socket, &ask, Duration::from_secs(1), &mut Vec::new()).unwrap_err();
assert!(
err.starts_with(&format!("cannot reach memoryd at {}: ", socket.display())),
"{err}"
);
}