fix(browser): default act timeout budget

Co-authored-by: Andy Lin <andyylin@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-25 08:10:54 +01:00
parent 712f7b218c
commit 5376a4a5d6
23 changed files with 283 additions and 11 deletions

View File

@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
- Gateway/nodes: add disabled-by-default `gateway.nodes.pairing.autoApproveCidrs` for first-time node pairing from explicit trusted CIDRs, while keeping operator/browser pairing and all upgrade flows manual. Fixes #60800. Thanks @sahilsatralkar.
- Browser: add viewport coordinate clicks for managed and existing-session automation, plus `openclaw browser click-coords` for CLI use. (#54452) Thanks @dluttz.
- Browser: add `browser.actionTimeoutMs` and use a 60s default action budget so healthy long browser waits do not fail at the client transport boundary. (#62589) Thanks @andyylin.
- Browser/config: support per-profile `browser.profiles.<name>.headless` overrides for locally launched browser profiles, so one profile can run headless without forcing all browser profiles headless. Thanks @nakamotoliu.
- Plugins/PDF: move local PDF extraction into a bundled `document-extract` plugin so core no longer owns `pdfjs-dist` or PDF image-rendering dependencies. Thanks @vincentkoc.
- Dependencies/memory: stop installing `node-llama-cpp` by default; local embeddings now load it only when operators install the optional runtime package. Thanks @vincentkoc.

View File

@@ -1,4 +1,4 @@
13b68287fec00108ca66032120909a0eac797ed541e026357e175e3fce5bacdd config-baseline.json
77ee66fb3b2cde94b393712bc03a132b096cf601c193bde1fe42902eecb0b66b config-baseline.core.json
f1fd4557473391980caf6d6b32f78e4de25f8504b29dfe083f7f9e325d0b204c config-baseline.json
68e0784ca0f9279d49b40ce4493e1cb2c416e1fb70a137a853a10a8c078c97ca config-baseline.core.json
d72032762ab46b99480b57deb81130a0ab5b1401189cfbaf4f7fef4a063a7f6c config-baseline.channel.json
0d5ba81f0030bd39b7ae285096276cc18b150836c2252fd2217329fc6154e80e config-baseline.plugin.json
0504c4f38d4c753fffeb465c93540d829df6b0fcef921eb0e2226ac16bdbbe07 config-baseline.plugin.json

View File

@@ -242,6 +242,8 @@ This path is host-only. For Docker, headless servers, Browserless, or other remo
Current existing-session limits:
- snapshot-driven actions use refs, not CSS selectors
- `browser.actionTimeoutMs` defaults supported `act` requests to 60000 ms when
callers omit `timeoutMs`; per-call `timeoutMs` still wins.
- `click` is left-click only
- `type` does not support `slowly=true`
- `press` does not support `delayMs`

View File

@@ -129,6 +129,7 @@ Browser settings live in `~/.openclaw/openclaw.json`.
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms)
remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms)
actionTimeoutMs: 60000, // default browser act timeout (ms)
tabCleanup: {
enabled: true, // default: true
idleMinutes: 120, // set 0 to disable idle cleanup
@@ -173,6 +174,7 @@ Browser settings live in `~/.openclaw/openclaw.json`.
- Control service binds to loopback on a port derived from `gateway.port` (default `18791` = gateway + 2). Overriding `gateway.port` or `OPENCLAW_GATEWAY_PORT` shifts the derived ports in the same family.
- Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl`; set those only for remote CDP. `cdpUrl` defaults to the managed local CDP port when unset.
- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP HTTP reachability checks; `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket handshakes.
- `actionTimeoutMs` is the default budget for browser `act` requests when the caller does not pass `timeoutMs`. The client transport adds a small slack window so long waits can finish instead of timing out at the HTTP boundary.
- `tabCleanup` is best-effort cleanup for tabs opened by primary-agent browser sessions. Subagent, cron, and ACP lifecycle cleanup still closes their explicit tracked tabs at session end; primary sessions keep active tabs reusable, then close idle or excess tracked tabs in the background.
</Accordion>

View File

@@ -15,6 +15,7 @@ import {
resolveProfile,
wrapExternalContent,
} from "./browser-tool.runtime.js";
import { DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./browser/constants.js";
const browserToolActionDeps = {
browserAct,
@@ -25,6 +26,94 @@ const browserToolActionDeps = {
loadConfig,
};
const BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS = 5_000;
type BrowserActRequest = Parameters<typeof browserAct>[1];
type BrowserActRequestWithTimeout = BrowserActRequest & { timeoutMs?: number };
function normalizePositiveTimeoutMs(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0
? Math.floor(value)
: undefined;
}
function supportsBrowserActTimeout(request: BrowserActRequest): boolean {
switch (request.kind) {
case "click":
case "type":
case "hover":
case "scrollIntoView":
case "drag":
case "select":
case "fill":
case "evaluate":
case "wait":
return true;
default:
return false;
}
}
function existingSessionRejectsActTimeout(request: BrowserActRequest): boolean {
switch (request.kind) {
case "type":
case "hover":
case "scrollIntoView":
case "drag":
case "select":
case "fill":
case "evaluate":
return true;
default:
return false;
}
}
function usesExistingSessionProfile(profileName: string | undefined): boolean {
const cfg = browserToolActionDeps.loadConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const profile = resolveProfile(resolved, profileName ?? resolved.defaultProfile);
return profile ? getBrowserProfileCapabilities(profile).usesChromeMcp : false;
}
function withConfiguredActTimeout(
request: BrowserActRequest,
profileName: string | undefined,
): BrowserActRequest {
const typedRequest = request as BrowserActRequestWithTimeout;
if (normalizePositiveTimeoutMs(typedRequest.timeoutMs) !== undefined) {
return request;
}
if (!supportsBrowserActTimeout(request)) {
return request;
}
if (existingSessionRejectsActTimeout(request) && usesExistingSessionProfile(profileName)) {
return request;
}
const cfg = browserToolActionDeps.loadConfig();
const configuredTimeout =
normalizePositiveTimeoutMs(cfg.browser?.actionTimeoutMs) ?? DEFAULT_BROWSER_ACTION_TIMEOUT_MS;
return { ...typedRequest, timeoutMs: configuredTimeout } as BrowserActRequest;
}
function resolveActProxyTimeoutMs(request: BrowserActRequest): number | undefined {
const candidateTimeouts: number[] = [];
const explicitTimeout = normalizePositiveTimeoutMs(
(request as BrowserActRequestWithTimeout).timeoutMs,
);
if (explicitTimeout !== undefined) {
candidateTimeouts.push(explicitTimeout + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
}
if (request.kind === "wait") {
const waitDuration = normalizePositiveTimeoutMs(request.timeMs);
if (waitDuration !== undefined) {
candidateTimeouts.push(waitDuration + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
}
}
return candidateTimeouts.length ? Math.max(...candidateTimeouts) : undefined;
}
export const __testing = {
setDepsForTest(
overrides: Partial<{
@@ -408,32 +497,34 @@ export async function executeConsoleAction(params: {
}
export async function executeActAction(params: {
request: Parameters<typeof browserAct>[1];
request: BrowserActRequest;
baseUrl?: string;
profile?: string;
proxyRequest: BrowserProxyRequest | null;
onTabActivity?: (targetId: string | undefined) => void;
}): Promise<AgentToolResult<unknown>> {
const { request, baseUrl, profile, proxyRequest } = params;
const effectiveRequest = withConfiguredActTimeout(request, profile);
try {
const result = proxyRequest
? await proxyRequest({
method: "POST",
path: "/act",
profile,
body: request,
body: effectiveRequest,
timeoutMs: resolveActProxyTimeoutMs(effectiveRequest),
})
: await browserToolActionDeps.browserAct(baseUrl, request, {
: await browserToolActionDeps.browserAct(baseUrl, effectiveRequest, {
profile,
});
params.onTabActivity?.(
readStringValue((result as { targetId?: unknown }).targetId) ??
readStringValue(request.targetId),
readStringValue(effectiveRequest.targetId),
);
return jsonResult(result);
} catch (err) {
if (isChromeStaleTargetError(profile, err)) {
const retryRequest = stripTargetIdFromActRequest(request);
const retryRequest = stripTargetIdFromActRequest(effectiveRequest);
const tabs = proxyRequest
? ((
(await proxyRequest({
@@ -445,7 +536,7 @@ export async function executeActAction(params: {
: await browserToolActionDeps.browserTabs(baseUrl, { profile }).catch(() => []);
// Some user-browser targetIds can go stale between snapshots and actions.
// Only retry safe read-only actions, and only when exactly one tab remains attached.
if (retryRequest && canRetryChromeActWithoutTargetId(request) && tabs.length === 1) {
if (retryRequest && canRetryChromeActWithoutTargetId(effectiveRequest) && tabs.length === 1) {
try {
const retryResult = proxyRequest
? await proxyRequest({
@@ -453,6 +544,7 @@ export async function executeActAction(params: {
path: "/act",
profile,
body: retryRequest,
timeoutMs: resolveActProxyTimeoutMs(retryRequest),
})
: await browserToolActionDeps.browserAct(baseUrl, retryRequest, {
profile,

View File

@@ -69,6 +69,7 @@ const browserConfigMocks = vi.hoisted(() => ({
controlPort: 18791,
profiles: {},
defaultProfile: "openclaw",
actionTimeoutMs: 60_000,
})),
resolveProfile: vi.fn((resolved: Record<string, unknown>, name: string) => {
const profile = (resolved.profiles as Record<string, Record<string, unknown>> | undefined)?.[
@@ -249,6 +250,7 @@ function resetBrowserToolMocks() {
controlPort: 18791,
profiles: {},
defaultProfile: "openclaw",
actionTimeoutMs: 60_000,
});
nodesUtilsMocks.listNodes.mockResolvedValue([]);
browserToolTesting.setDepsForTest({
@@ -292,6 +294,7 @@ function setResolvedBrowserProfiles(
controlPort: 18791,
profiles,
defaultProfile,
actionTimeoutMs: 60_000,
});
}
@@ -1078,6 +1081,87 @@ describe("browser tool act compatibility", () => {
expect.objectContaining({ profile: undefined }),
);
});
it("applies configured browser action timeout when act timeout is omitted", async () => {
configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } });
const tool = createBrowserTool();
await tool.execute?.("call-1", {
action: "act",
request: {
kind: "wait",
timeMs: 20_000,
},
});
expect(browserActionsMocks.browserAct).toHaveBeenCalledWith(
undefined,
{
kind: "wait",
timeMs: 20_000,
timeoutMs: 45_000,
},
expect.objectContaining({ profile: undefined }),
);
});
it("does not inject unsupported action timeout for existing-session type actions", async () => {
setResolvedBrowserProfiles({
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
});
configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } });
const tool = createBrowserTool();
await tool.execute?.("call-1", {
action: "act",
profile: "user",
target: "host",
request: {
kind: "type",
ref: "f1e3",
text: "Test Title",
},
});
expect(browserActionsMocks.browserAct).toHaveBeenCalledWith(
undefined,
{
kind: "type",
ref: "f1e3",
text: "Test Title",
},
expect.objectContaining({ profile: "user" }),
);
});
it("passes configured act timeout through node proxy with transport slack", async () => {
mockSingleBrowserProxyNode();
configMocks.loadConfig.mockReturnValue({
browser: {
actionTimeoutMs: 45_000,
},
gateway: { nodes: { browser: { node: "node-1" } } },
});
const tool = createBrowserTool();
await tool.execute?.("call-1", {
action: "act",
target: "node",
request: { kind: "wait", timeMs: 20_000 },
});
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
"node.invoke",
{ timeoutMs: 55_000 },
expect.objectContaining({
params: expect.objectContaining({
path: "/act",
body: { kind: "wait", timeMs: 20_000, timeoutMs: 45_000 },
timeoutMs: 45_000 + 5_000,
}),
}),
);
});
});
describe("browser tool snapshot labels", () => {

View File

@@ -681,6 +681,7 @@ describe("browser chrome launch args", () => {
evaluateEnabled: false,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
actionTimeoutMs: 60_000,
extraArgs: [],
color: "#FF4500",
headless: false,

View File

@@ -6,7 +6,10 @@ import type {
import { buildProfileQuery, withBaseUrl } from "./client-actions-url.js";
import type { BrowserActRequest, BrowserFormField } from "./client-actions.types.js";
import { fetchBrowserJson } from "./client-fetch.js";
import { DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS } from "./constants.js";
import {
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS,
} from "./constants.js";
export type { BrowserActRequest, BrowserFormField } from "./client-actions.types.js";
@@ -26,6 +29,29 @@ export type BrowserDownloadPayload = {
type BrowserDownloadResult = { ok: true; targetId: string; download: BrowserDownloadPayload };
const BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS = 5_000;
function normalizePositiveTimeoutMs(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value > 0
? Math.floor(value)
: undefined;
}
function resolveBrowserActRequestTimeoutMs(req: BrowserActRequest): number {
const explicitTimeout = normalizePositiveTimeoutMs((req as { timeoutMs?: unknown }).timeoutMs);
const candidateTimeouts =
explicitTimeout === undefined
? [DEFAULT_BROWSER_ACTION_TIMEOUT_MS]
: [explicitTimeout + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS];
if (req.kind === "wait") {
const waitDuration = normalizePositiveTimeoutMs(req.timeMs);
if (waitDuration !== undefined) {
candidateTimeouts.push(waitDuration + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
}
}
return Math.max(...candidateTimeouts);
}
async function postDownloadRequest(
baseUrl: string | undefined,
route: "/wait/download" | "/download",
@@ -167,7 +193,7 @@ export async function browserAct(
timeoutMs:
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
? Math.max(1, Math.floor(opts.timeoutMs))
: 20000,
: resolveBrowserActRequestTimeoutMs(req),
});
}

View File

@@ -334,4 +334,30 @@ describe("browser client", () => {
timeoutMs: 20_000,
});
});
it("gives browser act requests enough client timeout for long waits", async () => {
const calls: Array<{ url: string; init?: RequestInit & { timeoutMs?: number } }> = [];
vi.stubGlobal(
"fetch",
vi.fn(async (url: string, init?: RequestInit & { timeoutMs?: number }) => {
calls.push({ url, init });
return {
ok: true,
json: async () => ({ ok: true, targetId: "t1" }),
} as unknown as Response;
}),
);
await browserAct("http://127.0.0.1:18791", { kind: "click", ref: "1" });
await browserAct("http://127.0.0.1:18791", {
kind: "wait",
timeMs: 70_000,
});
await browserAct("http://127.0.0.1:18791", {
kind: "wait",
timeoutMs: 45_000,
});
expect(calls.map((call) => call.init?.timeoutMs)).toEqual([60_000, 75_000, 50_000]);
});
});

View File

@@ -60,6 +60,7 @@ describe("browser config", () => {
expect(resolveProfile(resolved, "chrome-relay")).toBe(null);
expect(resolved.remoteCdpTimeoutMs).toBe(1500);
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000);
expect(resolved.actionTimeoutMs).toBe(60_000);
expect(resolved.tabCleanup).toEqual({
enabled: true,
idleMinutes: 120,
@@ -119,9 +120,11 @@ describe("browser config", () => {
const resolved = resolveBrowserConfig({
remoteCdpTimeoutMs: 2200,
remoteCdpHandshakeTimeoutMs: 5000,
actionTimeoutMs: 45_000,
});
expect(resolved.remoteCdpTimeoutMs).toBe(2200);
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(5000);
expect(resolved.actionTimeoutMs).toBe(45_000);
});
it("supports custom browser tab cleanup policy", () => {

View File

@@ -20,6 +20,7 @@ import { resolveUserPath } from "../utils.js";
import { parseBrowserHttpUrl, redactCdpUrl, isLoopbackHost } from "./cdp.helpers.js";
import {
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES,
@@ -66,6 +67,7 @@ export type ResolvedBrowserConfig = {
cdpIsLoopback: boolean;
remoteCdpTimeoutMs: number;
remoteCdpHandshakeTimeoutMs: number;
actionTimeoutMs: number;
color: string;
executablePath?: string;
headless: boolean;
@@ -263,6 +265,10 @@ export function resolveBrowserConfig(
cfg?.remoteCdpHandshakeTimeoutMs,
Math.max(2000, remoteCdpTimeoutMs * 2),
);
const actionTimeoutMs = normalizeTimeoutMs(
cfg?.actionTimeoutMs,
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
);
const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start;
@@ -343,6 +349,7 @@ export function resolveBrowserConfig(
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
remoteCdpTimeoutMs,
remoteCdpHandshakeTimeoutMs,
actionTimeoutMs,
color: defaultColor,
executablePath,
headless,

View File

@@ -3,6 +3,7 @@ export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw";
export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 60_000;
export const DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS = 20_000;
export const DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES = 120;
export const DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION = 8;

View File

@@ -44,6 +44,7 @@ function makeState(): BrowserServerState {
cdpIsLoopback: true,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
actionTimeoutMs: 60_000,
color: "#FF4500",
headless: false,
noSandbox: false,

View File

@@ -24,6 +24,7 @@ export function makeState(
cdpIsLoopback: profile !== "remote",
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
actionTimeoutMs: 60_000,
evaluateEnabled: false,
extraArgs: [],
color: "#FF4500",

View File

@@ -37,6 +37,7 @@ export function makeBrowserServerState(params?: {
evaluateEnabled: false,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
actionTimeoutMs: 60_000,
extraArgs: [],
color: profile.color,
headless: true,

View File

@@ -6,6 +6,7 @@ import {
stopBrowserBridgeServer,
} from "../../plugin-sdk/browser-bridge.js";
import {
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
@@ -96,6 +97,7 @@ function buildSandboxBrowserResolvedConfig(params: {
cdpPortRangeEnd: cdpPortRange.end,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
actionTimeoutMs: DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
color: DEFAULT_OPENCLAW_BROWSER_COLOR,
executablePath: undefined,
headless: params.headless,

View File

@@ -603,6 +603,14 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
description:
"Timeout in milliseconds for post-connect CDP handshake readiness checks against remote browser targets. Raise this for slow-start remote browsers and lower to fail fast in automation loops.",
},
actionTimeoutMs: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
title: "Browser Action Timeout (ms)",
description:
"Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
},
color: {
type: "string",
title: "Browser Accent Color",
@@ -23933,6 +23941,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
help: "Remote CDP websocket URL used to attach to an externally managed browser instance. Use this for centralized browser hosts and keep URL access restricted to trusted network paths.",
tags: ["advanced", "url-secret"],
},
"browser.actionTimeoutMs": {
label: "Browser Action Timeout (ms)",
help: "Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
tags: ["performance"],
},
"browser.color": {
label: "Browser Accent Color",
help: "Default accent color used for browser profile/UI cues where colored identity hints are displayed. Use consistent colors to help operators identify active browser profile context quickly.",

View File

@@ -258,6 +258,8 @@ export const FIELD_HELP: Record<string, string> = {
"Enables browser capability wiring in the gateway so browser tools and CDP-driven workflows can run. Disable when browser automation is not needed to reduce surface area and startup work.",
"browser.cdpUrl":
"Remote CDP websocket URL used to attach to an externally managed browser instance. Use this for centralized browser hosts and keep URL access restricted to trusted network paths.",
"browser.actionTimeoutMs":
"Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
"browser.color":
"Default accent color used for browser profile/UI cues where colored identity hints are displayed. Use consistent colors to help operators identify active browser profile context quickly.",
"browser.executablePath":

View File

@@ -140,6 +140,7 @@ export const FIELD_LABELS: Record<string, string> = {
browser: "Browser",
"browser.enabled": "Browser Enabled",
"browser.cdpUrl": "Browser CDP URL",
"browser.actionTimeoutMs": "Browser Action Timeout (ms)",
"browser.color": "Browser Accent Color",
"browser.executablePath": "Browser Executable Path",
"browser.headless": "Browser Headless Mode",

View File

@@ -54,6 +54,8 @@ export type BrowserConfig = {
remoteCdpTimeoutMs?: number;
/** Remote CDP WebSocket handshake timeout (ms). Default: max(remoteCdpTimeoutMs * 2, 2000). */
remoteCdpHandshakeTimeoutMs?: number;
/** Default browser act timeout (ms). Default: 60000. */
actionTimeoutMs?: number;
/** Accent color for the openclaw browser profile (hex). Default: #FF4500 */
color?: string;
/** Override the browser executable path (all platforms). */

View File

@@ -381,6 +381,7 @@ export const OpenClawSchema = z
cdpUrl: z.string().optional(),
remoteCdpTimeoutMs: z.number().int().nonnegative().optional(),
remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(),
actionTimeoutMs: z.number().int().positive().optional(),
color: z.string().optional(),
executablePath: z.string().optional(),
headless: z.boolean().optional(),

View File

@@ -1,5 +1,6 @@
export {
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
DEFAULT_BROWSER_EVALUATE_ENABLED,
DEFAULT_OPENCLAW_BROWSER_COLOR,

View File

@@ -9,6 +9,7 @@ export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw";
export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 60_000;
export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
export const DEFAULT_UPLOAD_DIR = path.join(resolvePreferredOpenClawTmpDir(), "uploads");
@@ -30,6 +31,7 @@ export type ResolvedBrowserConfig = {
cdpIsLoopback: boolean;
remoteCdpTimeoutMs: number;
remoteCdpHandshakeTimeoutMs: number;
actionTimeoutMs: number;
color: string;
executablePath?: string;
headless: boolean;