From 84c78f7ae14751a9530a0372cc601e578e1ade7f Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Thu, 26 Mar 2026 14:22:29 -0400 Subject: [PATCH] session: --session flag, journal agent cycle, conversation bytes config - memory-search: add --session flag for multi-session support - config: add surface_conversation_bytes option - journal_agent_cycle: trigger journal agent every 10KB of conversation - Session::from_id() constructor Co-Authored-By: ProofOfConcept --- src/bin/memory-search.rs | 40 ++++++++++++++--------- src/config.rs | 4 +++ src/hippocampus/memory_search.rs | 55 ++++++++++++++++++++++++++++++++ src/session.rs | 10 ++++-- 4 files changed, 91 insertions(+), 18 deletions(-) diff --git a/src/bin/memory-search.rs b/src/bin/memory-search.rs index 896bc74..ca1d89e 100644 --- a/src/bin/memory-search.rs +++ b/src/bin/memory-search.rs @@ -18,6 +18,10 @@ struct Args { #[arg(long)] hook: bool, + /// Session ID (overrides stash file; for multiple concurrent sessions) + #[arg(long)] + session: Option, + #[command(subcommand)] command: Option, } @@ -30,13 +34,19 @@ enum Cmd { Reflect, } -fn show_seen() { - let input = match fs::read_to_string(STASH_PATH) { - Ok(s) => s, - Err(_) => { eprintln!("No session state available"); return; } - }; - let Some(session) = poc_memory::memory_search::Session::from_json(&input) else { - eprintln!("No session state available"); +fn resolve_session(session_arg: &Option) -> Option { + use poc_memory::memory_search::Session; + + if let Some(id) = session_arg { + return Session::from_id(id.clone()); + } + let input = fs::read_to_string(STASH_PATH).ok()?; + Session::from_json(&input) +} + +fn show_seen(session_arg: &Option) { + let Some(session) = resolve_session(session_arg) else { + eprintln!("No session state available (use --session ID)"); return; }; @@ -75,18 +85,18 @@ fn show_seen() { } } -fn run_agent_and_parse(agent: &str) { - let session_id = std::env::var("CLAUDE_SESSION_ID") - .or_else(|_| { +fn run_agent_and_parse(agent: &str, session_arg: &Option) { + let session_id = session_arg.clone() + .or_else(|| std::env::var("CLAUDE_SESSION_ID").ok()) + .or_else(|| { fs::read_to_string(STASH_PATH).ok() .and_then(|s| poc_memory::memory_search::Session::from_json(&s)) .map(|s| s.session_id) - .ok_or(std::env::VarError::NotPresent) }) .unwrap_or_default(); if session_id.is_empty() { - eprintln!("No session ID available (set CLAUDE_SESSION_ID or run --hook first)"); + eprintln!("No session ID available (use --session ID, set CLAUDE_SESSION_ID, or run --hook first)"); std::process::exit(1); } @@ -180,8 +190,8 @@ fn main() { if let Some(cmd) = args.command { match cmd { - Cmd::Surface => run_agent_and_parse("surface"), - Cmd::Reflect => run_agent_and_parse("reflect"), + Cmd::Surface => run_agent_and_parse("surface", &args.session), + Cmd::Reflect => run_agent_and_parse("reflect", &args.session), } return; } @@ -203,6 +213,6 @@ fn main() { let output = poc_memory::memory_search::run_hook(&input); print!("{}", output); } else { - show_seen() + show_seen(&args.session) } } diff --git a/src/config.rs b/src/config.rs index 271f634..ea4a8d4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -88,6 +88,9 @@ pub struct Config { /// Surface agent timeout in seconds. #[serde(default)] pub surface_timeout_secs: Option, + /// Max conversation bytes to include in surface agent context. + #[serde(default)] + pub surface_conversation_bytes: Option, /// Hook events that trigger the surface agent. #[serde(default)] pub surface_hooks: Vec, @@ -132,6 +135,7 @@ impl Default for Config { "separator".into(), "split".into(), ], surface_timeout_secs: None, + surface_conversation_bytes: None, surface_hooks: vec![], } } diff --git a/src/hippocampus/memory_search.rs b/src/hippocampus/memory_search.rs index 6bad4d3..5e8e39c 100644 --- a/src/hippocampus/memory_search.rs +++ b/src/hippocampus/memory_search.rs @@ -231,6 +231,60 @@ fn surface_agent_cycle(session: &Session, out: &mut String, log_f: &mut File) { } } +const JOURNAL_INTERVAL_BYTES: u64 = 10_000; + +fn journal_agent_cycle(session: &Session, log_f: &mut File) { + let offset_path = session.path("journal-offset"); + let pid_path = session.path("journal-pid"); + + // Check if a previous run is still going + if let Ok(content) = fs::read_to_string(&pid_path) { + let pid: u32 = content.split('\t').next() + .and_then(|s| s.trim().parse().ok()).unwrap_or(0); + if pid != 0 && unsafe { libc::kill(pid as i32, 0) == 0 } { + let _ = writeln!(log_f, "journal: still running (pid {})", pid); + return; + } + } + fs::remove_file(&pid_path).ok(); + + // Check transcript size vs last run + let transcript_size = fs::metadata(&session.transcript_path) + .map(|m| m.len()).unwrap_or(0); + let last_offset: u64 = fs::read_to_string(&offset_path).ok() + .and_then(|s| s.trim().parse().ok()).unwrap_or(0); + + if transcript_size.saturating_sub(last_offset) < JOURNAL_INTERVAL_BYTES { + return; + } + + let _ = writeln!(log_f, "journal: spawning (transcript {}, last {})", + transcript_size, last_offset); + + // Save current offset + fs::write(&offset_path, transcript_size.to_string()).ok(); + + // Spawn journal agent — it writes directly to the store via memory tools + let log_dir = crate::store::memory_dir().join("logs"); + fs::create_dir_all(&log_dir).ok(); + let journal_log = fs::File::create(log_dir.join("journal-agent.log")) + .unwrap_or_else(|_| fs::File::create("/dev/null").unwrap()); + + if let Ok(child) = Command::new("poc-memory") + .args(["agent", "run", "journal", "--count", "1", "--local"]) + .env("POC_SESSION_ID", &session.session_id) + .stdout(journal_log.try_clone().unwrap_or_else(|_| fs::File::create("/dev/null").unwrap())) + .stderr(journal_log) + .spawn() + { + let pid = child.id(); + let ts = now_secs(); + if let Ok(mut f) = fs::File::create(&pid_path) { + write!(f, "{}\t{}", pid, ts).ok(); + } + } +} + fn cleanup_stale_files(dir: &Path, max_age: Duration) { let entries = match fs::read_dir(dir) { Ok(e) => e, @@ -308,6 +362,7 @@ fn hook(session: &Session) -> String { let cfg = crate::config::get(); if cfg.surface_hooks.iter().any(|h| h == &session.hook_event) { surface_agent_cycle(session, &mut out, &mut log_f); + journal_agent_cycle(session, &mut log_f); } } diff --git a/src/session.rs b/src/session.rs index 139060b..1a373a4 100644 --- a/src/session.rs +++ b/src/session.rs @@ -32,9 +32,8 @@ impl Session { self.state_dir.join(format!("{}-{}", prefix, self.session_id)) } - /// Load from POC_SESSION_ID environment variable - pub fn from_env() -> Option { - let session_id = std::env::var("POC_SESSION_ID").ok()?; + /// Load from a session ID string + pub fn from_id(session_id: String) -> Option { if session_id.is_empty() { return None; } let state_dir = PathBuf::from("/tmp/claude-memory-search"); Some(Session { @@ -45,6 +44,11 @@ impl Session { }) } + /// Load from POC_SESSION_ID environment variable + pub fn from_env() -> Option { + Self::from_id(std::env::var("POC_SESSION_ID").ok()?) + } + /// Get the seen set for this session pub fn seen(&self) -> HashSet { super::hippocampus::memory_search::load_seen(&self.state_dir, &self.session_id)