fix(browser): reuse running loopback browser after probe miss

This commit is contained in:
Vincent Koc 2026-03-23 10:05:26 -07:00
parent a1df10caac
commit b4dd600b37
3 changed files with 40 additions and 0 deletions

View File

@ -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.

View File

@ -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

View File

@ -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();
});
});