loopd: write the next epoch from a compaction summary

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-03 00:25:04 -07:00
co-authored by ornith-1.5-35b-a3b
parent c8b0a01c41
commit fe8e3e9354
4 changed files with 284 additions and 2 deletions
+45 -2
View File
@@ -2,12 +2,14 @@
//! (M5d spec, section 3). Task 07 writes the next files; task 08 calls this after a turn.
use bxcore::sanitize::neutralise;
use bxcore::{LogRecord, Usage};
use bxcore::{Epoch, LogRecord, Timestamp, Usage};
use crate::baseline::messages_with_carry;
use crate::baseline::{Baseline, messages_with_carry};
use crate::config::Config;
use crate::llama::{ChatMessage, ChatRequest, Client};
use crate::memory::MemorySource;
use crate::session::Session;
use crate::tools::Registry;
use crate::turn::is_context_full;
/// Why a summary call or a next-epoch write failed. Every variant's `Display` ends with the
@@ -90,3 +92,44 @@ pub fn summarise(cfg: &Config, client: &Client, session: &Session) -> Result<Str
}),
}
}
/// Append `EpochEnd` (unless it is already last), rebuild the baseline from `memory`, and
/// switch `session` onto the next epoch. When the current log already ends with `EpochEnd`,
/// that record's summary is the carry and the argument is ignored.
pub fn commit(
session: &mut Session,
cfg: &Config,
registry: &Registry,
memory: &dyn MemorySource,
summary: String,
) -> Result<(), CompactError> {
let (next, summary) = match session.records().last() {
Some(LogRecord::EpochEnd { next, summary, .. }) => (*next, summary.clone()),
_ => {
let next = Epoch(session.epoch().0.saturating_add(1));
session
.append(LogRecord::EpochEnd {
time: Timestamp::now(),
next,
summary: summary.clone(),
})
.map_err(|e| CompactError::Failed {
detail: e.to_string(),
})?;
(next, summary)
}
};
let block = memory
.block()
.map_err(|e| CompactError::Failed { detail: e })?;
let baseline =
Baseline::assemble_with(cfg, registry, &block).map_err(|e| CompactError::Failed {
detail: e.to_string(),
})?;
session
.begin_epoch(next, baseline, summary)
.map_err(|e| CompactError::Failed {
detail: e.to_string(),
})?;
Ok(())
}
+37
View File
@@ -261,6 +261,43 @@ impl Session {
fn log_path(&self) -> PathBuf {
self.dir.join(format!("{}.jsonl", self.epoch.0))
}
/// Write `<n>.baseline.json` and `<n>.jsonl` with `SessionStart`, and switch this session
/// onto that epoch. The live slot and `next_call` stay. Callers pass the carry that
/// `messages_with_carry` will insert.
pub(crate) fn begin_epoch(
&mut self,
next: Epoch,
baseline: Baseline,
carry: String,
) -> Result<(), SessionError> {
let n = next.0;
let baseline_path = self.dir.join(format!("{n}.baseline.json"));
let json = baseline.to_json().map_err(SessionError::Encode)?;
std::fs::write(&baseline_path, json)
.map_err(|e| SessionError::Io(baseline_path.clone(), e))?;
let log_path = self.dir.join(format!("{n}.jsonl"));
let log = OpenOptions::new()
.create_new(true)
.append(true)
.open(&log_path)
.map_err(|e| SessionError::Io(log_path.clone(), e))?;
self.prior_users = self.prior_users.saturating_add(count_users(&self.records));
self.prior_untrusted = self.prior_untrusted || any_untrusted(&self.records);
self.epoch = next;
self.baseline = baseline;
self.carry = Some(carry);
self.records = Vec::new();
self.log = log;
let hash = self.baseline.hash().map_err(SessionError::Baseline)?;
self.persist(LogRecord::SessionStart {
time: Timestamp::now(),
session: self.id.clone(),
epoch: next,
slot: self.slot,
baseline: hash,
})
}
}
/// Epoch numbers that have both `<n>.jsonl` and `<n>.baseline.json`, sorted.