M5c-1 plan: task 12, memory writes decided for Honcho as they are journalled

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 09:08:56 -07:00
co-authored by Claude Opus 5.5
parent 5a0cee42ec
commit d3d37c955a
5 changed files with 1411 additions and 3 deletions
@@ -0,0 +1,173 @@
//! Memory writes mirrored in Honcho (M5c spec, section 4): each journalled edit is decided into
//! the ledger the moment it is journalled, one item per Honcho action, in the order they must be
//! sent. Only decided here; the worker sends (task 13). The text is kept as journalled.
use std::sync::{Arc, Mutex};
use bxcore::{SessionId, Timestamp};
use crate::files::Store;
use crate::honcho::ledger::{self, Action, Item, Ledger, Record, Workspace};
use crate::journal;
use crate::notes::{self, Kind};
/// The session the journal gives an owner's edit (`bxctl memory undo`).
pub const OWNER: &str = "owner";
/// The part of an edit that has no Boxmaker session to stamp its conclusion with.
pub const NO_SESSION: &str = "the edit has no Boxmaker session";
/// `before` and `after` as multisets of entry text: what `before` has more of (dropped, in
/// `before`'s order), and what `after` has more of (added, in `after`'s order).
pub fn diff(before: &[String], after: &[String]) -> (Vec<String>, Vec<String>) {
// `(minus(before, after), minus(after, before))`.
todo!()
}
/// `a` without one occurrence of each text of `b`, in `a`'s order.
fn minus(a: &[String], b: &[String]) -> Vec<String> {
// `let mut left: Vec<&String> = b.iter().collect();` then
// `a.iter().filter(|text| ..).cloned().collect()`: keep a `text` when
// `left.iter().position(|t| t == text)` is `None`; when it is `Some(at)`,
// `left.swap_remove(at);` (that occurrence is used up) and drop the text.
todo!()
}
/// The journalled notes of `record`, read back.
fn notes_of(record: &journal::Record) -> Vec<notes::Note> {
// `record.notes.iter().flat_map(|n| notes::parse(n)).collect()`.
todo!()
}
/// The session an edit's conclusions carry: its own, or for an owner's undo the undone edit's,
/// following `undoes` back through undos of undos. `None` when there is none.
pub fn session_of(record: &journal::Record, records: &[journal::Record]) -> Option<SessionId> {
// `let mut current = record;`, then `for _ in 0..=records.len()` (a bound on the chain):
// when `current.session != OWNER`, return `SessionId::new(&current.session).ok()`; else
// `let undone = current.undoes.as_deref()?;` and `current = records.iter().find(|r| r.id ==
// undone)?;`. After the loop, `None`.
todo!()
}
/// A conclusion about `peer` in the trusted workspace, stamped with `session`; without a session
/// it cannot be stamped, and is a `Skip`.
fn conclusion(peer: &str, session: Option<&SessionId>, text: &str) -> Action {
// `match session`: `Some(session)` -> `Action::Conclude { workspace: Workspace::Trusted,
// peer: peer.to_string(), session: session.clone(), text: text.to_string() }`; `None` ->
// `Action::Skip { why: NO_SESSION.to_string() }`.
todo!()
}
/// An identity edit (an undo included), `file` its name: each entry dropped, a conclusion
/// deleted, then each entry added, a conclusion made, about the owner for `user.md` and the agent
/// for `memory.md`. Entries `consolidate` moved to notes are not dropped: they are still held.
fn identity_actions(
record: &journal::Record,
file: &str,
notes: &[notes::Note],
session: Option<&SessionId>,
agent: &str,
owner: &str,
) -> Vec<Action> {
// `moved`: the `text` (cloned) of each note whose `kind` is `Kind::Moved(_)`. `let (dropped,
// added) = diff(&record.before, &record.after);`; `peer` is `owner` when `file == "user.md"`,
// else `agent`. `out` starts as each text of `minus(&dropped, &moved)` (`into_iter`) as
// `Action::Unconclude { workspace: Workspace::Trusted, text }`; then
// `out.extend(added.iter().map(|text| conclusion(peer, session, text)));`. Return `out`.
todo!()
}
/// A write saved as notes: when flagged, one `Skip` naming the rule; otherwise each `noted` note,
/// a fleet message when the write was untrusted, else a conclusion about the agent.
fn note_actions(
record: &journal::Record,
notes: &[notes::Note],
session: Option<&SessionId>,
agent: &str,
) -> Vec<Action> {
// When `record.flag` is `Some(rule)`: `vec![Action::Skip { why: format!("flagged: {rule}")
// }]`. Otherwise each note whose `kind` is `Kind::Noted { .. }`, by `(record.untrusted,
// session)`: `(true, Some(session))` -> `Action::Fleet { session: session.clone(), text:
// n.text.clone() }`; `(true, None)` -> `Action::Skip { why: NO_SESSION.to_string() }`;
// `(false, _)` -> `conclusion(agent, session, &n.text)`. `.collect()`.
todo!()
}
/// What `record` asks of Honcho, in the order to send it: an identity edit's, or a write saved as
/// notes. Anything else (a rejection) asks nothing.
pub fn actions(
record: &journal::Record,
session: Option<&SessionId>,
agent: &str,
owner: &str,
) -> Vec<Action> {
let notes = notes_of(record);
match &record.file {
Some(file) => identity_actions(record, file, &notes, session, agent, owner),
None => note_actions(record, &notes, session, agent),
}
}
/// With `[honcho]`: decides each journalled edit into the ledger.
pub struct Mirror {
ledger: Arc<Mutex<Ledger>>,
agent: String,
owner: String,
}
impl Mirror {
pub fn new(ledger: Arc<Mutex<Ledger>>, agent: &str, owner: &str) -> Mirror {
Mirror {
ledger,
agent: agent.to_string(),
owner: owner.to_string(),
}
}
/// 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
/// 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) {
Ok(records) => records,
Err(e) => {
log(&format!(
"memoryd: {e}; the edit is not mirrored in Honcho; {}",
ledger::RUNBOOK
));
return;
}
};
let found = match id {
Some(id) => records.iter().find(|r| r.id == id),
None => records.last(),
};
let Some(record) = found else {
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());
for (part, action) in actions.into_iter().enumerate() {
let Ok(part) = u32::try_from(part) else {
return;
};
let item = Item::Edit {
id: record.id.clone(),
part,
};
if book.is_decided(&item) {
continue;
}
if let Err(e) = book.append(Record::Decided {
time: now,
item,
action,
}) {
log(&format!("memoryd: {e}; {}", ledger::RUNBOOK));
return;
}
}
}
}