From 55ae1367cc38db3737e07fd423f1e9a852bd8aa6 Mon Sep 17 00:00:00 2001 From: donfaquir <1458918806@qq.com> Date: Thu, 9 Jul 2026 15:19:43 +0800 Subject: [PATCH] fix(agent-loop): prevent watchdog timeout during long async operations Root cause: tool result LLM summarization (~32s) and goal extraction (~33s) ran without emitting heartbeat events, exceeding the 30s frontend watchdog. - Add background heartbeat task (10s interval) for the entire agent loop lifetime, with Drop guard for automatic cleanup - Increase frontend watchdog timeout from 30s to 60s as safety margin --- src-tauri/src/llm/agent_loop.rs | 37 +++++++++++++++++++++++++++++- src/hooks/useAgentEventHandlers.ts | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/llm/agent_loop.rs b/src-tauri/src/llm/agent_loop.rs index 93f3d1e..87e59b0 100644 --- a/src-tauri/src/llm/agent_loop.rs +++ b/src-tauri/src/llm/agent_loop.rs @@ -228,6 +228,42 @@ pub async fn run_agent_stream( let mut iteration: u32 = 0; + let hb_stop = CancellationToken::new(); + let bg_hb_app = app.clone(); + let bg_hb_sid = session_id.clone(); + let bg_hb_stop = hb_stop.clone(); + let bg_heartbeat = tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(10)); + interval.tick().await; + loop { + tokio::select! { + _ = bg_hb_stop.cancelled() => break, + _ = interval.tick() => { + let _ = bg_hb_app.emit( + "llm:heartbeat", + serde_json::json!({ + "sessionId": bg_hb_sid, + "loopIteration": 0, + }), + ); + } + } + } + }); + struct HeartbeatGuard { + stop: CancellationToken, + handle: Option>, + } + impl Drop for HeartbeatGuard { + fn drop(&mut self) { + self.stop.cancel(); + if let Some(h) = self.handle.take() { + h.abort(); + } + } + } + let _hb_guard = HeartbeatGuard { stop: hb_stop, handle: Some(bg_heartbeat) }; + loop { iteration += 1; emit_heartbeat(&app, &session_id, iteration); @@ -539,7 +575,6 @@ pub async fn run_agent_stream( } else { vec![None; normalized_calls.len()] }; - for (i, tc) in normalized_calls.iter().enumerate() { let effective_content = if let Some(Some(pr)) = processed.get(i) { if pr.was_summarized { diff --git a/src/hooks/useAgentEventHandlers.ts b/src/hooks/useAgentEventHandlers.ts index 51bed6c..25dcdd0 100644 --- a/src/hooks/useAgentEventHandlers.ts +++ b/src/hooks/useAgentEventHandlers.ts @@ -198,7 +198,7 @@ export function useAgentEventHandlers(deps: AgentEventDeps): AgentSessionState { let disposed = false; const pending: UnlistenFn[] = []; - const WATCHDOG_TIMEOUT_MS = 30_000; + const WATCHDOG_TIMEOUT_MS = 60_000; let watchdogTimer: ReturnType | null = null; function resetWatchdog() {