From b4dd600b3756c91e79e819fdc9f4f2a99e7f68f2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 23 Mar 2026 10:05:26 -0700 Subject: [PATCH] fix(browser): reuse running loopback browser after probe miss --- CHANGELOG.md | 1 + src/browser/server-context.availability.ts | 11 ++++++++ ...wser-available.waits-for-cdp-ready.test.ts | 28 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c1a8d3fed..12eacfa3c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Browser/Chrome MCP: wait for existing-session browser tabs to become usable after attach instead of treating the initial Chrome MCP handshake as ready, which reduces user-profile timeouts and repeated consent churn on macOS Chrome attach flows. Fixes #52930. Thanks @vincentkoc. - Gateway/probe: stop successful gateway handshakes from timing out as unreachable while post-connect detail RPCs are still loading, so slow devices report a reachable RPC failure instead of a false negative dead gateway. Fixes #52927. Thanks @vincentkoc. - Config/plugins: treat stale unknown `plugins.allow` ids as warnings instead of fatal config errors, so recovery commands like `plugins install`, `doctor --fix`, and `status` still run when a plugin is missing locally. Fixes #52992. Thanks @vincentkoc. +- Browser/CDP: reuse an already-running loopback browser after a short initial reachability miss instead of immediately falling back to relaunch detection, which fixes second-run browser start/open regressions on slower headless Linux setups. Fixes #53004. Thanks @vincentkoc. - Plugins/message tool: make Discord `components` and Slack `blocks` optional again so pin/unpin/react flows stop failing schema validation and Slack media sends are no longer forced into an invalid blocks-plus-media payload. Fixes #52970 and #52962. Thanks @vincentkoc. - Plugins/Feishu: route `message(..., media=...)` sends through the Feishu outbound media path so file and image attachments actually send instead of being silently dropped. Fixes #52962. Thanks @vincentkoc. - ClawHub/skills: resolve the local ClawHub auth token for gateway skill browsing and switch browse-all requests to search so ClawControl stops falling into unauthenticated 429s and empty authenticated skill lists. Fixes #52949. Thanks @vincentkoc. diff --git a/src/browser/server-context.availability.ts b/src/browser/server-context.availability.ts index e24d28a3a6e..783dbb9e782 100644 --- a/src/browser/server-context.availability.ts +++ b/src/browser/server-context.availability.ts @@ -197,6 +197,17 @@ export function createProfileAvailability({ return; } } + // Browser control service can restart while a loopback OpenClaw browser is still + // alive. Give that pre-existing browser one longer probe window before falling + // back to local executable resolution. + if (!attachOnly && !remoteCdp && profile.cdpIsLoopback && !profileState.running) { + if ( + (await isHttpReachable(PROFILE_ATTACH_RETRY_TIMEOUT_MS)) && + (await isReachable(PROFILE_ATTACH_RETRY_TIMEOUT_MS)) + ) { + return; + } + } if (attachOnly || remoteCdp) { throw new BrowserProfileUnavailableError( remoteCdp diff --git a/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts b/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts index 1c0081fcdbc..ac9d4d89a33 100644 --- a/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts +++ b/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts @@ -110,4 +110,32 @@ describe("browser server-context ensureBrowserAvailable", () => { expect(launchOpenClawChrome).toHaveBeenCalledTimes(1); expect(stopOpenClawChrome).toHaveBeenCalledTimes(1); }); + + it("reuses a pre-existing loopback browser after an initial short probe miss", async () => { + const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile } = + setupEnsureBrowserAvailableHarness(); + const isChromeReachable = vi.mocked(chromeModule.isChromeReachable); + + isChromeReachable + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + isChromeCdpReady.mockResolvedValueOnce(true); + + await expect(profile.ensureBrowserAvailable()).resolves.toBeUndefined(); + + expect(isChromeReachable).toHaveBeenNthCalledWith( + 1, + "http://127.0.0.1:18800", + undefined, + { allowPrivateNetwork: true }, + ); + expect(isChromeReachable).toHaveBeenNthCalledWith( + 2, + "http://127.0.0.1:18800", + 1000, + { allowPrivateNetwork: true }, + ); + expect(launchOpenClawChrome).not.toHaveBeenCalled(); + expect(stopOpenClawChrome).not.toHaveBeenCalled(); + }); });