use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::io::{Read as _, Seek as _, SeekFrom};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::assimilate::{self, Carry, Found};
use crate::collective::{self, Config, Route};
use crate::harness::{self, Harness, Level};
use crate::learn::{self, Episode, Known};
pub fn on() -> bool {
learn::learning() && std::env::var("BORG_TRANSCRIPT_CAPTURE").as_deref() != Ok("off")
}
const HASHED: u64 = 64;
const FIRST_READ: u64 = 4 << 20;
#[derive(Debug, Default, Serialize, Deserialize)]
struct Swept {
session: String,
harness: String,
path: PathBuf,
offset: u64,
before: String,
carry: Carry,
}
fn swept_file(state: &Path, session: &str) -> PathBuf {
state.join(format!("{}.swept", crate::notes::safe(session)))
}
fn load(file: &Path) -> Option<Swept> {
serde_json::from_slice(&std::fs::read(file).ok()?).ok()
}
fn save(file: &Path, st: &Swept) {
let tmp = file.with_extension("swept.tmp");
if serde_json::to_vec(st).ok().is_some_and(|b| std::fs::write(&tmp, b).is_ok()) {
let _ = std::fs::rename(&tmp, file);
}
}
fn hash(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex()[..16].to_string()
}
fn user_home() -> PathBuf {
std::env::var_os("HOME").map(PathBuf::from).unwrap_or_default()
}
#[cfg(test)]
thread_local! {
static TOUCHES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
fn touched() {
#[cfg(test)]
TOUCHES.with(|t| t.set(t.get() + 1));
}
#[derive(Clone, Copy)]
enum Restart {
Tail,
Start,
}
struct Read {
lines: Vec<Value>,
end: u64,
end_hash: String,
}
fn read_from(f: &mut std::fs::File, from: u64, before: Option<&str>, align: bool) -> Option<Read> {
let start = from - from.min(HASHED);
let mut buf = Vec::new();
f.seek(SeekFrom::Start(start)).ok()?;
f.read_to_end(&mut buf).ok()?;
let pre = usize::try_from(from - start).ok()?;
if buf.len() < pre {
return None; }
if before.is_some_and(|b| hash(&buf[..pre]) != b) {
return None;
}
let data = &buf[pre..];
let skip = if align && from > 0 { data.iter().position(|b| *b == b'\n').map_or(data.len(), |i| i + 1) } else { 0 };
let body = &data[skip..];
let whole = body.iter().rposition(|b| *b == b'\n').map_or(0, |i| i + 1);
let lines = body[..whole].split(|b| *b == b'\n').filter_map(|l| serde_json::from_slice(l).ok()).collect();
let end = from + (skip + whole) as u64;
let upto = usize::try_from(end - start).ok()?;
let end_hash = hash(&buf[upto - upto.min(HASHED as usize)..upto]);
Some(Read { lines, end, end_hash })
}
fn size(path: &Path) -> Option<u64> {
touched();
std::fs::metadata(path).ok().map(|m| m.len())
}
fn swept_to_end(st: &Swept, path: &Path, len: u64) -> bool {
st.path == path && st.offset == len && len > 0
}
fn added(st: &Swept, path: &Path, len: u64, restart: Restart) -> Option<(Read, Carry)> {
let same = st.path == path && st.offset > 0 && st.offset <= len;
touched();
let mut f = std::fs::File::open(path).ok()?;
if same && let Some(r) = read_from(&mut f, st.offset, Some(&st.before), false) {
return Some((r, st.carry.clone()));
}
let from = match restart {
Restart::Tail => len.saturating_sub(FIRST_READ),
Restart::Start => 0,
};
read_from(&mut f, from, None, true).map(|r| (r, Carry::default()))
}
fn swept_of(h: &dyn Harness, file: &Path, session: &str) -> Swept {
load(file).filter(|s| s.harness == h.name() && s.session == session).unwrap_or_default()
}
fn transcript(st: &Swept, locate: &dyn Fn() -> Option<PathBuf>) -> Option<(PathBuf, u64)> {
if !st.path.as_os_str().is_empty()
&& let Some(len) = size(&st.path)
{
return Some((st.path.clone(), len));
}
let path = locate()?;
size(&path).map(|len| (path, len))
}
fn sweep(h: &dyn Harness, state: &Path, session: &str, locate: &dyn Fn() -> Option<PathBuf>, how: (Restart, bool), place: Place) -> BTreeSet<PathBuf> {
let (restart, whole) = how;
let file = swept_file(state, session);
if !whole {
let st = swept_of(h, &file, session);
if transcript(&st, locate).is_none_or(|(path, len)| swept_to_end(&st, &path, len)) {
return BTreeSet::new();
}
}
let _ = std::fs::create_dir_all(state);
crate::live::locked(&file, || {
let st = swept_of(h, &file, session);
let Some((path, len)) = transcript(&st, locate) else { return BTreeSet::new() };
let st = if whole { Swept::default() } else { st };
if swept_to_end(&st, &path, len) {
return BTreeSet::new();
}
let Some((read, carry)) = added(&st, &path, len, restart) else { return BTreeSet::new() };
let Some((found, carry)) = assimilate::log_lines(h, &path, session, carry, read.lines) else { return BTreeSet::new() };
let got = if found.is_empty() { BTreeSet::new() } else { place(found) };
save(&file, &Swept { session: session.into(), harness: h.name().into(), path, offset: read.end, before: read.end_hash, carry });
got
})
}
type Place<'a> = &'a dyn Fn(Vec<Found>) -> BTreeSet<PathBuf>;
fn record(found: Vec<Found>) -> BTreeSet<PathBuf> {
let Ok(cfg) = collective::load() else { return BTreeSet::new() };
let base = collective::root_of(&learn::home());
record_in(found, cfg.as_ref(), &base)
}
fn record_in(found: Vec<Found>, cfg: Option<&Config>, base: &Path) -> BTreeSet<PathBuf> {
let mut known: HashMap<PathBuf, Known> = HashMap::new();
let mut engineers = HashMap::new();
let mut got = BTreeSet::new();
for f in found {
let Some(mut p) = assimilate::place(cfg, base, f) else { continue };
let k = known.entry(p.home.clone()).or_insert_with(|| Known::read(&p.home));
if k.holds(&p.found.episode) {
continue;
}
assimilate::complete(&mut p.found, &mut engineers);
if learn::record_new_in(&p.home, k, &p.found.episode).unwrap_or(false) {
assimilate::count_veto(&p.home, &p.found.episode);
got.insert(p.home);
}
}
got
}
pub fn turn_end(h: &dyn Harness, session: &str, input: &Value) {
if !on() {
return;
}
let home = user_home();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
sweep(h, &crate::notes::state_dir(), session, &|| h.session_log(&home, session, input), (Restart::Tail, false), &record)
}));
}
fn state_dirs(cfg: Option<&Config>, base: &Path) -> Vec<PathBuf> {
let set = std::env::var_os("BORG_STATE").filter(|s| !s.is_empty()).map(PathBuf::from);
let homes = crate::install::collectives(cfg, base).into_iter().filter_map(|r| match r {
Route::Collective { home, .. } => Some(home.join("state")),
Route::Off => None,
});
set.into_iter().chain(std::iter::once(base.join("state"))).chain(homes).collect::<BTreeSet<_>>().into_iter().collect()
}
pub fn session_end(session: &str, cfg: Option<&Config>, base: &Path) -> BTreeSet<PathBuf> {
let hs: Vec<&dyn Harness> = match harness::named_current() {
Some(h) => vec![h],
None => harness::visible().collect(),
};
let home = user_home();
let fallback = || std::env::var_os("BORG_STATE").filter(|s| !s.is_empty()).map_or_else(|| learn::home().join("state"), PathBuf::from);
let state = state_dirs(cfg, base).into_iter().find(|d| swept_file(d, session).exists()).unwrap_or_else(fallback);
let here = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
let ends = Ends {
locate: &|h| h.session_log(&home, session, &Value::Null),
export: &|h| assimilate::exported(h, session, &here),
place: &|found| record_in(found, cfg, base),
};
end_of(&hs, session, &state, &ends)
}
struct Ends<'a> {
locate: &'a dyn Fn(&dyn Harness) -> Option<PathBuf>,
export: &'a dyn Fn(&dyn Harness) -> Option<Vec<Found>>,
place: Place<'a>,
}
fn end_of(hs: &[&dyn Harness], session: &str, state: &Path, e: &Ends) -> BTreeSet<PathBuf> {
let mut got = BTreeSet::new();
for &h in hs {
if let Some(found) = (e.export)(h) {
got.extend((e.place)(found));
continue;
}
got.extend(sweep(h, state, session, &|| (e.locate)(h), (Restart::Start, true), e.place));
}
got
}
pub fn idle(cfg: Option<&Config>, base: &Path, now: i64) -> BTreeSet<PathBuf> {
idle_in(&state_dirs(cfg, base), now, &|found| record_in(found, cfg, base))
}
fn idle_in(dirs: &[PathBuf], now: i64, place: Place) -> BTreeSet<PathBuf> {
let mut got = BTreeSet::new();
for dir in dirs {
let files = std::fs::read_dir(dir).into_iter().flatten().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "swept"));
for file in files {
let Some(st) = load(&file) else { continue };
let Some(h) = harness::by_name(&st.harness) else { continue };
let Ok(meta) = std::fs::metadata(&st.path) else {
let _ = std::fs::remove_file(&file);
continue;
};
let quiet = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.is_some_and(|d| d.as_millis() as i64 <= now - crate::outcome::IDLE_MS);
if quiet && meta.len() != st.offset {
got.extend(sweep(h, dir, &st.session, &|| None, (Restart::Start, false), place));
}
}
}
got
}
pub const DOCTOR_DAYS: i64 = 14;
const DOCTOR_FILES: usize = 200;
const CAPTURED_SHARE: f64 = 0.5;
#[derive(Debug, Default)]
struct Held {
hook: usize,
transcript: usize,
answers: usize,
first_transcript: Option<(i64, String)>,
}
fn held(cfg: Option<&Config>, base: &Path, since: i64) -> HashMap<String, Held> {
let mut out: HashMap<String, Held> = HashMap::new();
for r in crate::install::collectives(cfg, base) {
let Route::Collective { home, .. } = r else { continue };
let text = std::fs::read_to_string(home.join("episodes.jsonl")).unwrap_or_default();
for e in text.lines().filter_map(|l| serde_json::from_str::<Episode>(l).ok()).filter(|e| e.ms >= since) {
let h = out.entry(e.session.clone()).or_default();
match (e.via.as_str(), &e.rejected_tool, e.harness.is_empty()) {
(learn::ANSWER, ..) => h.answers += 1,
("", Some(_), true) => h.hook += 1,
("", Some(_), false) => {
h.transcript += 1;
if h.first_transcript.as_ref().is_none_or(|(ms, _)| e.ms < *ms) {
h.first_transcript = Some((e.ms, e.harness_version.clone()));
}
}
_ => {}
}
}
}
out
}
#[derive(Debug, Default)]
struct Tally {
sessions: usize,
rejections: usize,
rejections_held: usize,
answers: usize,
answers_held: usize,
first_lost_rejection: Option<(i64, String)>,
first_lost_answer: Option<(i64, String)>,
hook: usize,
transcript: usize,
first_transcript: Option<(i64, String)>,
last: i64,
}
fn earliest(slot: &mut Option<(i64, String)>, at: Option<&(i64, String)>) {
if let Some(at) = at
&& slot.as_ref().is_none_or(|(ms, _)| at.0 < *ms)
{
*slot = Some(at.clone());
}
}
impl Tally {
fn add(&mut self, m: &assimilate::Marks, h: Option<&Held>) {
let none = Held::default();
let h = h.unwrap_or(&none);
self.sessions += 1;
let rejections_held = (h.hook + h.transcript).min(m.rejections.len());
let answers_held = h.answers.min(m.answers.len());
if rejections_held < m.rejections.len() {
earliest(&mut self.first_lost_rejection, m.rejections.iter().min());
}
if answers_held < m.answers.len() {
earliest(&mut self.first_lost_answer, m.answers.iter().min());
}
(self.rejections, self.rejections_held) = (self.rejections + m.rejections.len(), self.rejections_held + rejections_held);
(self.answers, self.answers_held) = (self.answers + m.answers.len(), self.answers_held + answers_held);
(self.hook, self.transcript) = (self.hook + h.hook, self.transcript + h.transcript);
earliest(&mut self.first_transcript, h.first_transcript.as_ref());
self.last = m.rejections.iter().chain(&m.answers).map(|(ms, _)| *ms).fold(self.last, i64::max);
}
}
fn far_fewer(held: usize, marks: usize) -> bool {
marks > 0 && (held as f64) < marks as f64 * CAPTURED_SHARE
}
fn named(h: &dyn Harness, version: &str) -> String {
if version.is_empty() { h.name().to_string() } else { format!("{} {version}", h.name()) }
}
fn verdicts(h: &dyn Harness, versions: &BTreeMap<String, Tally>, note: &str, capture_on: bool) -> Vec<(Level, String)> {
let mut out = Vec::new();
let mut warned = false;
let mut ordered: Vec<(&String, &Tally)> = versions.iter().collect();
ordered.sort_by_key(|(_, t)| t.last);
for (version, t) in ordered {
let lost = |what: &str, marks: usize, held: usize, first: &Option<(i64, String)>| {
let day = crate::stats::day(first.as_ref().map_or(0, |(ms, _)| *ms));
format!(
"{}: {marks} {what} in its transcripts since {day}, {held} captured{note}: transcript capture recovers them (borg assimilate --since {day} to recover the older ones)",
named(h, version)
)
};
if far_fewer(t.rejections_held, t.rejections) {
out.push((Level::Warn, lost("rejections with the engineer's words", t.rejections, t.rejections_held, &t.first_lost_rejection)));
warned = true;
}
if far_fewer(t.answers_held, t.answers) {
out.push((Level::Warn, lost("answers to the drone's questions", t.answers, t.answers_held, &t.first_lost_answer)));
warned = true;
}
if h.caps().deny_capture && t.hook == 0 && t.transcript > 0 {
let covered = capture_on && !far_fewer(t.rejections_held, t.rejections);
let day = crate::stats::day(t.first_transcript.as_ref().map_or(0, |(ms, _)| *ms));
let tail = if covered { "transcript capture covers it" } else { "only transcript capture recovers them, and it is off (BORG_TRANSCRIPT_CAPTURE)" };
out.push((
if covered { Level::Ok } else { Level::Warn },
format!(
"{}: its hook captured no rejected tool call since {day}, its transcript {}: the harness stopped telling the hook ({tail})",
named(h, version),
t.transcript
),
));
warned |= !covered;
}
}
let sum = |f: fn(&Tally) -> usize| versions.values().map(f).sum::<usize>();
let (rejections, answers) = (sum(|t| t.rejections), sum(|t| t.answers));
if !warned && rejections + answers > 0 {
out.push((
Level::Ok,
format!(
"{}: capture holds {} of {rejections} rejections and {} of {answers} answers in {} sessions' transcripts of the last {DOCTOR_DAYS} days{note}",
h.name(),
sum(|t| t.rejections_held),
sum(|t| t.answers_held),
sum(|t| t.sessions)
),
));
}
out
}
fn modified_ms(p: &Path) -> i64 {
std::fs::metadata(p).and_then(|m| m.modified()).ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map_or(0, |d| d.as_millis() as i64)
}
pub fn doctor(cfg: Option<&Config>, base: &Path, home: &Path, now: i64) -> Vec<(Level, String)> {
if !learn::learning() {
return vec![(Level::Ok, "capture: learning is off (BORG_LEARN=off), nothing is captured".into())];
}
let mut out = doctor_with(cfg, base, now, on(), &|h| assimilate::logs(h, home));
if !on() {
out.push((Level::Warn, "capture: BORG_TRANSCRIPT_CAPTURE=off: corrections reach Borg through the hooks alone".into()));
}
out
}
fn doctor_with(cfg: Option<&Config>, base: &Path, now: i64, capture_on: bool, logs: &dyn Fn(&dyn Harness) -> Option<Vec<PathBuf>>) -> Vec<(Level, String)> {
let since = now - DOCTOR_DAYS * crate::clock::DAY_MS;
let held = held(cfg, base, since);
let mut out = Vec::new();
for h in harness::visible() {
let Some(files) = logs(h) else { continue };
let mut recent: Vec<(i64, PathBuf)> = files.into_iter().map(|f| (modified_ms(&f), f)).filter(|(ms, _)| *ms >= since).collect();
recent.sort_by_key(|(ms, _)| std::cmp::Reverse(*ms));
let note = if recent.len() > DOCTOR_FILES { format!(" (the newest {DOCTOR_FILES} of {} transcripts read)", recent.len()) } else { String::new() };
let mut versions: BTreeMap<String, Tally> = BTreeMap::new();
for (_, f) in recent.iter().take(DOCTOR_FILES) {
let Some(mut m) = assimilate::marks(h, f) else { continue };
if matches!(collective::route(cfg, &m.cwd, base), Route::Off) {
continue;
}
m.rejections.retain(|(ms, _)| *ms >= since);
m.answers.retain(|(ms, _)| *ms >= since);
versions.entry(m.version.clone()).or_default().add(&m, held.get(&m.session));
}
out.extend(verdicts(h, &versions, ¬e, capture_on));
}
out
}
pub fn sources(home: &Path) -> BTreeMap<String, (usize, i64)> {
let mut out: BTreeMap<String, (usize, i64)> = BTreeMap::new();
let text = std::fs::read_to_string(home.join("episodes.jsonl")).unwrap_or_default();
for e in text.lines().filter_map(|l| serde_json::from_str::<Episode>(l).ok()) {
let key = match (e.harness.is_empty(), e.kind.as_str()) {
(false, _) => format!("{} transcript", e.harness),
(true, crate::veto::KIND) => "borg no".to_string(),
(true, "") => "hook or older import".to_string(),
(true, _) => continue,
};
let s = out.entry(key).or_default();
(s.0, s.1) = (s.0 + 1, s.1.max(e.ms));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::harness::claude::Claude;
const DEV: &str = r#"{ "default": "off", "collectives": [ { "name": "dev", "paths": ["/home/dev/work"] } ] }"#;
const CLAUDE_SESSION: &str = "7f3c2a10-5b6d-4e8f-9a01-23456789abcd";
fn temp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("borg-sweep-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn fixture(rel: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(rel)
}
fn claude_fixture() -> PathBuf {
fixture(&format!("claude/transcripts/-home-dev-work-relay/{CLAUDE_SESSION}.jsonl"))
}
fn stored(base: &Path) -> Vec<Episode> {
let text = std::fs::read_to_string(base.join("collectives/dev/episodes.jsonl")).unwrap_or_default();
text.lines().map(|l| serde_json::from_str(l).unwrap()).collect()
}
fn ids(eps: &[Episode]) -> Vec<String> {
eps.iter().map(|e| e.id.clone()).collect()
}
fn lines_of(p: &Path) -> Vec<Vec<u8>> {
std::fs::read(p).unwrap().split_inclusive(|b| *b == b'\n').map(<[u8]>::to_vec).collect()
}
fn grow(t: &Path, bytes: &[u8]) {
use std::io::Write as _;
std::fs::OpenOptions::new().create(true).append(true).open(t).unwrap().write_all(bytes).unwrap();
}
fn touches() -> usize {
TOUCHES.with(std::cell::Cell::get)
}
#[test]
fn a_growing_transcript_gives_each_episode_once_and_a_call_pairs_with_its_result_across_turns() {
let root = temp("grow");
let (base, state, t) = (root.join("home"), root.join("state"), root.join("t.jsonl"));
let cfg: Config = serde_json::from_str(DEV).unwrap();
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
let turn = || sweep(&Claude, &state, CLAUDE_SESSION, &|| Some(t.clone()), (Restart::Tail, false), &place);
let lines = lines_of(&claude_fixture());
grow(&t, &lines[..2].concat());
assert!(turn().is_empty());
grow(&t, &lines[2]);
grow(&t, &lines[3][..40]);
assert_eq!(turn().len(), 1);
let first = stored(&base);
assert_eq!(ids(&first), ["7f3c2a10-Ttoolu_01RelayEdit"]);
assert!(first[0].rejected_tool.as_deref().is_some_and(|t| t.starts_with("Edit ")), "the call read a turn before pairs: {first:?}");
assert_eq!(first[0].task, "Add a retry to the relay delivery");
grow(&t, &lines[3][40..]);
grow(&t, &lines[4..].concat());
turn();
turn();
let all = stored(&base);
let whole: Vec<Episode> = assimilate::claude_session(&claude_fixture()).into_iter().map(|f| f.episode).collect();
assert_eq!(ids(&all), ids(&whole), "each once, the transcript's ids");
assert!(all.iter().all(|e| e.repo == "relay" && e.harness == Claude.name() && e.harness_version == "2.1.312"));
let before = touches();
assert!(turn().is_empty());
assert_eq!(touches() - before, 1);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn swept_line_by_line_every_harness_gives_what_one_read_of_the_whole_gives() {
let cfg: Config = serde_json::from_str(DEV).unwrap();
for (session, h, path) in fixtures().into_iter().filter(|(.., p)| p.extension().is_some_and(|x| x == "jsonl")) {
let root = temp(&format!("lines-{}", h.name()));
let (base, state, t) = (root.join("home"), root.join("state"), root.join("t.jsonl"));
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
for line in lines_of(&path) {
grow(&t, &line);
sweep(h, &state, session, &|| Some(t.clone()), (Restart::Tail, false), &place);
}
let all: Vec<Value> = std::fs::read_to_string(&path).unwrap().lines().map(|l| serde_json::from_str(l).unwrap()).collect();
let (whole, _) = assimilate::log_lines(h, &path, session, Carry::default(), all).unwrap();
let shape = |e: &Episode| (e.id.clone(), e.task.clone(), e.agent.clone(), e.rejected_tool.clone(), e.correction.clone(), e.via.clone());
let got: Vec<_> = stored(&base).iter().map(shape).collect();
let want: Vec<_> = whole.iter().map(|f| shape(&f.episode)).collect();
assert!(!want.is_empty());
assert_eq!(got, want, "{}", h.name());
let _ = std::fs::remove_dir_all(root);
}
}
#[test]
fn a_truncated_or_rewritten_transcript_is_read_again_safely() {
let root = temp("truncate");
let (base, state, t) = (root.join("home"), root.join("state"), root.join("t.jsonl"));
let cfg: Config = serde_json::from_str(DEV).unwrap();
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
let turn = || sweep(&Claude, &state, CLAUDE_SESSION, &|| Some(t.clone()), (Restart::Tail, false), &place);
std::fs::copy(claude_fixture(), &t).unwrap();
turn();
assert_eq!(stored(&base).len(), 3);
let lines = lines_of(&claude_fixture());
std::fs::write(&t, lines[..3].concat()).unwrap();
assert!(turn().is_empty());
assert_eq!(stored(&base).len(), 3);
let other = String::from_utf8(lines.concat())
.unwrap()
.replace("toolu_01RelayEdit", "toolu_01RelayEdit2")
.replace("a fixed sleep hammers", "a fixed sleep floods");
std::fs::write(&t, format!("{other}\n")).unwrap();
assert_eq!(turn().len(), 1);
let eps = stored(&base);
assert_eq!(eps.len(), 4);
assert_eq!(eps[3].id, "7f3c2a10-Ttoolu_01RelayEdit2");
turn();
assert_eq!(stored(&base).len(), 4);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn a_long_omp_sessions_first_tail_read_knows_its_cwd_from_the_session_record() {
let root = temp("omp-tail");
let (base, state, t) = (root.join("home"), root.join("state"), root.join("t.jsonl"));
let cfg: Config = serde_json::from_str(DEV).unwrap();
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
let omp = fixture("omp/sessions/-home-dev-work-relay/2026-10-28T10-00-00-000Z_omp-relay-1.jsonl");
let lines = lines_of(&omp);
grow(&t, &lines[0]);
let pad = format!("{}\n", serde_json::json!({"type": "custom", "data": "x".repeat(64 << 10)}));
for _ in 0..=(FIRST_READ >> 16) {
grow(&t, pad.as_bytes());
}
grow(&t, &lines[1..].concat());
let turn = || sweep(&harness::omp::Omp, &state, "omp-relay-1", &|| Some(t.clone()), (Restart::Tail, false), &place);
assert_eq!(turn(), BTreeSet::from([base.join("collectives/dev")]), "routed by the record's cwd");
assert_eq!(stored(&base).len(), 4);
assert_eq!(load(&swept_file(&state, "omp-relay-1")).unwrap().carry.cwd, "/home/dev/work/relay", "later turns go on from it");
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn a_transcript_that_shrank_under_a_read_gives_nothing_and_no_panic() {
let root = temp("shrank");
let t = root.join("t.jsonl");
std::fs::write(&t, b"{}\n").unwrap();
let mut f = std::fs::File::open(&t).unwrap();
assert!(read_from(&mut f, FIRST_READ, None, true).is_none());
assert!(read_from(&mut f, 100, Some("x"), false).is_none());
let _ = std::fs::remove_dir_all(root);
}
fn fixtures() -> Vec<(&'static str, &'static dyn Harness, PathBuf)> {
vec![
(CLAUDE_SESSION, &Claude, claude_fixture()),
("omp-relay-1", &harness::omp::Omp, fixture("omp/sessions/-home-dev-work-relay/2026-10-28T10-00-00-000Z_omp-relay-1.jsonl")),
(
"0199e0aa-1111-7222-8333-444455556666",
&harness::codex::Codex,
fixture("codex/sessions/2026/10/28/rollout-2026-10-28T11-00-00-0199e0aa-1111-7222-8333-444455556666.jsonl"),
),
("ses_0c1a", &harness::opencode::Opencode, fixture("opencode/v2/session-export-question.json")),
]
}
fn end_all(state: &Path, place: Place) -> BTreeSet<PathBuf> {
let hs: Vec<&dyn Harness> = harness::visible().collect();
let mut got = BTreeSet::new();
for (session, owner, path) in fixtures() {
let mine = |h: &dyn Harness| h.name() == owner.name();
let export = |h: &dyn Harness| {
(mine(h) && path.extension().is_some_and(|x| x == "json"))
.then(|| assimilate::opencode_session(&serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap()))
};
let ends = Ends { locate: &|h| mine(h).then(|| path.clone()), export: &export, place };
got.extend(end_of(&hs, session, state, &ends));
}
got
}
#[test]
fn a_sessions_end_reads_its_whole_transcript_on_every_harness_once() {
let root = temp("end");
let (base, state) = (root.join("home"), root.join("state"));
let cfg: Config = serde_json::from_str(DEV).unwrap();
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
assert_eq!(end_all(&state, &place), BTreeSet::from([base.join("collectives/dev")]));
let eps = stored(&base);
let per = |h: &str| eps.iter().filter(|e| e.harness == h).count();
assert_eq!((per("claude"), per("omp"), per("opencode"), per("codex")), (3, 4, 2, 1), "{:?}", ids(&eps));
assert_eq!(eps.iter().filter(|e| e.via == learn::ANSWER).count(), 7);
assert!(end_all(&state, &place).is_empty());
assert_eq!(stored(&base).len(), 10);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn an_idle_sessions_new_lines_are_swept_and_learned() {
let root = temp("idle");
let (base, state, t) = (root.join("home"), root.join("state"), root.join("t.jsonl"));
let cfg: Config = serde_json::from_str(DEV).unwrap();
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
let lines = lines_of(&claude_fixture());
grow(&t, &lines[..1].concat());
sweep(&Claude, &state, CLAUDE_SESSION, &|| Some(t.clone()), (Restart::Tail, false), &place);
grow(&t, &lines[1..].concat());
let now = learn::now_ms();
assert!(idle_in(std::slice::from_ref(&state), now, &place).is_empty(), "written just now: not idle yet");
let later = now + crate::outcome::IDLE_MS + 60_000;
let store = base.join("collectives/dev");
assert_eq!(idle_in(std::slice::from_ref(&state), later, &place), BTreeSet::from([store.clone()]));
assert_eq!(stored(&base).len(), 3);
assert!(idle_in(std::slice::from_ref(&state), later, &place).is_empty(), "swept to its end");
let ask =
|_: &str| Some(serde_json::json!({"is_lesson": true, "question": "Q?", "situations": [], "consequence": "c", "same_as": "", "supersedes": []}));
let (new, merged, waiting) = learn::learn_in(&store, Some("another"), &ask, true, later);
assert_eq!((new + merged, waiting), (3, 0));
std::fs::remove_file(&t).unwrap();
idle_in(std::slice::from_ref(&state), later, &place);
assert!(!swept_file(&state, CLAUDE_SESSION).exists());
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn the_doctor_warns_on_marks_without_episodes_and_on_a_silent_hook() {
let root = temp("doctor");
let (base, state) = (root.join("home"), root.join("state"));
let cfg: Config = serde_json::from_str(DEV).unwrap();
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
let t = root.join(format!("{CLAUDE_SESSION}.jsonl"));
std::fs::copy(claude_fixture(), &t).unwrap();
let logs = |h: &dyn Harness| (h.name() == Claude.name()).then(|| vec![t.clone()]);
let now = borg_trace::parse_ts("2026-10-29T00:00:00Z");
let doctor = || doctor_with(Some(&cfg), &base, now, true, &logs);
let warns = |c: &[(Level, String)]| c.iter().filter(|(l, _)| *l == Level::Warn).map(|(_, m)| m.clone()).collect::<Vec<_>>();
let w = warns(&doctor());
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[0].starts_with("claude 2.1.312: 1 rejections with the engineer's words in its transcripts since 2026-10-28, 0 captured"), "{w:?}");
assert!(w[1].starts_with("claude 2.1.312: 2 answers to the drone's questions in its transcripts since 2026-10-28, 0 captured"), "{w:?}");
let store = base.join("collectives/dev");
std::fs::create_dir_all(&store).unwrap();
let hook = Episode {
id: "7f3c2a10-1793178071000".into(),
ms: borg_trace::parse_ts("2026-10-28T09:01:11.000Z"),
session: CLAUDE_SESSION.into(),
rejected_tool: Some("Edit {}".into()),
correction: "use exponential backoff with jitter, a fixed sleep hammers the broker".into(),
..Episode::default()
};
learn::record_episode_in(&store, &hook).unwrap();
let ends = Ends { locate: &|h| (h.name() == Claude.name()).then(|| t.clone()), export: &|_| None, place: &place };
end_of(&[&Claude], CLAUDE_SESSION, &state, &ends);
let ok = doctor();
assert!(warns(&ok).is_empty(), "{ok:?}");
assert!(
ok.iter().any(|(l, m)| *l == Level::Ok && m.starts_with("claude: capture holds 1 of 1 rejections and 2 of 2 answers in 1 sessions'")),
"{ok:?}"
);
let _ = std::fs::remove_dir_all(&store);
let _ = std::fs::remove_dir_all(&state);
end_of(&[&Claude], CLAUDE_SESSION, &state, &ends);
let covered = doctor();
assert!(warns(&covered).is_empty(), "{covered:?}");
let silent = "claude 2.1.312: its hook captured no rejected tool call since 2026-10-28, its transcript 1";
assert!(covered.iter().any(|(l, m)| *l == Level::Ok && m.starts_with(silent)), "{covered:?}");
let w = warns(&doctor_with(Some(&cfg), &base, now, false, &logs));
assert!(w.len() == 1 && w[0].starts_with(silent), "{w:?}");
let off: Config = serde_json::from_str(r#"{ "default": "off", "collectives": [ { "name": "dev", "paths": ["/elsewhere"] } ] }"#).unwrap();
assert!(doctor_with(Some(&off), &base, now, true, &logs).is_empty());
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn the_doctor_tells_harness_versions_apart() {
let root = temp("doctor-versions");
let (base, state) = (root.join("home"), root.join("state"));
let cfg: Config = serde_json::from_str(DEV).unwrap();
let place = |f: Vec<Found>| record_in(f, Some(&cfg), &base);
let store = base.join("collectives/dev");
std::fs::create_dir_all(&store).unwrap();
const OLD: [&str; 2] = ["0a0a0a0a-5b6d-4e8f-9a01-23456789abcd", "0b0b0b0b-5b6d-4e8f-9a01-23456789abcd"];
let fixture_text = std::fs::read_to_string(claude_fixture()).unwrap();
let mut logs = Vec::new();
for (i, old) in OLD.iter().enumerate() {
let t = root.join(format!("{old}.jsonl"));
let day = format!("2026-10-2{}T09", 5 + i);
std::fs::write(&t, fixture_text.replace("2.1.312", "2.1.311").replace(CLAUDE_SESSION, old).replace("2026-10-28T09", &day)).unwrap();
let hook = Episode {
id: format!("{}-{i}", &old[..8]),
ms: borg_trace::parse_ts(&format!("{day}:01:11.000Z")),
session: (*old).into(),
rejected_tool: Some("Edit {}".into()),
correction: "use exponential backoff with jitter, a fixed sleep hammers the broker".into(),
..Episode::default()
};
learn::record_episode_in(&store, &hook).unwrap();
logs.push(t);
}
let t = root.join(format!("{CLAUDE_SESSION}.jsonl"));
std::fs::copy(claude_fixture(), &t).unwrap();
logs.push(t.clone());
let now = borg_trace::parse_ts("2026-10-29T00:00:00Z");
let list = |h: &dyn Harness| (h.name() == Claude.name()).then(|| logs.clone());
let warns = |c: &[(Level, String)]| c.iter().filter(|(l, _)| *l == Level::Warn).map(|(_, m)| m.clone()).collect::<Vec<_>>();
let w = warns(&doctor_with(Some(&cfg), &base, now, true, &list));
assert!(
w.iter().any(|m| m.starts_with("claude 2.1.312: 1 rejections with the engineer's words in its transcripts since 2026-10-28, 0 captured")),
"{w:?}"
);
assert!(!w.iter().any(|m| m.starts_with("claude 2.1.311: 2 rejections")), "the old version's were captured: {w:?}");
let ends = Ends { locate: &|h| (h.name() == Claude.name()).then(|| t.clone()), export: &|_| None, place: &place };
end_of(&[&Claude], CLAUDE_SESSION, &state, &ends);
let off = warns(&doctor_with(Some(&cfg), &base, now, false, &list));
assert!(off.iter().any(|m| m.starts_with("claude 2.1.312: its hook captured no rejected tool call since 2026-10-28, its transcript 1")), "{off:?}");
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn status_counts_captures_per_source() {
let root = temp("sources");
let e = |id: &str, harness: &str, kind: &str, ms: i64| Episode { id: id.into(), ms, harness: harness.into(), kind: kind.into(), ..Episode::default() };
for x in [e("a", "", "", 5), e("b", "claude", "", 7), e("c", "claude", "", 6), e("d", "", crate::veto::KIND, 9), e("r", "", learn::regret::KIND, 11)] {
learn::record_episode_in(&root, &x).unwrap();
}
assert_eq!(
sources(&root),
BTreeMap::from([("claude transcript".to_string(), (2, 7)), ("borg no".to_string(), (1, 9)), ("hook or older import".to_string(), (1, 5))]),
"a regret is no capture"
);
let _ = std::fs::remove_dir_all(root);
}
}