From 7776d87d530bf3a89e1a5137bd195d5fabd629af Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Thu, 2 Apr 2026 01:50:36 -0400 Subject: [PATCH] Journal: walk backwards with token budget, not load-all Iterate journal entries backwards from the conversation cutoff, accumulating within ~10K token budget (~8% of context window). Stops when budget is full, keeps at least one entry. Much more efficient than loading all entries and trimming. Co-Authored-By: Proof of Concept --- src/agent/runner.rs | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/agent/runner.rs b/src/agent/runner.rs index dee8bac..dd22d66 100644 --- a/src/agent/runner.rs +++ b/src/agent/runner.rs @@ -800,51 +800,51 @@ impl Agent { .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 { + // Find the cutoff index — entries older than conversation, plus one overlap + let cutoff_idx = if let Some(cutoff) = oldest_msg_ts { + let mut idx = journal_nodes.len(); + for (i, node) in journal_nodes.iter().enumerate() { 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 { + if ts >= cutoff { + idx = i + 1; // include this overlapping entry break; } } - result + idx } 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() + journal_nodes.len() }; + // Walk backwards from cutoff, accumulating entries within token budget + let count = |s: &str| self.tokenizer.encode_with_special_tokens(s).len(); + let journal_budget_tokens = 10_000; // ~8% of 128K context + let mut entries = Vec::new(); + let mut total_tokens = 0; + + for node in journal_nodes[..cutoff_idx].iter().rev() { + let tokens = count(&node.content); + if total_tokens + tokens > journal_budget_tokens && !entries.is_empty() { + break; + } + entries.push(journal::JournalEntry { + timestamp: chrono::DateTime::from_timestamp(node.created_at, 0) + .unwrap_or_default(), + content: node.content.clone(), + }); + total_tokens += tokens; + } + entries.reverse(); // chronological order + if entries.is_empty() { return; } - let count = |s: &str| self.tokenizer.encode_with_special_tokens(s).len(); let context_message = self.context.render_context_message(); - let plan = crate::agent::context::plan_context( &self.context.system_prompt, &context_message, - &[], // no conversation yet + &[], &entries, &self.client.model, &count,