loopd: open the highest epoch and carry its summary

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-02 23:38:10 -07:00
co-authored by ornith-1.5-35b-a3b
parent 518d6ec416
commit 18039fa2e4
3 changed files with 453 additions and 60 deletions
+193 -60
View File
@@ -1,5 +1,6 @@
//! A session on disk: `sessions/<id>/0.baseline.json` and `0.jsonl`. The log is the state —
//! create, open, append and resume all live here, and nothing is durable until `append` syncs it.
//! A session on disk: `sessions/<id>/<epoch>.baseline.json` and `<epoch>.jsonl`. The log is the
//! state — create, open, append and resume all live here, and nothing is durable until `append`
//! syncs it. Epoch 0 is the first; later epochs appear after compaction.
use std::fs::{File, OpenOptions};
use std::io::Write;
@@ -21,6 +22,9 @@ pub enum SessionError {
},
Baseline(BaselineError),
Encode(serde_json::Error),
NoCarry {
path: PathBuf,
},
}
impl std::fmt::Display for SessionError {
@@ -36,6 +40,11 @@ impl std::fmt::Display for SessionError {
),
SessionError::Baseline(err) => write!(f, "{err}"),
SessionError::Encode(err) => write!(f, "{err}"),
SessionError::NoCarry { path } => write!(
f,
"{}: compacted epoch is missing its EpochEnd; see docs/runbook.md#session-log-damaged",
path.display()
),
}
}
}
@@ -61,6 +70,11 @@ pub struct Session {
records: Vec<LogRecord>,
log: File,
next_call: CallId,
epoch: Epoch,
slot: u32,
carry: Option<String>,
prior_users: u64,
prior_untrusted: bool,
}
impl Session {
@@ -99,6 +113,11 @@ impl Session {
records: Vec::new(),
log,
next_call: CallId(1),
epoch: Epoch(0),
slot,
carry: None,
prior_users: 0,
prior_untrusted: false,
};
session.persist(LogRecord::SessionStart {
time: Timestamp::now(),
@@ -112,51 +131,26 @@ impl Session {
pub fn open(home: &Path, id: SessionId) -> Result<Session, SessionError> {
let dir = Session::dir_for(home, &id);
let log_path = dir.join("0.jsonl");
if !log_path.exists() {
let epochs = complete_epochs(&dir)?;
let Some(&n) = epochs.last() else {
return Err(SessionError::NotFound(id));
}
let baseline_path = dir.join("0.baseline.json");
};
let epoch = Epoch(n);
let baseline_path = dir.join(format!("{n}.baseline.json"));
let baseline = Baseline::load(&baseline_path).map_err(SessionError::Baseline)?;
let text = std::fs::read_to_string(&log_path)
.map_err(|e| SessionError::Io(log_path.clone(), e))?;
let mut records = Vec::new();
if !text.is_empty() {
for (idx, line) in text.split_inclusive('\n').enumerate() {
let line_no = idx + 1;
if !line.ends_with('\n') {
return Err(SessionError::Torn {
path: log_path.clone(),
line: line_no,
why: "last line does not end in a newline".to_string(),
});
let log_path = dir.join(format!("{n}.jsonl"));
let records = load_records(&log_path)?;
let (carry, prior_users, prior_untrusted) = priors(&dir, n)?;
let mut highest = max_call(&records);
for earlier in &epochs[..epochs.len() - 1] {
let path = dir.join(format!("{earlier}.jsonl"));
highest = highest.max(max_call(&load_records(&path)?));
}
match serde_json::from_str::<LogRecord>(line) {
Ok(record) => records.push(record),
Err(why) => {
return Err(SessionError::Torn {
path: log_path.clone(),
line: line_no,
why: why.to_string(),
});
}
}
}
}
let mut highest = 0u64;
for record in &records {
if let LogRecord::ToolResult { call, .. } = record {
highest = highest.max(call.0);
}
}
let slot = live_slot(&records, 0);
let log = OpenOptions::new()
.append(true)
.open(&log_path)
.map_err(|e| SessionError::Io(log_path.clone(), e))?;
Ok(Session {
id,
dir,
@@ -164,6 +158,11 @@ impl Session {
records,
log,
next_call: CallId(highest + 1),
epoch,
slot,
carry,
prior_users,
prior_untrusted,
})
}
@@ -183,6 +182,18 @@ impl Session {
&self.records
}
pub fn epoch(&self) -> Epoch {
self.epoch
}
pub fn slot(&self) -> u32 {
self.slot
}
pub fn carry(&self) -> Option<&str> {
self.carry.as_deref()
}
pub fn append(&mut self, record: LogRecord) -> Result<(), SessionError> {
self.persist(record)
}
@@ -197,28 +208,13 @@ impl Session {
/// grant, or a recalled memory-tainted note, which `brokerd` labels so. The labels are in
/// the log, so this holds after the session is reopened, and it never becomes false again.
pub fn untrusted(&self) -> bool {
self.records.iter().any(|record| {
matches!(
record,
LogRecord::ToolResult {
untrusted: true,
..
}
)
})
self.prior_untrusted || any_untrusted(&self.records)
}
/// How many turns the session has had: its `User` records, one per turn that reached the
/// log, a failed turn's too. Counted from `records`, which `open` reads back whole, so the
/// count goes on after a reopen or a restart; right after a turn it is that turn's number,
/// the first being 1. Epoch 0 is a session's only log until compaction is built.
/// How many turns the session has had: its `User` records across every epoch, a failed
/// turn's too. The first `User` of the session is turn 1 for its life.
pub fn turns(&self) -> u64 {
let users = self
.records
.iter()
.filter(|record| matches!(record, LogRecord::User { .. }))
.count();
u64::try_from(users).unwrap_or(u64::MAX)
self.prior_users.saturating_add(count_users(&self.records))
}
pub fn last_usage(&self) -> Option<bxcore::Usage> {
@@ -263,6 +259,143 @@ impl Session {
}
fn log_path(&self) -> PathBuf {
self.dir.join("0.jsonl")
self.dir.join(format!("{}.jsonl", self.epoch.0))
}
}
/// Epoch numbers that have both `<n>.jsonl` and `<n>.baseline.json`, sorted.
fn complete_epochs(dir: &Path) -> Result<Vec<u32>, SessionError> {
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(SessionError::Io(dir.to_path_buf(), e)),
};
let mut out = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| SessionError::Io(dir.to_path_buf(), e))?;
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 };
if dir.join(format!("{n}.baseline.json")).exists() {
out.push(n);
}
}
out.sort_unstable();
Ok(out)
}
fn load_records(path: &Path) -> Result<Vec<LogRecord>, SessionError> {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(SessionError::Io(path.to_path_buf(), e));
}
Err(e) => return Err(SessionError::Io(path.to_path_buf(), e)),
};
let mut records = Vec::new();
if text.is_empty() {
return Ok(records);
}
for (idx, line) in text.split_inclusive('\n').enumerate() {
let line_no = idx + 1;
if !line.ends_with('\n') {
return Err(SessionError::Torn {
path: path.to_path_buf(),
line: line_no,
why: "last line does not end in a newline".to_string(),
});
}
match serde_json::from_str::<LogRecord>(line) {
Ok(record) => records.push(record),
Err(why) => {
return Err(SessionError::Torn {
path: path.to_path_buf(),
line: line_no,
why: why.to_string(),
});
}
}
}
Ok(records)
}
/// Carry from epoch `n-1`, and user / untrusted counts from every epoch before `n`.
fn priors(dir: &Path, n: u32) -> Result<(Option<String>, u64, bool), SessionError> {
let mut prior_users = 0u64;
let mut prior_untrusted = false;
let mut carry = None;
if n == 0 {
return Ok((None, 0, false));
}
for earlier in 0..n {
let path = dir.join(format!("{earlier}.jsonl"));
let records = match load_records(&path) {
Ok(r) => r,
Err(SessionError::Io(path, e)) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(SessionError::NoCarry { path });
}
Err(e) => return Err(e),
};
prior_users = prior_users.saturating_add(count_users(&records));
prior_untrusted = prior_untrusted || any_untrusted(&records);
if earlier + 1 == n {
carry = carry_from(&records, Epoch(n)).map(str::to_string);
if carry.is_none() {
return Err(SessionError::NoCarry { path });
}
}
}
Ok((carry, prior_users, prior_untrusted))
}
fn carry_from(records: &[LogRecord], next: Epoch) -> Option<&str> {
records.iter().rev().find_map(|record| match record {
LogRecord::EpochEnd {
next: n, summary, ..
} if *n == next => Some(summary.as_str()),
_ => None,
})
}
fn count_users(records: &[LogRecord]) -> u64 {
let users = records
.iter()
.filter(|record| matches!(record, LogRecord::User { .. }))
.count();
u64::try_from(users).unwrap_or(u64::MAX)
}
fn any_untrusted(records: &[LogRecord]) -> bool {
records.iter().any(|record| {
matches!(
record,
LogRecord::ToolResult {
untrusted: true,
..
}
)
})
}
fn max_call(records: &[LogRecord]) -> u64 {
let mut highest = 0u64;
for record in records {
if let LogRecord::ToolResult { call, .. } = record {
highest = highest.max(call.0);
}
}
highest
}
fn live_slot(records: &[LogRecord], fallback: u32) -> u32 {
records
.iter()
.find_map(|record| match record {
LogRecord::SessionStart { slot, .. } => Some(*slot),
_ => None,
})
.unwrap_or(fallback)
}
+259
View File
@@ -0,0 +1,259 @@
//! Tests for multi-epoch session open, turns, trust and call ids. Do not edit.
mod support;
use bxcore::{CallId, DataClass, Epoch, LogRecord, SessionId, Timestamp};
use loopd::baseline::Baseline;
use loopd::session::{Session, SessionError};
use support::Home;
fn baseline() -> Baseline {
Baseline {
system: "sys".to_string(),
tools: vec![],
}
}
fn later() -> Baseline {
Baseline {
system: "sys-epoch-1".to_string(),
tools: vec![],
}
}
fn id(s: &str) -> SessionId {
SessionId::new(s).unwrap()
}
fn ts() -> Timestamp {
Timestamp::parse("2026-09-18T08:00:00.000Z").unwrap()
}
fn user(content: &str) -> LogRecord {
LogRecord::User {
time: ts(),
content: content.to_string(),
speaker: None,
}
}
fn write_jsonl(home: &Home, session: &str, epoch: u32, records: &[LogRecord]) {
let text: String = records
.iter()
.map(|r| format!("{}\n", serde_json::to_string(r).unwrap()))
.collect();
home.write(&format!("sessions/{session}/{epoch}.jsonl"), &text);
}
fn write_baseline(home: &Home, session: &str, epoch: u32, b: &Baseline) {
home.write(
&format!("sessions/{session}/{epoch}.baseline.json"),
&b.to_json().unwrap(),
);
}
fn start(session: &str, epoch: u32, slot: u32, b: &Baseline) -> LogRecord {
LogRecord::SessionStart {
time: ts(),
session: id(session),
epoch: Epoch(epoch),
slot,
baseline: b.hash().unwrap(),
}
}
fn epoch_end(next: u32, summary: &str) -> LogRecord {
LogRecord::EpochEnd {
time: ts(),
next: Epoch(next),
summary: summary.to_string(),
}
}
fn tool_result(call: u64, untrusted: bool) -> LogRecord {
LogRecord::ToolResult {
time: ts(),
call: CallId(call),
tool_call_id: format!("c{call}"),
content: "out".to_string(),
class: DataClass::Public,
untrusted,
truncated: false,
}
}
#[test]
fn open_loads_the_highest_complete_epoch_and_its_carry() {
let home = Home::new();
let mut s = Session::create(&home.dir, id("a"), baseline(), 3).unwrap();
s.append(user("one")).unwrap();
s.append(user("two")).unwrap();
s.append(epoch_end(1, "carry-text")).unwrap();
drop(s);
write_baseline(&home, "a", 1, &later());
write_jsonl(&home, "a", 1, &[start("a", 1, 3, &later()), user("three")]);
let s = Session::open(&home.dir, id("a")).unwrap();
assert_eq!(s.epoch(), Epoch(1));
assert_eq!(s.slot(), 3);
assert_eq!(s.carry(), Some("carry-text"));
assert_eq!(s.baseline(), &later());
assert_eq!(s.records().len(), 2);
assert!(matches!(s.records()[1], LogRecord::User { ref content, .. } if content == "three"));
}
#[test]
fn a_missing_carry_on_epoch_1_is_an_error() {
let home = Home::new();
let s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
drop(s);
write_baseline(&home, "a", 1, &later());
write_jsonl(&home, "a", 1, &[start("a", 1, 0, &later())]);
match Session::open(&home.dir, id("a")) {
Err(SessionError::NoCarry { ref path, .. }) => {
assert!(path.ends_with("0.jsonl"), "{path:?}");
}
other => panic!("{other:?}"),
}
let e: Box<dyn std::error::Error> =
Box::new(Session::open(&home.dir, id("a")).map(|_| ()).unwrap_err());
assert!(e.to_string().contains("0.jsonl"), "{e}");
assert!(
e.to_string()
.contains("see docs/runbook.md#session-log-damaged"),
"{e}"
);
}
#[test]
fn epoch_0_has_no_carry() {
let home = Home::new();
let s = Session::create(&home.dir, id("a"), baseline(), 7).unwrap();
drop(s);
let s = Session::open(&home.dir, id("a")).unwrap();
assert_eq!(s.epoch(), Epoch(0));
assert_eq!(s.carry(), None);
assert_eq!(s.slot(), 7);
}
#[test]
fn turns_count_users_across_every_epoch() {
let home = Home::new();
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
s.append(user("one")).unwrap();
s.append(user("two")).unwrap();
s.append(epoch_end(1, "sum")).unwrap();
drop(s);
write_baseline(&home, "a", 1, &later());
write_jsonl(&home, "a", 1, &[start("a", 1, 0, &later()), user("three")]);
let s = Session::open(&home.dir, id("a")).unwrap();
assert_eq!(s.turns(), 3);
assert_eq!(
s.records()
.iter()
.filter(|r| matches!(r, LogRecord::User { .. }))
.count(),
1,
"records() is the current epoch only"
);
}
#[test]
fn untrusted_reads_every_epoch() {
let home = Home::new();
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
s.append(tool_result(1, true)).unwrap();
s.append(epoch_end(1, "sum")).unwrap();
drop(s);
write_baseline(&home, "a", 1, &later());
write_jsonl(
&home,
"a",
1,
&[start("a", 1, 0, &later()), tool_result(2, false)],
);
let s = Session::open(&home.dir, id("a")).unwrap();
assert!(s.untrusted(), "an untrusted result in epoch 0 still taints");
assert!(
!s.records().iter().any(|r| matches!(
r,
LogRecord::ToolResult {
untrusted: true,
..
}
)),
"the current epoch's records look clean"
);
}
#[test]
fn call_ids_continue_across_epochs() {
let home = Home::new();
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
s.append(tool_result(4, false)).unwrap();
s.append(epoch_end(1, "sum")).unwrap();
drop(s);
write_baseline(&home, "a", 1, &later());
write_jsonl(
&home,
"a",
1,
&[start("a", 1, 0, &later()), tool_result(2, false)],
);
let mut s = Session::open(&home.dir, id("a")).unwrap();
assert_eq!(
s.next_call(),
CallId(5),
"one more than the highest call in any epoch"
);
}
#[test]
fn append_writes_the_current_epoch_file() {
let home = Home::new();
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
s.append(epoch_end(1, "sum")).unwrap();
drop(s);
write_baseline(&home, "a", 1, &later());
write_jsonl(&home, "a", 1, &[start("a", 1, 0, &later())]);
let mut s = Session::open(&home.dir, id("a")).unwrap();
s.append(user("after")).unwrap();
let text = home.read("sessions/a/1.jsonl");
assert!(text.contains("after"), "{text}");
let old = home.read("sessions/a/0.jsonl");
assert!(!old.contains("after"), "{old}");
}
#[test]
fn an_incomplete_next_epoch_is_ignored() {
let home = Home::new();
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
s.append(user("still-here")).unwrap();
drop(s);
write_baseline(&home, "a", 1, &later());
let s = Session::open(&home.dir, id("a")).unwrap();
assert_eq!(s.epoch(), Epoch(0));
assert_eq!(s.carry(), None);
assert_eq!(s.turns(), 1);
drop(s);
write_jsonl(&home, "a", 2, &[start("a", 2, 0, &later())]);
let s = Session::open(&home.dir, id("a")).unwrap();
assert_eq!(s.epoch(), Epoch(0), "a log without its baseline is ignored");
}
#[test]
fn epoch_end_without_the_next_files_stays_on_the_old_epoch() {
let home = Home::new();
let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap();
s.append(user("x")).unwrap();
s.append(epoch_end(1, "sum")).unwrap();
drop(s);
let s = Session::open(&home.dir, id("a")).unwrap();
assert_eq!(s.epoch(), Epoch(0));
assert_eq!(s.carry(), None);
assert_eq!(s.turns(), 1);
assert!(
matches!(s.records().last(), Some(LogRecord::EpochEnd { next, .. }) if *next == Epoch(1))
);
}
+1
View File
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|---|---|---|---|---|---|---|---|
| M5d1/02-loopd-session-epochs | 2026-10-02 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/session_epochs.rs` (new). Step 2 (`cargo build --tests --keep-going -p loopd`) showed exactly the named failures: `session_epochs` with E0599 (`no method named epoch` / `carry` / `slot`) and E0433 (`NoCarry`); every other target compiled and the existing `session` tests still compiled. Made S1 (module doc naming `<epoch>.baseline.json` / `<epoch>.jsonl` and epochs after compaction), S2 (`SessionError` gains `NoCarry { path: PathBuf }`), S3 (its Display arm naming the damaged-log runbook pointer), S4 (`Session` gains `epoch`, `slot`, `carry`, `prior_users`, `prior_untrusted` in that order), S5 (`create` inits the five new fields, `slot` from the argument), S6 (whole `open`: `complete_epochs` picks the highest epoch with both files, `records` is the current log, `priors` walks every earlier complete epoch for user/untrusted counts and the epoch-1 carry, `max_call` over every epoch for `next_call`, `live_slot` from the current `SessionStart`), S7 (accessors `epoch` / `slot` / `carry`), S8 (`untrusted` = `prior_untrusted || any_untrusted`, `turns` = `prior_users.saturating_add(count_users)`), S9 (`log_path` to the current epoch's file plus helpers `complete_epochs`, `load_records`, `priors`, `carry_from`, `count_users`, `any_untrusted`, `max_call`, `live_slot`). `cargo check -p loopd` clean, no error or warning. `wc -l crates/loopd/src/session.rs` prints 402. `cargo test -p loopd --test session_epochs --test session` passes: 9 in `session_epochs`, 7 in `session`. `git diff --stat` lists only `session.rs`; `cargo fmt --all` changed nothing. `make gate` printed `gate: ok` with 1473 tests (1472 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +9 over the 1464 before is exactly the tests of `session_epochs.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
| M5d1/01-loopd-compact-config | 2026-10-02 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/config_compact.rs` (new). Step 2 (`cargo build --tests --keep-going -p loopd`) showed exactly the named failures: `config_compact` with E0609 (`no field \`compact\` on type \`Config\``), E0599 (`no method named \`compact_prompt\``) and E0433 (`no variant or associated item named \`default\` for \`Compact\``); every other target compiled. Made the edits exactly as the task gives: C1 (new `Compact` struct after `Memory`, `#[derive(Debug, Clone, PartialEq, Deserialize)]` plus `#[serde(deny_unknown_fields, default)]`, fields in order `threshold: f64`, `summary_chars: usize`, `summarise: Vec<String>`, and `Default` = threshold `0.85`, `summary_chars` `4_000`, `summarise` `vec!["http_fetch".to_string()]`); C2 (`Config` gains `#[serde(default)] pub compact: Compact` after `memory`); C3 (`compact_prompt` after `channel_socket`, `self.baseline.system.with_file_name("compact.md")`). `cargo check -p loopd` clean, no error or warning. `cargo test -p loopd --test config_compact --test config` passes: 8 in `config_compact`, 11 in `config`. `git diff --stat` lists only `config.rs`; `cargo fmt --all` changed nothing. `make gate` printed `gate: ok` with 1464 tests (1463 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +8 over the 1456 before is exactly the tests of `config_compact.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
| M5c2/12-memoryd-serve-ask-memory | 2026-09-29 | done | 1 | pass | none | Copied the given test `crates/memoryd/tests/honcho_ask_serve.rs` (new). Step 2 (`cargo test --no-fail-fast -p memoryd --test honcho_ask_serve`) showed exactly the six named failures: every `ask_memory` answered with `memoryd runs only recall and memory`, and the hang test after its 10 s deadline (`the chat was never sent`). Made A1 (`ask.rs`: `NOT_CONFIGURED` before `NO_ANSWER`, the sentence letter for letter), E1 (`use bxcore::AskMemoryArgs;`), E2 (`use crate::honcho::ask;`), E3 (`call` gains the third arm `bxcore::memory::ASK_MEMORY => self.ask_memory(&call)` and the unknown-tool sentence becomes `memoryd runs only recall, memory and ask_memory`), E4 (new `ask_memory`: decode `AskMemoryArgs`, `args.check()?`, the `[honcho]`-less branch returns `NOT_CONFIGURED` labelled private and trusted, else `ask::ask` then `ask::answer`). `cargo check -p memoryd` clean, no error or warning. `cargo test` passes: 6 in `honcho_ask_serve`, 10 in `honcho_ask`, 4 in `honcho_recall_serve`, 4 in `serve_admin`, 8 in `serve`. Step 5 all pass: `git diff --stat` lists only `ask.rs` and `serve.rs`; `wc -l crates/memoryd/src/serve.rs` prints 456. `make gate` printed `gate: ok` with 1454 tests (1453 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +6 over the 1448 before is exactly the tests of `honcho_ask_serve.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |
| M5c2/11-memoryd-serve-admin | 2026-09-29 | done | 1 | pass | none | Copied the given test `crates/memoryd/tests/serve_admin.rs` (new) and the new module `crates/memoryd/src/serve/admin.rs` (given whole, `reindex` and `admin` moved unchanged from `serve.rs`, `fn` becoming `pub(super) fn`). Step 1 (`cargo test -p memoryd --test serve_admin`) passed all 4 before the move. Made E1 (dropped `MemoryAdminReply`, `MemoryCommand`, `Reindexed` from the `bxcore` use in `serve.rs`), E2 (dropped `use crate::honcho::report;`), E3 (added `mod admin;` after `use crate::{journal, recall, review};`), E4 (deleted `reindex` and `admin` from the end of `index_notes` to the end of the file). `cargo check -p memoryd` clean, no error or warning. `cargo test -p memoryd` passes: 4 in `serve_admin`, every other suite as before. Step 5 all pass: `wc -l crates/memoryd/src/serve.rs` prints 433; the `diff` of the moved `reindex` (the deleted one in `serve.rs` vs the `pub(super) fn` in `admin.rs`, `pub(super)` normalised) prints nothing; `git diff --stat` lists only `serve.rs` and `git status --short` shows `serve/` and `serve_admin.rs` as new. `make gate` printed `gate: ok` with 1448 tests (1447 in `cargo test --workspace`, plus the 1 ignored `end_to_end`; the +4 over the 1444 before is exactly the tests of `serve_admin.rs`). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |