diff --git a/CHANGELOG.md b/CHANGELOG.md index bfea48b2852..d75fcaefcb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai - CLI/Browser start timeout: honor `openclaw browser --timeout start` and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc. - Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast. - Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up `PortInUseError` races after `browser start`/`open`. (#29538) Thanks @AaronWander. +- Browser/Managed tab cap: limit loopback managed `openclaw` page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego. - Docker/Image health checks: add Dockerfile `HEALTHCHECK` that probes gateway `GET /healthz` so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc. - Docker/Sandbox bootstrap hardening: make `OPENCLAW_SANDBOX` opt-in parsing explicit (`1|true|yes|on`), support custom Docker socket paths via `OPENCLAW_DOCKER_SOCKET`, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to `off` when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc. - Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc. 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 78f08d822da..eb93eb00d64 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 @@ -27,8 +27,8 @@ function makeBrowserState(): BrowserServerState { cdpProtocol: "http", cdpHost: "127.0.0.1", cdpIsLoopback: true, - cdpPortRangeStart: null, - cdpPortRangeEnd: null, + cdpPortRangeStart: 18800, + cdpPortRangeEnd: 18810, evaluateEnabled: false, remoteCdpTimeoutMs: 1500, remoteCdpHandshakeTimeoutMs: 3000, diff --git a/src/browser/server-context.remote-tab-ops.test.ts b/src/browser/server-context.remote-tab-ops.test.ts index 8d310f8bda9..0173ce9a948 100644 --- a/src/browser/server-context.remote-tab-ops.test.ts +++ b/src/browser/server-context.remote-tab-ops.test.ts @@ -494,6 +494,113 @@ describe("browser server-context tab selection state", () => { }); }); + it("never closes the just-opened managed tab during cap cleanup", async () => { + vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" }); + + const existingTabs = [ + { + id: "NEW", + title: "9", + url: "http://127.0.0.1:3009", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/NEW", + type: "page", + }, + { + id: "OLD1", + title: "1", + url: "http://127.0.0.1:3001", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD1", + type: "page", + }, + { + id: "OLD2", + title: "2", + url: "http://127.0.0.1:3002", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD2", + type: "page", + }, + { + id: "OLD3", + title: "3", + url: "http://127.0.0.1:3003", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD3", + type: "page", + }, + { + id: "OLD4", + title: "4", + url: "http://127.0.0.1:3004", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD4", + type: "page", + }, + { + id: "OLD5", + title: "5", + url: "http://127.0.0.1:3005", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD5", + type: "page", + }, + { + id: "OLD6", + title: "6", + url: "http://127.0.0.1:3006", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD6", + type: "page", + }, + { + id: "OLD7", + title: "7", + url: "http://127.0.0.1:3007", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD7", + type: "page", + }, + { + id: "OLD8", + title: "8", + url: "http://127.0.0.1:3008", + webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/OLD8", + type: "page", + }, + ]; + + const fetchMock = vi.fn(async (url: unknown) => { + const value = String(url); + if (value.includes("/json/list")) { + return { ok: true, json: async () => existingTabs } as unknown as Response; + } + if (value.includes("/json/close/OLD1")) { + return { ok: true, json: async () => ({}) } as unknown as Response; + } + if (value.includes("/json/close/NEW")) { + throw new Error("cleanup must not close NEW"); + } + throw new Error(`unexpected fetch: ${value}`); + }); + + global.fetch = withFetchPreconnect(fetchMock); + const state = makeState("openclaw"); + (state.profiles as Map).set("openclaw", { + profile: { name: "openclaw" }, + running: { pid: 1234, proc: { on: vi.fn() } }, + lastTargetId: null, + }); + const ctx = createBrowserRouteContext({ getState: () => state }); + const openclaw = ctx.forProfile("openclaw"); + + const opened = await openclaw.openTab("http://127.0.0.1:3009"); + expect(opened.targetId).toBe("NEW"); + await vi.waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/json/close/OLD1"), + expect.any(Object), + ); + }); + expect(fetchMock).not.toHaveBeenCalledWith( + expect.stringContaining("/json/close/NEW"), + expect.anything(), + ); + }); + it("does not fail tab open when managed-tab cleanup list fails", async () => { vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" });