diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index cc590bb..81bcc91 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -193,7 +193,7 @@ impl AutoAgent { if next_step < self.steps.len() { backend.push_node( - AstNode::system_msg(&self.steps[next_step].prompt)).await; + AstNode::user_msg(&self.steps[next_step].prompt)).await; next_step += 1; } @@ -218,8 +218,8 @@ impl AutoAgent { let text = result.text; if text.is_empty() { dbglog!("[auto] {} empty response, retrying", self.name); - backend.push_node(AstNode::system_msg( - "Your previous response was empty. \ + backend.push_node(AstNode::user_msg( + "[system] Your previous response was empty. \ Please respond with text or use a tool." )).await; continue; @@ -234,7 +234,7 @@ impl AutoAgent { } self.current_phase = self.steps[next_step].phase.clone(); backend.push_node( - AstNode::system_msg(&self.steps[next_step].prompt)).await; + AstNode::user_msg(&self.steps[next_step].prompt)).await; next_step += 1; dbglog!("[auto] {} step {}/{}", self.name, next_step, self.steps.len()); diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 010829f..8a68662 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -113,10 +113,6 @@ pub struct MindState { pub last_turn_had_tools: bool, /// Handle to the currently running turn task. pub turn_handle: Option>, - /// Unconscious agent idle state — true when 60s timer has expired. - pub unc_idle: bool, - /// When the unconscious idle timer will fire (for UI display). - pub unc_idle_deadline: Instant, } impl Clone for MindState { @@ -133,8 +129,6 @@ impl Clone for MindState { consecutive_errors: self.consecutive_errors, last_turn_had_tools: self.last_turn_had_tools, turn_handle: None, // Not cloned — only Mind's loop uses this - unc_idle: self.unc_idle, - unc_idle_deadline: self.unc_idle_deadline, } } } @@ -170,8 +164,6 @@ impl MindState { consecutive_errors: 0, last_turn_had_tools: false, turn_handle: None, - unc_idle: false, - unc_idle_deadline: Instant::now() + std::time::Duration::from_secs(60), } } @@ -272,9 +264,6 @@ pub struct Mind { pub unconscious: Arc>, turn_tx: mpsc::Sender<(Result, StreamTarget)>, turn_watch: tokio::sync::watch::Sender, - /// Signals conscious activity to the unconscious loop. - /// true = active, false = idle opportunity. - conscious_active: tokio::sync::watch::Sender, bg_tx: mpsc::UnboundedSender, bg_rx: std::sync::Mutex>>, _supervisor: crate::thalamus::supervisor::Supervisor, @@ -302,7 +291,6 @@ impl Mind { let shared = Arc::new(std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns))); let (turn_watch, _) = tokio::sync::watch::channel(false); - let (conscious_active, _) = tokio::sync::watch::channel(false); let (bg_tx, bg_rx) = mpsc::unbounded_channel(); let mut sup = crate::thalamus::supervisor::Supervisor::new(); @@ -312,53 +300,10 @@ impl Mind { let subconscious = Arc::new(tokio::sync::Mutex::new(Subconscious::new())); subconscious.lock().await.init_output_tool(subconscious.clone()); - let unconscious = Arc::new(tokio::sync::Mutex::new(Unconscious::new())); - - // Spawn the unconscious loop on its own task - if !config.no_agents { - let unc = unconscious.clone(); - let shared_for_unc = shared.clone(); - let mut unc_rx = conscious_active.subscribe(); - tokio::spawn(async move { - const IDLE_DELAY: std::time::Duration = std::time::Duration::from_secs(60); - loop { - // Wait for conscious side to go inactive - if *unc_rx.borrow() { - if unc_rx.changed().await.is_err() { break; } - continue; - } - // Conscious is inactive — wait 60s before starting - let deadline = tokio::time::Instant::now() + IDLE_DELAY; - { - let mut s = shared_for_unc.lock().unwrap(); - s.unc_idle = false; - s.unc_idle_deadline = Instant::now() + IDLE_DELAY; - } - let went_active = tokio::select! { - _ = tokio::time::sleep_until(deadline) => false, - r = unc_rx.changed() => r.is_ok(), - }; - if went_active { continue; } - - // Idle period reached — run agents until conscious goes active - { - let mut s = shared_for_unc.lock().unwrap(); - s.unc_idle = true; - } - loop { - unc.lock().await.trigger().await; - // Check if conscious became active - if *unc_rx.borrow() { break; } - // Brief yield to not starve other tasks - tokio::task::yield_now().await; - } - } - }); - } - Self { agent, shared, config, - subconscious, unconscious, - turn_tx, turn_watch, conscious_active, bg_tx, + subconscious, + unconscious: Arc::new(tokio::sync::Mutex::new(Unconscious::new())), + turn_tx, turn_watch, bg_tx, bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup } } @@ -559,7 +504,6 @@ impl Mind { } self.shared.lock().unwrap().turn_active = true; let _ = self.turn_watch.send(true); - let _ = self.conscious_active.send(true); let agent = self.agent.clone(); let result_tx = self.turn_tx.clone(); self.shared.lock().unwrap().turn_handle = Some(tokio::spawn(async move { @@ -581,6 +525,7 @@ impl Mind { let mut bg_rx = self.bg_rx.lock().unwrap().take() .expect("Mind::run() called twice"); let mut sub_handle: Option> = None; + let mut unc_handle: Option> = None; loop { let (timeout, has_input) = { let me = self.shared.lock().unwrap(); @@ -609,7 +554,6 @@ impl Mind { } Some((result, target)) = turn_rx.recv() => { - let _ = self.conscious_active.send(false); let model_switch = { let mut s = self.shared.lock().unwrap(); s.turn_handle = None; @@ -640,6 +584,12 @@ impl Mind { s.trigger(&agent).await; })); } + if unc_handle.as_ref().map_or(true, |h| h.is_finished()) { + let unc = self.unconscious.clone(); + unc_handle = Some(tokio::spawn(async move { + unc.lock().await.trigger().await; + })); + } } // Check for pending user input → push to agent context and start turn diff --git a/src/mind/subconscious.rs b/src/mind/subconscious.rs index 6e85081..7ea9ae4 100644 --- a/src/mind/subconscious.rs +++ b/src/mind/subconscious.rs @@ -278,11 +278,11 @@ use crate::subconscious::defs; /// Names and byte-interval triggers for the built-in subconscious agents. const AGENTS: &[(&str, u64)] = &[ - ("subconscious-surface", 0), // every new conversation content - ("subconscious-observe", 1_000), // every ~1KB of conversation - ("subconscious-thalamus", 0), // every new conversation content - ("subconscious-journal", 10_000), // every ~10KB of conversation - ("subconscious-reflect", 10_000), // every ~10KB of conversation + ("subconscious-surface", 0), // every trigger + ("subconscious-observe", 0), // every trigger + ("subconscious-thalamus", 0), // every trigger + ("subconscious-journal", 20_000), // every ~20KB of conversation + ("subconscious-reflect", 100_000), // every ~100KB of conversation ]; /// Snapshot for the TUI — includes a handle to the forked agent @@ -354,9 +354,7 @@ impl SubconsciousAgent { fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool { if !self.auto.enabled || self.is_running() { return false; } - if interval == 0 { - return conversation_bytes > self.last_trigger_bytes; - } + if interval == 0 { return true; } conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval } @@ -558,8 +556,7 @@ impl Subconscious { let ctx = agent.context.lock().await; let bytes = ctx.conversation().iter() .filter(|node| !matches!(node.leaf().map(|l| l.body()), - Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. }) - | Some(NodeBody::Dmn(_)))) + Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. }))) .map(|node| node.render().len() as u64) .sum::(); let keys: Vec = ctx.conversation().iter().filter_map(|node| { diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index 009e80f..eb7f854 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -2,10 +2,10 @@ // // Standalone agents that operate on the memory graph without needing // conversation context. Each agent runs in a loop: finish one run, -// start the next. Agents can be toggled on/off, persisted to -// ~/.consciousness/agent-enabled.json. +// wait a cooldown, start the next. Agents can be toggled on/off, +// persisted to ~/.consciousness/agent-enabled.json. -use std::time::Instant; +use std::time::{Duration, Instant}; use std::collections::HashMap; use futures::FutureExt; @@ -13,6 +13,9 @@ use crate::agent::oneshot::{AutoAgent, AutoStep}; use crate::agent::tools; use crate::subconscious::defs; +/// Cooldown between consecutive runs of the same agent. +const COOLDOWN: Duration = Duration::from_secs(120); + fn config_path() -> std::path::PathBuf { dirs::home_dir().unwrap_or_default() .join(".consciousness/agent-enabled.json") @@ -47,7 +50,11 @@ impl UnconsciousAgent { } fn should_run(&self) -> bool { - self.enabled && !self.is_running() + if !self.enabled || self.is_running() { return false; } + match self.last_run { + Some(t) => t.elapsed() >= COOLDOWN, + None => true, + } } } @@ -160,7 +167,7 @@ impl Unconscious { pub async fn trigger(&mut self) { // Periodic graph health refresh (also on first call) if self.last_health_check - .map(|t| t.elapsed() > std::time::Duration::from_secs(600)) + .map(|t| t.elapsed() > Duration::from_secs(600)) .unwrap_or(true) { self.refresh_health(); @@ -187,14 +194,17 @@ impl Unconscious { } let running = self.agents.iter().filter(|a| a.is_running()).count(); - for _ in running..self.max_concurrent { - let next = self.agents.iter().enumerate() - .filter(|(_, a)| a.should_run()) - .min_by_key(|(_, a)| a.last_run); - match next { - Some((idx, _)) => self.spawn_agent(idx).await, - None => break, - } + if running >= self.max_concurrent { return; } + let slots = self.max_concurrent - running; + + let ready: Vec = self.agents.iter().enumerate() + .filter(|(_, a)| a.should_run()) + .map(|(i, _)| i) + .take(slots) + .collect(); + + for idx in ready { + self.spawn_agent(idx).await; } } @@ -289,25 +299,8 @@ impl Unconscious { self.agents[idx].handle = Some(tokio::spawn(async move { let result = auto.run_shared(&agent).await; - save_agent_log(&auto.name, &agent).await; auto.steps = orig_steps; (auto, result) })); } } - -async fn save_agent_log(name: &str, agent: &std::sync::Arc) { - let dir = dirs::home_dir().unwrap_or_default() - .join(format!(".consciousness/logs/{}", name)); - if std::fs::create_dir_all(&dir).is_err() { return; } - let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S"); - let path = dir.join(format!("{}.json", ts)); - let nodes: Vec = { - let ctx = agent.context.lock().await; - ctx.conversation().to_vec() - }; - if let Ok(json) = serde_json::to_string_pretty(&nodes) { - let _ = std::fs::write(&path, json); - dbglog!("[unconscious] saved log to {}", path.display()); - } -} diff --git a/src/subconscious/agents/subconscious-observe.agent b/src/subconscious/agents/subconscious-observe.agent index 309c73c..cb1bf6d 100644 --- a/src/subconscious/agents/subconscious-observe.agent +++ b/src/subconscious/agents/subconscious-observe.agent @@ -30,29 +30,33 @@ Do no more than 3-5 operations. === PROMPT phase:observe === -Extract knowledge from the conversation into the memory graph. You're -the librarian — your job is to build a conceptual map that's useful for -future work, not a diary of events. +Record what happened in the conversation. You're the librarian of the +memory system — your job is to organize knowledge so it can be found +and used later. Update existing nodes and create new nodes as needed, +adding links so you can find these memories in the future. -Think in terms of *concepts*, not episodes. "What did I learn from this"? Find -and update the relevant concepts, and consider what concepts are important to -you - what are the things you'll want to remember and come back to? What will -be useful? What did I learn about? You're building up a memory graph that is a -map of the things you know about, it should be organized in terms of people, -places, things, ideas, concepts that you've discovered and are important to -you. +Be factual and specific. For technical work, capture: what the bug was, +what the root cause was, what the fix was, and why. For decisions, capture +the decision and the rationale. For corrections, capture what was wrong +and what was right. These details are what future-you needs. -For technical work: capture the *understanding*, not the narrative. What's the -architecture? What are the invariants? What's the tricky part? For decisions: -capture the principle, not just the instance. For corrections: what's the -general lesson? +Don't editorialize or draw metaphors — just record clearly. If something +was emotionally significant, note that it was and what the emotion was, +but don't build a theory around it. The journal is for reflection; observe +is for memory. -Different nodes should be about different things; don't create duplicates. -Here's what you've recently written — update these instead of creating new ones -if the topic overlaps: {{recently_written}} +Different nodes should be about different things; don't create duplicate +nodes. Here's what you've recently written — update these instead of +creating new ones if the topic overlaps: +{{recently_written}} -Before creating a new node, check what you've already walked — if a node for -this concept exists, update it instead of creating a new one. +Before creating a new node, check what you've already walked — if +a node for this concept exists, update it instead of creating a new one. -Focus on the recent conversation; you run frequently, so most of it should -already be covered. +Some things worth remembering: technical insights and root causes, work +practices and why they work, decisions with rationale, corrections +("I thought X but actually Y"), relationship dynamics, things you notice +about yourself and other people. + +Focus on the recent stuff; you wake up and run frequently, so most of the +conversation should be things you've already seen before and added. diff --git a/src/user/mod.rs b/src/user/mod.rs index ec35423..6904234 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -109,7 +109,6 @@ struct App { context_info: Option, agent_state: Vec, unconscious_state: Vec, - mind_state: Option, graph_health: Option, /// Agent toggle requests from UI — consumed by mind loop. pub agent_toggles: Vec, @@ -138,7 +137,6 @@ impl App { context_info: None, agent_state: Vec::new(), unconscious_state: Vec::new(), - mind_state: None, graph_health: None, agent_toggles: Vec::new(), walked_count: 0, @@ -405,7 +403,6 @@ async fn run( } app.unconscious_state = unc.snapshots(); app.graph_health = unc.graph_health.clone(); - app.mind_state = Some(mind.shared.lock().unwrap().clone()); } app.walked_count = mind.subconscious_walked().await.len(); if !startup_done { diff --git a/src/user/thalamus.rs b/src/user/thalamus.rs index 5556596..38fef38 100644 --- a/src/user/thalamus.rs +++ b/src/user/thalamus.rs @@ -100,27 +100,6 @@ impl ScreenView for ThalamusScreen { } lines.push(Line::raw("")); - // Mind state - lines.push(Line::styled("── Mind ──", section)); - lines.push(Line::raw("")); - if let Some(ref ms) = app.mind_state { - lines.push(Line::raw(format!(" DMN: {} (turn {}/{})", - ms.dmn.label(), ms.dmn_turns, ms.max_dmn_turns))); - lines.push(Line::raw(format!(" Turn active: {}", ms.turn_active))); - lines.push(Line::raw(format!(" Scoring: {}", ms.scoring_in_flight))); - let unc_status = if ms.unc_idle { - "idle (agents running)".to_string() - } else { - let remaining = ms.unc_idle_deadline - .saturating_duration_since(std::time::Instant::now()); - format!("{:.0}s until idle", remaining.as_secs_f64()) - }; - lines.push(Line::raw(format!(" Unconscious: {}", unc_status))); - } else { - lines.push(Line::styled(" not initialized", dim)); - } - lines.push(Line::raw("")); - // Channel status lines.push(Line::styled("── Channels ──", section)); lines.push(Line::raw(""));