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
+48 -8
View File
@@ -94,6 +94,15 @@ pub struct Counts {
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)]
pub enum LedgerError {
Io(PathBuf, std::io::Error),
@@ -116,9 +125,9 @@ impl std::fmt::Display for LedgerError {
write!(f, "{}:{line}: {why}; {RUNBOOK}", path.display())
}
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) => {
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,
Waiting,
Sent(Vec<String>),
Failed,
Failed {
time: Timestamp,
status: Option<u16>,
kind: String,
},
}
#[derive(Debug, Clone)]
@@ -150,6 +163,8 @@ pub struct Ledger {
entries: HashMap<Item, Entry>,
/// Every sent item, in the order of its `Sent` record.
sends: Vec<Item>,
/// Every failed item, in the order of its `Failed` record.
fails: Vec<Item>,
repaired: bool,
}
@@ -163,6 +178,7 @@ impl Ledger {
order: Vec::new(),
entries: HashMap::new(),
sends: Vec::new(),
fails: Vec::new(),
repaired: false,
};
let Some(bytes) = read(path)? else {
@@ -267,6 +283,23 @@ impl Ledger {
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.
pub fn counts(&self) -> Counts {
let mut counts = Counts::default();
@@ -276,7 +309,7 @@ impl Ledger {
State::Skipped => counts.skipped += 1,
State::Waiting => counts.waiting += 1,
State::Sent(_) => counts.sent += 1,
State::Failed => counts.failed += 1,
State::Failed { .. } => counts.failed += 1,
}
}
counts
@@ -318,10 +351,16 @@ impl Ledger {
}
self.sends.push(item);
}
Record::Failed { item, .. } => {
Record::Failed {
time,
item,
status,
kind,
} => {
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)
}
/// An item as the error messages name it.
fn describe(item: &Item) -> String {
/// An item as the error messages and `bxctl memory sync` name it: `turn 3 of session mm-a`,
/// `part 0 of edit 2026-09-28-0001`.
pub fn label(item: &Item) -> String {
match item {
Item::Turn { session, turn } => format!("turn {turn} of session {}", session.as_str()),
Item::Edit { id, part } => format!("part {part} of edit {id}"),
+6
View File
@@ -8,6 +8,7 @@ use bxcore::{SessionId, Timestamp};
use crate::files::Store;
use crate::honcho::ledger::{self, Action, Item, Ledger, Record, Workspace};
use crate::honcho::report;
use crate::journal;
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
/// 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
+1
View File
@@ -7,6 +7,7 @@ pub mod edits;
pub mod ledger;
pub mod mirror;
pub mod naming;
pub mod report;
pub mod start;
pub mod state;
pub mod turns;
+33
View File
@@ -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
}
+5
View File
@@ -18,6 +18,7 @@ use crate::edit::{self, Context};
use crate::files::Store;
use crate::honcho::ledger::{self, LedgerError};
use crate::honcho::mirror::Mirror;
use crate::honcho::report;
use crate::honcho::state;
use crate::honcho::turns::TurnSync;
use crate::honcho::worker::{self, Wake};
@@ -454,6 +455,10 @@ impl Memoryd {
MemoryCommand::Pending => review::list(&self.store),
MemoryCommand::Accept => review::accept(&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) {
self.mirror(None);