memoryd: Honcho concatenates every epoch of a session

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-02 23:54:38 -07:00
co-authored by ornith-1.5-35b-a3b
parent 45984b45be
commit 3716595950
4 changed files with 279 additions and 8 deletions
+39 -3
View File
@@ -52,7 +52,8 @@ pub struct TurnSync {
status: Shared,
}
/// The path of `session`'s log under `sessions`: epoch 0, the only one `loopd` writes.
/// The path of `session`'s epoch-0 log under `sessions`. Later epochs are sibling files;
/// `read_session` concatenates them. Existing tests pin this path.
pub fn log_path(sessions: &Path, session: &SessionId) -> PathBuf {
sessions.join(session.as_str()).join("0.jsonl")
}
@@ -115,6 +116,41 @@ pub fn read_records(path: &Path) -> Result<Vec<LogRecord>, LogError> {
Ok(records)
}
/// Every `<n>.jsonl` of `session`, sorted by `n`. A missing directory is empty.
pub fn epoch_logs(sessions: &Path, session: &SessionId) -> Result<Vec<PathBuf>, LogError> {
let dir = sessions.join(session.as_str());
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(LogError::Io(format!("{}: {e}", dir.display()))),
};
let mut found: Vec<(u32, PathBuf)> = Vec::new();
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => return Err(LogError::Io(format!("{}: {e}", dir.display()))),
};
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
let Some(stem) = name.strip_suffix(".jsonl") else {
continue;
};
let Ok(n) = stem.parse::<u32>() else { continue };
found.push((n, entry.path()));
}
found.sort_by_key(|(n, _)| *n);
Ok(found.into_iter().map(|(_, p)| p).collect())
}
/// Every epoch's records, in epoch order. A missing session has none.
pub fn read_session(sessions: &Path, session: &SessionId) -> Result<Vec<LogRecord>, LogError> {
let mut out = Vec::new();
for path in epoch_logs(sessions, session)? {
out.extend(read_records(&path)?);
}
Ok(out)
}
/// Whether a turn has ended with an answer: its last `Assistant` record asks for no tool.
pub fn finished(turn: &[LogRecord]) -> bool {
let last = turn
@@ -178,7 +214,7 @@ impl TurnSync {
}
}
for session in sessions {
if let Ok(records) = read_log(&log_path(&self.sessions, &session)) {
if let Ok(records) = read_session(&self.sessions, &session) {
self.reported.insert(
session.as_str().to_string(),
(session.clone(), turns(&records)),
@@ -271,7 +307,7 @@ impl TurnSync {
/// `session`'s log, read. A log that does not read is logged once, until it reads again.
fn records(&mut self, session: &SessionId, log: &dyn Fn(&str)) -> Option<Vec<LogRecord>> {
match read_log(&log_path(&self.sessions, session)) {
match read_session(&self.sessions, session).map_err(|e| e.to_string()) {
Ok(records) => {
self.unreadable.remove(session.as_str());
Some(records)
+14 -5
View File
@@ -13,11 +13,12 @@ use std::time::Duration;
use bxcore::{SessionId, SessionStateReply, Timestamp};
use crate::honcho::client::{Clients, HonchoError, classify};
use crate::honcho::decide::{final_reply, turn_records, user_text};
use crate::honcho::edits;
use crate::honcho::ledger::{self, Action, Item, Ledger, Record, Workspace};
use crate::honcho::naming::{HonchoSession, honcho_session};
use crate::honcho::status::{self, Shared};
use crate::honcho::turns::{TurnSync, log_path};
use crate::honcho::turns::{LogError, TurnSync, log_path, read_session};
use crate::serve::Log;
use crate::honcho::messages::ids;
@@ -338,11 +339,19 @@ impl Worker {
honcho: Option<&HonchoSession>,
) -> Result<Vec<String>, Unsent> {
let path = log_path(&self.sessions, session);
let read = read_turn(&path, turn).map_err(Unsent::Unreadable)?;
self.readable(&path);
let (text, reply) = read.ok_or(Unsent::Local("log"))?;
let records = match read_session(&self.sessions, session) {
Ok(records) => {
self.readable(&path);
records
}
Err(LogError::Damaged(_)) => return Err(Unsent::Local("log")),
Err(e) => return Err(Unsent::Unreadable(e.to_string())),
};
let slice = turn_records(&records, turn).ok_or(Unsent::Local("log"))?;
let (text, _) = user_text(slice);
let reply = final_reply(slice).ok_or(Unsent::Local("log"))?;
let honcho = honcho.ok_or(Unsent::Local("log"))?;
let messages = batch(&text, &reply, speaker, &self.agent, session, turn);
let messages = batch(text, reply, speaker, &self.agent, session, turn);
if messages.len() > MAX_BATCH {
return Err(Unsent::Local("too-long"));
}