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:
@@ -282,6 +282,8 @@ pub enum MemoryCommand {
|
|||||||
Pending,
|
Pending,
|
||||||
Accept,
|
Accept,
|
||||||
Reject,
|
Reject,
|
||||||
|
/// What the Honcho ledger holds: its counts and failed items (M5c spec, section 5).
|
||||||
|
Sync,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `bxctl` → `memoryd`: one command; `arg` is the count for `log`, the edit or pending id for
|
/// `bxctl` → `memoryd`: one command; `arg` is the count for `log`, the edit or pending id for
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ usage: bxctl <command> [options]
|
|||||||
bxctl reindex [--memory-socket PATH]
|
bxctl reindex [--memory-socket PATH]
|
||||||
Rebuild memoryd's index from the session logs and notes.
|
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.
|
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]
|
bxctl sanitize check [TEXT]
|
||||||
Show which sanitizer rules TEXT, or standard input, breaks; writes nothing. Refuse shows
|
Show which sanitizer rules TEXT, or standard input, breaks; writes nothing. Refuse shows
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! The owner's memory commands over `memoryd`'s owner socket: `reindex`, and `memory log`,
|
//! 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::io::Write;
|
||||||
use std::os::unix::net::UnixStream;
|
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
|
/// `bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> | sync`, each with an
|
||||||
/// `--memory-socket PATH`.
|
/// optional `--memory-socket PATH`.
|
||||||
pub fn parse_memory(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
pub fn parse_memory(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||||
let (socket, rest) = socket_flag(args, home)?;
|
let (socket, rest) = socket_flag(args, home)?;
|
||||||
let rest: Vec<&str> = rest.iter().map(String::as_str).collect();
|
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()),
|
["undo", id] => Ask::Admin(MemoryCommand::Undo, id.to_string()),
|
||||||
["accept", id] => Ask::Admin(MemoryCommand::Accept, id.to_string()),
|
["accept", id] => Ask::Admin(MemoryCommand::Accept, id.to_string()),
|
||||||
["reject", id] => Ask::Admin(MemoryCommand::Reject, id.to_string()),
|
["reject", id] => Ask::Admin(MemoryCommand::Reject, id.to_string()),
|
||||||
|
["sync"] => Ask::Admin(MemoryCommand::Sync, String::new()),
|
||||||
_ => return Err(UsageError),
|
_ => return Err(UsageError),
|
||||||
};
|
};
|
||||||
Ok(Command::Memory { socket, ask })
|
Ok(Command::Memory { socket, ask })
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -94,6 +94,15 @@ pub struct Counts {
|
|||||||
pub skipped: usize,
|
pub skipped: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A failed item, as its `Failed` record says.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Failure {
|
||||||
|
pub item: Item,
|
||||||
|
pub time: Timestamp,
|
||||||
|
pub status: Option<u16>,
|
||||||
|
pub kind: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LedgerError {
|
pub enum LedgerError {
|
||||||
Io(PathBuf, std::io::Error),
|
Io(PathBuf, std::io::Error),
|
||||||
@@ -116,9 +125,9 @@ impl std::fmt::Display for LedgerError {
|
|||||||
write!(f, "{}:{line}: {why}; {RUNBOOK}", path.display())
|
write!(f, "{}:{line}: {why}; {RUNBOOK}", path.display())
|
||||||
}
|
}
|
||||||
LedgerError::Encode(err) => write!(f, "a ledger record could not be encoded: {err}"),
|
LedgerError::Encode(err) => write!(f, "a ledger record could not be encoded: {err}"),
|
||||||
LedgerError::AlreadyDecided(item) => write!(f, "{}: decided already", describe(item)),
|
LedgerError::AlreadyDecided(item) => write!(f, "{}: decided already", label(item)),
|
||||||
LedgerError::NotWaiting(item) => {
|
LedgerError::NotWaiting(item) => {
|
||||||
write!(f, "{}: not waiting to be sent", describe(item))
|
write!(f, "{}: not waiting to be sent", label(item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +141,11 @@ enum State {
|
|||||||
Skipped,
|
Skipped,
|
||||||
Waiting,
|
Waiting,
|
||||||
Sent(Vec<String>),
|
Sent(Vec<String>),
|
||||||
Failed,
|
Failed {
|
||||||
|
time: Timestamp,
|
||||||
|
status: Option<u16>,
|
||||||
|
kind: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -150,6 +163,8 @@ pub struct Ledger {
|
|||||||
entries: HashMap<Item, Entry>,
|
entries: HashMap<Item, Entry>,
|
||||||
/// Every sent item, in the order of its `Sent` record.
|
/// Every sent item, in the order of its `Sent` record.
|
||||||
sends: Vec<Item>,
|
sends: Vec<Item>,
|
||||||
|
/// Every failed item, in the order of its `Failed` record.
|
||||||
|
fails: Vec<Item>,
|
||||||
repaired: bool,
|
repaired: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +178,7 @@ impl Ledger {
|
|||||||
order: Vec::new(),
|
order: Vec::new(),
|
||||||
entries: HashMap::new(),
|
entries: HashMap::new(),
|
||||||
sends: Vec::new(),
|
sends: Vec::new(),
|
||||||
|
fails: Vec::new(),
|
||||||
repaired: false,
|
repaired: false,
|
||||||
};
|
};
|
||||||
let Some(bytes) = read(path)? else {
|
let Some(bytes) = read(path)? else {
|
||||||
@@ -267,6 +283,23 @@ impl Ledger {
|
|||||||
live.pop()
|
live.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every failed item, oldest first (the order of the `Failed` records), with its record's
|
||||||
|
/// time, status and kind.
|
||||||
|
pub fn failures(&self) -> Vec<Failure> {
|
||||||
|
self.fails
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match &self.entries.get(item)?.state {
|
||||||
|
State::Failed { time, status, kind } => Some(Failure {
|
||||||
|
item: item.clone(),
|
||||||
|
time: *time,
|
||||||
|
status: *status,
|
||||||
|
kind: kind.clone(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// How many items are in each state.
|
/// How many items are in each state.
|
||||||
pub fn counts(&self) -> Counts {
|
pub fn counts(&self) -> Counts {
|
||||||
let mut counts = Counts::default();
|
let mut counts = Counts::default();
|
||||||
@@ -276,7 +309,7 @@ impl Ledger {
|
|||||||
State::Skipped => counts.skipped += 1,
|
State::Skipped => counts.skipped += 1,
|
||||||
State::Waiting => counts.waiting += 1,
|
State::Waiting => counts.waiting += 1,
|
||||||
State::Sent(_) => counts.sent += 1,
|
State::Sent(_) => counts.sent += 1,
|
||||||
State::Failed => counts.failed += 1,
|
State::Failed { .. } => counts.failed += 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
counts
|
counts
|
||||||
@@ -318,10 +351,16 @@ impl Ledger {
|
|||||||
}
|
}
|
||||||
self.sends.push(item);
|
self.sends.push(item);
|
||||||
}
|
}
|
||||||
Record::Failed { item, .. } => {
|
Record::Failed {
|
||||||
|
time,
|
||||||
|
item,
|
||||||
|
status,
|
||||||
|
kind,
|
||||||
|
} => {
|
||||||
if let Some(entry) = self.entries.get_mut(&item) {
|
if let Some(entry) = self.entries.get_mut(&item) {
|
||||||
entry.state = State::Failed;
|
entry.state = State::Failed { time, status, kind };
|
||||||
}
|
}
|
||||||
|
self.fails.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,8 +414,9 @@ fn cut(path: &Path, keep: usize) -> Result<(), LedgerError> {
|
|||||||
file.sync_data().map_err(io)
|
file.sync_data().map_err(io)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An item as the error messages name it.
|
/// An item as the error messages and `bxctl memory sync` name it: `turn 3 of session mm-a`,
|
||||||
fn describe(item: &Item) -> String {
|
/// `part 0 of edit 2026-09-28-0001`.
|
||||||
|
pub fn label(item: &Item) -> String {
|
||||||
match item {
|
match item {
|
||||||
Item::Turn { session, turn } => format!("turn {turn} of session {}", session.as_str()),
|
Item::Turn { session, turn } => format!("turn {turn} of session {}", session.as_str()),
|
||||||
Item::Edit { id, part } => format!("part {part} of edit {id}"),
|
Item::Edit { id, part } => format!("part {part} of edit {id}"),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use bxcore::{SessionId, Timestamp};
|
|||||||
|
|
||||||
use crate::files::Store;
|
use crate::files::Store;
|
||||||
use crate::honcho::ledger::{self, Action, Item, Ledger, Record, Workspace};
|
use crate::honcho::ledger::{self, Action, Item, Ledger, Record, Workspace};
|
||||||
|
use crate::honcho::report;
|
||||||
use crate::journal;
|
use crate::journal;
|
||||||
use crate::notes::{self, Kind};
|
use crate::notes::{self, Kind};
|
||||||
|
|
||||||
@@ -167,6 +168,11 @@ impl Mirror {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `bxctl memory sync`'s report of the ledger, under the ledger's lock alone.
|
||||||
|
pub fn report(&self) -> String {
|
||||||
|
report::report(&self.ledger.lock().unwrap_or_else(|p| p.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
/// 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. Its parts are
|
/// right after it was journalled, so the ledger has the journal's order. Its parts are
|
||||||
/// `Edit { id, part }` from 0. An edit already decided, or asking nothing, adds no line. A
|
/// `Edit { id, part }` from 0. An edit already decided, or asking nothing, adds no line. A
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod edits;
|
|||||||
pub mod ledger;
|
pub mod ledger;
|
||||||
pub mod mirror;
|
pub mod mirror;
|
||||||
pub mod naming;
|
pub mod naming;
|
||||||
|
pub mod report;
|
||||||
pub mod start;
|
pub mod start;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod turns;
|
pub mod turns;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! What `bxctl memory sync` prints (M5c spec, section 5): how many items the ledger holds in each
|
||||||
|
//! state, and every failed item, oldest first, by its label, never by its text.
|
||||||
|
|
||||||
|
use crate::honcho::ledger::{Ledger, label};
|
||||||
|
|
||||||
|
/// The answer without `[honcho]`: not an error.
|
||||||
|
pub const NOT_CONFIGURED: &str = "honcho sync: not configured (memoryd.toml has no [honcho])";
|
||||||
|
|
||||||
|
/// The report of `ledger`: one line of counts, then, when any item failed, a heading and one line
|
||||||
|
/// per failed item: its time, its label, its kind, and its HTTP status when it had one.
|
||||||
|
pub fn report(ledger: &Ledger) -> String {
|
||||||
|
let c = ledger.counts();
|
||||||
|
let mut out = format!(
|
||||||
|
"honcho sync: {} decided: {} sent, {} waiting, {} failed, {} skipped",
|
||||||
|
c.decided, c.sent, c.waiting, c.failed, c.skipped
|
||||||
|
);
|
||||||
|
let failures = ledger.failures();
|
||||||
|
if !failures.is_empty() {
|
||||||
|
out.push_str("\nfailed, oldest first:");
|
||||||
|
for f in failures {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"\n {} {} {}",
|
||||||
|
f.time.to_rfc3339(),
|
||||||
|
label(&f.item),
|
||||||
|
f.kind
|
||||||
|
));
|
||||||
|
if let Some(status) = f.status {
|
||||||
|
out.push_str(&format!(" (HTTP {status})"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ use crate::edit::{self, Context};
|
|||||||
use crate::files::Store;
|
use crate::files::Store;
|
||||||
use crate::honcho::ledger::{self, LedgerError};
|
use crate::honcho::ledger::{self, LedgerError};
|
||||||
use crate::honcho::mirror::Mirror;
|
use crate::honcho::mirror::Mirror;
|
||||||
|
use crate::honcho::report;
|
||||||
use crate::honcho::state;
|
use crate::honcho::state;
|
||||||
use crate::honcho::turns::TurnSync;
|
use crate::honcho::turns::TurnSync;
|
||||||
use crate::honcho::worker::{self, Wake};
|
use crate::honcho::worker::{self, Wake};
|
||||||
@@ -454,6 +455,10 @@ impl Memoryd {
|
|||||||
MemoryCommand::Pending => review::list(&self.store),
|
MemoryCommand::Pending => review::list(&self.store),
|
||||||
MemoryCommand::Accept => review::accept(&self.store, arg, now),
|
MemoryCommand::Accept => review::accept(&self.store, arg, now),
|
||||||
MemoryCommand::Reject => review::reject(&self.store, arg, now),
|
MemoryCommand::Reject => review::reject(&self.store, arg, now),
|
||||||
|
MemoryCommand::Sync => Ok(match &self.mirror {
|
||||||
|
Some(mirror) => mirror.report(),
|
||||||
|
None => report::NOT_CONFIGURED.to_string(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
if result.is_ok() && matches!(command, MemoryCommand::Undo | MemoryCommand::Accept) {
|
if result.is_ok() && matches!(command, MemoryCommand::Undo | MemoryCommand::Accept) {
|
||||||
self.mirror(None);
|
self.mirror(None);
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
//! 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();
|
||||||
|
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.
|
||||||
|
assert_eq!(
|
||||||
|
sync(&config),
|
||||||
|
"honcho sync: 1 decided: 0 sent, 1 waiting, 0 failed, 0 skipped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
@@ -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 |
|
||||||
|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| M5c1/14-bxctl-memory-sync | 2026-09-28 | done | 1 | pass | none | Copied the two given tests from `docs/plans/M5c1/files/`: `crates/memoryd/tests/honcho_sync.rs` (from `honcho_sync.rs-task14`) and `crates/bxctl/tests/cli_memory_sync.rs`, and the skeleton `crates/memoryd/src/honcho/report.rs`. Step 2 showed exactly the named failures: `honcho_sync` with 5 (E0432 for `memoryd::honcho::report`, E0432 for `...::ledger::Failure`/`label`, two E0599 for `failures`, one E0599 for the missing `Sync`); `cli_memory_sync` with 6 (all the missing `Sync`). Made X1 (`MemoryCommand::Sync`), B1-B3 (`memory sync` parsed as `Ask::Admin(MemoryCommand::Sync, String::new())`, the module doc), U1 (the usage line), L1-L9 (`Failure { item, time, status, kind }`; `State::Failed { time, status, kind }`; the `fails: Vec<Item>` field, kept in `apply` on every `Failed` record; `failures()` oldest first via `filter_map`; renamed private `describe` to public `label`); R1 (`use crate::honcho::report`; `Mirror::report` under the ledger's lock); H1 (`pub mod report;`); S1/S2 (`Sync` answered with `mirror.report()` or `NOT_CONFIGURED`). Copied the skeleton. The task's U1 code block rendered every line with one extra leading space, but the actual `USAGE` literal uses two spaces before a command and six before its explanation (as `cli::usage_explains_the_speaker_flag` enforces), so I matched those. `cargo check -p memoryd` clean after filling the body: `counts()` for the first line, then when `failures()` is non-empty push `"\nfailed, oldest first:"` and each `format!("\n {} {} {}", time.to_rfc3339(), label(&item), kind)` with ` (HTTP {status})` appended when `status` is `Some`. `cargo test` all pass: `honcho_sync` 4, `honcho_ledger` 12, `honcho_ledger_file` 12, `honcho_worker` 12, `cli_memory_sync` 4, every other suite unchanged. `grep -n 'MemoryCommand::Sync'` prints two lines, `git diff --stat -- crates/loopd crates/brokerd crates/gatewayd Cargo.toml Cargo.lock` prints nothing. `make gate` printed `gate: ok` with 1332 tests (1331 in `cargo test --workspace`, plus the 1 ignored `end_to_end` the gate runs separately). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c1/13-memoryd-honcho-edits | 2026-09-28 | done | 1 | pass | none | Copied the given files from `docs/plans/M5c1/files/`: the test `crates/memoryd/tests/honcho_edits.rs` (from `honcho_edits.rs-task13`), new versions of `crates/memoryd/tests/honcho_worker_more.rs` (from `honcho_worker_more.rs-task13`) and `tests/support/honcho.rs` (from `support/honcho.rs`), and the skeletons `crates/memoryd/src/honcho/edits.rs` and `crates/memoryd/src/honcho/start.rs` (given whole). Step 2 showed exactly the named failure: `honcho_edits` with one E0432 (`unresolved import memoryd::honcho::edits`); every other target compiled; `honcho_worker_more` 10, `honcho_client` 13, `honcho_worker` 12 passed. Made H1 (`pub mod edits;` after `pub mod decide;` in `honcho/mod.rs`), H2 (`pub mod start;` after `pub mod naming;`), C1 (`owner: String` field in `Clients`), C2 (`owner: config.owner.clone()` in `Clients::new`) and C3 (`ensure_memory_session`: ensure workspace, the agent's peer, the owner's, then the `<id>_memory` session with both peers `observe_me(false) observe_others(false)`); copied the two skeletons. `cargo check -p memoryd` gave 22 warnings (six unused imports, sixteen unused variables, exactly the task named). Filled its five bodies in the order of the file, deleting each instruction comment and keeping the doc comments, each from its comment and `cargo check` after: `memory_session` (`format!("{}{MEMORY_SUFFIX}", session.as_str())`); `conclude` (`memory_session`, `ensure_memory_session`, `ConclusionCreate::new(neutralise(text), peer, peer).with_session_id(name)` in `ConclusionBatchCreate::new(vec![..])`, `create_conclusions` -> the ids); `unconclude` (`Some(id)` -> `delete_conclusion` then `vec![id]`, `None` -> `Vec::new()`); `fleet_batch` (`Map` metadata `agent`/`boxmaker_session`/`edit`, `parts(neutralise(text))` each `MessageCreate::new(part, agent)` with the metadata); `fleet` (`HonchoSession { name: session id, speakers: [] }`, `ensure_session(Fleet, ...)`, `add_messages` -> the ids). `cargo check` clean. Made W1 (imports: drop `Config`/`Timeouts`/`HonchoConfig`/`mirror`/`state`/`LEDGER`/`read_log`/`StartError`, add `edits`, re-export `start::{Opened, open, spawn}`); W2 (`Job` enum `Turn`/`Conclude`/`Unconclude`/`Fleet`, `Next` holds `item`/`workspace`/`job`); W3 (`label` naming a turn or a part); W4 and W5 (the two log lines use `label(&next.item)`); W6 (`next` matching every `(Item, Action)` pair into a `Job`, `send` dispatching and `send_turn` split out); W7 (the start code removed, it moves to `start.rs`). `cargo check -p memoryd` clean, `worker.rs` 486 lines. Step 6 all pass: `honcho_edits` 10, `honcho_worker_more` 10, `honcho_worker` 12, `honcho_client` 13, `honcho_keys` 3, `honcho_mirror_serve` 6, `honcho_serve` 6. Step 7: `fn open`/`fn spawn` both in `start.rs`, no `Action::Retract` in `worker.rs`, the other-crates `git diff --stat` empty. `cargo fmt --all`; `make gate` printed `gate: ok` with 1324 tests (1313 in `cargo test --workspace`, plus the 10 in `honcho_edits.rs` and the 1 ignored `end_to_end` the gate runs separately). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5c1/13-memoryd-honcho-edits | 2026-09-28 | done | 1 | pass | none | Copied the given files from `docs/plans/M5c1/files/`: the test `crates/memoryd/tests/honcho_edits.rs` (from `honcho_edits.rs-task13`), new versions of `crates/memoryd/tests/honcho_worker_more.rs` (from `honcho_worker_more.rs-task13`) and `tests/support/honcho.rs` (from `support/honcho.rs`), and the skeletons `crates/memoryd/src/honcho/edits.rs` and `crates/memoryd/src/honcho/start.rs` (given whole). Step 2 showed exactly the named failure: `honcho_edits` with one E0432 (`unresolved import memoryd::honcho::edits`); every other target compiled; `honcho_worker_more` 10, `honcho_client` 13, `honcho_worker` 12 passed. Made H1 (`pub mod edits;` after `pub mod decide;` in `honcho/mod.rs`), H2 (`pub mod start;` after `pub mod naming;`), C1 (`owner: String` field in `Clients`), C2 (`owner: config.owner.clone()` in `Clients::new`) and C3 (`ensure_memory_session`: ensure workspace, the agent's peer, the owner's, then the `<id>_memory` session with both peers `observe_me(false) observe_others(false)`); copied the two skeletons. `cargo check -p memoryd` gave 22 warnings (six unused imports, sixteen unused variables, exactly the task named). Filled its five bodies in the order of the file, deleting each instruction comment and keeping the doc comments, each from its comment and `cargo check` after: `memory_session` (`format!("{}{MEMORY_SUFFIX}", session.as_str())`); `conclude` (`memory_session`, `ensure_memory_session`, `ConclusionCreate::new(neutralise(text), peer, peer).with_session_id(name)` in `ConclusionBatchCreate::new(vec![..])`, `create_conclusions` -> the ids); `unconclude` (`Some(id)` -> `delete_conclusion` then `vec![id]`, `None` -> `Vec::new()`); `fleet_batch` (`Map` metadata `agent`/`boxmaker_session`/`edit`, `parts(neutralise(text))` each `MessageCreate::new(part, agent)` with the metadata); `fleet` (`HonchoSession { name: session id, speakers: [] }`, `ensure_session(Fleet, ...)`, `add_messages` -> the ids). `cargo check` clean. Made W1 (imports: drop `Config`/`Timeouts`/`HonchoConfig`/`mirror`/`state`/`LEDGER`/`read_log`/`StartError`, add `edits`, re-export `start::{Opened, open, spawn}`); W2 (`Job` enum `Turn`/`Conclude`/`Unconclude`/`Fleet`, `Next` holds `item`/`workspace`/`job`); W3 (`label` naming a turn or a part); W4 and W5 (the two log lines use `label(&next.item)`); W6 (`next` matching every `(Item, Action)` pair into a `Job`, `send` dispatching and `send_turn` split out); W7 (the start code removed, it moves to `start.rs`). `cargo check -p memoryd` clean, `worker.rs` 486 lines. Step 6 all pass: `honcho_edits` 10, `honcho_worker_more` 10, `honcho_worker` 12, `honcho_client` 13, `honcho_keys` 3, `honcho_mirror_serve` 6, `honcho_serve` 6. Step 7: `fn open`/`fn spawn` both in `start.rs`, no `Action::Retract` in `worker.rs`, the other-crates `git diff --stat` empty. `cargo fmt --all`; `make gate` printed `gate: ok` with 1324 tests (1313 in `cargo test --workspace`, plus the 10 in `honcho_edits.rs` and the 1 ignored `end_to_end` the gate runs separately). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c1/12-memoryd-honcho-mirror | 2026-09-28 | done | 3 | pass | none | Copied the two given tests from `docs/plans/M5c1/files/crates/memoryd/tests/`: `honcho_mirror.rs` (from `honcho_mirror.rs-task12`) and `honcho_mirror_serve.rs` (from `honcho_mirror_serve.rs-task12`), and the skeleton `crates/memoryd/src/honcho/mirror.rs`. Step 2 showed exactly the named failures: `honcho_mirror` with one E0432 (`unresolved import memoryd::honcho::mirror`); `honcho_mirror_serve` 1 pass (`without_honcho_nothing_is_decided`) and 5 fail. Made H1 (`pub mod mirror;` after `pub mod ledger;` in `honcho/mod.rs`) and copied the skeleton; `cargo check` gave 24 warnings (unused `Kind` and `Workspace`, and the twenty unused variables the task named). Filled its seven bodies in the order of the file, deleting each instruction comment and keeping the doc comments, each from its comment and `cargo check` after: `diff` (`minus(before, after), minus(after, before)`); `minus` (a `left` of `b`'s refs, keep a `text` when `left.iter().position` is `None`, else `swap_remove` that occurrence then drop it); `notes_of` (`flat_map` `notes::parse` over `record.notes`); `session_of` (walk the `undoes` chain bound by `records.len()`, return `SessionId::new` when the session is not `OWNER`, else follow `undoes` and `None` when it or the id is missing); `conclusion` (`Conclude` in the trusted workspace with the session, or `Skip` with `NO_SESSION`); `identity_actions` (`moved` notes' texts filtered out of the diff's dropped side as `Unconclude`, the added side each `conclusion` about the owner for `user.md` else the agent); `note_actions` (`Skip` `flagged: <rule>` when flagged, else each `noted` note, `Fleet` when untrusted with a session else `Skip` without one, else a conclusion about the agent). The `before` block for W2 (`use crate::honcho::naming::{HonchoSession, honcho_session}`) was rustfmt-ordered `{honcho_session, HonchoSession}` in the tree, so I anchored on that actual line. Made T1-T8 (the ledger's own `Arc<Mutex<Ledger>>` in `TurnSync`, `ledger()` returning a `MutexGuard`, the new `shared_ledger()`, and every `self.ledger` call going through it), W1-W9 (the worker takes `self.ledger` and a `sessions` field of its own, `Opened` gains `owner` and `mirror()`), and S1-S7 (`Memoryd` gains `mirror: Option<Mirror>`, `start` builds it from `opened.mirror()`, and `memory` calls `self.mirror(Some(&id))` after `journal::record`, `admin` calls `self.mirror(None)` after an undo or accept). `cargo check` clean with no warning; `honcho_mirror` 8, `honcho_mirror_serve` 6, and every earlier suite pass. `make gate` printed `gate: ok` with 1314 tests (1300 before plus the 14 new). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5c1/12-memoryd-honcho-mirror | 2026-09-28 | done | 3 | pass | none | Copied the two given tests from `docs/plans/M5c1/files/crates/memoryd/tests/`: `honcho_mirror.rs` (from `honcho_mirror.rs-task12`) and `honcho_mirror_serve.rs` (from `honcho_mirror_serve.rs-task12`), and the skeleton `crates/memoryd/src/honcho/mirror.rs`. Step 2 showed exactly the named failures: `honcho_mirror` with one E0432 (`unresolved import memoryd::honcho::mirror`); `honcho_mirror_serve` 1 pass (`without_honcho_nothing_is_decided`) and 5 fail. Made H1 (`pub mod mirror;` after `pub mod ledger;` in `honcho/mod.rs`) and copied the skeleton; `cargo check` gave 24 warnings (unused `Kind` and `Workspace`, and the twenty unused variables the task named). Filled its seven bodies in the order of the file, deleting each instruction comment and keeping the doc comments, each from its comment and `cargo check` after: `diff` (`minus(before, after), minus(after, before)`); `minus` (a `left` of `b`'s refs, keep a `text` when `left.iter().position` is `None`, else `swap_remove` that occurrence then drop it); `notes_of` (`flat_map` `notes::parse` over `record.notes`); `session_of` (walk the `undoes` chain bound by `records.len()`, return `SessionId::new` when the session is not `OWNER`, else follow `undoes` and `None` when it or the id is missing); `conclusion` (`Conclude` in the trusted workspace with the session, or `Skip` with `NO_SESSION`); `identity_actions` (`moved` notes' texts filtered out of the diff's dropped side as `Unconclude`, the added side each `conclusion` about the owner for `user.md` else the agent); `note_actions` (`Skip` `flagged: <rule>` when flagged, else each `noted` note, `Fleet` when untrusted with a session else `Skip` without one, else a conclusion about the agent). The `before` block for W2 (`use crate::honcho::naming::{HonchoSession, honcho_session}`) was rustfmt-ordered `{honcho_session, HonchoSession}` in the tree, so I anchored on that actual line. Made T1-T8 (the ledger's own `Arc<Mutex<Ledger>>` in `TurnSync`, `ledger()` returning a `MutexGuard`, the new `shared_ledger()`, and every `self.ledger` call going through it), W1-W9 (the worker takes `self.ledger` and a `sessions` field of its own, `Opened` gains `owner` and `mirror()`), and S1-S7 (`Memoryd` gains `mirror: Option<Mirror>`, `start` builds it from `opened.mirror()`, and `memory` calls `self.mirror(Some(&id))` after `journal::record`, `admin` calls `self.mirror(None)` after an undo or accept). `cargo check` clean with no warning; `honcho_mirror` 8, `honcho_mirror_serve` 6, and every earlier suite pass. `make gate` printed `gate: ok` with 1314 tests (1300 before plus the 14 new). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
| M5c1/11-memoryd-honcho-worker | 2026-09-28 | done | 1 | pass | none | Copied the four given files from `docs/plans/M5c1/files/`: the tests `crates/memoryd/tests/honcho_worker.rs` and `honcho_worker_more.rs` (from `honcho_worker_more.rs-task11`), a new version of `crates/memoryd/tests/honcho_serve.rs` (from `honcho_serve.rs-task11`, replacing the tree's) and the support `crates/memoryd/tests/support/worker.rs` (from `worker.rs-task11`), then the skeleton `crates/memoryd/src/honcho/worker.rs`. Step 2 showed exactly the named failures: `honcho_worker` with three (E0432 for `memoryd::honcho::worker` in the test and in `support/worker.rs`, E0599 for the missing `resume` on `TurnSync`); `honcho_worker_more` with four (the same three plus E0599 for the missing `Secret` on `StartError`); every other target compiled and `honcho_serve` passed its 6 already. Made D1/D2 (`user_text`/`final_reply` `pub` in `decide.rs`), H1 (`pub mod worker;` after `pub mod turns;` in `honcho/mod.rs`), T1 (`sessions` in `impl TurnSync`), T2 (`resume` walking `decisions` deduped by id then reading each log, and `append` delegating to `ledger.append` before `updated`'s doc), T3 (`updated` calling `self.retry` then the new `retry`), S1 (imports: drop `Ledger`, add `worker::{self, Wake}`), S2 (`Memoryd` holds `Arc<Mutex<TurnSync>>` and a `wake`), S3 (`StartError::Secret(String)`), S4 (the `Display` arm), S5 (`start` calling `worker::open`), S6 (`wake` built and `worker::spawn` bound after the sockets), S7 (`wake` in the `Memoryd`), S8 (`decide_turns` dropping the lock then `wake.notify()`), M1 (`main.rs` matching both `Ledger` and `Secret`). Filled its eleven bodies in the order of the file, deleting each instruction comment and keeping the doc comments, each from its comment and `cargo check` after: `notify` (lock, set flag, `notify_one`); `wait` (lock, `wait_timeout_while(..., |woken| !*woken)`, clear the flag); `parts` (collect `chars`, `chunks(MAX_PART)`); `read_turn` (`read_log` ok, `turn_records`, `user_text`, `final_reply`, both `to_string`); `batch` (a `Map` with `agent`/`boxmaker_session`/`turn` in order, then for each of `(text,speaker)`/`(reply,agent)` the neutralised `parts`, each a `MessageCreate` with the metadata); `ids` (`m.id.clone()`); `next` (lock, `outbox().find_map`, `Item::Turn` and `Action::Turn` by `let .. else`, skip when `refused.contains(workspace)`, build `Next` with `honcho_session`); `unreachable` (the `outage` match, log the start once, double-and-cap the wait, `Backoff`); `recovered` (`outage.take()` ends it, log once, `retry`); `retry` (lock, `sync.retry`); `record` (`append`, `Ok` -> `Done`, `Err` logs with `ledger::RUNBOOK`, sets `stopped`, `Stopped`). After the last body `cargo check -p memoryd` is clean with no warning. Step 5 all pass: `honcho_worker` 12, `honcho_worker_more` 10, `honcho_serve` 6, and every unchanged suite. Step 6 all pass: the `thread::spawn`/`Builder::new` grep prints one line in `spawn`, the three runbook-pointer greps print the three constants, and the other-crates `git diff --stat` is empty. Appended the three runbook fragments (`runbook-honcho-unreachable`, `-send-refused`, `-key-refused`) with `{ echo; cat ... } >>`; `grep -c '^## honcho-'` printed 5. `make gate` printed `gate: ok` with 1300 tests (1299 in `cargo test --workspace`, plus the 1 ignored `end_to_end` the gate runs separately). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
| M5c1/11-memoryd-honcho-worker | 2026-09-28 | done | 1 | pass | none | Copied the four given files from `docs/plans/M5c1/files/`: the tests `crates/memoryd/tests/honcho_worker.rs` and `honcho_worker_more.rs` (from `honcho_worker_more.rs-task11`), a new version of `crates/memoryd/tests/honcho_serve.rs` (from `honcho_serve.rs-task11`, replacing the tree's) and the support `crates/memoryd/tests/support/worker.rs` (from `worker.rs-task11`), then the skeleton `crates/memoryd/src/honcho/worker.rs`. Step 2 showed exactly the named failures: `honcho_worker` with three (E0432 for `memoryd::honcho::worker` in the test and in `support/worker.rs`, E0599 for the missing `resume` on `TurnSync`); `honcho_worker_more` with four (the same three plus E0599 for the missing `Secret` on `StartError`); every other target compiled and `honcho_serve` passed its 6 already. Made D1/D2 (`user_text`/`final_reply` `pub` in `decide.rs`), H1 (`pub mod worker;` after `pub mod turns;` in `honcho/mod.rs`), T1 (`sessions` in `impl TurnSync`), T2 (`resume` walking `decisions` deduped by id then reading each log, and `append` delegating to `ledger.append` before `updated`'s doc), T3 (`updated` calling `self.retry` then the new `retry`), S1 (imports: drop `Ledger`, add `worker::{self, Wake}`), S2 (`Memoryd` holds `Arc<Mutex<TurnSync>>` and a `wake`), S3 (`StartError::Secret(String)`), S4 (the `Display` arm), S5 (`start` calling `worker::open`), S6 (`wake` built and `worker::spawn` bound after the sockets), S7 (`wake` in the `Memoryd`), S8 (`decide_turns` dropping the lock then `wake.notify()`), M1 (`main.rs` matching both `Ledger` and `Secret`). Filled its eleven bodies in the order of the file, deleting each instruction comment and keeping the doc comments, each from its comment and `cargo check` after: `notify` (lock, set flag, `notify_one`); `wait` (lock, `wait_timeout_while(..., |woken| !*woken)`, clear the flag); `parts` (collect `chars`, `chunks(MAX_PART)`); `read_turn` (`read_log` ok, `turn_records`, `user_text`, `final_reply`, both `to_string`); `batch` (a `Map` with `agent`/`boxmaker_session`/`turn` in order, then for each of `(text,speaker)`/`(reply,agent)` the neutralised `parts`, each a `MessageCreate` with the metadata); `ids` (`m.id.clone()`); `next` (lock, `outbox().find_map`, `Item::Turn` and `Action::Turn` by `let .. else`, skip when `refused.contains(workspace)`, build `Next` with `honcho_session`); `unreachable` (the `outage` match, log the start once, double-and-cap the wait, `Backoff`); `recovered` (`outage.take()` ends it, log once, `retry`); `retry` (lock, `sync.retry`); `record` (`append`, `Ok` -> `Done`, `Err` logs with `ledger::RUNBOOK`, sets `stopped`, `Stopped`). After the last body `cargo check -p memoryd` is clean with no warning. Step 5 all pass: `honcho_worker` 12, `honcho_worker_more` 10, `honcho_serve` 6, and every unchanged suite. Step 6 all pass: the `thread::spawn`/`Builder::new` grep prints one line in `spawn`, the three runbook-pointer greps print the three constants, and the other-crates `git diff --stat` is empty. Appended the three runbook fragments (`runbook-honcho-unreachable`, `-send-refused`, `-key-refused`) with `{ echo; cat ... } >>`; `grep -c '^## honcho-'` printed 5. `make gate` printed `gate: ok` with 1300 tests (1299 in `cargo test --workspace`, plus the 1 ignored `end_to_end` the gate runs separately). No new dependency, so `Cargo.lock` is unchanged. | ornith-1.5-35b-a3b |
|
||||||
|
|||||||
Reference in New Issue
Block a user