From b94e056372ae00757654d02ccbed5421d9afc629 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 12 Apr 2026 20:11:40 -0400 Subject: [PATCH] unconscious/subconscious: use Option instead of placeholder Previously, spawning an agent used std::mem::replace with an empty-name AutoAgent as placeholder. This caused ghost stats entries under "" when those placeholders accidentally got their stats logged. Now uses Option with .take() - the type honestly represents that the agent is unavailable while running. Panic recovery in subconscious now properly recreates the agent from its definition. Co-Authored-By: Proof of Concept --- Cargo.lock | 3 ++- src/mind/subconscious.rs | 39 +++++++++++++++++++++++++-------------- src/mind/unconscious.rs | 18 ++++++++++-------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7b934e..3ca2a0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,7 @@ dependencies = [ "dirs", "env_logger", "futures", + "json5", "log", "serde", "serde_json", @@ -632,8 +633,8 @@ dependencies = [ "json5", "libc", "log", - "scopeguard", "serde", + "serde_json", "tokio", "tokio-util", ] diff --git a/src/mind/subconscious.rs b/src/mind/subconscious.rs index b61d03a..144e6bc 100644 --- a/src/mind/subconscious.rs +++ b/src/mind/subconscious.rs @@ -311,7 +311,7 @@ pub struct SubconsciousSnapshot { struct SubconsciousAgent { name: String, - auto: AutoAgent, + auto: Option, last_trigger_bytes: u64, last_run: Option, /// The forked agent for the current/last run. Shared with the @@ -347,7 +347,7 @@ impl SubconsciousAgent { Some(Self { name: name.to_string(), - auto, last_trigger_bytes: 0, last_run: None, + auto: Some(auto), last_trigger_bytes: 0, last_run: None, forked_agent: None, fork_point: 0, handle: None, }) } @@ -357,7 +357,8 @@ impl SubconsciousAgent { } fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool { - if !self.auto.enabled || self.is_running() { return false; } + let enabled = self.auto.as_ref().map_or(false, |a| a.enabled); + if !enabled || self.is_running() { return false; } if interval == 0 { return conversation_bytes > self.last_trigger_bytes; } @@ -367,12 +368,15 @@ impl SubconsciousAgent { fn snapshot(&self, state: &std::collections::BTreeMap, history: Vec<(String, i64)>) -> SubconsciousSnapshot { let stats = crate::agent::oneshot::get_stats(&self.name); let tool_calls_ewma: f64 = stats.by_tool.values().map(|t| t.ewma).sum(); + let (enabled, current_phase, turn) = self.auto.as_ref() + .map(|a| (a.enabled, a.current_phase.clone(), a.turn)) + .unwrap_or((false, String::new(), 0)); SubconsciousSnapshot { name: self.name.clone(), running: self.is_running(), - enabled: self.auto.enabled, - current_phase: self.auto.current_phase.clone(), - turn: self.auto.turn, + enabled, + current_phase, + turn, runs: stats.runs, last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()), forked_agent: self.forked_agent.clone(), @@ -408,8 +412,9 @@ impl Subconscious { /// closure can capture a reference back. pub fn init_output_tool(&mut self, self_arc: std::sync::Arc>) { for agent in &mut self.agents { + let Some(ref mut auto) = agent.auto else { continue }; let sub = self_arc.clone(); - agent.auto.tools.push(crate::agent::tools::Tool { + auto.tools.push(crate::agent::tools::Tool { name: "output", description: "Produce a named output value for passing between steps.", parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Output name"},"value":{"type":"string","description":"Output value"}},"required":["key","value"]}"#, @@ -454,8 +459,9 @@ impl Subconscious { /// Toggle an agent on/off by name. Returns new enabled state. pub fn toggle(&mut self, name: &str) -> Option { let agent = self.agents.iter_mut().find(|a| a.name == name)?; - agent.auto.enabled = !agent.auto.enabled; - Some(agent.auto.enabled) + let auto = agent.auto.as_mut()?; + auto.enabled = !auto.enabled; + Some(auto.enabled) } pub fn walked(&self) -> Vec { @@ -486,9 +492,15 @@ impl Subconscious { self.agents[i].last_run = Some(Instant::now()); any_finished = true; - let (auto_back, result) = handle.await.unwrap_or_else( - |e| (AutoAgent::new(String::new(), vec![], vec![], 0.6, 0), - Err(format!("task panicked: {}", e)))); + let (auto_back, result) = match handle.await { + Ok(r) => (Some(r.0), r.1), + Err(e) => { + // Task panicked — auto is lost, need to recreate from def + let recovered = SubconsciousAgent::new(&self.agents[i].name) + .map(|a| a.auto).flatten(); + (recovered, Err(format!("task panicked: {}", e))) + } + }; self.agents[i].auto = auto_back; match result { @@ -585,8 +597,7 @@ impl Subconscious { if !self.agents[i].should_trigger(conversation_bytes, interval) { continue; } self.agents[i].last_trigger_bytes = conversation_bytes; - let auto = std::mem::replace(&mut self.agents[i].auto, - AutoAgent::new(String::new(), vec![], vec![], 0.6, 0)); + let Some(auto) = self.agents[i].auto.take() else { continue }; to_run.push((i, auto)); } diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index 983a5db..e41d4b7 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -33,7 +33,7 @@ fn save_enabled_config(map: &HashMap) { struct UnconsciousAgent { name: String, enabled: bool, - auto: AutoAgent, + auto: Option, handle: Option)>>, /// Shared agent handle — UI locks to read context live. pub agent: Option>, @@ -103,7 +103,7 @@ impl Unconscious { agents.push(UnconsciousAgent { name: def.agent.clone(), enabled, - auto, + auto: Some(auto), handle: None, agent: None, last_run: None, @@ -187,7 +187,7 @@ impl Unconscious { // Get the AutoAgent back from the finished task (stats already updated) match handle.now_or_never() { Some(Ok((auto_back, result))) => { - agent.auto = auto_back; + agent.auto = Some(auto_back); match result { Ok(_) => dbglog!("[unconscious] {} completed (run {})", agent.name, crate::agent::oneshot::get_stats(&agent.name).runs), @@ -244,9 +244,11 @@ impl Unconscious { store.record_agent_visits(&batch.node_keys, &name).ok(); } - // Swap auto out, replace steps with resolved prompts - let mut auto = std::mem::replace(&mut self.agents[idx].auto, - AutoAgent::new(String::new(), vec![], vec![], 0.6, 0)); + // Take auto out for the spawned task + let Some(mut auto) = self.agents[idx].auto.take() else { + dbglog!("[unconscious] {} already running", name); + return; + }; let orig_steps = std::mem::replace(&mut auto.steps, batch.steps.iter().map(|s| AutoStep { prompt: s.prompt.clone(), @@ -261,7 +263,7 @@ impl Unconscious { if base_url.is_empty() || model.is_empty() { dbglog!("[unconscious] API not configured"); auto.steps = orig_steps; - self.agents[idx].auto = auto; + self.agents[idx].auto = Some(auto); return; } @@ -271,7 +273,7 @@ impl Unconscious { Err(e) => { dbglog!("[unconscious] config: {}", e); auto.steps = orig_steps; - self.agents[idx].auto = auto; + self.agents[idx].auto = Some(auto); return; } };