diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8b11461f52..338345e7d20d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Skills: add a meme-maker skill for curated template search, local SVG/PNG rendering, Imgflip hosted rendering, and Know Your Meme provenance links. - Skills CLI: allow `openclaw skills install` and `openclaw skills update` to target shared managed skills with `--global`. (#74466) Thanks @Marvae. - Browser: surface pending and recently handled modal dialogs in snapshots, return `blockedByDialog` when an action opens a modal, and allow `browser dialog --dialog-id` to answer pending dialogs. +- Browser CLI: add `openclaw browser evaluate --timeout-ms` so long-running page functions can extend both the evaluate action and request timeout budgets. (#83447) Thanks @eefreenyc. - Codex app-server: scope OpenClaw prompt guidance by runtime surface so native Codex keeps Codex-owned base/personality instructions while OpenClaw contributes only runtime context, delivery guidance, and explicitly scoped command hints. (#83454) Thanks @100yenadmin. - Agents/tools: shorten built-in tool descriptions and schema hints across media, messaging, sessions, cron, Gateway, web, image/PDF, TTS, nodes, and plan tools while preserving routing guardrails. - Skills: add node inspector debugging, fused diagram generation, and throwaway spike workflow skills. diff --git a/docs/cli/browser.md b/docs/cli/browser.md index 9190ba810e0f..ffdc93cfa4fd 100644 --- a/docs/cli/browser.md +++ b/docs/cli/browser.md @@ -191,8 +191,12 @@ openclaw browser select OptionA OptionB openclaw browser fill --fields '[{"ref":"1","value":"Ada"}]' openclaw browser wait --text "Done" openclaw browser evaluate --fn '(el) => el.textContent' --ref +openclaw browser evaluate --timeout-ms 30000 --fn 'async () => { await window.ready; return true; }' ``` +Use `evaluate --timeout-ms ` when the page-side function may need longer +than the default evaluate timeout. + Action responses return the current raw `targetId` after action-triggered page replacement when OpenClaw can prove the replacement tab. Scripts should still store and pass `suggestedTargetId`/labels for long-lived workflows. diff --git a/docs/tools/browser-control.md b/docs/tools/browser-control.md index 8e258f491a36..0bb611278a37 100644 --- a/docs/tools/browser-control.md +++ b/docs/tools/browser-control.md @@ -197,6 +197,7 @@ openclaw browser dialog --dismiss --dialog-id d1 openclaw browser wait --text "Done" openclaw browser wait "#main" --url "**/dash" --load networkidle --fn "window.ready===true" openclaw browser evaluate --fn '(el) => el.textContent' --ref 7 +openclaw browser evaluate --timeout-ms 30000 --fn 'async () => { await window.ready; return true; }' openclaw browser highlight e12 openclaw browser trace start openclaw browser trace stop @@ -362,6 +363,8 @@ These are useful for "make the site behave like X" workflows: - `browser act kind=evaluate` / `openclaw browser evaluate` and `wait --fn` execute arbitrary JavaScript in the page context. Prompt injection can steer this. Disable it with `browser.evaluateEnabled=false` if you do not need it. +- Use `openclaw browser evaluate --timeout-ms ` when the page-side function + may need longer than the default evaluate timeout. - For logins and anti-bot notes (X/Twitter, etc.), see [Browser login + X/Twitter posting](/tools/browser-login). - Keep the Gateway/node host private (loopback or tailnet-only). - Remote CDP endpoints are powerful; tunnel and protect them. diff --git a/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.test.ts b/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.test.ts index ed39700fb794..88d11046605a 100644 --- a/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.test.ts +++ b/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.test.ts @@ -65,3 +65,28 @@ describe("browser action input wait command", () => { expect(options?.timeoutMs).toBeGreaterThan(21000); }); }); + +describe("browser action input evaluate command", () => { + beforeEach(() => { + mocks.callBrowserRequest.mockClear(); + getBrowserCliRuntimeCapture().resetRuntimeCapture(); + }); + + it("passes timeout-ms through to the evaluate action and outer request", async () => { + const program = createActionInputProgram(); + + await program.parseAsync( + ["browser", "evaluate", "--fn", "() => true", "--timeout-ms", "30000"], + { from: "user" }, + ); + + const request = mocks.callBrowserRequest.mock.calls.at(-1)?.[1] as + | { body?: { timeoutMs?: number } } + | undefined; + const options = mocks.callBrowserRequest.mock.calls.at(-1)?.[2] as + | { timeoutMs?: number } + | undefined; + expect(request?.body?.timeoutMs).toBe(30000); + expect(options?.timeoutMs).toBeGreaterThan(30000); + }); +}); diff --git a/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.ts b/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.ts index c643a01d2a53..c82410caca6d 100644 --- a/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.ts +++ b/extensions/browser/src/cli/browser-cli-actions-input/register.form-wait-eval.ts @@ -107,6 +107,11 @@ export function registerBrowserFormWaitEvalCommands( .description("Evaluate a function against the page or a ref") .option("--fn ", "Function source, e.g. (el) => el.textContent") .option("--ref ", "Ref from snapshot") + .option( + "--timeout-ms ", + "How long to allow the evaluate function to run (default: 20000)", + (v: string) => Number(v), + ) .option("--target-id ", "CDP target id (or unique prefix)") .action(async (opts, cmd) => { const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts); @@ -116,6 +121,7 @@ export function registerBrowserFormWaitEvalCommands( return; } try { + const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined; const result = await callBrowserAct<{ result?: unknown }>({ parent, profile, @@ -124,7 +130,9 @@ export function registerBrowserFormWaitEvalCommands( fn: opts.fn, ref: normalizeOptionalString(opts.ref), targetId: normalizeOptionalString(opts.targetId), + timeoutMs, }, + timeoutMs, }); if (parent?.json) { defaultRuntime.writeJson(result); diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 1af90132f663..a92ccba28d63 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -433,6 +433,24 @@ export const configHandlers: GatewayRequestHandlers = { ); return; } + const restoredChangedPaths = diffConfigPaths(snapshot.config, restoredMerge.result); + const actor = resolveControlPlaneActor(client); + if (restoredChangedPaths.length === 0) { + context?.logGateway?.info( + `config.patch noop ${formatControlPlaneActor(actor)} (no changed paths)`, + ); + respond( + true, + { + ok: true, + noop: true, + path: resolveGatewayConfigPath(snapshot), + config: redactConfigObject(snapshot.config, schemaPatch.uiHints), + }, + undefined, + ); + return; + } const validated = validateConfigObjectWithPlugins(restoredMerge.result); if (!validated.ok) { respond( @@ -452,7 +470,6 @@ export const configHandlers: GatewayRequestHandlers = { return; } const changedPaths = diffConfigPaths(snapshot.config, validated.config); - const actor = resolveControlPlaneActor(client); // No-op: if the validated config is identical to the current config, // skip the file write and SIGUSR1 restart entirely. This avoids a full