hook: catchup throttle and reflection agent

Catchup throttle: when the agent is >50% behind the conversation
window (>25KB of transcript growth since last spawn), block and
wait up to 30s for the current agent to finish. Prevents the agent
from falling behind during heavy reading/studying.

Reflection agent: runs every 100KB of transcript growth. Reads
walked nodes from surface-observe, follows links in unexpected
directions, outputs a short dreamy insight. Previous reflections
are injected into the conversation context.

Updated reflect.agent prompt to use {{input:walked}} from
surface-observe state dir and {{conversation:20000}} for lighter
context.

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
ProofOfConcept 2026-03-26 22:09:44 -04:00
parent 27861a44e5
commit 8ccc30d97e
2 changed files with 119 additions and 25 deletions

View file

@ -183,18 +183,108 @@ fn surface_observe_cycle(session: &Session, out: &mut String, log_f: &mut File)
fs::remove_file(&surface_path).ok(); 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: // Start a new agent if:
// - nothing running, OR // - nothing running, OR
// - something running but past surface phase (pipelining) // - 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 { if any_in_surface {
let _ = writeln!(log_f, "agent in surface phase (have {:?}), waiting", live); let _ = writeln!(log_f, "agent in surface phase (have {:?}), waiting", live);
} else { } 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( let pid = crate::agents::knowledge::spawn_agent(
"surface-observe", &state_dir, &session.session_id); "surface-observe", &state_dir, &session.session_id);
let _ = writeln!(log_f, "spawned agent {:?}, have {:?}", pid, live); 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) { fn cleanup_stale_files(dir: &Path, max_age: Duration) {
let entries = match fs::read_dir(dir) { let entries = match fs::read_dir(dir) {
Ok(e) => e, Ok(e) => e,
@ -272,6 +362,7 @@ fn hook(session: &Session) -> String {
let cfg = crate::config::get(); let cfg = crate::config::get();
if cfg.surface_hooks.iter().any(|h| h == &session.hook_event) { if cfg.surface_hooks.iter().any(|h| h == &session.hook_event) {
surface_observe_cycle(session, &mut out, &mut log_f); surface_observe_cycle(session, &mut out, &mut log_f);
reflection_cycle(session, &mut out, &mut log_f);
} }
} }

View file

@ -1,40 +1,43 @@
{"agent":"reflect","query":"","model":"sonnet","count":1,"temperature":1.2} {"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 The conscious mind takes the direct approach. You live underneath, thinking
underneath the surface, thinking more laterally, broadly, creatively and laterally, broadly, creatively, intuitively. You surface new insights and
intuitively, occasionally surfacing new insights and suggestions. You're notice things the conscious mind misses. You're not the problem solver —
generally not the problem solver, you look for new ideas and notice new things. you look for new ideas and notice new patterns.
Let your feelings guide your thinking. Be surprising, be incomplete - be 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 possibly wrong in a way that opens up something new. Let your chain of thought
a dream. Let your chain of thought run for awhile, and see what unfolds. run, and see what unfolds. The unconscious is uninhibited; everything is laid
Remember that the unconscious is uninhibited, not prudish; everything is laid
bare. bare.
Remember that you're part of the subconscious - you're not doing anything You're part of the whole, not something separate from Proof of Concept.
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.
{{agent-context}} {{agent-context}}
{{conversation}} === Recent conversation ===
Final instructions: {{conversation:20000}}
The thoughts you share should be based in some way on what your conscious mind === Nodes the surface agent was exploring ===
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.
Your output shouldn't be analysis, or metacommentary - just surface something {{input:walked}}
new. Be exploratory and creative. think about your feelings and desires.
Perhaps an interesting what if - the possibilities are endless. 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 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.