Use own state for spawn decisions, not pid file scanning

AgentCycleState tracks its own children — agent_running() checks
child handles instead of scan_pid_files(). poll_children() reaps
completed processes. No filesystem scanning for agent lifecycle.

The Claude Code hook path will need serialized AgentCycleState
to persist across invocations (next step).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-02 01:26:58 -04:00
parent 9ac50bd999
commit 90d2717423

View file

@ -206,6 +206,10 @@ impl AgentCycleState {
}
}
fn agent_running(&self, name: &str) -> bool {
self.agents.iter().any(|a| a.name == name && a.child.is_some())
}
fn agent_spawned(&mut self, name: &str, phase: &str,
result: crate::agents::knowledge::SpawnResult) {
if let Some(agent) = self.agents.iter_mut().find(|a| a.name == name) {
@ -306,15 +310,6 @@ impl AgentCycleState {
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
let timeout = crate::config::get()
.surface_timeout_secs
.unwrap_or(300) as u64;
let live = crate::agents::knowledge::scan_pid_files(&state_dir, timeout);
for (phase, pid) in &live {
self.log(format_args!("alive pid-{}: phase={}\n", pid, phase));
}
// Read surfaced keys
let mut surfaced_keys = Vec::new();
let surface_path = state_dir.join("surface");
@ -337,12 +332,10 @@ impl AgentCycleState {
fs::remove_file(&surface_path).ok();
}
// Spawn new agent if needed
let live = crate::agents::knowledge::scan_pid_files(&state_dir, timeout);
let any_in_surface = live.iter().any(|(p, _)| p == "surface");
if any_in_surface {
self.log(format_args!("agent in surface phase, waiting\n"));
// Spawn new agent if not already running
let running = self.agent_running("surface-observe");
if running {
self.log(format_args!("surface-observe already running\n"));
} else {
if transcript.size > 0 {
fs::write(&offset_path, transcript.size.to_string()).ok();
@ -358,7 +351,7 @@ impl AgentCycleState {
let mut sleep_secs = None;
let conversation_budget: u64 = 50_000;
if !live.is_empty() && transcript.size > 0 {
if running && transcript.size > 0 {
let behind = transcript.size.saturating_sub(last_offset);
if behind > conversation_budget / 2 {
@ -367,8 +360,8 @@ impl AgentCycleState {
for _ in 0..5 {
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; }
self.poll_children();
if !self.agent_running("surface-observe") { break; }
}
let secs = (Instant::now() - sleep_start).as_secs_f64();
@ -394,9 +387,8 @@ impl AgentCycleState {
return None;
}
let live = crate::agents::knowledge::scan_pid_files(&state_dir, 300);
if let Some((phase, pid)) = live.first() {
self.log(format_args!("reflect: already running pid {}\n", pid));
if self.agent_running("reflect") {
self.log(format_args!("reflect: already running\n"));
return None;
}
@ -438,9 +430,8 @@ impl AgentCycleState {
return;
}
let live = crate::agents::knowledge::scan_pid_files(&state_dir, 300);
if let Some((phase, pid)) = live.first() {
self.log(format_args!("journal: already running pid {}\n", pid));
if self.agent_running("journal") {
self.log(format_args!("journal: already running\n"));
return;
}