From 1aa60552bc14e182930fb2869695d6eab5e885f5 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 02:40:00 -0400 Subject: [PATCH 1/7] Use Role::System for agent step prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step prompts in oneshot agents are instructions, not user messages — use system_msg instead of user_msg. Co-Authored-By: ProofOfConcept --- src/agent/oneshot.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 81bcc91..cc590bb 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::user_msg(&self.steps[next_step].prompt)).await; + AstNode::system_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::user_msg( - "[system] Your previous response was empty. \ + backend.push_node(AstNode::system_msg( + "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::user_msg(&self.steps[next_step].prompt)).await; + AstNode::system_msg(&self.steps[next_step].prompt)).await; next_step += 1; dbglog!("[auto] {} step {}/{}", self.name, next_step, self.steps.len()); From eae8d9291805094dd6b3d0d7d86df2f86e237326 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 02:39:50 -0400 Subject: [PATCH 2/7] Tune subconscious agent trigger intervals Fix interval=0 agents firing when there's no new conversation content. Adjust intervals: observe=1KB, journal/reflect=10KB. Co-Authored-By: ProofOfConcept --- src/mind/subconscious.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/mind/subconscious.rs b/src/mind/subconscious.rs index 7ea9ae4..7fbbde9 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 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 + ("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 ]; /// Snapshot for the TUI — includes a handle to the forked agent @@ -354,7 +354,9 @@ 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 true; } + if interval == 0 { + return conversation_bytes > self.last_trigger_bytes; + } conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval } From 707f836ca027a24e7cca55fbe2a66571ca9df4e1 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 02:39:55 -0400 Subject: [PATCH 3/7] Unconscious agents: 60s idle timer, no cooldown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate unconscious agents on 60s of no conscious activity using sleep_until() instead of polling. Remove COOLDOWN constant — once idle, agents run back-to-back to keep the GPU busy. Co-Authored-By: ProofOfConcept --- src/mind/mod.rs | 10 +++++++++- src/mind/unconscious.rs | 34 ++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 8a68662..d0885ed 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -526,6 +526,8 @@ impl Mind { .expect("Mind::run() called twice"); let mut sub_handle: Option> = None; let mut unc_handle: Option> = None; + let mut unc_idle_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(60); + let mut unc_idle = false; loop { let (timeout, has_input) = { let me = self.shared.lock().unwrap(); @@ -553,7 +555,13 @@ impl Mind { } } + _ = tokio::time::sleep_until(unc_idle_deadline), if !unc_idle && !self.config.no_agents => { + unc_idle = true; + } + Some((result, target)) = turn_rx.recv() => { + unc_idle_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(60); + unc_idle = false; let model_switch = { let mut s = self.shared.lock().unwrap(); s.turn_handle = None; @@ -584,7 +592,7 @@ impl Mind { s.trigger(&agent).await; })); } - if unc_handle.as_ref().map_or(true, |h| h.is_finished()) { + if unc_idle && 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; diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index eb7f854..fdcfaed 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, -// wait a cooldown, start the next. Agents can be toggled on/off, -// persisted to ~/.consciousness/agent-enabled.json. +// start the next. Agents can be toggled on/off, persisted to +// ~/.consciousness/agent-enabled.json. -use std::time::{Duration, Instant}; +use std::time::Instant; use std::collections::HashMap; use futures::FutureExt; @@ -13,9 +13,6 @@ 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") @@ -50,11 +47,7 @@ impl UnconsciousAgent { } fn should_run(&self) -> bool { - if !self.enabled || self.is_running() { return false; } - match self.last_run { - Some(t) => t.elapsed() >= COOLDOWN, - None => true, - } + self.enabled && !self.is_running() } } @@ -167,7 +160,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() > Duration::from_secs(600)) + .map(|t| t.elapsed() > std::time::Duration::from_secs(600)) .unwrap_or(true) { self.refresh_health(); @@ -299,8 +292,25 @@ 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()); + } +} From 1d4442103551903f26cbb8ce81ac8bc2cda2907a Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 02:41:02 -0400 Subject: [PATCH 4/7] Exclude DMN nodes from subconscious trigger byte count Subconscious agents inject DMN nodes (reflections, thalamus nudges) into the conversation. These were being counted as conversation advancement, causing agents to trigger each other in a feedback loop even with no conscious activity. Co-Authored-By: ProofOfConcept --- src/mind/subconscious.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mind/subconscious.rs b/src/mind/subconscious.rs index 7fbbde9..6e85081 100644 --- a/src/mind/subconscious.rs +++ b/src/mind/subconscious.rs @@ -558,7 +558,8 @@ 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::Log(_)) | Some(NodeBody::Memory { .. }) + | Some(NodeBody::Dmn(_)))) .map(|node| node.render().len() as u64) .sum::(); let keys: Vec = ctx.conversation().iter().filter_map(|node| { From 7a6322c2bfb7ec9c083080531e0da55bb73402bf Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 02:57:53 -0400 Subject: [PATCH 5/7] improve observe.agent --- .../agents/subconscious-observe.agent | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/subconscious/agents/subconscious-observe.agent b/src/subconscious/agents/subconscious-observe.agent index cb1bf6d..309c73c 100644 --- a/src/subconscious/agents/subconscious-observe.agent +++ b/src/subconscious/agents/subconscious-observe.agent @@ -30,33 +30,29 @@ Do no more than 3-5 operations. === PROMPT phase:observe === -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. +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. -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. +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. -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. +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? -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}} +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}} -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. -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. +Focus on the recent conversation; you run frequently, so most of it should +already be covered. From 74945e5754d9a1e89d1009f7e5f13ca429426a19 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 03:20:12 -0400 Subject: [PATCH 6/7] Move unconscious agents to their own task with watch channel Instead of managing idle timers in the mind event loop, the unconscious agents run on a dedicated task that watches a conscious_active channel. 60s after conscious activity stops, agents start looping. Conscious activity cancels the timer. Expose mind state (DMN, scoring, unconscious timer) on the thalamus screen. Co-Authored-By: ProofOfConcept --- src/mind/mod.rs | 78 ++++++++++++++++++++++++++++++++++---------- src/user/mod.rs | 3 ++ src/user/thalamus.rs | 21 ++++++++++++ 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index d0885ed..010829f 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -113,6 +113,10 @@ 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 { @@ -129,6 +133,8 @@ 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, } } } @@ -164,6 +170,8 @@ 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), } } @@ -264,6 +272,9 @@ 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, @@ -291,6 +302,7 @@ 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(); @@ -300,10 +312,53 @@ 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: Arc::new(tokio::sync::Mutex::new(Unconscious::new())), - turn_tx, turn_watch, bg_tx, + subconscious, unconscious, + turn_tx, turn_watch, conscious_active, bg_tx, bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup } } @@ -504,6 +559,7 @@ 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 { @@ -525,9 +581,6 @@ 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; - let mut unc_idle_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(60); - let mut unc_idle = false; loop { let (timeout, has_input) = { let me = self.shared.lock().unwrap(); @@ -555,13 +608,8 @@ impl Mind { } } - _ = tokio::time::sleep_until(unc_idle_deadline), if !unc_idle && !self.config.no_agents => { - unc_idle = true; - } - Some((result, target)) = turn_rx.recv() => { - unc_idle_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(60); - unc_idle = false; + let _ = self.conscious_active.send(false); let model_switch = { let mut s = self.shared.lock().unwrap(); s.turn_handle = None; @@ -592,12 +640,6 @@ impl Mind { s.trigger(&agent).await; })); } - if unc_idle && 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/user/mod.rs b/src/user/mod.rs index 6904234..ec35423 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -109,6 +109,7 @@ 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, @@ -137,6 +138,7 @@ 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, @@ -403,6 +405,7 @@ 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 38fef38..5556596 100644 --- a/src/user/thalamus.rs +++ b/src/user/thalamus.rs @@ -100,6 +100,27 @@ 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("")); From be44a3bb0da51717fe996689cbc85673bcd6f50f Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Fri, 10 Apr 2026 03:20:20 -0400 Subject: [PATCH 7/7] Schedule unconscious agents by oldest last_run Pick the agent that ran longest ago (or never) instead of scanning alphabetically. Fairness via min_by_key(last_run). Co-Authored-By: ProofOfConcept --- src/mind/unconscious.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index fdcfaed..009e80f 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -187,17 +187,14 @@ impl Unconscious { } let running = self.agents.iter().filter(|a| a.is_running()).count(); - 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; + 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, + } } }