From d519e357a845d88aed7319791b6e68759abf410f Mon Sep 17 00:00:00 2001 From: DarkSkyXD Date: Sun, 22 Mar 2026 20:50:20 -0500 Subject: [PATCH] fix: resolve worker tool call errors (browser_evaluate, browser_close, compaction) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes for tool call errors observed during worker execution: 1. Enable browser_evaluate by default (config/types.rs) Workers using browser_evaluate were always hitting "JavaScript evaluation is disabled in browser config". Changed the default from false to true so workers can extract page content without requiring explicit config changes. 2. Handle browser_close gracefully on dead connections (tools/browser.rs) When the browser's WebSocket connection is already gone (e.g. process killed, connection timeout), browser.close() fails with "oneshot canceled". This is not a real error — the browser is effectively closed. Now logs a warning and returns success instead of propagating the error to the worker. 3. Fix history compaction orphaning tool_result blocks (agent/worker.rs, agent/compactor.rs) When compaction removes messages by draining from the front of history, the drain boundary could fall between an assistant message (with tool_use blocks) and the following user message (with tool_result blocks). This left orphaned tool_results referencing removed tool_use_ids, causing the Anthropic API to reject requests with: "unexpected tool_use_id found in tool_result blocks" Fixed in all three compaction paths (worker compact_history, channel emergency_truncate, channel run_compaction) by advancing the removal boundary past any User messages containing ToolResult blocks. All fixes are platform-independent (Windows, macOS, Linux/Docker). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/agent/compactor.rs | 38 ++++++++++++++++++++++++++++++++++++-- src/agent/worker.rs | 22 +++++++++++++++++++++- src/config/types.rs | 2 +- src/tools/browser.rs | 17 +++++++++++------ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/agent/compactor.rs b/src/agent/compactor.rs index ea2856b45..d308fd5dd 100644 --- a/src/agent/compactor.rs +++ b/src/agent/compactor.rs @@ -172,7 +172,24 @@ impl Compactor { return Ok(()); } - let remove_count = total / 2; + let mut remove_count = total / 2; + + // Advance past User messages containing ToolResult blocks so we never + // orphan tool_results whose matching tool_use was in a removed assistant + // message (the Anthropic API rejects orphaned tool_result blocks). + while remove_count < total.saturating_sub(2) { + let has_tool_result = matches!( + history.get(remove_count), + Some(Message::User { content }) + if content.iter().any(|item| + matches!(item, UserContent::ToolResult(_))) + ); + if has_tool_result { + remove_count += 1; + } else { + break; + } + } let removed: Vec = history.drain(..remove_count).collect(); drop(removed); @@ -206,12 +223,29 @@ async fn run_compaction( let (removed_messages, remove_count) = { let mut hist = history.write().await; let total = hist.len(); - let remove_count = ((total as f32 * fraction) as usize) + let mut remove_count = ((total as f32 * fraction) as usize) .max(1) .min(total.saturating_sub(2)); if remove_count == 0 { return Ok(0); } + + // Advance past User messages containing ToolResult blocks so we never + // orphan tool_results whose matching tool_use was in a removed message. + while remove_count < total.saturating_sub(2) { + let has_tool_result = matches!( + hist.get(remove_count), + Some(Message::User { content }) + if content.iter().any(|item| + matches!(item, UserContent::ToolResult(_))) + ); + if has_tool_result { + remove_count += 1; + } else { + break; + } + } + let removed: Vec = hist.drain(..remove_count).collect(); (removed, remove_count) }; diff --git a/src/agent/worker.rs b/src/agent/worker.rs index bfaf89170..dccf358df 100644 --- a/src/agent/worker.rs +++ b/src/agent/worker.rs @@ -746,9 +746,29 @@ impl Worker { let estimated = estimate_history_tokens(history); let usage = estimated as f32 / context_window as f32; - let remove_count = ((total as f32 * fraction) as usize) + let mut remove_count = ((total as f32 * fraction) as usize) .max(1) .min(total.saturating_sub(2)); + + // Advance the boundary past User messages that contain ToolResult blocks. + // If we stop right after an Assistant message with ToolCalls, the next + // User message holds the corresponding ToolResults. Leaving those orphaned + // causes the Anthropic API to reject the request with: + // "unexpected tool_use_id found in tool_result blocks" + while remove_count < total.saturating_sub(2) { + let has_tool_result = matches!( + history.get(remove_count), + Some(rig::message::Message::User { content }) + if content.iter().any(|item| + matches!(item, rig::message::UserContent::ToolResult(_))) + ); + if has_tool_result { + remove_count += 1; + } else { + break; + } + } + let removed: Vec = history.drain(..remove_count).collect(); compacted_history.extend(removed.iter().cloned()); diff --git a/src/config/types.rs b/src/config/types.rs index 111f63238..9cce13984 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -842,7 +842,7 @@ impl Default for BrowserConfig { Self { enabled: true, headless: true, - evaluate_enabled: false, + evaluate_enabled: true, executable_path: None, screenshot_dir: None, persist_session: false, diff --git a/src/tools/browser.rs b/src/tools/browser.rs index e218131cc..378a4d818 100644 --- a/src/tools/browser.rs +++ b/src/tools/browser.rs @@ -2108,12 +2108,17 @@ impl Tool for BrowserCloseTool { task.abort(); } - if let Some(mut browser) = browser - && let Err(error) = browser.close().await - { - let message = format!("failed to close browser: {error}"); - tracing::warn!(policy = "close_browser", %message); - return Err(BrowserError::new(message)); + if let Some(mut browser) = browser { + if let Err(error) = browser.close().await { + // The browser connection may already be dead (e.g. "oneshot canceled" + // from a dropped WebSocket). That's fine — the browser is effectively + // closed. Log it but don't fail the tool call. + tracing::warn!( + policy = "close_browser", + %error, + "browser.close() failed, treating as already closed" + ); + } } if !persistent_profile && let Some(dir) = user_data_dir {