From bb2e3b9fbb52c1c0b2a03006cdecb631034e2377 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Thu, 26 Mar 2026 23:24:25 -0400 Subject: [PATCH] session: add TranscriptInfo struct, consolidate transcript lookups TranscriptInfo provides cached transcript metadata (path, size) with a single read. Replaces scattered fs::metadata calls in surface_observe_cycle, reflection_cycle, resolve_conversation, and resolve_memory_ratio. Session::transcript() resolves the path from transcript_path or by searching projects dir, returning a TranscriptInfo. Co-Authored-By: Kent Overstreet --- src/hippocampus/memory_search.rs | 22 +++++++------ src/session.rs | 54 ++++++++++++++++++++++++++++++-- src/subconscious/defs.rs | 48 +++++++++------------------- 3 files changed, 79 insertions(+), 45 deletions(-) diff --git a/src/hippocampus/memory_search.rs b/src/hippocampus/memory_search.rs index 00e27d5..ce71326 100644 --- a/src/hippocampus/memory_search.rs +++ b/src/hippocampus/memory_search.rs @@ -10,7 +10,7 @@ use std::fs::File; use std::io::Write; use std::path::Path; use std::process::Command; -use std::time::{Duration, SystemTime}; +use std::time::{Duration, Instant, SystemTime}; /// Max bytes per context chunk (hook output limit is ~10K chars) @@ -188,9 +188,8 @@ fn surface_observe_cycle(session: &Session, out: &mut String, log_f: &mut File) // (studying, reading a book, etc.) let conversation_budget: u64 = 50_000; let offset_path = state_dir.join("transcript-offset"); - let transcript_size = if !session.transcript_path.is_empty() { - fs::metadata(&session.transcript_path).map(|m| m.len()).unwrap_or(0) - } else { 0 }; + let transcript = session.transcript(); + let transcript_size = transcript.size; if !live.is_empty() && transcript_size > 0 { let last_offset: u64 = fs::read_to_string(&offset_path).ok() @@ -199,14 +198,20 @@ fn surface_observe_cycle(session: &Session, out: &mut String, log_f: &mut File) let behind = transcript_size.saturating_sub(last_offset); if behind > conversation_budget / 2 { - let _ = writeln!(log_f, "agent {}KB behind (budget {}KB), waiting for catchup", - behind / 1024, conversation_budget / 1024); // Wait up to 30s for the current agent to finish + let sleep_start = Instant::now(); + for _ in 0..30 { std::thread::sleep(std::time::Duration::from_secs(1)); let still_live = crate::agents::knowledge::scan_pid_files(&state_dir, timeout); if still_live.is_empty() { break; } } + + let sleep_duration = (Instant::now() - sleep_start).as_secs(); + + let _ = writeln!(log_f, "agent {}KB behind (budget {}KB), slept for {sleep_duration} seconds", + behind / 1024, conversation_budget / 1024); + out.push_str(&format!("Slept for {sleep_duration} seconds to let observe catch up\n")); } } @@ -240,9 +245,8 @@ fn reflection_cycle(session: &Session, out: &mut String, log_f: &mut File) { // Check transcript growth since last reflection let offset_path = state_dir.join("transcript-offset"); - let transcript_size = if !session.transcript_path.is_empty() { - fs::metadata(&session.transcript_path).map(|m| m.len()).unwrap_or(0) - } else { 0 }; + let transcript = session.transcript(); + let transcript_size = transcript.size; let last_offset: u64 = fs::read_to_string(&offset_path).ok() .and_then(|s| s.trim().parse().ok()) diff --git a/src/session.rs b/src/session.rs index 1a373a4..398fd16 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,7 +1,10 @@ -// session.rs — Session state for ambient hooks and agent interactions +// session.rs — Session state and transcript info // -// Tracks per-session state (seen set, state directory) across hook +// Session: per-session state (seen set, state directory) across hook // invocations. Created from hook JSON input or POC_SESSION_ID env var. +// +// TranscriptInfo: cached metadata about the current session's transcript +// file — size, path, compaction offset. Read once, used by all callers. use std::collections::HashSet; use std::fs; @@ -53,4 +56,51 @@ impl Session { pub fn seen(&self) -> HashSet { super::hippocampus::memory_search::load_seen(&self.state_dir, &self.session_id) } + + /// Get transcript metadata, resolving the path if needed. + pub fn transcript(&self) -> TranscriptInfo { + if !self.transcript_path.is_empty() { + return TranscriptInfo::from_path(&self.transcript_path); + } + + // Find transcript by session ID in projects dir + let projects = crate::config::get().projects_dir.clone(); + if let Ok(dirs) = fs::read_dir(&projects) { + for dir in dirs.filter_map(|e| e.ok()) { + let path = dir.path().join(format!("{}.jsonl", self.session_id)); + if path.exists() { + return TranscriptInfo::from_path(&path.to_string_lossy()); + } + } + } + + TranscriptInfo::empty() + } +} + +/// Cached transcript metadata — read once, use everywhere. +pub struct TranscriptInfo { + pub path: String, + pub size: u64, +} + +impl TranscriptInfo { + pub fn from_path(path: &str) -> Self { + let size = fs::metadata(path).map(|m| m.len()).unwrap_or(0); + TranscriptInfo { + path: path.to_string(), + size, + } + } + + pub fn empty() -> Self { + TranscriptInfo { + path: String::new(), + size: 0, + } + } + + pub fn exists(&self) -> bool { + !self.path.is_empty() && self.size > 0 + } } diff --git a/src/subconscious/defs.rs b/src/subconscious/defs.rs index ceb0b0b..8af9dfc 100644 --- a/src/subconscious/defs.rs +++ b/src/subconscious/defs.rs @@ -572,26 +572,14 @@ fn resolve( /// Reads POC_SESSION_ID to find the transcript, extracts the last /// segment (post-compaction), returns the tail (~100K chars). fn resolve_conversation(budget: Option) -> String { - let session_id = std::env::var("POC_SESSION_ID").unwrap_or_default(); - if session_id.is_empty() { return String::new(); } + let session = crate::session::Session::from_env(); + let transcript = session.as_ref() + .map(|s| s.transcript()) + .unwrap_or_else(crate::session::TranscriptInfo::empty); - let projects = crate::config::get().projects_dir.clone(); - // Find the transcript file matching this session - let mut transcript = None; - if let Ok(dirs) = std::fs::read_dir(&projects) { - for dir in dirs.filter_map(|e| e.ok()) { - let path = dir.path().join(format!("{}.jsonl", session_id)); - if path.exists() { - transcript = Some(path); - break; - } - } - } + if !transcript.exists() { return String::new(); } - let Some(path) = transcript else { return String::new() }; - let path_str = path.to_string_lossy(); - - let Some(iter) = crate::transcript::TailMessages::open(&path_str) else { + let Some(iter) = crate::transcript::TailMessages::open(&transcript.path) else { return String::new(); }; @@ -680,22 +668,14 @@ fn resolve_memory_ratio() -> String { let state_dir = std::path::PathBuf::from("/tmp/claude-memory-search"); // Get post-compaction transcript size - let projects = crate::config::get().projects_dir.clone(); - let transcript_size: u64 = std::fs::read_dir(&projects).ok() - .and_then(|dirs| { - for dir in dirs.filter_map(|e| e.ok()) { - let path = dir.path().join(format!("{}.jsonl", session_id)); - if path.exists() { - let file_len = path.metadata().map(|m| m.len()).unwrap_or(0); - let compaction_offset: u64 = std::fs::read_to_string( - state_dir.join(format!("compaction-{}", session_id)) - ).ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0); - return Some(file_len.saturating_sub(compaction_offset)); - } - } - None - }) - .unwrap_or(0); + let session = crate::session::Session::from_env(); + let transcript = session.as_ref() + .map(|s| s.transcript()) + .unwrap_or_else(crate::session::TranscriptInfo::empty); + let compaction_offset: u64 = std::fs::read_to_string( + state_dir.join(format!("compaction-{}", session_id)) + ).ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0); + let transcript_size = transcript.size.saturating_sub(compaction_offset); if transcript_size == 0 { return "0% of context is recalled memories (new session)".to_string();