diff --git a/CHANGELOG.md b/CHANGELOG.md index 70caeeca2..7ede5d9d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer. - Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page). - Browser: upgrade `ws` → `wss` when remote CDP uses `https` (fixes Browserless handshake). +- Browser: increase remote CDP reachability timeouts + add `remoteCdpTimeoutMs`/`remoteCdpHandshakeTimeoutMs`. - Browser: preserve auth/query tokens for remote CDP endpoints and pass Basic auth for CDP HTTP/WS. (#895) — thanks @mukhtharcm. - Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi. - Telegram: allow custom commands in the bot menu (merged with native; conflicts ignored). (#860) — thanks @nachoiacovino. diff --git a/docs/tools/browser.md b/docs/tools/browser.md index 2fab16c50..f1d9b7700 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -59,6 +59,8 @@ Browser settings live in `~/.clawdbot/clawdbot.json`. enabled: true, // default: true controlUrl: "http://127.0.0.1:18791", cdpUrl: "http://127.0.0.1:18792", // defaults to controlUrl + 1 + remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms) + remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms) defaultProfile: "chrome", color: "#FF4500", headless: false, @@ -79,6 +81,8 @@ Notes: - If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`), the default browser ports shift to stay in the same “family” (control = gateway + 2). - `cdpUrl` defaults to `controlUrl + 1` when unset. +- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP reachability checks. +- `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket reachability checks. - `attachOnly: true` means “never launch a local browser; only attach if it is already running.” - `color` + per-profile `color` tint the browser UI so you can see which profile is active. - Default profile is `chrome` (extension relay). Use `defaultProfile: "clawd"` for the managed browser. diff --git a/src/browser/config.test.ts b/src/browser/config.test.ts index a07e11655..0304896dc 100644 --- a/src/browser/config.test.ts +++ b/src/browser/config.test.ts @@ -19,6 +19,8 @@ describe("browser config", () => { expect(clawd?.driver).toBe("clawd"); expect(clawd?.cdpPort).toBe(18800); expect(clawd?.cdpUrl).toBe("http://127.0.0.1:18800"); + expect(resolved.remoteCdpTimeoutMs).toBe(1500); + expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000); }); it("derives default ports from CLAWDBOT_GATEWAY_PORT when unset", () => { @@ -52,6 +54,16 @@ describe("browser config", () => { expect(resolved.color).toBe("#FF4500"); }); + it("supports custom remote CDP timeouts", () => { + const resolved = resolveBrowserConfig({ + controlUrl: "http://127.0.0.1:18791", + remoteCdpTimeoutMs: 2200, + remoteCdpHandshakeTimeoutMs: 5000, + }); + expect(resolved.remoteCdpTimeoutMs).toBe(2200); + expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(5000); + }); + it("falls back to default color for invalid hex", () => { const resolved = resolveBrowserConfig({ controlUrl: "http://localhost:18791", diff --git a/src/browser/config.ts b/src/browser/config.ts index 28d44b74f..e8e1bb128 100644 --- a/src/browser/config.ts +++ b/src/browser/config.ts @@ -21,6 +21,8 @@ export type ResolvedBrowserConfig = { cdpProtocol: "http" | "https"; cdpHost: string; cdpIsLoopback: boolean; + remoteCdpTimeoutMs: number; + remoteCdpHandshakeTimeoutMs: number; color: string; executablePath?: string; headless: boolean; @@ -61,6 +63,11 @@ function normalizeHexColor(raw: string | undefined) { return normalized.toUpperCase(); } +function normalizeTimeoutMs(raw: number | undefined, fallback: number) { + const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback; + return value < 0 ? fallback : value; +} + export function parseHttpUrl(raw: string, label: string) { const trimmed = raw.trim(); const parsed = new URL(trimmed); @@ -149,6 +156,11 @@ export function resolveBrowserConfig(cfg: BrowserConfig | undefined): ResolvedBr ); const controlPort = controlInfo.port; const defaultColor = normalizeHexColor(cfg?.color); + const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500); + const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs( + cfg?.remoteCdpHandshakeTimeoutMs, + Math.max(2000, remoteCdpTimeoutMs * 2), + ); const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort); @@ -206,6 +218,8 @@ export function resolveBrowserConfig(cfg: BrowserConfig | undefined): ResolvedBr cdpProtocol, cdpHost: cdpInfo.parsed.hostname, cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname), + remoteCdpTimeoutMs, + remoteCdpHandshakeTimeoutMs, color: defaultColor, executablePath, headless, diff --git a/src/browser/server-context.ts b/src/browser/server-context.ts index a425a49cf..2b8375593 100644 --- a/src/browser/server-context.ts +++ b/src/browser/server-context.ts @@ -187,13 +187,36 @@ function createProfileContext( }; }; - const isReachable = async (timeoutMs = 300) => { - const wsTimeout = Math.max(200, Math.min(2000, timeoutMs * 2)); - return await isChromeCdpReady(profile.cdpUrl, timeoutMs, wsTimeout); + const resolveRemoteHttpTimeout = (timeoutMs: number | undefined) => { + if (profile.cdpIsLoopback) return timeoutMs ?? 300; + const resolved = state().resolved; + if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) { + return Math.max(Math.floor(timeoutMs), resolved.remoteCdpTimeoutMs); + } + return resolved.remoteCdpTimeoutMs; }; - const isHttpReachable = async (timeoutMs = 300) => { - return await isChromeReachable(profile.cdpUrl, timeoutMs); + const resolveRemoteWsTimeout = (timeoutMs: number | undefined) => { + if (profile.cdpIsLoopback) { + const base = timeoutMs ?? 300; + return Math.max(200, Math.min(2000, base * 2)); + } + const resolved = state().resolved; + if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) { + return Math.max(Math.floor(timeoutMs) * 2, resolved.remoteCdpHandshakeTimeoutMs); + } + return resolved.remoteCdpHandshakeTimeoutMs; + }; + + const isReachable = async (timeoutMs?: number) => { + const httpTimeout = resolveRemoteHttpTimeout(timeoutMs); + const wsTimeout = resolveRemoteWsTimeout(timeoutMs); + return await isChromeCdpReady(profile.cdpUrl, httpTimeout, wsTimeout); + }; + + const isHttpReachable = async (timeoutMs?: number) => { + const httpTimeout = resolveRemoteHttpTimeout(timeoutMs); + return await isChromeReachable(profile.cdpUrl, httpTimeout); }; const attachRunning = (running: NonNullable) => { diff --git a/src/config/schema.ts b/src/config/schema.ts index 66742d9fa..6f82cd13f 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -167,6 +167,8 @@ const FIELD_LABELS: Record = { "commands.useAccessGroups": "Use Access Groups", "ui.seamColor": "Accent Color", "browser.controlUrl": "Browser Control URL", + "browser.remoteCdpTimeoutMs": "Remote CDP Timeout (ms)", + "browser.remoteCdpHandshakeTimeoutMs": "Remote CDP Handshake Timeout (ms)", "session.dmScope": "DM Session Scope", "session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns", "messages.ackReaction": "Ack Reaction Emoji", diff --git a/src/config/types.browser.ts b/src/config/types.browser.ts index b89da49ee..3b77be9cf 100644 --- a/src/config/types.browser.ts +++ b/src/config/types.browser.ts @@ -21,6 +21,10 @@ export type BrowserConfig = { controlToken?: string; /** Base URL of the CDP endpoint. Default: controlUrl with port + 1. */ cdpUrl?: string; + /** Remote CDP HTTP timeout (ms). Default: 1500. */ + remoteCdpTimeoutMs?: number; + /** Remote CDP WebSocket handshake timeout (ms). Default: max(remoteCdpTimeoutMs * 2, 2000). */ + remoteCdpHandshakeTimeoutMs?: number; /** Accent color for the clawd browser profile (hex). Default: #FF4500 */ color?: string; /** Override the browser executable path (all platforms). */ diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 4ebcc48ee..f25c707d6 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -67,6 +67,8 @@ export const ClawdbotSchema = z controlUrl: z.string().optional(), controlToken: z.string().optional(), cdpUrl: z.string().optional(), + remoteCdpTimeoutMs: z.number().int().nonnegative().optional(), + remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(), color: z.string().optional(), executablePath: z.string().optional(), headless: z.boolean().optional(),