mind: Mind is fully &self — no &mut needed

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 <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-05 04:42:50 -04:00
parent 8e3137fe3f
commit aae9687de2
2 changed files with 33 additions and 17 deletions

View file

@ -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<String>,
@ -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<tokio::task::JoinHandle<()>>,
}
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<std::sync::Mutex<MindState>>;
@ -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<TurnResult>, StreamTarget)>,
turn_handle: Option<tokio::task::JoinHandle<()>>,
turn_watch: tokio::sync::watch::Sender<bool>,
_supervisor: crate::thalamus::supervisor::Supervisor,
}
const _: () = {
fn _assert_send<T: Send>() {}
fn _assert_sync<T: Sync>() {}
fn _check() { _assert_send::<Mind>(); _assert_sync::<Mind>(); }
};
fn _assert_send_sync<T: Send + Sync>() {}
const _: fn() = _assert_send_sync::<Mind>;
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<MindCommand>) {
async fn run_commands(&self, cmds: Vec<MindCommand>) {
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<MindCommand>,
mut turn_rx: mpsc::Receiver<(Result<TurnResult>, 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);