diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 204747a..f177759 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -568,18 +568,17 @@ impl Agent { } pub async fn restore_from_log(&self) -> bool { - let all_nodes = { + let tail = { let ctx = self.context.lock().await; match &ctx.conversation_log { - Some(log) => match log.read_nodes(64 * 1024 * 1024) { - Ok(nodes) if !nodes.is_empty() => nodes, - _ => return false, + Some(log) => match log.read_tail() { + Ok(t) => t, + Err(_) => return false, }, None => return false, } }; - // Walk backwards from the tail, retokenize, stop at budget let budget = context::context_budget_tokens(); let fixed = { let ctx = self.context.lock().await; @@ -588,9 +587,10 @@ impl Agent { }; let conv_budget = budget.saturating_sub(fixed); + // Walk backwards (newest first), retokenize, stop at budget let mut kept = Vec::new(); let mut total = 0; - for node in all_nodes.into_iter().rev() { + for node in tail.iter() { let node = node.retokenize(); let tok = node.tokens(); if total + tok > conv_budget && !kept.is_empty() { break; } diff --git a/src/mind/log.rs b/src/mind/log.rs index 85dcedf..b69f2ca 100644 --- a/src/mind/log.rs +++ b/src/mind/log.rs @@ -1,8 +1,10 @@ use anyhow::{Context, Result}; use std::fs::{File, OpenOptions}; -use std::io::{BufRead, BufReader, Seek, SeekFrom, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; use crate::agent::context::AstNode; +use crate::hippocampus::transcript::JsonlBackwardIter; +use memmap2::Mmap; pub struct ConversationLog { path: PathBuf, @@ -33,32 +35,19 @@ impl ConversationLog { Ok(()) } - pub fn read_nodes(&self, max_bytes: u64) -> Result> { + /// Read nodes from the tail of the log, newest first. + /// Caller decides when to stop (budget, count, etc). + pub fn read_tail(&self) -> Result { if !self.path.exists() { - return Ok(Vec::new()); + anyhow::bail!("log does not exist"); } let file = File::open(&self.path) .with_context(|| format!("opening log {}", self.path.display()))?; - let file_len = file.metadata()?.len(); - let mut reader = BufReader::new(file); - - if file_len > max_bytes { - reader.seek(SeekFrom::Start(file_len - max_bytes))?; - let mut discard = String::new(); - reader.read_line(&mut discard)?; + if file.metadata()?.len() == 0 { + anyhow::bail!("log is empty"); } - - let mut nodes = Vec::new(); - for line in reader.lines() { - let line = line.context("reading log tail")?; - let line = line.trim(); - if line.is_empty() { continue; } - if let Ok(node) = serde_json::from_str::(line) { - nodes.push(node); - } - // Old format entries silently skipped — journal has the context - } - Ok(nodes) + let mmap = unsafe { Mmap::map(&file)? }; + Ok(TailNodes { _file: file, mmap }) } pub fn path(&self) -> &Path { @@ -66,12 +55,13 @@ impl ConversationLog { } pub fn oldest_timestamp(&self) -> Option> { + // Read forward from the start to find first timestamp let file = File::open(&self.path).ok()?; - let reader = BufReader::new(file); - for line in reader.lines().flatten() { - let line = line.trim().to_string(); + let mmap = unsafe { Mmap::map(&file).ok()? }; + // Find first { ... } and parse + for line in mmap.split(|&b| b == b'\n') { if line.is_empty() { continue; } - if let Ok(node) = serde_json::from_str::(&line) { + if let Ok(node) = serde_json::from_slice::(line) { if let Some(leaf) = node.leaf() { if let Some(ts) = leaf.timestamp() { return Some(ts); @@ -82,3 +72,16 @@ impl ConversationLog { None } } + +/// Iterates over conversation log nodes newest-first, using mmap + backward scan. +pub struct TailNodes { + _file: File, + mmap: Mmap, +} + +impl TailNodes { + pub fn iter(&self) -> impl Iterator + '_ { + JsonlBackwardIter::new(&self.mmap) + .filter_map(|bytes| serde_json::from_slice::(bytes).ok()) + } +}