diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 52a4764..a361c3d 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -497,42 +497,33 @@ impl Agent { } async fn load_startup_journal(&self) { + use crate::agent::tools::memory::journal_tail; + let oldest_msg_ts = { let ctx = self.context.lock().await; ctx.conversation_log.as_ref().and_then(|log| log.oldest_timestamp()) }; - let store = match crate::store::Store::load() { - Ok(s) => s, + // Get recent journal entries (newest first) + let journal_entries = match journal_tail(None, Some(100), Some(0), None).await { + Ok(e) => e, Err(_) => return, }; - let mut journal_nodes: Vec<_> = store.nodes.values() - .filter(|n| n.node_type == crate::store::NodeType::EpisodicSession) + // Filter to entries before the conversation started + let cutoff_ts = oldest_msg_ts.map(|t| t.timestamp()); + let filtered: Vec<_> = journal_entries.into_iter() + .filter(|e| cutoff_ts.map(|ts| e.created_at < ts).unwrap_or(true)) .collect(); - journal_nodes.sort_by_key(|n| n.created_at); - - let cutoff_idx = if let Some(cutoff) = oldest_msg_ts { - let cutoff_ts = cutoff.timestamp(); - let mut idx = journal_nodes.len(); - for (i, node) in journal_nodes.iter().enumerate() { - if node.created_at >= cutoff_ts { - idx = i + 1; - break; - } - } - idx - } else { - journal_nodes.len() - }; let journal_budget = context::context_window() * 15 / 100; let mut entries = Vec::new(); let mut total_tokens = 0; - for node in journal_nodes[..cutoff_idx].iter().rev() { - let ts = chrono::DateTime::from_timestamp(node.created_at, 0); - let ast = AstNode::memory(&node.key, &node.content) + // Take entries within budget (they're newest-first, so reverse for display) + for entry in filtered.iter() { + let ts = chrono::DateTime::from_timestamp(entry.created_at, 0); + let ast = AstNode::memory(&entry.key, &entry.content) .with_timestamp(ts.unwrap_or_else(chrono::Utc::now)); let tok = ast.tokens(); if total_tokens + tok > journal_budget && !entries.is_empty() { diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index c95d19b..81f24bc 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -332,7 +332,9 @@ memory_tool!(memory_links, ref -> Vec, key: [str]); // ── Journal tools ────────────────────────────────────────────── -memory_tool!(journal_tail, ref, count: [Option], level: [Option], format: [Option<&str>], after: [Option<&str>]); +pub use crate::hippocampus::JournalEntry; + +memory_tool!(journal_tail, ref -> Vec, count: [Option], level: [Option], after: [Option<&str>]); memory_tool!(journal_new, mut, name: [str], title: [str], body: [str], level: [Option]); memory_tool!(journal_update, mut, body: [str], level: [Option]); diff --git a/src/cli/journal.rs b/src/cli/journal.rs index c1ec6fe..2152f9d 100644 --- a/src/cli/journal.rs +++ b/src/cli/journal.rs @@ -68,10 +68,17 @@ pub fn cmd_tail(n: usize, full: bool, provenance: Option<&str>, dedup: bool) -> } pub async fn cmd_journal_tail(n: usize, full: bool, level: u8) -> Result<(), String> { - let format = if full { Some("full") } else { Some("compact") }; - let result = memory::journal_tail(None, Some(n as u64), Some(level as u64), format, None).await + let entries = memory::journal_tail(None, Some(n as u64), Some(level as u64), None).await .map_err(|e| e.to_string())?; - print!("{}", result); + for entry in entries { + if full { + println!("--- {} ---", entry.key); + println!("{}\n", entry.content); + } else { + let first_line = entry.content.lines().next().unwrap_or("(empty)"); + println!("{}: {}", entry.key, first_line); + } + } Ok(()) } diff --git a/src/hippocampus/mod.rs b/src/hippocampus/mod.rs index 6e003ca..987217e 100644 --- a/src/hippocampus/mod.rs +++ b/src/hippocampus/mod.rs @@ -257,31 +257,46 @@ pub fn memory_query(store: &Store, _provenance: &str, query_str: &str, format: O // ── Journal tools ────────────────────────────────────────────── -pub fn journal_tail(store: &Store, _provenance: &str, count: Option, level: Option, format: Option<&str>, after: Option<&str>) -> Result { - let count = count.unwrap_or(1); - let level = level.unwrap_or(0); - let format = format.unwrap_or("full"); +/// A journal entry with key, content, and timestamp. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct JournalEntry { + pub key: String, + pub content: String, + pub created_at: i64, +} - let type_name = match level { - 0 => "episodic", - 1 => "daily", - 2 => "weekly", - 3 => "monthly", - _ => return Err(anyhow::anyhow!("invalid level: {} (0=journal, 1=daily, 2=weekly, 3=monthly)", level)), +/// Get journal entries, sorted by timestamp (newest first). +/// level: 0=session, 1=daily, 2=weekly, 3=monthly +/// after: only entries after this date (YYYY-MM-DD) +pub fn journal_tail(store: &Store, _provenance: &str, count: Option, level: Option, after: Option<&str>) -> Result> { + let count = count.unwrap_or(10) as usize; + let level = level.unwrap_or(0); + let node_type = match level { + 0 => crate::store::NodeType::EpisodicSession, + 1 => crate::store::NodeType::EpisodicDaily, + 2 => crate::store::NodeType::EpisodicWeekly, + 3 => crate::store::NodeType::EpisodicMonthly, + _ => return Err(anyhow::anyhow!("invalid level: {}", level)), }; - let mut q = std::format!("all | type:{} | sort:timestamp", type_name); - if let Some(date) = after { - // Convert date to age in seconds - if let Ok(nd) = chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d") { - let ts = nd.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp(); - let age = chrono::Utc::now().timestamp() - ts; - q.push_str(&std::format!(" | age:<{}", age)); - } - } - q.push_str(&std::format!(" | limit:{}", count)); + let after_ts = after.and_then(|date| { + chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d").ok() + .and_then(|nd| nd.and_hms_opt(0, 0, 0)) + .map(|dt| dt.and_utc().timestamp()) + }); - memory_query(store, _provenance, &q, Some(format)) + let mut entries: Vec<_> = store.nodes.values() + .filter(|n| n.node_type == node_type) + .filter(|n| after_ts.map(|ts| n.created_at >= ts).unwrap_or(true)) + .map(|n| JournalEntry { + key: n.key.clone(), + content: n.content.clone(), + created_at: n.created_at, + }) + .collect(); + entries.sort_by_key(|e| std::cmp::Reverse(e.created_at)); + entries.truncate(count); + Ok(entries) } fn level_to_node_type(level: i64) -> crate::store::NodeType {