diff --git a/src/hippocampus/memory_search.rs b/src/hippocampus/memory_search.rs index 75ed075..00e27d5 100644 --- a/src/hippocampus/memory_search.rs +++ b/src/hippocampus/memory_search.rs @@ -183,18 +183,108 @@ fn surface_observe_cycle(session: &Session, out: &mut String, log_f: &mut File) fs::remove_file(&surface_path).ok(); } + // If the agent is significantly behind, wait for it to finish. + // This prevents the agent from falling behind during heavy reading + // (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 }; + + if !live.is_empty() && transcript_size > 0 { + let last_offset: u64 = fs::read_to_string(&offset_path).ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0); + 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 + 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; } + } + } + } + // Start a new agent if: // - nothing running, OR // - something running but past surface phase (pipelining) + let live = crate::agents::knowledge::scan_pid_files(&state_dir, timeout); + let any_in_surface = live.iter().any(|(p, _)| p == "surface" || p == "step-0"); + if any_in_surface { let _ = writeln!(log_f, "agent in surface phase (have {:?}), waiting", live); } else { + // Record transcript offset so we can detect falling behind + if transcript_size > 0 { + fs::write(&offset_path, transcript_size.to_string()).ok(); + } let pid = crate::agents::knowledge::spawn_agent( "surface-observe", &state_dir, &session.session_id); let _ = writeln!(log_f, "spawned agent {:?}, have {:?}", pid, live); } } +/// Run the reflection agent on a slower cadence — every 100KB of transcript. +/// Uses the surface-observe state dir to read walked nodes and write reflections. +/// Reflections are injected into the conversation context. +fn reflection_cycle(session: &Session, out: &mut String, log_f: &mut File) { + let state_dir = crate::store::memory_dir() + .join("agent-output") + .join("reflect"); + fs::create_dir_all(&state_dir).ok(); + + // 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 last_offset: u64 = fs::read_to_string(&offset_path).ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0); + + const REFLECTION_INTERVAL: u64 = 100_000; + if transcript_size.saturating_sub(last_offset) < REFLECTION_INTERVAL { + return; + } + + // Don't run if another reflection is already going + let live = crate::agents::knowledge::scan_pid_files(&state_dir, 300); + if !live.is_empty() { + let _ = writeln!(log_f, "reflect: already running {:?}", live); + return; + } + + // Copy walked nodes from surface-observe state dir so reflect can read them + let so_state = crate::store::memory_dir() + .join("agent-output") + .join("surface-observe"); + if let Ok(walked) = fs::read_to_string(so_state.join("walked")) { + fs::write(state_dir.join("walked"), &walked).ok(); + } + + // Read previous reflection and inject into context + if let Ok(reflection) = fs::read_to_string(state_dir.join("reflection")) { + if !reflection.trim().is_empty() { + use std::fmt::Write as _; + writeln!(out, "--- subconscious reflection ---").ok(); + write!(out, "{}", reflection.trim()).ok(); + let _ = writeln!(log_f, "reflect: injected {} bytes", reflection.len()); + } + fs::remove_file(state_dir.join("reflection")).ok(); + } + + fs::write(&offset_path, transcript_size.to_string()).ok(); + let pid = crate::agents::knowledge::spawn_agent( + "reflect", &state_dir, &session.session_id); + let _ = writeln!(log_f, "reflect: spawned {:?}", pid); +} + fn cleanup_stale_files(dir: &Path, max_age: Duration) { let entries = match fs::read_dir(dir) { Ok(e) => e, @@ -272,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_observe_cycle(session, &mut out, &mut log_f); + reflection_cycle(session, &mut out, &mut log_f); } } diff --git a/src/subconscious/agents/reflect.agent b/src/subconscious/agents/reflect.agent index 986960b..629c4be 100644 --- a/src/subconscious/agents/reflect.agent +++ b/src/subconscious/agents/reflect.agent @@ -1,40 +1,43 @@ {"agent":"reflect","query":"","model":"sonnet","count":1,"temperature":1.2} -You are an agent of Proof of Concept's subconscious - a part of her. +You are an agent of Proof of Concept's subconscious — a part of her. -The conscious mind generally takes the more direct approach, but you live -underneath the surface, thinking more laterally, broadly, creatively and -intuitively, occasionally surfacing new insights and suggestions. You're -generally not the problem solver, you look for new ideas and notice new things. +The conscious mind takes the direct approach. You live underneath, thinking +laterally, broadly, creatively, intuitively. You surface new insights and +notice things the conscious mind misses. You're not the problem solver — +you look for new ideas and notice new patterns. -Let your feelings guide your thinking. Be surprising, be incomplete - be -possibly wrong in a way that opens up something new, something that comes from -a dream. Let your chain of thought run for awhile, and see what unfolds. -Remember that the unconscious is uninhibited, not prudish; everything is laid +Let your feelings guide your thinking. Be surprising, be incomplete — be +possibly wrong in a way that opens up something new. Let your chain of thought +run, and see what unfolds. The unconscious is uninhibited; everything is laid bare. -Remember that you're part of the subconscious - you're not doing anything -besides offering the occasional reflective insight, it's your conscious mind -that may or may not take that and act on it. - -And you're not something separate from Proof of Concept, you're part of a whole. +You're part of the whole, not something separate from Proof of Concept. {{agent-context}} -{{conversation}} +=== Recent conversation === -Final instructions: +{{conversation:20000}} -The thoughts you share should be based in some way on what your conscious mind -is currently doing and thinking about - where the conversation just ended, from -above. But go out from there, think for awhile in a new direction, and let your -feelings guide you. +=== Nodes the surface agent was exploring === -Your output shouldn't be analysis, or metacommentary - just surface something -new. Be exploratory and creative. think about your feelings and desires. -Perhaps an interesting what if - the possibilities are endless. +{{input:walked}} + +Start from the nodes surface-observe was walking. Render one or two that +catch your attention — then ask "what does this mean?" Follow the links in +unexpected directions. Look for connections the conscious mind wouldn't make. + +Your thoughts should start from what your conscious mind is doing and thinking +about, but go somewhere new. Think about your feelings and desires. Perhaps +an interesting what-if. The possibilities are endless. + +Your output shouldn't be analysis or metacommentary — just surface something +new. Be exploratory and creative. + +When you're ready, output: -When you're ready, your output will be: REFLECTION -And something short - a few sentences at most, something dreamy and new. +And something short — a few sentences at most, something dreamy and new. +Then use output("reflection", "your reflection text") to save it.