@diegosouzacdv/jev-browser-mcp 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +356 -117
- package/config/ui-testing.json +2 -2
- package/docs/jev-browser-mcp.md +356 -117
- package/mcp_servers/jev-browser-npm/src/config.mjs +27 -12
- package/mcp_servers/jev-browser-npm/src/flow.mjs +2181 -914
- package/mcp_servers/jev-browser-npm/src/server.mjs +270 -69
- package/package.json +1 -1
|
@@ -7,33 +7,74 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
7
7
|
import { chromium } from "playwright";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
9
|
import packageManifest from "../../../package.json" with { type: "json" };
|
|
10
|
-
import { loadSettings, JevBrowserError } from "./config.mjs";
|
|
11
|
-
import { chooseNextAction, createMutationAuthorizationStore, executeBrowserFlow } from "./flow.mjs";
|
|
12
|
-
|
|
13
|
-
const
|
|
10
|
+
import { loadSettings, JevBrowserError } from "./config.mjs";
|
|
11
|
+
import { chooseNextAction, collectFlowContractIssues, createMutationAuthorizationStore, describeBrowserActions, executeBrowserFlow } from "./flow.mjs";
|
|
12
|
+
|
|
13
|
+
const browserContract = describeBrowserActions();
|
|
14
|
+
const jsonObject = z.record(z.string(), z.unknown());
|
|
15
|
+
const browserOptionsShape = Object.fromEntries(browserContract.options.map((key) => {
|
|
16
|
+
const value = key === "return_snapshot" ? z.enum(["diff", "full", "none"])
|
|
17
|
+
: key === "capture_network" ? z.enum(["none", "errors", "all"])
|
|
18
|
+
: key === "reuse_page_match" ? z.enum(["url", "path"])
|
|
19
|
+
: key === "color_scheme" ? z.enum(["light", "dark", "no-preference"])
|
|
20
|
+
: key === "permissions" ? z.array(z.enum(["microphone", "geolocation"]))
|
|
21
|
+
: key === "console_levels" ? z.array(z.enum(["log", "info", "debug", "warn", "error"]))
|
|
22
|
+
: key === "ready" ? z.union([z.string(), z.record(z.string(), z.unknown()), z.array(z.union([z.string(), z.record(z.string(), z.unknown())]))])
|
|
23
|
+
: ["allow_mutations", "block_trackers", "capture_console_errors", "capture_network_error_bodies", "capture_network_errors", "auto_angular_idle", "block_fonts", "continue_from_current_page", "dry_run", "fast_path", "local_only", "ready_network_idle", "reuse_page", "screenshot_on_failure", "screenshot_on_success", "record_video", "mobile", "snapshot_include_hidden", "include_unnamed_controls", "stop_on_expected", "trace_on_failure", "trace_on_success", "fresh_context", "clear_storage"].includes(key) ? z.boolean()
|
|
24
|
+
: ["max_flow_steps", "ready_stable_ms", "ready_timeout_seconds", "step_timeout_seconds"].includes(key) ? z.number().positive()
|
|
25
|
+
: ["busy_selectors", "login_text", "login_url_contains"].includes(key) ? z.array(z.string())
|
|
26
|
+
: ["viewport", "geolocation", "wait_for_http", "fake_media"].includes(key) ? z.union([z.string(), jsonObject])
|
|
27
|
+
: z.string();
|
|
28
|
+
return [key, value.optional()];
|
|
29
|
+
}));
|
|
30
|
+
const browserOptionsSchema = z.object(browserOptionsShape).passthrough();
|
|
31
|
+
const candidatePlanSchema = z.object({
|
|
32
|
+
description: z.string().optional(),
|
|
33
|
+
steps: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
34
|
+
comment: z.string().optional(),
|
|
35
|
+
when: z.unknown().optional(),
|
|
36
|
+
}).strict();
|
|
37
|
+
const expectedOutcomeSchema = z.union([
|
|
38
|
+
z.string(),
|
|
39
|
+
z.object({
|
|
40
|
+
text: z.string().optional(),
|
|
41
|
+
request: z.string().optional(),
|
|
42
|
+
method: z.string().optional(),
|
|
43
|
+
status: z.number().int().min(100).max(599).optional(),
|
|
44
|
+
message_contains: z.string().optional(),
|
|
45
|
+
}).strict().refine((value) => Boolean(value.text || value.request), "expected_outcome needs text or request"),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const browserPools = new Set();
|
|
14
49
|
|
|
15
50
|
class BrowserPool {
|
|
16
51
|
#settings;
|
|
17
52
|
#contextPromise;
|
|
18
53
|
#context;
|
|
19
|
-
#browser;
|
|
54
|
+
#browser;
|
|
55
|
+
#extraContexts = new Set();
|
|
56
|
+
#extraBrowsers = new Set();
|
|
57
|
+
#contextBrowsers = new Map();
|
|
20
58
|
#page;
|
|
21
59
|
#startedAt;
|
|
22
|
-
#everStarted = false;
|
|
23
|
-
#navigationError;
|
|
60
|
+
#everStarted = false;
|
|
61
|
+
#navigationError;
|
|
62
|
+
#unrestoredPage = false;
|
|
24
63
|
#flowQueue = Promise.resolve();
|
|
25
64
|
#configurationKey;
|
|
65
|
+
#lastConfiguration = {};
|
|
26
66
|
|
|
27
67
|
constructor(settings) {
|
|
28
68
|
this.#settings = settings;
|
|
29
69
|
}
|
|
30
70
|
|
|
31
|
-
getNavigationError() {
|
|
32
|
-
return this.#navigationError;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
setNavigationError(error) {
|
|
36
|
-
this.#navigationError = error || undefined;
|
|
71
|
+
getNavigationError() {
|
|
72
|
+
return this.#unrestoredPage ? "BROWSER_PAGE_NOT_RESTORED" : this.#navigationError;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setNavigationError(error) {
|
|
76
|
+
this.#navigationError = error || undefined;
|
|
77
|
+
this.#unrestoredPage = error === "BROWSER_PAGE_NOT_RESTORED";
|
|
37
78
|
}
|
|
38
79
|
|
|
39
80
|
async runFlow(work) {
|
|
@@ -46,9 +87,12 @@ class BrowserPool {
|
|
|
46
87
|
} finally {
|
|
47
88
|
release();
|
|
48
89
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async getPage(configuration
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async getPage(configuration) {
|
|
93
|
+
const hadStartedBeforeRequest = this.#everStarted;
|
|
94
|
+
if (configuration === undefined) configuration = this.#lastConfiguration;
|
|
95
|
+
else this.#lastConfiguration = configuration;
|
|
52
96
|
const configurationKey = JSON.stringify(configuration);
|
|
53
97
|
if (this.#contextPromise && this.#configurationKey !== configurationKey) await this.close();
|
|
54
98
|
let isOpening = !this.#contextPromise;
|
|
@@ -75,7 +119,12 @@ class BrowserPool {
|
|
|
75
119
|
throw error;
|
|
76
120
|
}
|
|
77
121
|
}
|
|
78
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
page: this.#page,
|
|
124
|
+
context: this.#context,
|
|
125
|
+
sessionMs: isOpening ? performance.now() - this.#startedAt : 0,
|
|
126
|
+
recovered: isOpening && hadStartedBeforeRequest,
|
|
127
|
+
};
|
|
79
128
|
}
|
|
80
129
|
|
|
81
130
|
#invalidate(reason) {
|
|
@@ -102,7 +151,7 @@ class BrowserPool {
|
|
|
102
151
|
await this.close();
|
|
103
152
|
this.#everStarted = true;
|
|
104
153
|
this.#startedAt = performance.now();
|
|
105
|
-
this.#contextPromise = this.#open();
|
|
154
|
+
this.#contextPromise = this.#open(this.#lastConfiguration);
|
|
106
155
|
try {
|
|
107
156
|
await this.#contextPromise;
|
|
108
157
|
return { status: "connected", connected: true, recovered: true, restarted: true, current_url: this.#page.url() };
|
|
@@ -115,6 +164,44 @@ class BrowserPool {
|
|
|
115
164
|
if (this.#page !== page || !this.#context) return;
|
|
116
165
|
this.#page = this.#context.pages()[0] || await this.#context.newPage();
|
|
117
166
|
}
|
|
167
|
+
|
|
168
|
+
async createIsolatedContext(contextOptions = {}, launchArgs = []) {
|
|
169
|
+
let browser = this.#browser;
|
|
170
|
+
let ownsBrowser = false;
|
|
171
|
+
if (!browser) {
|
|
172
|
+
browser = await chromium.launch({
|
|
173
|
+
channel: this.#settings.browser.channel,
|
|
174
|
+
headless: this.#settings.browser.mode !== "computer",
|
|
175
|
+
args: launchArgs,
|
|
176
|
+
});
|
|
177
|
+
ownsBrowser = true;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const context = await browser.newContext(contextOptions);
|
|
181
|
+
this.#extraContexts.add(context);
|
|
182
|
+
this.#contextBrowsers.set(context, { browser, ownsBrowser });
|
|
183
|
+
context.once("close", () => this.#extraContexts.delete(context));
|
|
184
|
+
if (ownsBrowser) {
|
|
185
|
+
this.#extraBrowsers.add(browser);
|
|
186
|
+
browser.once("disconnected", () => this.#extraBrowsers.delete(browser));
|
|
187
|
+
}
|
|
188
|
+
return context;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (ownsBrowser) await browser.close().catch(() => {});
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async releaseContext(context) {
|
|
196
|
+
const ownership = this.#contextBrowsers.get(context);
|
|
197
|
+
await context?.close().catch(() => {});
|
|
198
|
+
this.#extraContexts.delete(context);
|
|
199
|
+
this.#contextBrowsers.delete(context);
|
|
200
|
+
if (ownership?.ownsBrowser) {
|
|
201
|
+
await ownership.browser.close().catch(() => {});
|
|
202
|
+
this.#extraBrowsers.delete(ownership.browser);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
118
205
|
|
|
119
206
|
#watchContext(context, browser) {
|
|
120
207
|
context.once("close", () => {
|
|
@@ -125,8 +212,9 @@ class BrowserPool {
|
|
|
125
212
|
});
|
|
126
213
|
}
|
|
127
214
|
|
|
128
|
-
async #open({ contextOptions = {}, launchArgs = [] } = {}) {
|
|
129
|
-
const { mode, channel
|
|
215
|
+
async #open({ contextOptions = {}, launchArgs = [], profileDir: configuredProfileDir } = {}) {
|
|
216
|
+
const { mode, channel } = this.#settings.browser;
|
|
217
|
+
const profileDir = configuredProfileDir || this.#settings.browser.profileDir;
|
|
130
218
|
try {
|
|
131
219
|
if (mode === "computer") {
|
|
132
220
|
this.#context = await chromium.launchPersistentContext(profileDir, {
|
|
@@ -156,7 +244,12 @@ class BrowserPool {
|
|
|
156
244
|
}
|
|
157
245
|
}
|
|
158
246
|
|
|
159
|
-
async close() {
|
|
247
|
+
async close() {
|
|
248
|
+
await Promise.all([...this.#extraContexts].map((context) => this.releaseContext(context)));
|
|
249
|
+
await Promise.all([...this.#extraBrowsers].map((browser) => browser.close().catch(() => {})));
|
|
250
|
+
this.#extraContexts.clear();
|
|
251
|
+
this.#extraBrowsers.clear();
|
|
252
|
+
this.#contextBrowsers.clear();
|
|
160
253
|
if (this.#context) await this.#context.close().catch(() => {});
|
|
161
254
|
if (this.#browser) await this.#browser.close().catch(() => {});
|
|
162
255
|
this.#context = undefined;
|
|
@@ -165,17 +258,47 @@ class BrowserPool {
|
|
|
165
258
|
this.#contextPromise = undefined;
|
|
166
259
|
this.#navigationError = undefined;
|
|
167
260
|
this.#configurationKey = undefined;
|
|
261
|
+
this.#unrestoredPage = false;
|
|
168
262
|
}
|
|
169
263
|
}
|
|
170
264
|
|
|
171
|
-
function
|
|
172
|
-
let message = error instanceof JevBrowserError ? error.message : error?.message || `Jev browser flow failed (${error?.name || "Error"})`;
|
|
173
|
-
const credential = settings.env?.[settings.jev.credentialEnv];
|
|
174
|
-
if (credential) message = message.split(credential).join("[redacted]");
|
|
175
|
-
message = message.replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
|
|
176
|
-
.replace(/([?&](?:token|api[_-]?key|password|secret)=)[^&\s]+/gi, "$1[redacted]");
|
|
177
|
-
return
|
|
178
|
-
}
|
|
265
|
+
function safeErrorMessage(error, settings) {
|
|
266
|
+
let message = error instanceof JevBrowserError ? error.message : error?.message || `Jev browser flow failed (${error?.name || "Error"})`;
|
|
267
|
+
const credential = settings.env?.[settings.jev.credentialEnv];
|
|
268
|
+
if (credential) message = message.split(credential).join("[redacted]");
|
|
269
|
+
message = message.replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
|
|
270
|
+
.replace(/([?&](?:token|api[_-]?key|password|secret)=)[^&\s]+/gi, "$1[redacted]");
|
|
271
|
+
return message;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toolError(error, settings) {
|
|
275
|
+
const message = safeErrorMessage(error, settings);
|
|
276
|
+
return { isError: true, content: [{ type: "text", text: `[jev-browser ${packageManifest.version}] ${message}` }] };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isBrowserDisconnectError(error) {
|
|
280
|
+
return /(?:target page, context or browser has been closed|browser has been closed|browser disconnected|context has been closed|connection closed|connection is closed)/i
|
|
281
|
+
.test(String(error?.message || error?.name || error || ""));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isBrowserStartupError(error) {
|
|
285
|
+
return /(?:could not start the configured .* browser|browser profile is in use by PID \d+)/i
|
|
286
|
+
.test(String(error?.message || error?.name || error || ""));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function browserEnvironmentError(reason, navigationError, extra = {}) {
|
|
290
|
+
return {
|
|
291
|
+
server_version: packageManifest.version,
|
|
292
|
+
status: "environment_error",
|
|
293
|
+
navigation_error: navigationError,
|
|
294
|
+
reason,
|
|
295
|
+
acceptance_criteria_met: false,
|
|
296
|
+
expected_outcome_visible: false,
|
|
297
|
+
steps_executed: 0,
|
|
298
|
+
steps: [],
|
|
299
|
+
...extra,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
179
302
|
|
|
180
303
|
export function fitToolResponse(value, maxBytes) {
|
|
181
304
|
const clone = structuredClone(value && typeof value === "object" ? value : { result: value });
|
|
@@ -290,11 +413,33 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
290
413
|
description: "Check the managed browser connection; set restart=true to close and restart only the browser session owned by this MCP process.",
|
|
291
414
|
inputSchema: { restart: z.boolean().optional() },
|
|
292
415
|
},
|
|
293
|
-
async ({ restart = false } = {}) =>
|
|
294
|
-
restart ? await browserPool.restart() : await browserPool.health()
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
416
|
+
async ({ restart = false } = {}) => {
|
|
417
|
+
const health = restart ? await browserPool.restart() : await browserPool.health();
|
|
418
|
+
return toolText({
|
|
419
|
+
...health,
|
|
420
|
+
server_version: packageManifest.version,
|
|
421
|
+
browser_mode: settings.browser.mode,
|
|
422
|
+
session_id: settings.browser.sessionId,
|
|
423
|
+
capabilities: {
|
|
424
|
+
actions: Object.keys(describeBrowserActions().actions),
|
|
425
|
+
options: describeBrowserActions().options,
|
|
426
|
+
media_permissions: ["microphone", "geolocation"],
|
|
427
|
+
fake_media: "WAV audio input",
|
|
428
|
+
storage_state: "named account snapshots",
|
|
429
|
+
contexts: ["current", "new isolated context"],
|
|
430
|
+
},
|
|
431
|
+
}, settings.browser.maxToolResponseBytes);
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
server.registerTool(
|
|
436
|
+
"describe_actions",
|
|
437
|
+
{
|
|
438
|
+
description: "Return the live JSON contract for every supported Jev Browser MCP action, option, alias, timeout and placeholder rule.",
|
|
439
|
+
inputSchema: {},
|
|
440
|
+
},
|
|
441
|
+
async () => toolText({ server_version: packageManifest.version, ...describeBrowserActions() }, settings.browser.maxToolResponseBytes),
|
|
442
|
+
);
|
|
298
443
|
|
|
299
444
|
server.registerTool(
|
|
300
445
|
"choose_next_action",
|
|
@@ -320,42 +465,98 @@ export function createJevBrowserServer({ env = process.env, configPath, fetchImp
|
|
|
320
465
|
},
|
|
321
466
|
);
|
|
322
467
|
|
|
323
|
-
server.registerTool(
|
|
324
|
-
"run_browser_flow",
|
|
325
|
-
{
|
|
326
|
-
description: "Run a bounded screen flow in
|
|
327
|
-
inputSchema: {
|
|
328
|
-
flow: z.string(),
|
|
329
|
-
initial_url: z.string(),
|
|
330
|
-
expected_outcome:
|
|
331
|
-
candidate_plans: z.record(z.string(),
|
|
332
|
-
params: z.record(z.string(), z.unknown()).optional(),
|
|
333
|
-
options:
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
async ({ flow, initial_url: initialUrl, expected_outcome: expectedOutcome, candidate_plans: candidatePlans, params, options }) => {
|
|
337
|
-
try {
|
|
468
|
+
server.registerTool(
|
|
469
|
+
"run_browser_flow",
|
|
470
|
+
{
|
|
471
|
+
description: "Run a bounded screen flow in an isolated per-server browser profile. Continues the current SPA page by default. Supports media fakes, tabs and contexts, read-only page inspection, conditional plans, HTTP/network/WebSocket assertions, named storage state, guarded mutation confirmation, per-step timeouts and evidence. expected_outcome accepts descriptive text or structured text/request assertions. Use describe_actions for required and optional fields and the exact options contract. Jev chooses among eligible supplied plans; a single plan skips the decision call when fast_path is enabled.",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
flow: z.string().optional(),
|
|
474
|
+
initial_url: z.string().optional(),
|
|
475
|
+
expected_outcome: expectedOutcomeSchema.optional(),
|
|
476
|
+
candidate_plans: z.record(z.string(), candidatePlanSchema).optional(),
|
|
477
|
+
params: z.record(z.string(), z.unknown()).optional(),
|
|
478
|
+
options: browserOptionsSchema.optional(),
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
async ({ flow, initial_url: initialUrl, expected_outcome: expectedOutcome, candidate_plans: candidatePlans, params, options }) => {
|
|
482
|
+
try {
|
|
483
|
+
const contractIssues = collectFlowContractIssues({ flow, candidatePlans });
|
|
484
|
+
if (contractIssues.length) throw new JevBrowserError(`flow contract validation failed:\n- ${contractIssues.join("\n- ")}`);
|
|
338
485
|
return toolText(await browserPool.runFlow(async () => {
|
|
339
|
-
const args = {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
};
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
486
|
+
const args = {
|
|
487
|
+
flow,
|
|
488
|
+
initialUrl,
|
|
489
|
+
expectedOutcome,
|
|
490
|
+
candidatePlans,
|
|
491
|
+
params,
|
|
492
|
+
options,
|
|
493
|
+
settings,
|
|
494
|
+
browserPool,
|
|
495
|
+
fetchImpl,
|
|
496
|
+
serverVersion: packageManifest.version,
|
|
497
|
+
mutationAuthorizationStore,
|
|
498
|
+
};
|
|
499
|
+
const retryAfterRestart = async (first) => {
|
|
500
|
+
const recovery = await browserPool.restart();
|
|
501
|
+
if (recovery.reason) recovery.reason = safeErrorMessage(new Error(recovery.reason), settings);
|
|
502
|
+
if (!recovery.connected) {
|
|
503
|
+
if (!initialUrl) browserPool.setNavigationError("BROWSER_PAGE_NOT_RESTORED");
|
|
504
|
+
return {
|
|
505
|
+
...first,
|
|
506
|
+
...(initialUrl ? {} : { navigation_error: "BROWSER_PAGE_NOT_RESTORED" }),
|
|
507
|
+
browser_restart: recovery,
|
|
508
|
+
retry_count: 0,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (!initialUrl) {
|
|
512
|
+
browserPool.setNavigationError("BROWSER_PAGE_NOT_RESTORED");
|
|
513
|
+
return {
|
|
514
|
+
...first,
|
|
515
|
+
navigation_error: "BROWSER_PAGE_NOT_RESTORED",
|
|
516
|
+
reason: "The browser reconnected, but the previous page was not restored. Provide initial_url to start a new navigation.",
|
|
517
|
+
browser_restart: recovery,
|
|
518
|
+
retry_count: 0,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const retry = await executeBrowserFlow({ ...args, options: { ...options, reuse_page: false } });
|
|
523
|
+
return { ...retry, browser_restart: recovery, retry_count: 1 };
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (!isBrowserDisconnectError(error) && !isBrowserStartupError(error)) throw error;
|
|
526
|
+
return {
|
|
527
|
+
...browserEnvironmentError(
|
|
528
|
+
`The browser reconnected, but the flow could not restart: ${safeErrorMessage(error, settings)}`,
|
|
529
|
+
"BROWSER_RESTART_FAILED",
|
|
530
|
+
),
|
|
531
|
+
browser_restart: recovery,
|
|
532
|
+
retry_count: 1,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
let first;
|
|
538
|
+
try {
|
|
539
|
+
first = await executeBrowserFlow(args);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (isBrowserStartupError(error)) {
|
|
542
|
+
const profileInUse = /browser profile is in use by PID \d+/i.test(String(error.message || ""));
|
|
543
|
+
return browserEnvironmentError(
|
|
544
|
+
safeErrorMessage(error, settings),
|
|
545
|
+
profileInUse ? "BROWSER_PROFILE_IN_USE" : "BROWSER_START_FAILED",
|
|
546
|
+
{ retry_count: 0 },
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (!isBrowserDisconnectError(error)) throw error;
|
|
550
|
+
const failedBeforeSteps = browserEnvironmentError(
|
|
551
|
+
"The browser disconnected before the flow could obtain its page.",
|
|
552
|
+
"BROWSER_DISCONNECTED",
|
|
553
|
+
);
|
|
554
|
+
return retryAfterRestart(failedBeforeSteps);
|
|
555
|
+
}
|
|
556
|
+
const disconnected = /^(?:BROWSER_DISCONNECTED|CONNECTION_CLOSED)$/.test(String(first.navigation_error || ""));
|
|
557
|
+
if (first.status !== "environment_error" || !disconnected
|
|
558
|
+
|| first.steps_executed > 0 || first.mutating_steps?.length || options?.confirmation_token) return first;
|
|
559
|
+
return retryAfterRestart(first);
|
|
359
560
|
}), settings.browser.maxToolResponseBytes);
|
|
360
561
|
} catch (error) {
|
|
361
562
|
return toolError(error, settings);
|