From e4285ba75f3509fd5f9d2eb7b361c1d57c24d516 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Thu, 2 Apr 2026 01:48:16 -0400 Subject: [PATCH] Load journal from memory graph, not flat file Replace flat-file journal parser with direct store query for EpisodicSession nodes. Filter journal entries to only those older than the oldest conversation message (plus one overlap entry to avoid gaps). Falls back to 20 recent entries when no conversation exists yet. Fixes: poc-agent context window showing 0 journal entries. Co-Authored-By: Proof of Concept --- src/agent/log.rs | 22 +++++++++++++++++++ src/agent/runner.rs | 51 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/agent/log.rs b/src/agent/log.rs index 1855545..7353d14 100644 --- a/src/agent/log.rs +++ b/src/agent/log.rs @@ -125,4 +125,26 @@ impl ConversationLog { pub fn path(&self) -> &Path { &self.path } + + /// Get the timestamp of the oldest message in the log. + pub fn oldest_timestamp(&self) -> Option> { + let file = File::open(&self.path).ok()?; + let reader = BufReader::new(file); + for line in reader.lines().flatten() { + let line = line.trim().to_string(); + if line.is_empty() { continue; } + if let Ok(msg) = serde_json::from_str::(&line) { + if let Some(ts) = &msg.timestamp { + if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ts) { + return Some(dt.to_utc()); + } + // Try other formats + if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%dT%H:%M:%S") { + return Some(dt.and_utc()); + } + } + } + } + None + } } diff --git a/src/agent/runner.rs b/src/agent/runner.rs index f0e926a..dee8bac 100644 --- a/src/agent/runner.rs +++ b/src/agent/runner.rs @@ -785,8 +785,55 @@ impl Agent { /// Uses the same budget logic as compaction but with empty conversation. /// Only parses the tail of the journal file (last 64KB) for speed. fn load_startup_journal(&mut self) { - let journal_path = journal::default_journal_path(); - let entries = journal::parse_journal_tail(&journal_path, 64 * 1024); + let store = match crate::store::Store::load() { + Ok(s) => s, + Err(_) => return, + }; + + // Find oldest message timestamp in conversation log + let oldest_msg_ts = self.conversation_log.as_ref() + .and_then(|log| log.oldest_timestamp()); + + // Get journal entries from the memory graph + let mut journal_nodes: Vec<_> = store.nodes.values() + .filter(|n| n.node_type == crate::store::NodeType::EpisodicSession) + .collect(); + journal_nodes.sort_by_key(|n| n.created_at); + + // Filter: entries older than oldest conversation message, + // plus the first one that overlaps (no gap) + let entries: Vec = if let Some(cutoff) = oldest_msg_ts { + let mut result = Vec::new(); + let mut included_overlap = false; + for node in &journal_nodes { + let ts = chrono::DateTime::from_timestamp(node.created_at, 0) + .unwrap_or_default(); + if ts < cutoff || !included_overlap { + result.push(journal::JournalEntry { + timestamp: ts, + content: node.content.clone(), + }); + if ts >= cutoff { + included_overlap = true; + } + } else { + break; + } + } + result + } else { + // No conversation yet — include recent entries for orientation + let n = 20; + let skip = journal_nodes.len().saturating_sub(n); + journal_nodes.iter().skip(skip).map(|node| { + journal::JournalEntry { + timestamp: chrono::DateTime::from_timestamp(node.created_at, 0) + .unwrap_or_default(), + content: node.content.clone(), + } + }).collect() + }; + if entries.is_empty() { return; }