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}"),