From e50af1d46922ffba67b92b39a8cad9c87c6251bb Mon Sep 17 00:00:00 2001 From: Droid Agent Date: Tue, 27 Jan 2026 15:06:02 +0000 Subject: [PATCH] fix(tui): propagate error messages when subagent tasks crash When a subagent (Task tool) terminates unexpectedly due to a panic or cancellation, the main agent now receives a proper error message instead of no response at all. Added check_crashed_tasks() method that: - Periodically checks for finished JoinHandles in running_tool_tasks - Detects tasks that ended without sending Completed/Failed events - Extracts panic messages or cancellation status from the JoinHandle - Sends ToolEvent::Failed with descriptive error back to main agent This fixes the issue where the todo list disappears and no message is received by the main agent when a subagent encounters an unexpected error. --- cortex-tui/src/runner/event_loop.rs | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/cortex-tui/src/runner/event_loop.rs b/cortex-tui/src/runner/event_loop.rs index 00896003..9ae96801 100644 --- a/cortex-tui/src/runner/event_loop.rs +++ b/cortex-tui/src/runner/event_loop.rs @@ -1422,6 +1422,10 @@ impl EventLoop { // Update toast notifications (for auto-dismiss with fade) self.app_state.toasts.tick(); + // Check for crashed subagent tasks (panics, cancellations) + // This ensures the main agent always receives a response even if a subagent crashes + self.check_crashed_tasks().await; + // Render frame (respecting frame time to avoid over-rendering) if self.last_render.elapsed() >= self.min_frame_time { self.render(terminal)?; @@ -3466,6 +3470,66 @@ impl EventLoop { } } + /// Checks for crashed background tool tasks (panics or cancelled). + /// + /// This method is called periodically to detect subagent tasks that have + /// terminated unexpectedly without sending a completion or failure event. + /// When a crashed task is detected, it sends a ToolEvent::Failed to ensure + /// the main agent always receives a response. + async fn check_crashed_tasks(&mut self) { + // Collect crashed task IDs and their errors + let mut crashed_tasks: Vec<(String, String)> = Vec::new(); + + // Check all running tool tasks + for (id, handle) in self.running_tool_tasks.iter() { + if handle.is_finished() { + // Task finished - check if it panicked by trying to get the result + // Note: We can't actually await the handle here since we don't own it, + // but is_finished() alone tells us the task ended unexpectedly + // (if it ended normally, it would have sent Completed/Failed events + // which would have removed it from running_tool_tasks) + crashed_tasks.push(( + id.clone(), + "Subagent task terminated unexpectedly (possible panic or cancellation)" + .to_string(), + )); + } + } + + // Process crashed tasks + for (id, error) in crashed_tasks { + tracing::error!("Detected crashed subagent task: {} - {}", id, error); + + // Remove from running tasks + if let Some(handle) = self.running_tool_tasks.remove(&id) { + // Try to get the panic message if possible + let error_msg = match handle.await { + Ok(()) => error, // Task completed but didn't send event - shouldn't happen + Err(join_error) => { + if join_error.is_panic() { + format!("Subagent panicked: {:?}", join_error.into_panic()) + } else if join_error.is_cancelled() { + "Subagent task was cancelled".to_string() + } else { + format!("Subagent task failed: {}", join_error) + } + } + }; + + // Send failure event through the tool event channel + let _ = self + .tool_event_tx + .send(ToolEvent::Failed { + id: id.clone(), + name: "Task".to_string(), + error: error_msg, + duration: std::time::Duration::from_secs(0), + }) + .await; + } + } + } + // ======================================================================== // SUBAGENT EVENT HANDLING // ========================================================================