From aae9687de2a75829a0cdce1f04aab8dabcd40e45 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 5 Apr 2026 04:42:50 -0400 Subject: [PATCH] =?UTF-8?q?mind:=20Mind=20is=20fully=20&self=20=E2=80=94?= =?UTF-8?q?=20no=20&mut=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move turn_handle into MindState (behind the mutex). All Mind methods now take &self. Mind can be shared across tasks without Arc — it's Send + Sync and immutable from the outside. Manual Clone impl for MindState skips turn_handle (not needed for UI diffing). Co-Authored-By: Kent Overstreet --- src/mind/mod.rs | 48 ++++++++++++++++++++++++++++-------------- src/user/event_loop.rs | 2 +- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index ea94ff1..7b4cb1c 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -35,7 +35,6 @@ fn compaction_threshold(app: &AppConfig) -> u32 { /// Shared state between Mind and UI. -#[derive(Clone)] pub struct MindState { /// Pending user input — UI pushes, Mind consumes after turn completes. pub input: Vec, @@ -53,6 +52,26 @@ pub struct MindState { pub last_user_input: Instant, pub consecutive_errors: u32, pub last_turn_had_tools: bool, + /// Handle to the currently running turn task. + pub turn_handle: Option>, +} + +impl Clone for MindState { + fn clone(&self) -> Self { + Self { + input: self.input.clone(), + turn_active: self.turn_active, + dmn: self.dmn.clone(), + dmn_turns: self.dmn_turns, + max_dmn_turns: self.max_dmn_turns, + scoring_in_flight: self.scoring_in_flight, + compaction_in_flight: self.compaction_in_flight, + last_user_input: self.last_user_input, + 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 + } + } } pub type SharedMindState = Arc>; @@ -87,6 +106,7 @@ impl MindState { last_user_input: Instant::now(), consecutive_errors: 0, last_turn_had_tools: false, + turn_handle: None, } } @@ -175,16 +195,12 @@ pub struct Mind { pub config: SessionConfig, ui_tx: ui_channel::UiSender, turn_tx: mpsc::Sender<(Result, StreamTarget)>, - turn_handle: Option>, turn_watch: tokio::sync::watch::Sender, _supervisor: crate::thalamus::supervisor::Supervisor, } -const _: () = { - fn _assert_send() {} - fn _assert_sync() {} - fn _check() { _assert_send::(); _assert_sync::(); } -}; +fn _assert_send_sync() {} +const _: fn() = _assert_send_sync::; impl Mind { pub fn new( @@ -218,11 +234,11 @@ impl Mind { sup.load_config(); sup.ensure_running(); - Self { agent, shared, config, ui_tx, turn_tx, turn_handle: None, turn_watch, _supervisor: sup } + Self { agent, shared, config, ui_tx, turn_tx, turn_watch, _supervisor: sup } } /// Initialize — restore log, start daemons and background agents. - pub async fn init(&mut self) { + pub async fn init(&self) { // Restore conversation let mut ag = self.agent.lock().await; ag.restore_from_log(); @@ -242,7 +258,7 @@ impl Mind { } /// Execute an Action from a MindState method. - async fn run_commands(&mut self, cmds: Vec) { + async fn run_commands(&self, cmds: Vec) { for cmd in cmds { match cmd { MindCommand::None => {} @@ -267,7 +283,7 @@ impl Mind { let mut tools = ag.active_tools.lock().unwrap(); for entry in tools.drain(..) { entry.handle.abort(); } drop(tools); drop(ag); - if let Some(h) = self.turn_handle.take() { h.abort(); } + if let Some(h) = self.shared.lock().unwrap().turn_handle.take() { h.abort(); } self.shared.lock().unwrap().turn_active = false; let _ = self.turn_watch.send(false); } @@ -296,7 +312,7 @@ impl Mind { let agent = self.agent.clone(); let ui_tx = self.ui_tx.clone(); let result_tx = self.turn_tx.clone(); - self.turn_handle = Some(tokio::spawn(async move { + self.shared.lock().unwrap().turn_handle = Some(tokio::spawn(async move { let result = Agent::turn(agent, &input, &ui_tx, target).await; let _ = result_tx.send((result, target)).await; })); @@ -331,13 +347,13 @@ impl Mind { }); } - pub async fn shutdown(&mut self) { - if let Some(handle) = self.turn_handle.take() { handle.abort(); } + pub async fn shutdown(&self) { + if let Some(handle) = self.shared.lock().unwrap().turn_handle.take() { handle.abort(); } } /// Mind event loop — locks MindState, calls state methods, executes actions. pub async fn run( - &mut self, + &self, mut input_rx: tokio::sync::mpsc::UnboundedReceiver, mut turn_rx: mpsc::Receiver<(Result, StreamTarget)>, ) { @@ -355,7 +371,7 @@ impl Mind { } Some((result, target)) = turn_rx.recv() => { - self.turn_handle = None; + self.shared.lock().unwrap().turn_handle = None; let model_switch = self.shared.lock().unwrap().complete_turn(&result, target); let _ = self.turn_watch.send(false); diff --git a/src/user/event_loop.rs b/src/user/event_loop.rs index f3e47d7..50b8e65 100644 --- a/src/user/event_loop.rs +++ b/src/user/event_loop.rs @@ -30,7 +30,7 @@ pub async fn start(cli: crate::user::CliArgs) -> Result<()> { let (turn_tx, turn_rx) = tokio::sync::mpsc::channel(1); let (mind_tx, mind_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut mind = crate::mind::Mind::new(config, ui_tx.clone(), turn_tx); + let mind = crate::mind::Mind::new(config, ui_tx.clone(), turn_tx); mind.init().await; let ui_agent = mind.agent.clone();