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 <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-02 01:48:16 -04:00
parent c814ed1345
commit e4285ba75f
2 changed files with 71 additions and 2 deletions

View file

@ -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<chrono::DateTime<chrono::Utc>> {
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::<Message>(&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
}
}

View file

@ -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<journal::JournalEntry> = 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;
}