replace try_lock() with lock_blocking() across UI thread

Add lock_blocking() to TrackedMutex: blocks current thread using
block_in_place + futures::executor::block_on, safe for sync contexts.

Replace all try_lock() calls with lock_blocking() in slash commands,
UI rendering, and status reads. Lock hold times are fast enough that
blocking briefly is fine, and this eliminates the spurious 'lock
unavailable' paths that were never actually hit.

Kept rx_mutex.try_lock() in mod.rs (std::sync::Mutex for stderr rx).
This commit is contained in:
Kent Overstreet 2026-04-25 15:35:14 -04:00
commit 4225294d16
28 changed files with 4199 additions and 67 deletions

View file

@ -34,12 +34,12 @@ fn commands() -> Vec<SlashCommand> { vec![
handler: |s, _| { let _ = s.mind_tx.send(MindCommand::NewSession); } },
SlashCommand { name: "/save", help: "Save session to disk",
handler: |s, _| {
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("saved"); }
{ let mut ag = s.agent.state.lock_blocking(); ag.notify("saved"); }
} },
SlashCommand { name: "/model", help: "Show/switch model (/model <name>)",
handler: |s, arg| {
if arg.is_empty() {
if let Ok(mut ag) = s.agent.state.try_lock() {
{ let mut ag = s.agent.state.lock_blocking();
let names = s.agent.app_config.model_names();
let label = if names.is_empty() {
format!("model: {}", s.agent.model())
@ -62,7 +62,7 @@ fn commands() -> Vec<SlashCommand> { vec![
SlashCommand { name: "/dmn", help: "Show DMN state",
handler: |s, _| {
let st = s.shared_mind.lock().unwrap();
if let Ok(mut ag) = s.agent.state.try_lock() {
{ let mut ag = s.agent.state.lock_blocking();
ag.notify(format!("DMN: {:?} ({}/{})", st.dmn, st.dmn_turns, st.max_dmn_turns));
}
} },
@ -71,7 +71,7 @@ fn commands() -> Vec<SlashCommand> { vec![
let mut st = s.shared_mind.lock().unwrap();
st.dmn = crate::mind::subconscious::State::Resting { since: std::time::Instant::now() };
st.dmn_turns = 0;
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("DMN sleeping"); }
{ let mut ag = s.agent.state.lock_blocking(); ag.notify("DMN sleeping"); }
} },
SlashCommand { name: "/wake", help: "Wake DMN to foraging",
handler: |s, _| {
@ -79,14 +79,14 @@ fn commands() -> Vec<SlashCommand> { vec![
if matches!(st.dmn, crate::mind::subconscious::State::Off) { crate::mind::subconscious::set_off(false); }
st.dmn = crate::mind::subconscious::State::Foraging;
st.dmn_turns = 0;
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("DMN foraging"); }
{ let mut ag = s.agent.state.lock_blocking(); ag.notify("DMN foraging"); }
} },
SlashCommand { name: "/pause", help: "Full stop — no autonomous ticks (Ctrl+P)",
handler: |s, _| {
let mut st = s.shared_mind.lock().unwrap();
st.dmn = crate::mind::subconscious::State::Paused;
st.dmn_turns = 0;
if let Ok(mut ag) = s.agent.state.try_lock() { ag.notify("DMN paused"); }
{ let mut ag = s.agent.state.lock_blocking(); ag.notify("DMN paused"); }
} },
SlashCommand { name: "/help", help: "Show this help",
handler: |s, _| { notify_help(&s.agent); } },
@ -116,7 +116,7 @@ pub async fn cmd_switch_model(
}
fn notify_help(agent: &std::sync::Arc<crate::agent::Agent>) {
if let Ok(mut ag) = agent.state.try_lock() {
{ let mut ag = agent.state.lock_blocking();
let mut help = String::new();
for cmd in &commands() {
help.push_str(&format!("{:12} {}\n", cmd.name, cmd.help));
@ -581,16 +581,10 @@ impl InteractScreen {
self.pending_display_count = 0;
let (generation, entries) = {
let st = match self.agent.state.try_lock() {
Ok(st) => st,
Err(_) => return,
};
let st = self.agent.state.lock_blocking();
let generation = st.generation;
drop(st);
let ctx = match self.agent.context.try_lock() {
Ok(ctx) => ctx,
Err(_) => return,
};
let ctx = self.agent.context.lock_blocking();
(generation, ctx.conversation().to_vec())
};
@ -654,7 +648,7 @@ impl InteractScreen {
if let Some(cmd) = dispatch_command(input) {
(cmd.handler)(self, &input[cmd.name.len()..].trim_start());
} else {
if let Ok(mut ag) = self.agent.state.try_lock() {
{ let mut ag = self.agent.state.lock_blocking();
ag.notify(format!("unknown: {}", input.split_whitespace().next().unwrap_or(input)));
}
}
@ -770,9 +764,8 @@ impl InteractScreen {
/// Draw the main (F1) screen — four-pane layout with status bar.
fn draw_main(&mut self, frame: &mut Frame, size: Rect, app: &App) {
// Main layout: content area + active tools overlay + status bar
let st_guard = app.agent.state.try_lock().ok();
let tool_lines = st_guard.as_ref()
.map(|st| st.active_tools.len() as u16).unwrap_or(0);
let st_guard = app.agent.state.lock_blocking();
let tool_lines = st_guard.active_tools.len() as u16;
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
@ -861,10 +854,9 @@ impl InteractScreen {
frame.render_widget(gutter, input_chunks[0]);
frame.render_widget(&self.textarea, input_chunks[1]);
if let Some(ref st) = st_guard {
if !st.active_tools.is_empty() {
if !st_guard.active_tools.is_empty() {
let tool_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::DIM);
let tool_text: Vec<Line> = st.active_tools.iter().map(|t| {
let tool_text: Vec<Line> = st_guard.active_tools.iter().map(|t| {
let elapsed = t.started.elapsed().as_secs();
let line = if t.detail.is_empty() {
format!(" [{}] ({}s)", t.name, elapsed)
@ -875,7 +867,7 @@ impl InteractScreen {
}).collect();
let tool_para = Paragraph::new(tool_text);
frame.render_widget(tool_para, tools_overlay_area);
}}
}
// Draw status bar with live activity indicator
let timer = if !app.activity.is_empty() {
@ -1026,7 +1018,7 @@ impl ScreenView for InteractScreen {
self.sync_from_agent();
// Read status from agent + mind state
if let Ok(mut st) = self.agent.state.try_lock() {
{ let mut st = self.agent.state.lock_blocking();
st.expire_activities();
app.status.prompt_tokens = st.last_prompt_tokens;
app.status.model = self.agent.model().to_string();
@ -1036,7 +1028,7 @@ impl ScreenView for InteractScreen {
app.activity_started = st.activities.last()
.map(|a| a.started);
}
if let Ok(ctx) = self.agent.context.try_lock() {
{ let ctx = self.agent.context.lock_blocking();
let window = crate::agent::context::context_window();
if window > 0 {
let sys = ctx.system().iter().map(|n| n.tokens()).sum::<usize>();