Revert to tokio::sync::Mutex, fix lock-across-await bugs, move input ownership to InteractScreen

The std::sync::Mutex detour caught every place a MutexGuard lived
across an await point in Agent::turn — the compiler enforced Send
safety that tokio::sync::Mutex silently allows. With those fixed,
switch back to tokio::sync::Mutex (std::sync blocks tokio worker
threads and panics inside the runtime).

Input and command dispatch now live in InteractScreen (chat.rs):
- Enter pushes directly to SharedMindState.input (no app.submitted hop)
- sync_from_agent displays pending input with dimmed color
- Slash command table moved from event_loop.rs to chat.rs
- cmd_switch_model kept as pub fn for tool-initiated switches

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-05 21:13:48 -04:00
parent 3e1be4d353
commit 48beb8b663
9 changed files with 404 additions and 370 deletions

View file

@ -21,8 +21,6 @@ use anyhow::Result;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::mpsc;
use std::sync::Mutex;
use crate::agent::{Agent, TurnResult};
use crate::agent::api::ApiClient;
use crate::config::{AppConfig, SessionConfig};
@ -191,8 +189,8 @@ enum BgEvent {
pub type SharedMindState = std::sync::Mutex<MindState>;
pub struct Mind {
pub agent: Arc<Mutex<Agent>>,
pub shared: SharedMindState,
pub agent: Arc<tokio::sync::Mutex<Agent>>,
pub shared: Arc<SharedMindState>,
pub config: SessionConfig,
ui_tx: ui_channel::UiSender,
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
@ -216,7 +214,7 @@ impl Mind {
config.session_dir.join("conversation.jsonl"),
).ok();
let agent = Arc::new(Mutex::new(Agent::new(
let agent = Arc::new(tokio::sync::Mutex::new(Agent::new(
client,
config.system_prompt.clone(),
config.context_parts.clone(),
@ -227,7 +225,7 @@ impl Mind {
shared_active_tools,
)));
let shared = std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns));
let shared = Arc::new(std::sync::Mutex::new(MindState::new(config.app.dmn.max_turns)));
let (turn_watch, _) = tokio::sync::watch::channel(false);
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
@ -242,7 +240,7 @@ impl Mind {
/// Initialize — restore log, start daemons and background agents.
pub async fn init(&self) {
// Restore conversation
let mut ag = self.agent.lock().unwrap();
let mut ag = self.agent.lock().await;
ag.restore_from_log();
drop(ag);
}
@ -258,7 +256,7 @@ impl Mind {
MindCommand::None => {}
MindCommand::Compact => {
let threshold = compaction_threshold(&self.config.app);
let mut ag = self.agent.lock().unwrap();
let mut ag = self.agent.lock().await;
if ag.last_prompt_tokens() > threshold {
ag.compact();
}
@ -273,7 +271,7 @@ impl Mind {
}
MindCommand::Interrupt => {
self.shared.lock().unwrap().interrupt();
let ag = self.agent.lock().unwrap();
let ag = self.agent.lock().await;
let mut tools = ag.active_tools.lock().unwrap();
for entry in tools.drain(..) { entry.handle.abort(); }
drop(tools); drop(ag);
@ -290,7 +288,7 @@ impl Mind {
let new_log = log::ConversationLog::new(
self.config.session_dir.join("conversation.jsonl"),
).ok();
let mut ag = self.agent.lock().unwrap();
let mut ag = self.agent.lock().await;
let shared_ctx = ag.shared_context.clone();
let shared_tools = ag.active_tools.clone();
*ag = Agent::new(
@ -324,7 +322,7 @@ impl Mind {
let response_window = cfg.scoring_response_window;
tokio::spawn(async move {
let (context, client) = {
let mut ag = agent.lock().unwrap();
let mut ag = agent.lock().await;
if ag.agent_cycles.memory_scoring_in_flight { return; }
ag.agent_cycles.memory_scoring_in_flight = true;
(ag.context.clone(), ag.client_clone())
@ -333,7 +331,7 @@ impl Mind {
&context, max_age as i64, response_window, &client, &ui_tx,
).await;
{
let mut ag = agent.lock().unwrap();
let mut ag = agent.lock().await;
ag.agent_cycles.memory_scoring_in_flight = false;
if let Ok(ref scores) = result { ag.agent_cycles.memory_scores = scores.clone(); }
}
@ -383,12 +381,12 @@ impl Mind {
let _ = self.turn_watch.send(false);
if let Some(name) = model_switch {
crate::user::event_loop::cmd_switch_model(&self.agent, &name, &self.ui_tx).await;
crate::user::chat::cmd_switch_model(&self.agent, &name, &self.ui_tx).await;
}
// Post-turn maintenance
{
let mut ag = self.agent.lock().unwrap();
let mut ag = self.agent.lock().await;
ag.age_out_images();
ag.publish_context_state();
}