From 6802b155a806e9b6a5d2a4ef8461becef82c9278 Mon Sep 17 00:00:00 2001 From: Tag Date: Sun, 15 Feb 2026 21:53:49 -0500 Subject: [PATCH] fix: stop LLM retry loop when browser control service is unavailable (#17673) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: 90f47fe13289feecb36b75d289d1efee171a25fa Co-authored-by: tag-assistant <260167501+tag-assistant@users.noreply.github.com> Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com> Reviewed-by: @sebslight --- CHANGELOG.md | 1 + src/browser/client-fetch.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b07e3adeb..106c6d17072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai - Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168. - Skills/Security: restrict `download` installer `targetDir` to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code. - Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07. +- Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone. - Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07. - Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent. - Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07. diff --git a/src/browser/client-fetch.ts b/src/browser/client-fetch.ts index 3fe71934b3e..c8617d0f79c 100644 --- a/src/browser/client-fetch.ts +++ b/src/browser/client-fetch.ts @@ -90,9 +90,16 @@ function withLoopbackBrowserAuth( } function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): Error { - const hint = isAbsoluteHttp(url) - ? "If this is a sandboxed session, ensure the sandbox browser is running and try again." - : `Start (or restart) the OpenClaw gateway (OpenClaw.app menubar, or \`${formatCliCommand("openclaw gateway")}\`) and try again.`; + const isLocal = !isAbsoluteHttp(url); + // Human-facing hint for logs/diagnostics. + const operatorHint = isLocal + ? `Restart the OpenClaw gateway (OpenClaw.app menubar, or \`${formatCliCommand("openclaw gateway")}\`).` + : "If this is a sandboxed session, ensure the sandbox browser is running."; + // Model-facing suffix: explicitly tell the LLM NOT to retry. + // Without this, models see "try again" and enter an infinite tool-call loop. + const modelHint = + "Do NOT retry the browser tool — it will keep failing. " + + "Use an alternative approach or inform the user that the browser is currently unavailable."; const msg = String(err); const msgLower = msg.toLowerCase(); const looksLikeTimeout = @@ -103,10 +110,12 @@ function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): msgLower.includes("aborterror"); if (looksLikeTimeout) { return new Error( - `Can't reach the OpenClaw browser control service (timed out after ${timeoutMs}ms). ${hint}`, + `Can't reach the OpenClaw browser control service (timed out after ${timeoutMs}ms). ${operatorHint} ${modelHint}`, ); } - return new Error(`Can't reach the OpenClaw browser control service. ${hint} (${msg})`); + return new Error( + `Can't reach the OpenClaw browser control service. ${operatorHint} ${modelHint} (${msg})`, + ); } async function fetchHttpJson(