review: an undo concludes again only what it had deleted

Undoing a consolidate concluded its moved entries a second time, and a later
removal deleted only the newer conclusion. An undo's added entry is now
concluded only when the ledger decided a Conclude for it and then an
Unconclude: an entry still concluded, or never concluded (a secret session's,
one from before sync), gets nothing. Ledger::concluded and unconcluded answer
from the decisions, in a module of their own to keep ledger.rs under 500 lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:06:28 -07:00
co-authored by Claude Opus 5.5
parent b392af9ca4
commit 7c207f6ec6
4 changed files with 283 additions and 3 deletions
+40
View File
@@ -0,0 +1,40 @@
//! What the ledger has decided about one conclusion (M5c spec, section 4): whether it is made, or
//! was made and then deleted, by the `Conclude` and `Unconclude` decisions alone, sent or not.
//! The owner's undo asks this, so that it concludes again only what it had deleted.
use crate::honcho::ledger::{Action, Ledger, Workspace};
impl Ledger {
/// Whether the latest decided `Conclude` or `Unconclude` of `text` about `peer` in
/// `workspace`, in the ledger's order, is a `Conclude`: its conclusion is made, or will be.
pub fn concluded(&self, workspace: Workspace, peer: &str, text: &str) -> bool {
self.last_of(workspace, peer, text) == Some(true)
}
/// Whether a `Conclude` of the same was decided, and the latest was followed by an
/// `Unconclude`: its conclusion was made, then deleted.
pub fn unconcluded(&self, workspace: Workspace, peer: &str, text: &str) -> bool {
self.last_of(workspace, peer, text) == Some(false)
}
/// The latest of `Conclude` (true) and, after a `Conclude`, `Unconclude` (false) for the key;
/// `None` when no `Conclude` was ever decided.
fn last_of(&self, workspace: Workspace, peer: &str, text: &str) -> Option<bool> {
let key = |w: &Workspace, p: &str, t: &str| *w == workspace && p == peer && t == text;
self.decisions()
.fold(None, |last, (_, action)| match action {
Action::Conclude {
workspace: w,
peer: p,
text: t,
..
} if key(w, p, t) => Some(true),
Action::Unconclude {
workspace: w,
peer: p,
text: t,
} if key(w, p, t) => last.map(|_| false),
_ => last,
})
}
}
+23 -3
View File
@@ -185,6 +185,22 @@ pub fn actions(
kept
}
/// Whether an undo's `action` is kept: an entry it brings back is concluded again only when its
/// conclusion was made and then deleted (M5c spec, section 4: "any it had deleted, written
/// again"). One never concluded (a secret session's, from before sync, skipped) or still
/// concluded (moved out by `consolidate`) gets nothing. Every other action is kept.
fn brought_back(ledger: &Ledger, action: &Action) -> bool {
match action {
Action::Conclude {
workspace,
peer,
text,
..
} => ledger.unconcluded(*workspace, peer, text),
_ => true,
}
}
/// With `[honcho]`: decides each journalled edit into the ledger.
pub struct Mirror {
ledger: Arc<Mutex<Ledger>>,
@@ -207,8 +223,9 @@ impl Mirror {
}
/// 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
/// right after it was journalled, so the ledger has the journal's order; the ledger's lock is
/// taken inside the index lock, and before an undo is decided against it. Its parts are
/// `Edit { id, part }` from 0, numbering what is decided. An edit already decided, or asking nothing, adds no line. A
/// failure is logged; the edit itself stands.
pub fn journalled(&self, store: &Store, id: Option<&str>, now: Timestamp, log: &dyn Fn(&str)) {
let records = match journal::records(store) {
@@ -229,8 +246,11 @@ impl Mirror {
return;
};
let session = session_of(record, &records);
let actions = actions(record, session.as_ref(), &self.agent, &self.owner);
let mut book = self.ledger.lock().unwrap_or_else(|p| p.into_inner());
let mut actions = actions(record, session.as_ref(), &self.agent, &self.owner);
if record.undoes.is_some() {
actions.retain(|a| brought_back(&book, a));
}
for (part, action) in actions.into_iter().enumerate() {
let Ok(part) = u32::try_from(part) else {
return;
+1
View File
@@ -1,6 +1,7 @@
//! Honcho, the memory this agent shares with the fleet, reached through `roncho` (M5c spec).
pub mod client;
pub mod concluded;
pub mod config;
pub mod decide;
pub mod edits;