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

@ -59,7 +59,7 @@ const ACTIVITY_LINGER: std::time::Duration = std::time::Duration::from_secs(5);
impl Drop for ActivityGuard {
fn drop(&mut self) {
if let Ok(mut st) = self.agent.state.try_lock() {
{ let mut st = self.agent.state.lock_blocking();
if let Some(entry) = st.activities.iter_mut().find(|a| a.id == self.id) {
entry.label.push_str(" (complete)");
entry.expires_at = std::time::Instant::now() + ACTIVITY_LINGER;

View file

@ -152,7 +152,7 @@ async fn ensure_init(agent: Option<&std::sync::Arc<super::super::Agent>>) -> Res
let msg = format!("MCP server {} failed: {:#}", cfg.name, e);
dbglog!("{}", msg);
if let Some(a) = agent {
if let Ok(mut st) = a.state.try_lock() {
{ let mut st = a.state.lock_blocking();
st.notify(msg);
}
}

View file

@ -135,6 +135,23 @@ impl<T> TrackedMutex<T> {
location,
})
}
/// Block the current thread until the lock is acquired.
/// Safe to call from sync contexts (UI thread, slash commands) where
/// .await isn't available. Uses block_in_place so the tokio runtime
/// can schedule other tasks while we wait.
#[track_caller]
pub fn lock_blocking(&self) -> TrackedMutexGuard<'_, T> {
let location = Location::caller();
let guard = tokio::task::block_in_place(|| {
futures::executor::block_on(self.inner.lock())
});
TrackedMutexGuard {
guard,
acquired_at: Instant::now(),
location,
}
}
}
pub struct TrackedMutexGuard<'a, T> {

View file

@ -104,6 +104,6 @@ async fn run(
prior_context: render_prior_context(entries, entry_idx, 2),
timestamp_ns: node_timestamp_ns(node),
});
if let Ok(st) = agent.state.try_lock() { st.changed.notify_one(); }
{ let st = agent.state.lock_blocking(); st.changed.notify_one(); }
}
}

View file

@ -736,7 +736,7 @@ async fn run_finetune(
gen_alternates, &activity,
move |c| {
shared.lock().unwrap().finetune_candidates.push(c);
if let Ok(st) = agent.state.try_lock() { st.changed.notify_one(); }
{ let st = agent.state.lock_blocking(); st.changed.notify_one(); }
},
).await {
Ok((above_threshold, max_div)) => FinetuneScoringStats {

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>();

View file

@ -20,10 +20,7 @@ impl ConsciousScreen {
}
fn read_context_views(&self) -> Vec<SectionView> {
let ctx = match self.agent.context.try_lock() {
Ok(ctx) => ctx,
Err(_) => return Vec::new(),
};
let ctx = self.agent.context.lock_blocking();
let mut views: Vec<SectionView> = Vec::new();
@ -161,8 +158,7 @@ impl ScreenView for ConsciousScreen {
)));
lines.push(Line::raw(format!(" Reasoning: {}", app.reasoning_effort)));
lines.push(Line::raw(format!(" Running processes: {}", app.running_processes)));
let tool_count = app.agent.state.try_lock()
.map(|st| st.active_tools.len()).unwrap_or(0);
let tool_count = { let st = app.agent.state.lock_blocking(); st.active_tools.len() };
lines.push(Line::raw(format!(" Active tools: {}", tool_count)));
let block = pane_block("context")

View file

@ -292,7 +292,7 @@ async fn start(cli: crate::user::CliArgs) -> Result<()> {
}
fn hotkey_cycle_reasoning(mind: &crate::mind::Mind) {
if let Ok(mut ag) = mind.agent.state.try_lock() {
{ let mut ag = mind.agent.state.lock_blocking();
let next = match ag.reasoning_effort.as_str() {
"none" => "low",
"low" => "high",
@ -344,7 +344,7 @@ fn hotkey_cycle_autonomy(mind: &crate::mind::Mind) {
};
s.dmn_turns = 0;
drop(s);
if let Ok(mut ag) = mind.agent.state.try_lock() {
{ let mut ag = mind.agent.state.lock_blocking();
ag.notify(format!("DMN → {}", label));
}
}
@ -419,7 +419,7 @@ async fn run(
terminal.hide_cursor()?;
if let Ok(mut ag) = agent.state.try_lock() { ag.notify("consciousness v0.3"); }
{ let mut ag = agent.state.lock_blocking(); ag.notify("consciousness v0.3"); }
// Initial render
{
@ -526,7 +526,7 @@ async fn run(
}
app.walked_count = mind.subconscious_walked().await.len();
if !startup_done {
if let Ok(mut ag) = agent.state.try_lock() {
{ let mut ag = agent.state.lock_blocking();
let model = agent.model().to_string();
ag.notify(format!("model: {}", model));
startup_done = true;
@ -545,7 +545,7 @@ async fn run(
if let Some(rx_mutex) = STDERR_RX.get() {
if let Ok(rx) = rx_mutex.try_lock() {
while let Ok(line) = rx.try_recv() {
if let Ok(mut ag) = agent.state.try_lock() {
{ let mut ag = agent.state.lock_blocking();
ag.notify(format!("stderr: {}", line));
dirty = true;
}

View file

@ -222,31 +222,30 @@ impl SubconsciousScreen {
let fork_point = app.agent_state.get(self.selected())
.map(|s| s.fork_point).unwrap_or(0);
agent.context.try_lock().ok()
.map(|ctx| {
let mut views = Vec::new();
views.push(section_to_view("System", ctx.system()));
views.push(section_to_view("Identity", ctx.identity()));
views.push(section_to_view("Journal", ctx.journal()));
{
let ctx = agent.context.lock_blocking();
let mut views = Vec::new();
views.push(section_to_view("System", ctx.system()));
views.push(section_to_view("Identity", ctx.identity()));
views.push(section_to_view("Journal", ctx.journal()));
// Conversation: skip to fork point for subconscious agents
let conv = ctx.conversation();
let conv_view = section_to_view("Conversation", conv);
let fork = fork_point.min(conv_view.children.len());
let conv_children: Vec<SectionView> = conv_view.children
.into_iter().skip(fork).collect();
views.push(SectionView {
name: format!("Conversation ({} entries)", conv_children.len()),
tokens: conv_children.iter().map(|c| c.tokens).sum(),
content: String::new(),
token_ids: Vec::new(),
children: conv_children,
status: String::new(),
});
// Conversation: skip to fork point for subconscious agents
let conv = ctx.conversation();
let conv_view = section_to_view("Conversation", conv);
let fork = fork_point.min(conv_view.children.len());
let conv_children: Vec<SectionView> = conv_view.children
.into_iter().skip(fork).collect();
views.push(SectionView {
name: format!("Conversation ({} entries)", conv_children.len()),
tokens: conv_children.iter().map(|c| c.tokens).sum(),
content: String::new(),
token_ids: Vec::new(),
children: conv_children,
status: String::new(),
});
views
})
.unwrap_or_default()
views
}
}
fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) {

View file

@ -45,7 +45,7 @@ impl ScreenView for ThalamusScreen {
}
KeyCode::Char('t') => {
app.think_native = !app.think_native;
if let Ok(mut st) = app.agent.state.try_lock() {
{ let mut st = app.agent.state.lock_blocking();
st.think_native = app.think_native;
let status = if app.think_native { "enabled" } else { "disabled" };
st.notify(format!("native thinking {}", status));
@ -53,7 +53,7 @@ impl ScreenView for ThalamusScreen {
}
KeyCode::Char('T') => {
app.think_tool = !app.think_tool;
if let Ok(mut st) = app.agent.state.try_lock() {
{ let mut st = app.agent.state.lock_blocking();
st.think_tool = app.think_tool;
// Add or remove the think tool from the tools list
if app.think_tool {