memoryd: a reported turn decided after loopd has its answer, from brokerd and the log

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-28 11:59:34 -07:00
co-authored by ornith-1.5-35b-a3b
parent a339eb58b6
commit 28df2a0e3b
12 changed files with 1483 additions and 22 deletions
+90 -6
View File
@@ -1,5 +1,7 @@
//! Serving: three sockets, one request per connection, each answered by one frame (M5a spec,
//! section 7). One lock covers the files and the index, so writes never interleave.
//! section 7). One lock covers the files and the index, so writes never interleave. With
//! `[honcho]`, a reported turn is decided after `loopd` has its answer, under a lock of its own
//! (M5c spec, section 5).
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{Arc, Mutex, MutexGuard};
@@ -7,13 +9,16 @@ use std::time::Duration;
use bxcore::{
DataClass, Empty, Envelope, ErrorCode, MemoryAdminReply, MemoryArgs, MemoryBlockReply,
MemoryCall, MemoryCommand, Message, PROTOCOL_VERSION, RecallArgs, Reindexed, Timestamp,
ToolResponse, WireError, read_frame, write_frame,
MemoryCall, MemoryCommand, Message, PROTOCOL_VERSION, RecallArgs, Reindexed, SessionId,
SessionUpdated, Timestamp, ToolResponse, WireError, read_frame, write_frame,
};
use crate::config::Config;
use crate::edit::{self, Context};
use crate::files::Store;
use crate::honcho::ledger::{self, Ledger, LedgerError};
use crate::honcho::state;
use crate::honcho::turns::{self, TurnSync};
use crate::index::{self, Index};
use crate::recall::Recalled;
use crate::{journal, recall, review};
@@ -32,9 +37,39 @@ pub struct Memoryd {
config: Config,
store: Store,
state: Mutex<State>,
/// With `[honcho]`: the turns reported and not yet decided, and the ledger. Never taken
/// while `state` is held, and never held by a request that waits for its answer.
honcho: Option<Mutex<TurnSync>>,
log: Log,
}
/// Why `start` failed. `main` prints a start failure with the pointer `memoryd-start-failed`, and
/// a ledger that does not read through with its own.
#[derive(Debug)]
pub enum StartError {
Failed(String),
Ledger(LedgerError),
}
impl std::fmt::Display for StartError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StartError::Failed(why) => write!(f, "{why}"),
// `Damaged` names its pointer itself.
StartError::Ledger(e @ LedgerError::Damaged { .. }) => write!(f, "{e}"),
StartError::Ledger(e) => write!(f, "{e}; {}", ledger::RUNBOOK),
}
}
}
impl std::error::Error for StartError {}
impl From<String> for StartError {
fn from(why: String) -> StartError {
StartError::Failed(why)
}
}
/// A started `memoryd`: its three listeners, bound and waiting.
pub struct Running {
memoryd: Arc<Memoryd>,
@@ -48,9 +83,30 @@ enum Which {
Admin,
}
/// Create what is missing under `memory/`, open the index and catch it up, then bind the three
/// sockets. The error names what failed; the caller points at `memoryd-start-failed`.
pub fn start(config: Config, log: Log) -> Result<Running, String> {
/// With `[honcho]`, read the ledger through first. Then create what is missing under `memory/`,
/// open the index and catch it up, and bind the three sockets. The error names what failed.
pub fn start(config: Config, log: Log) -> Result<Running, StartError> {
let honcho = match config.honcho_config()? {
Some(honcho) => {
let path = config.memory_dir().join(turns::LEDGER);
let ledger = Ledger::open(&path).map_err(StartError::Ledger)?;
if ledger.repaired() {
(log)(
format!(
"memoryd: {}: a last line cut off by a crash was dropped",
path.display()
)
.as_str(),
);
}
Some(Mutex::new(TurnSync::new(
ledger,
&honcho.agent,
&config.sessions_dir(),
)))
}
None => None,
};
let store = Store::new(&config.memory_dir());
store.init(&config.memory.soul_starter)?;
let index = Index::open(&config.index_path(), config.index.max_class)?;
@@ -71,6 +127,7 @@ pub fn start(config: Config, log: Log) -> Result<Running, String> {
config,
store,
state: Mutex::new(State { index }),
honcho,
log,
}),
listeners,
@@ -141,6 +198,10 @@ impl Memoryd {
Ok(request) => request,
Err(_) => return,
};
let update = match (which, &request.msg) {
(Which::Loop, Message::SessionUpdated(update)) => Some(update.clone()),
_ => None,
};
let reply = self.handle(request.msg, which);
let env = Envelope {
v: PROTOCOL_VERSION,
@@ -149,6 +210,29 @@ impl Memoryd {
msg: reply,
};
let _ = write_frame(&mut stream, &env);
// `loopd` has its answer before `brokerd` is asked: it sends the turn's reply only after
// this frame, and `brokerd` may take seconds.
drop(stream);
if let Some(update) = update {
self.decide_turns(&update);
}
}
/// With `[honcho]`: decide the reported turn, and every other that waits (`TurnSync`).
fn decide_turns(&self, update: &SessionUpdated) {
let Some(honcho) = &self.honcho else {
return;
};
let socket = self.config.state_socket();
let ask = |session: &SessionId| state::ask(&socket, session, state::WAIT);
let mut sync = honcho.lock().unwrap_or_else(|p| p.into_inner());
sync.updated(
&update.session,
update.turn,
&ask,
Timestamp::now(),
&*self.log,
);
}
/// The answer to one request on one socket. A message sent to the wrong socket is refused.