@orkestrel/browser 0.0.23 → 0.0.25

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.
@@ -1,5 +1,5 @@
1
- import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_JOURNEY_FORMAT_VERSION, BROWSER_JOURNEY_NAME_PATTERN, BROWSER_JOURNEY_TOOL_NAMES, BROWSER_RUN_ID_PATTERN, BROWSER_TOOL_COPY, BROWSER_TOOL_NAMES, BrowserConnectionError, BrowserContext, BrowserError, BrowserTransition, CDPClient, createBrowserToolset, createCDPClient, generateBrowserRunId, isBrowserConnectionError, isBrowserError, isCDPTimeoutError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
2
- import { attempt, isArray, isError, isInstance, isInteger, isNumber, isRecord, isString, parseJSON } from "@orkestrel/contract";
1
+ import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_JOURNEY_FORMAT_VERSION, BROWSER_JOURNEY_NAME_PATTERN, BROWSER_JOURNEY_TOOL_NAMES, BROWSER_RUN_ID_PATTERN, BROWSER_TOOL_COPY, BROWSER_TOOL_NAMES, BrowserConnectionError, BrowserContext, BrowserError, BrowserTransition, CDPClient, createBrowserToolset, createCDPClient, generateBrowserRunId, isBrowserConnectionError, isBrowserError, isCDPConnectionError, isCDPTimeoutError, normalizeBrowserJourneyReason, validateBrowserContextOptions, validateBrowserJourney, validateBrowserRun, validateBrowserStorePage } from "../core/index.js";
2
+ import { attempt, isArray, isError, isInstance, isInteger, isNumber, isRecord, isString, parseInteger, parseJSON } from "@orkestrel/contract";
3
3
  import { existsSync, globSync, realpathSync } from "node:fs";
4
4
  import { lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, rmdir, unlink, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
@@ -37,8 +37,18 @@ var BROWSER_CDP_VERSION_PATH = "/json/version";
37
37
  * other target category Chromium reports — `'/json/list'`.
38
38
  */
39
39
  var BROWSER_CDP_LIST_PATH = "/json/list";
40
- /** Lists the flags always passed to a launched browser process, alongside the caller's own. */
41
- var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
40
+ /**
41
+ * Lists the flags always passed to a launched browser process, alongside the caller's own.
42
+ *
43
+ * @remarks
44
+ * Disables account sync so extension installs cannot reset URL loader factories during page imports.
45
+ * Edge can still sign a fresh Windows profile in to the operating-system account.
46
+ */
47
+ var BROWSER_LAUNCH_ARGS = Object.freeze([
48
+ "--no-first-run",
49
+ "--no-default-browser-check",
50
+ "--disable-sync"
51
+ ]);
42
52
  /**
43
53
  * Names the flag that enables headless mode on a launched browser process, `'--headless=new'`.
44
54
  */
@@ -218,6 +228,10 @@ var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
218
228
  var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
219
229
  /** Sets the default number of warm browsers to 1. */
220
230
  var BROWSER_SERVER_POOL_SIZE = 1;
231
+ /** Sets the default context capacity per browser, including the shared holder. */
232
+ var BROWSER_SERVER_CONTEXTS = 2;
233
+ /** Limits context capacity to the largest measured per-browser count. */
234
+ var BROWSER_SERVER_CONTEXTS_LIMIT = 4;
221
235
  /** Limits the number of warm browsers to 3. */
222
236
  var BROWSER_SERVER_POOL_LIMIT = 3;
223
237
  /** Permits one failed refill before the next failure spends the bound. */
@@ -236,6 +250,84 @@ var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
236
250
  var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
237
251
  /** Names an interrupted call whose outcome is unknown. */
238
252
  var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
253
+ /** Names the refusal when every browser is admitted to a holder. */
254
+ var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
255
+ /** Names the refusal for an unknown or ended holder. */
256
+ var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
257
+ /** Defines the holder tools independently of the shared browser's vocabulary. */
258
+ var BROWSER_SERVER_COPY = Object.freeze({
259
+ acquire: Object.freeze({
260
+ name: "acquire",
261
+ description: "Acquires an independent browser and returns its holder id and tool catalog. Destroy the holder when its work ends.",
262
+ parameters: Object.freeze({
263
+ type: "object",
264
+ properties: Object.freeze({ purpose: Object.freeze({
265
+ type: "string",
266
+ description: "The work this browser will serve."
267
+ }) }),
268
+ required: Object.freeze(["purpose"])
269
+ })
270
+ }),
271
+ execute: Object.freeze({
272
+ name: "execute",
273
+ description: "Runs a tool on the selected holder, with that tool’s arguments inside arguments. Get its available tools with tools.",
274
+ parameters: Object.freeze({
275
+ type: "object",
276
+ properties: Object.freeze({
277
+ holder: Object.freeze({
278
+ type: "string",
279
+ description: "The holder id returned by acquire."
280
+ }),
281
+ name: Object.freeze({
282
+ type: "string",
283
+ description: "The tool name from this holder’s catalog."
284
+ }),
285
+ arguments: Object.freeze({
286
+ type: "object",
287
+ description: "The arguments for the selected tool."
288
+ })
289
+ }),
290
+ required: Object.freeze([
291
+ "holder",
292
+ "name",
293
+ "arguments"
294
+ ])
295
+ }),
296
+ annotations: Object.freeze({
297
+ pure: false,
298
+ untrusted: true
299
+ })
300
+ }),
301
+ tools: Object.freeze({
302
+ name: "tools",
303
+ description: "Lists the selected holder’s tools, including tools supplied by its page.",
304
+ parameters: Object.freeze({
305
+ type: "object",
306
+ properties: Object.freeze({ holder: Object.freeze({
307
+ type: "string",
308
+ description: "The holder id returned by acquire."
309
+ }) }),
310
+ required: Object.freeze(["holder"])
311
+ }),
312
+ annotations: Object.freeze({
313
+ pure: true,
314
+ untrusted: true
315
+ })
316
+ }),
317
+ destroy: Object.freeze({
318
+ name: "destroy",
319
+ description: "Ends the selected holder and destroys its browser. Calls still running on that holder report an unresolved outcome.",
320
+ parameters: Object.freeze({
321
+ type: "object",
322
+ properties: Object.freeze({ holder: Object.freeze({
323
+ type: "string",
324
+ description: "The holder id returned by acquire."
325
+ }) }),
326
+ required: Object.freeze(["holder"])
327
+ }),
328
+ annotations: Object.freeze({ pure: false })
329
+ })
330
+ });
239
331
  //#endregion
240
332
  //#region src/server/errors.ts
241
333
  /**
@@ -279,6 +371,25 @@ function isBrowserDestroyedError(value) {
279
371
  //#endregion
280
372
  //#region src/server/helpers.ts
281
373
  /**
374
+ * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
375
+ * @param value - The viewport environment variable, or undefined when unset
376
+ * @returns The viewport, or undefined when absent or malformed
377
+ * @example
378
+ * parseBrowserViewport('1280x720') // { width: 1280, height: 720 }
379
+ */
380
+ function parseBrowserViewport(value) {
381
+ if (value === void 0) return void 0;
382
+ const match = /^([0-9]+)x([0-9]+)$/u.exec(value);
383
+ if (match === null || match[0] !== value) return void 0;
384
+ const width = parseInteger(match[1]);
385
+ const height = parseInteger(match[2]);
386
+ if (width === void 0 || height === void 0 || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
387
+ return {
388
+ width,
389
+ height
390
+ };
391
+ }
392
+ /**
282
393
  * Probes whether a process has not been confirmed absent.
283
394
  * @param pid - Process identifier to probe with signal zero
284
395
  * @returns True if the host has not confirmed absence with ESRCH; false otherwise
@@ -944,7 +1055,7 @@ var FileBrowserWriter = class {
944
1055
  };
945
1056
  //#endregion
946
1057
  //#region package.json
947
- var version = "0.0.23";
1058
+ var version = "0.0.25";
948
1059
  //#endregion
949
1060
  //#region src/server/BrowserMCPServer.ts
950
1061
  /**
@@ -959,21 +1070,28 @@ var version = "0.0.23";
959
1070
  * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
960
1071
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
961
1072
  * removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
962
- * client is notified. Startup warms the pool and leases one validated browser. The legacy
963
- * handshake and every tool call await that setup; concurrent callers share a replacement lease.
1073
+ * client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
1074
+ * `BROWSER_SERVER_COPY`. Startup warms the pool and assigns a prepared context to the shared holder. The legacy
1075
+ * handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
1076
+ * and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
1077
+ * closes its context and releases its lease, and admission counts that holder until disposal settles.
964
1078
  * A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
965
1079
  * existing endpoint, opens one page in an isolated context, and constructs the page toolset with
966
1080
  * that context and the file journey and run stores under the root, on a manager the toolset owns.
967
1081
  * A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
968
1082
  * manager and returns that manager's value, or rejects with its failure message; a text value
969
1083
  * reaches the client as one text block rather than as its JSON text.
970
- * A disconnect or current-page crash retires its slot. A failed call receives a liveness ping;
1084
+ * A disconnect retires its browser and every hosted generation. A current-page crash retires only
1085
+ * that holder's context. A failed call receives a liveness ping;
971
1086
  * loss makes its outcome unresolved, while a known success remains successful. Pending losses
972
1087
  * prefix the next outcome on a successor, or a refusal, and calls are never repeated.
1088
+ * Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
1089
+ * its deadline and records loss for the next call.
973
1090
  *
974
1091
  * `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
975
1092
  * `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
976
- * slot through the pool — its toolset, which aborts the active replay, then its browser, then its
1093
+ * slot through the pool — each toolset, which aborts its active replay, then its context and downloads,
1094
+ * then its browser and its
977
1095
  * profile — and rechecks the profile folders this server made or kept.
978
1096
  *
979
1097
  * Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
@@ -993,6 +1111,7 @@ var BrowserMCPServer = class {
993
1111
  #root;
994
1112
  #headless;
995
1113
  #executable;
1114
+ #viewport;
996
1115
  #readonly;
997
1116
  #launcher;
998
1117
  #input;
@@ -1001,22 +1120,40 @@ var BrowserMCPServer = class {
1001
1120
  #abort = new AbortController();
1002
1121
  #signal;
1003
1122
  #vocabulary;
1004
- #added;
1005
- #removed;
1006
- #cleared;
1123
+ #mirrored;
1007
1124
  #pool;
1008
1125
  #log;
1009
1126
  #folders = /* @__PURE__ */ new Set();
1010
1127
  #faults = /* @__PURE__ */ new Set();
1011
1128
  #losses = /* @__PURE__ */ new WeakMap();
1129
+ #departures = /* @__PURE__ */ new WeakMap();
1130
+ #pings = /* @__PURE__ */ new WeakMap();
1012
1131
  #watches = /* @__PURE__ */ new Map();
1013
- #notice = /* @__PURE__ */ new Set();
1132
+ #observed = /* @__PURE__ */ new Map();
1133
+ #prepared = /* @__PURE__ */ new Map();
1134
+ #owned = /* @__PURE__ */ new Map();
1135
+ #building = /* @__PURE__ */ new Map();
1136
+ #cleaning = /* @__PURE__ */ new WeakMap();
1137
+ #endings = /* @__PURE__ */ new WeakMap();
1138
+ #retentions = /* @__PURE__ */ new Map();
1139
+ #shared = {
1140
+ id: "shared",
1141
+ purpose: "Shared browser",
1142
+ abort: this.#abort
1143
+ };
1144
+ #holders = /* @__PURE__ */ new Map([[this.#shared.id, this.#shared]]);
1145
+ #leases = /* @__PURE__ */ new Map();
1146
+ #grants = /* @__PURE__ */ new Map();
1147
+ #notices = /* @__PURE__ */ new Map();
1148
+ #disposals = /* @__PURE__ */ new Map();
1149
+ #drains = /* @__PURE__ */ new Map();
1150
+ #readers = /* @__PURE__ */ new Map();
1151
+ #writers = /* @__PURE__ */ new Set();
1152
+ #admission;
1014
1153
  #reference;
1015
1154
  #references = 0;
1016
1155
  #failure;
1017
1156
  #stranded;
1018
- #lease;
1019
- #granting;
1020
1157
  #starting;
1021
1158
  #sweeping;
1022
1159
  #closing;
@@ -1027,30 +1164,51 @@ var BrowserMCPServer = class {
1027
1164
  */
1028
1165
  constructor(options) {
1029
1166
  const size = options?.pool?.size ?? 1;
1167
+ const contexts = options?.pool?.contexts ?? 2;
1030
1168
  if (!isInteger(size) || size < 1 || size > 3) throw new BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
1169
+ if (!isInteger(contexts) || contexts < 1 || contexts > 4) throw new BrowserError(`pool.contexts must be an integer from 1 through 4`, BROWSER_SERVER_OPTIONS);
1170
+ this.#admission = size * contexts;
1031
1171
  this.#log = options?.log ?? process.stderr;
1032
1172
  this.#reference = this.#issue.bind(this);
1033
1173
  this.#pool = createPool({
1034
1174
  create: this.#warm.bind(this),
1035
- destroy: (slot) => this.#destroySlot(slot.profile, slot.browser, slot.toolset, this.#losses.get(slot)?.cause),
1175
+ destroy: this.#destroyRecord.bind(this),
1036
1176
  validate: this.#validate.bind(this),
1037
1177
  watch: this.#watch.bind(this),
1038
1178
  error: this.#fault.bind(this),
1039
1179
  min: size,
1180
+ capacity: contexts,
1040
1181
  restarts: 1
1041
1182
  });
1042
1183
  this.#root = resolve(options?.root ?? "tmp/browsers");
1043
1184
  this.#headless = options?.headless ?? true;
1044
1185
  this.#executable = options?.executable;
1186
+ this.#viewport = options?.viewport;
1045
1187
  this.#readonly = options?.readonly ?? false;
1046
1188
  this.#launcher = options?.launch ?? createBrowser;
1047
1189
  this.#input = options?.stdio?.input ?? process.stdin;
1048
1190
  this.#tools = createToolManager();
1049
1191
  const names = [...BROWSER_TOOL_NAMES, ...BROWSER_JOURNEY_TOOL_NAMES];
1050
- this.#vocabulary = new Set(names);
1192
+ this.#vocabulary = /* @__PURE__ */ new Set([...names, ...Object.keys(BROWSER_SERVER_COPY)]);
1051
1193
  for (const name of names) this.#tools.add(createTool({
1052
1194
  ...BROWSER_TOOL_COPY[name],
1053
- execute: this.#forward.bind(this, name)
1195
+ execute: this.#forward.bind(this, this.#shared, name)
1196
+ }));
1197
+ this.#tools.add(createTool({
1198
+ ...BROWSER_SERVER_COPY.acquire,
1199
+ execute: this.#acquire.bind(this)
1200
+ }));
1201
+ this.#tools.add(createTool({
1202
+ ...BROWSER_SERVER_COPY.execute,
1203
+ execute: this.#dispatch.bind(this)
1204
+ }));
1205
+ this.#tools.add(createTool({
1206
+ ...BROWSER_SERVER_COPY.tools,
1207
+ execute: this.#catalog.bind(this)
1208
+ }));
1209
+ this.#tools.add(createTool({
1210
+ ...BROWSER_SERVER_COPY.destroy,
1211
+ execute: this.#dispose.bind(this)
1054
1212
  }));
1055
1213
  const server = createMCPServer({
1056
1214
  identity: {
@@ -1066,9 +1224,6 @@ var BrowserMCPServer = class {
1066
1224
  ...options?.stdio?.output === void 0 ? {} : { output: options.stdio.output }
1067
1225
  });
1068
1226
  this.#signal = this.#end.bind(this);
1069
- this.#added = this.#mirror.bind(this);
1070
- this.#removed = this.#withdraw.bind(this);
1071
- this.#cleared = this.#clear.bind(this);
1072
1227
  }
1073
1228
  async start() {
1074
1229
  if (this.#closing !== void 0) throw this.#ended();
@@ -1089,7 +1244,7 @@ var BrowserMCPServer = class {
1089
1244
  if (this.#closing !== void 0) return;
1090
1245
  this.#sweeping = this.#sweep();
1091
1246
  const warming = this.#pool.start().then(void 0, (error) => error);
1092
- await this.#grant(this.#abort.signal);
1247
+ await this.#grant(this.#shared, this.#abort.signal);
1093
1248
  if (this.#closing !== void 0) return;
1094
1249
  warming.then(this.#exhaust.bind(this));
1095
1250
  } catch (error) {
@@ -1105,13 +1260,18 @@ var BrowserMCPServer = class {
1105
1260
  this.#transport.stop();
1106
1261
  for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
1107
1262
  this.#unmirror();
1108
- this.#lease = void 0;
1263
+ for (const generation of this.#observed.keys()) this.#untrack(generation);
1264
+ this.#leases.clear();
1265
+ for (const holder of this.#holders.values()) holder.abort.abort();
1109
1266
  this.#abort.abort();
1110
1267
  const barrier = this.#pool.destroy().catch((error) => {
1111
1268
  for (const failure of isPoolError(error) ? error.context?.failures ?? [error] : [error]) this.#faults.add(failure);
1112
1269
  });
1113
1270
  await this.#starting?.catch(() => void 0);
1114
1271
  await barrier;
1272
+ await Promise.allSettled(this.#grants.values());
1273
+ await Promise.allSettled(this.#disposals.values());
1274
+ this.#holders.clear();
1115
1275
  await this.#sweeping;
1116
1276
  if (this.#stranded !== void 0) this.#faults.add(this.#stranded);
1117
1277
  for (const folder of this.#folders) {
@@ -1156,22 +1316,143 @@ var BrowserMCPServer = class {
1156
1316
  process.exitCode = 1;
1157
1317
  this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
1158
1318
  }
1159
- async #forward(name, args, context) {
1160
- let slot;
1319
+ async #acquire(args, context) {
1320
+ context.signal.throwIfAborted();
1321
+ if (this.#closing !== void 0) throw this.#ended();
1322
+ const purpose = args["purpose"];
1323
+ if (!isString(purpose) || purpose.trim().length === 0) throw new BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1324
+ if (this.#holders.size >= this.#admission) throw new BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`, BROWSER_SERVER_BUSY);
1325
+ const holder = {
1326
+ id: randomUUID(),
1327
+ purpose,
1328
+ abort: new AbortController()
1329
+ };
1330
+ this.#holders.set(holder.id, holder);
1331
+ const listener = addAbortListener(context.signal, () => holder.abort.abort(context.signal.reason));
1332
+ try {
1333
+ const lease = await this.#serve(holder, context.signal);
1334
+ context.signal.throwIfAborted();
1335
+ if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
1336
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1337
+ const catalog = {
1338
+ holder: holder.id,
1339
+ tools: lease.generation.toolset.tools.definitions()
1340
+ };
1341
+ return JSON.stringify(catalog);
1342
+ } catch (error) {
1343
+ await this.#retire(holder).catch(this.#fault.bind(this));
1344
+ throw error;
1345
+ } finally {
1346
+ listener[Symbol.dispose]();
1347
+ }
1348
+ }
1349
+ #holder(value) {
1350
+ const holder = isString(value) ? this.#holders.get(value) : void 0;
1351
+ if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder`, BROWSER_SERVER_HOLDER);
1352
+ return holder;
1353
+ }
1354
+ async #dispatch(args, context) {
1355
+ const holder = this.#holder(args["holder"]);
1356
+ const name = args["name"];
1357
+ const parameters = args["arguments"];
1358
+ if (!isString(name) || !isRecord(parameters)) throw new BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
1359
+ return this.#forward(holder, name, parameters, context);
1360
+ }
1361
+ async #catalog(args, context) {
1362
+ const holder = this.#holder(args["holder"]);
1363
+ const signal = AbortSignal.any([context.signal, holder.abort.signal]);
1364
+ try {
1365
+ const lease = await this.#serve(holder, signal);
1366
+ signal.throwIfAborted();
1367
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1368
+ const catalog = {
1369
+ holder: holder.id,
1370
+ tools: lease.generation.toolset.tools.definitions()
1371
+ };
1372
+ return this.#annotate(holder, JSON.stringify(catalog));
1373
+ } catch (error) {
1374
+ context.signal.throwIfAborted();
1375
+ if (holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1376
+ if (isBrowserError(error)) throw new BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1377
+ throw error;
1378
+ }
1379
+ }
1380
+ async #dispose(args) {
1381
+ const id = args["holder"];
1382
+ await ((isString(id) ? this.#disposals.get(id) : void 0) ?? this.#retire(this.#holder(id)));
1383
+ return "Holder destroyed.";
1384
+ }
1385
+ #retire(holder) {
1386
+ const pending = this.#disposals.get(holder.id);
1387
+ if (pending !== void 0) return pending;
1388
+ holder.abort.abort();
1389
+ const lease = this.#leases.get(holder);
1390
+ this.#leases.delete(holder);
1391
+ if (lease !== void 0) this.#untrack(lease.generation);
1392
+ const disposal = Promise.resolve().then(async () => {
1393
+ try {
1394
+ await this.#grants.get(holder)?.catch(() => void 0);
1395
+ if (lease !== void 0) await this.#release(holder, lease);
1396
+ await Promise.all(this.#drains.get(holder) ?? []);
1397
+ await Promise.all([...this.#notices.get(holder) ?? []].filter((lost) => this.#losses.has(lost.token.value)).map((lost) => this.#endings.get(lost.token.value)?.promise));
1398
+ } catch (error) {
1399
+ this.#faults.add(error);
1400
+ } finally {
1401
+ if (this.#holders.get(holder.id) === holder && !this.#retentions.has(holder)) this.#holders.delete(holder.id);
1402
+ this.#notices.delete(holder);
1403
+ this.#drains.delete(holder);
1404
+ }
1405
+ });
1406
+ this.#disposals.set(holder.id, disposal);
1407
+ return disposal;
1408
+ }
1409
+ async #forward(holder, name, args, context) {
1410
+ context.signal.throwIfAborted();
1411
+ const journey = args["journey"];
1412
+ if (name !== "replay" && name !== "forget" || !isString(journey)) return this.#perform(holder, name, args, context);
1413
+ if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new BrowserError(`BROWSER_JOURNEY_LOCKED: Journey ${journey} is locked; call ${name} again.`, "BROWSER_JOURNEY_LOCKED", { name: journey });
1414
+ const reader = Symbol(journey);
1415
+ if (name === "forget") this.#writers.add(journey);
1416
+ else {
1417
+ const readers = this.#readers.get(journey) ?? /* @__PURE__ */ new Set();
1418
+ readers.add(reader);
1419
+ this.#readers.set(journey, readers);
1420
+ }
1421
+ try {
1422
+ return await this.#perform(holder, name, args, context);
1423
+ } finally {
1424
+ if (name === "forget") this.#writers.delete(journey);
1425
+ else {
1426
+ const readers = this.#readers.get(journey);
1427
+ readers?.delete(reader);
1428
+ if (readers?.size === 0) this.#readers.delete(journey);
1429
+ }
1430
+ }
1431
+ }
1432
+ async #perform(holder, name, args, context) {
1433
+ const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
1434
+ let lease;
1161
1435
  try {
1162
- slot = await this.#serve(context.signal);
1436
+ lease = await this.#serve(holder, signal);
1163
1437
  } catch (error) {
1164
1438
  context.signal.throwIfAborted();
1165
- if (isBrowserError(error)) throw new BrowserError(this.#annotate(`${error.code}: ${error.message}`), error.code);
1439
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1440
+ if (isBrowserError(error)) throw new BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1166
1441
  throw error;
1167
1442
  }
1168
1443
  let result;
1169
1444
  try {
1170
- result = await slot.toolset.tools.execute({
1445
+ const execution = lease.generation.toolset.tools.execute({
1171
1446
  id: name,
1172
1447
  name,
1173
1448
  arguments: args
1174
- }, context);
1449
+ }, {
1450
+ ...context,
1451
+ signal
1452
+ });
1453
+ const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
1454
+ if (outcome === void 0) throw this.#ended();
1455
+ result = outcome;
1175
1456
  } catch (error) {
1176
1457
  result = {
1177
1458
  id: name,
@@ -1181,28 +1462,27 @@ var BrowserMCPServer = class {
1181
1462
  };
1182
1463
  }
1183
1464
  context.signal.throwIfAborted();
1465
+ if (holder !== this.#shared && holder.abort.signal.aborted) throw new BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
1184
1466
  if (!result.success) {
1185
- if (this.#lease?.value === slot && !context.signal.aborted) try {
1186
- await slot.browser.ping({ signal: AbortSignal.any([context.signal, this.#abort.signal]) });
1187
- } catch (error) {
1188
- if (!context.signal.aborted && !this.#abort.signal.aborted) this.#lose(slot, error);
1189
- }
1467
+ if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1468
+ await this.#race(this.#ping(lease.token.value), context.signal);
1469
+ } catch {}
1190
1470
  context.signal.throwIfAborted();
1191
- if (this.#lease?.value !== slot) {
1192
- const loss = this.#losses.get(slot);
1193
- throw new BrowserError(this.#annotate(describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
1471
+ if (this.#leases.get(holder) !== lease) {
1472
+ const loss = this.#departures.get(lease);
1473
+ throw new BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease), BROWSER_SERVER_UNRESOLVED);
1194
1474
  }
1195
- throw new BrowserError(this.#annotate(result.error, slot));
1475
+ throw new BrowserError(this.#annotate(holder, result.error, lease));
1196
1476
  }
1197
- return typeof result.value === "string" ? this.#annotate(result.value, slot) : result.value;
1477
+ return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1198
1478
  }
1199
- #annotate(value, slot) {
1479
+ #annotate(holder, value, lease) {
1200
1480
  const notes = [];
1201
- for (const lost of this.#notice) {
1202
- if (lost === slot) continue;
1203
- const loss = this.#losses.get(lost);
1481
+ for (const lost of this.#notices.get(holder) ?? []) {
1482
+ if (lost === lease) continue;
1483
+ const loss = this.#departures.get(lost);
1204
1484
  notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
1205
- this.#notice.delete(lost);
1485
+ this.#notices.get(holder)?.delete(lost);
1206
1486
  }
1207
1487
  return [...notes, value].join("\n");
1208
1488
  }
@@ -1226,52 +1506,95 @@ var BrowserMCPServer = class {
1226
1506
  listener[Symbol.dispose]();
1227
1507
  }
1228
1508
  }
1229
- async #grant(signal) {
1230
- if (this.#lease !== void 0) return this.#lease;
1231
- this.#granting ??= this.#pool.acquire(this.#abort.signal).then(this.#hold.bind(this), this.#refuse.bind(this));
1232
- const token = await this.#race(this.#granting, signal);
1233
- if (token === void 0) throw this.#ended();
1234
- return token;
1509
+ async #grant(holder, signal) {
1510
+ const held = this.#leases.get(holder);
1511
+ if (held !== void 0) return held;
1512
+ let granting = this.#grants.get(holder);
1513
+ if (granting === void 0) {
1514
+ granting = Promise.resolve().then(async () => {
1515
+ await Promise.allSettled(this.#drains.get(holder) ?? []);
1516
+ if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
1517
+ const token = await this.#pool.acquire(holder.abort.signal);
1518
+ return this.#hold(holder, token);
1519
+ });
1520
+ this.#grants.set(holder, granting);
1521
+ granting.finally(() => {
1522
+ if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1523
+ }).catch(() => void 0);
1524
+ }
1525
+ const lease = await this.#race(granting, signal);
1526
+ if (lease === void 0) throw this.#ended();
1527
+ return lease;
1528
+ }
1529
+ async #hold(holder, token) {
1530
+ const slot = token.value;
1531
+ if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
1532
+ token.release();
1533
+ throw this.#ended();
1534
+ }
1535
+ if (!this.#prepared.has(slot)) await this.#ping(slot);
1536
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) {
1537
+ token.release();
1538
+ throw this.#ended();
1539
+ }
1540
+ if (this.#losses.has(slot)) {
1541
+ token.release();
1542
+ throw this.#unavailable(this.#losses.get(slot)?.cause);
1543
+ }
1544
+ let generation = this.#prepared.get(slot);
1545
+ this.#prepared.delete(slot);
1546
+ try {
1547
+ generation ??= await this.#build(slot, token);
1548
+ } catch (error) {
1549
+ if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
1550
+ else token.release();
1551
+ throw this.#unavailable(error);
1552
+ }
1553
+ const lease = {
1554
+ token,
1555
+ generation
1556
+ };
1557
+ if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted || this.#losses.has(slot)) {
1558
+ await this.#release(holder, lease);
1559
+ throw this.#ended();
1560
+ }
1561
+ this.#leases.set(holder, lease);
1562
+ this.#observe(slot, generation);
1563
+ if (holder === this.#shared) {
1564
+ const tools = generation.toolset.tools;
1565
+ const mirrored = {
1566
+ lease,
1567
+ added: this.#mirror.bind(this, lease),
1568
+ removed: this.#withdraw.bind(this, lease),
1569
+ cleared: this.#clear.bind(this, lease)
1570
+ };
1571
+ this.#mirrored = mirrored;
1572
+ for (const tool of tools.tools()) this.#mirror(lease, tool);
1573
+ tools.emitter.on("add", mirrored.added);
1574
+ tools.emitter.on("remove", mirrored.removed);
1575
+ tools.emitter.on("clear", mirrored.cleared);
1576
+ }
1577
+ return lease;
1235
1578
  }
1236
- #hold(token) {
1237
- this.#granting = void 0;
1238
- if (this.#closing !== void 0) throw this.#ended();
1239
- if (this.#losses.has(token.value)) {
1240
- token.destroy().catch(() => void 0);
1241
- throw this.#unavailable(this.#losses.get(token.value)?.cause);
1242
- }
1243
- this.#lease = token;
1244
- const tools = token.value.toolset.tools;
1245
- for (const tool of tools.tools()) this.#mirror(tool);
1246
- tools.emitter.on("add", this.#added);
1247
- tools.emitter.on("remove", this.#removed);
1248
- tools.emitter.on("clear", this.#cleared);
1249
- return token;
1250
- }
1251
- #refuse(error) {
1252
- this.#granting = void 0;
1253
- throw error;
1254
- }
1255
- async #serve(signal) {
1256
- if (this.#closing !== void 0) throw this.#ended();
1579
+ async #serve(holder, signal) {
1580
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1257
1581
  await this.#race(this.#starting, signal);
1258
- if (this.#closing !== void 0) throw this.#ended();
1259
- const held = this.#lease;
1582
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1583
+ const held = this.#leases.get(holder);
1260
1584
  if (held !== void 0) try {
1261
- await held.value.browser.ping({ signal: AbortSignal.any([signal, this.#abort.signal]) });
1262
- if (this.#lease === held) return held.value;
1263
- } catch (error) {
1585
+ await this.#race(this.#ping(held.token.value), signal);
1586
+ if (this.#leases.get(holder) === held) return held;
1587
+ } catch {
1264
1588
  signal.throwIfAborted();
1265
- if (this.#abort.signal.aborted) throw this.#ended();
1266
- this.#lose(held.value, error);
1589
+ if (holder.abort.signal.aborted) throw this.#ended();
1267
1590
  }
1268
1591
  try {
1269
- const token = await this.#grant(signal);
1270
- if (this.#lease !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1271
- return token.value;
1592
+ const lease = await this.#grant(holder, signal);
1593
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1594
+ return lease;
1272
1595
  } catch (error) {
1273
1596
  signal.throwIfAborted();
1274
- if (this.#abort.signal.aborted || isPoolError(error) && error.code === "destroyed") throw this.#ended();
1597
+ if (holder.abort.signal.aborted || isPoolError(error) && error.code === "destroyed") throw this.#ended();
1275
1598
  if (isPoolError(error) && (error.code === "create" || error.code === "cleanup")) {
1276
1599
  this.#exhaust(error);
1277
1600
  throw this.#unavailable(error.cause ?? this.#failure);
@@ -1279,6 +1602,15 @@ var BrowserMCPServer = class {
1279
1602
  throw error;
1280
1603
  }
1281
1604
  }
1605
+ #ping(slot) {
1606
+ const pending = this.#pings.get(slot);
1607
+ if (pending !== void 0) return pending;
1608
+ const ping = slot.browser.ping({ signal: this.#abort.signal }).catch((error) => {
1609
+ if (!this.#abort.signal.aborted) this.#lose(slot, error);
1610
+ }).finally(() => this.#pings.delete(slot));
1611
+ this.#pings.set(slot, ping);
1612
+ return ping;
1613
+ }
1282
1614
  async #validate(slot) {
1283
1615
  try {
1284
1616
  await slot.browser.ping({ signal: this.#abort.signal });
@@ -1290,71 +1622,211 @@ var BrowserMCPServer = class {
1290
1622
  }
1291
1623
  #lose(slot, cause) {
1292
1624
  if (this.#closing !== void 0 || this.#losses.has(slot)) return;
1293
- this.#losses.set(slot, {
1625
+ this.#losses.set(slot, { cause });
1626
+ this.#failure = cause;
1627
+ for (const [holder, lease] of this.#leases) {
1628
+ if (lease.token.value !== slot) continue;
1629
+ this.#detach(holder, lease, cause);
1630
+ }
1631
+ const watch = this.#watches.get(slot);
1632
+ this.#unwatch(slot);
1633
+ watch?.resolve(cause);
1634
+ }
1635
+ #detach(holder, lease, cause) {
1636
+ if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
1637
+ this.#departures.set(lease, {
1294
1638
  cause,
1295
- url: slot.toolset.view.url
1639
+ url: lease.generation.toolset.view.url
1296
1640
  });
1297
- this.#failure = cause;
1298
- if (this.#lease?.value !== slot) return;
1299
- const token = this.#lease;
1300
- this.#unmirror();
1301
- this.#lease = void 0;
1302
- this.#notice.add(slot);
1303
- token.destroy().catch(() => void 0);
1641
+ if (holder === this.#shared) this.#unmirror();
1642
+ this.#leases.delete(holder);
1643
+ this.#untrack(lease.generation);
1644
+ const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
1645
+ notices.add(lease);
1646
+ this.#notices.set(holder, notices);
1304
1647
  }
1305
1648
  #watch(slot, signal) {
1306
1649
  const loss = Promise.withResolvers();
1307
1650
  const disconnect = this.#disconnect.bind(this, slot);
1308
- const page = this.#track.bind(this, slot);
1309
1651
  const subscription = addAbortListener(signal, this.#unwatch.bind(this, slot));
1310
1652
  this.#watches.set(slot, {
1311
1653
  resolve: loss.resolve,
1312
1654
  disconnect,
1313
- page,
1314
- subscription,
1315
- crashes: /* @__PURE__ */ new Map()
1655
+ subscription
1316
1656
  });
1317
1657
  slot.browser.emitter.on("disconnect", disconnect);
1318
- slot.context.emitter.on("page", page);
1319
- for (const current of slot.context.pages()) this.#track(slot, current);
1320
1658
  if (signal.aborted) this.#unwatch(slot);
1321
1659
  else if (slot.browser.status !== "connected") this.#disconnect(slot);
1322
1660
  return loss.promise;
1323
1661
  }
1324
- #track(slot, page) {
1325
- const watch = this.#watches.get(slot);
1662
+ #observe(slot, generation) {
1663
+ if (this.#observed.has(generation)) return;
1664
+ const page = this.#track.bind(this, slot, generation);
1665
+ this.#observed.set(generation, {
1666
+ page,
1667
+ crashes: /* @__PURE__ */ new Map()
1668
+ });
1669
+ generation.context.emitter.on("page", page);
1670
+ for (const current of generation.context.pages()) this.#track(slot, generation, current);
1671
+ }
1672
+ #track(slot, generation, page) {
1673
+ const watch = this.#observed.get(generation);
1326
1674
  if (watch === void 0 || watch.crashes.has(page)) return;
1327
- const crash = this.#createCrash(slot, page);
1675
+ const crash = this.#crash.bind(this, slot, generation, page);
1328
1676
  page.emitter.on("crash", crash);
1329
- this.#watches.set(slot, {
1677
+ this.#observed.set(generation, {
1330
1678
  ...watch,
1331
1679
  crashes: new Map([...watch.crashes, [page, crash]])
1332
1680
  });
1333
1681
  }
1334
- #createCrash(slot, page) {
1335
- return () => {
1336
- if (page === slot.toolset.view) this.#report(slot, /* @__PURE__ */ new Error("The current page renderer crashed"));
1337
- };
1682
+ #crash(slot, generation, page) {
1683
+ if (this.#prepared.get(slot) === generation) {
1684
+ this.#prepared.delete(slot);
1685
+ this.#untrack(generation);
1686
+ this.#clean(slot, generation).catch(this.#fault.bind(this));
1687
+ return;
1688
+ }
1689
+ const held = [...this.#leases].find(([, lease]) => lease.generation === generation);
1690
+ if (held === void 0 || page !== generation.toolset.view) return;
1691
+ const [holder, lease] = held;
1692
+ this.#detach(holder, lease, /* @__PURE__ */ new Error("The current page renderer crashed"));
1693
+ const drain = this.#release(holder, lease);
1694
+ const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
1695
+ drains.add(drain);
1696
+ this.#drains.set(holder, drains);
1697
+ drain.finally(() => drains.delete(drain)).catch(this.#fault.bind(this));
1698
+ }
1699
+ #untrack(generation) {
1700
+ const watch = this.#observed.get(generation);
1701
+ if (watch === void 0) return;
1702
+ this.#observed.delete(generation);
1703
+ generation.context.emitter.off("page", watch.page);
1704
+ for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
1338
1705
  }
1339
1706
  #disconnect(slot) {
1340
- this.#report(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1341
- }
1342
- #report(slot, cause) {
1343
- const watch = this.#watches.get(slot);
1344
- if (watch === void 0) return;
1345
- this.#unwatch(slot);
1346
- this.#lose(slot, cause);
1347
- watch.resolve(cause);
1707
+ this.#lose(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1348
1708
  }
1349
1709
  #unwatch(slot) {
1350
1710
  const watch = this.#watches.get(slot);
1351
1711
  if (watch === void 0) return;
1352
1712
  this.#watches.delete(slot);
1353
1713
  slot.browser.emitter.off("disconnect", watch.disconnect);
1354
- slot.context.emitter.off("page", watch.page);
1355
- for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
1356
1714
  watch.subscription[Symbol.dispose]();
1357
1715
  }
1716
+ async #release(holder, lease) {
1717
+ const slot = lease.token.value;
1718
+ const removed = await this.#clean(slot, lease.generation);
1719
+ const disposal = lease.generation.context.disposal;
1720
+ if (!removed) this.#retentions.set(holder, /* @__PURE__ */ new Error("The context downloads directory remains owned"));
1721
+ if (disposal?.confirmed !== true && !this.#losses.has(slot) && this.#closing === void 0) {
1722
+ if (![...this.#leases.values()].some((held) => held.token.value === slot && held !== lease) && (this.#building.get(slot)?.size ?? 0) === 0) lease.token.release();
1723
+ this.#lose(slot, disposal?.error ?? /* @__PURE__ */ new Error("The context disposal is unconfirmed"));
1724
+ await this.#endings.get(slot)?.promise;
1725
+ } else if (removed) lease.token.release();
1726
+ }
1727
+ #clean(slot, generation) {
1728
+ let cleaning = this.#cleaning.get(generation);
1729
+ if (cleaning !== void 0) return cleaning;
1730
+ cleaning = Promise.resolve().then(async () => {
1731
+ await generation.toolset.destroy().catch(this.#fault.bind(this));
1732
+ await generation.context.close().catch((error) => {
1733
+ if (!this.#losses.has(slot) && slot.browser.status === "connected") this.#fault(error);
1734
+ });
1735
+ try {
1736
+ await rm(generation.directory, {
1737
+ recursive: true,
1738
+ force: true
1739
+ });
1740
+ this.#owned.get(slot)?.delete(generation);
1741
+ return true;
1742
+ } catch (error) {
1743
+ this.#fault(error);
1744
+ return false;
1745
+ }
1746
+ });
1747
+ this.#cleaning.set(generation, cleaning);
1748
+ return cleaning;
1749
+ }
1750
+ #build(slot, token) {
1751
+ const construction = this.#construct(slot, token);
1752
+ const building = this.#building.get(slot) ?? /* @__PURE__ */ new Set();
1753
+ building.add(construction);
1754
+ this.#building.set(slot, building);
1755
+ construction.finally(() => building.delete(construction)).catch(() => void 0);
1756
+ return construction;
1757
+ }
1758
+ async #construct(slot, token) {
1759
+ const directory = join(slot.profile, "contexts", randomUUID(), "downloads");
1760
+ let context;
1761
+ let toolset;
1762
+ try {
1763
+ await mkdir(directory, { recursive: true });
1764
+ let page;
1765
+ try {
1766
+ context = await slot.browser.isolate({
1767
+ reference: this.#reference,
1768
+ downloads: { path: directory },
1769
+ ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1770
+ });
1771
+ page = await context.create();
1772
+ } catch (error) {
1773
+ if (token !== void 0) {
1774
+ if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
1775
+ this.#lose(slot, error);
1776
+ }
1777
+ throw error;
1778
+ }
1779
+ toolset = createBrowserToolset(page, {
1780
+ context,
1781
+ journeys: {
1782
+ store: createFileBrowserJourneyStore({ root: this.#root }),
1783
+ runs: createFileBrowserRunStore({ root: this.#root }),
1784
+ readonly: this.#readonly
1785
+ }
1786
+ });
1787
+ await toolset.start();
1788
+ const generation = {
1789
+ context,
1790
+ toolset,
1791
+ directory
1792
+ };
1793
+ const owned = this.#owned.get(slot) ?? /* @__PURE__ */ new Set();
1794
+ owned.add(generation);
1795
+ this.#owned.set(slot, owned);
1796
+ return generation;
1797
+ } catch (error) {
1798
+ await toolset?.destroy().catch(this.#fault.bind(this));
1799
+ await context?.close().catch((failure) => {
1800
+ if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !isCDPConnectionError(failure)) this.#fault(failure);
1801
+ });
1802
+ await rm(directory, {
1803
+ recursive: true,
1804
+ force: true
1805
+ }).catch(this.#fault.bind(this));
1806
+ throw error;
1807
+ }
1808
+ }
1809
+ async #destroyRecord(slot) {
1810
+ if (isCDPTimeoutError(this.#losses.get(slot)?.cause) && slot.browser.pid !== void 0) try {
1811
+ process.kill(slot.browser.pid, "SIGKILL");
1812
+ } catch (error) {
1813
+ if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
1814
+ }
1815
+ try {
1816
+ await Promise.allSettled(this.#building.get(slot) ?? []);
1817
+ const prepared = this.#prepared.get(slot);
1818
+ if (prepared !== void 0) this.#untrack(prepared);
1819
+ this.#prepared.delete(slot);
1820
+ await Promise.all([...this.#owned.get(slot) ?? []].map((generation) => this.#clean(slot, generation)));
1821
+ await this.#destroySlot(slot.profile, slot.browser);
1822
+ this.#owned.delete(slot);
1823
+ this.#building.delete(slot);
1824
+ this.#endings.get(slot)?.resolve();
1825
+ } catch (error) {
1826
+ this.#endings.get(slot)?.reject(error);
1827
+ throw error;
1828
+ }
1829
+ }
1358
1830
  #fault(error) {
1359
1831
  this.#faults.add(error);
1360
1832
  }
@@ -1363,7 +1835,7 @@ var BrowserMCPServer = class {
1363
1835
  if (this.#stranded !== void 0) throw new BrowserError(isError(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, isBrowserError(this.#stranded) ? this.#stranded.context : void 0);
1364
1836
  const profile = join(this.#root, ".profiles", formatBrowserLockEntry(process.pid, randomUUID()));
1365
1837
  let browser;
1366
- let toolset;
1838
+ let slot;
1367
1839
  try {
1368
1840
  await mkdir(profile);
1369
1841
  this.#folders.add(profile);
@@ -1373,6 +1845,7 @@ var BrowserMCPServer = class {
1373
1845
  cdp: { discover: false },
1374
1846
  signal: this.#abort.signal,
1375
1847
  ...this.#executable === void 0 ? {} : { executable: this.#executable },
1848
+ ...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
1376
1849
  ...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
1377
1850
  });
1378
1851
  await browser.connect();
@@ -1384,38 +1857,26 @@ var BrowserMCPServer = class {
1384
1857
  }));
1385
1858
  await rename(`${record}.tmp`, record);
1386
1859
  }
1387
- const context = await browser.isolate({ reference: this.#reference });
1388
- const page = await context.create();
1389
- toolset = createBrowserToolset(page, {
1390
- context,
1391
- journeys: {
1392
- store: createFileBrowserJourneyStore({ root: this.#root }),
1393
- runs: createFileBrowserRunStore({ root: this.#root }),
1394
- readonly: this.#readonly
1395
- }
1396
- });
1397
- await toolset.start();
1398
- return {
1860
+ slot = {
1399
1861
  browser,
1400
- profile,
1401
- context,
1402
- toolset
1862
+ profile
1403
1863
  };
1864
+ const ending = Promise.withResolvers();
1865
+ ending.promise.catch(() => void 0);
1866
+ this.#endings.set(slot, ending);
1867
+ const generation = await this.#build(slot);
1868
+ this.#prepared.set(slot, generation);
1869
+ this.#observe(slot, generation);
1870
+ return slot;
1404
1871
  } catch (error) {
1405
- if (this.#folders.has(profile)) await this.#destroySlot(profile, browser, toolset).catch((failure) => {
1872
+ if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
1406
1873
  this.#stranded = failure;
1407
1874
  });
1408
1875
  if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
1409
1876
  throw error;
1410
1877
  }
1411
1878
  }
1412
- async #destroySlot(profile, browser, toolset, cause) {
1413
- if (isCDPTimeoutError(cause) && browser?.pid !== void 0) try {
1414
- process.kill(browser.pid, "SIGKILL");
1415
- } catch (error) {
1416
- if (!isError(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
1417
- }
1418
- await toolset?.destroy().catch((error) => this.#faults.add(error));
1879
+ async #destroySlot(profile, browser) {
1419
1880
  try {
1420
1881
  await browser?.destroy();
1421
1882
  } catch (error) {
@@ -1491,29 +1952,31 @@ var BrowserMCPServer = class {
1491
1952
  #issue() {
1492
1953
  return `e${++this.#references}`;
1493
1954
  }
1494
- #mirror(tool) {
1495
- if (this.#vocabulary.has(tool.name)) return;
1955
+ #mirror(lease, tool) {
1956
+ if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
1496
1957
  this.#tools.add(createTool({
1497
1958
  ...toolToDefinition(tool),
1498
- execute: this.#forward.bind(this, tool.name)
1959
+ execute: this.#forward.bind(this, this.#shared, tool.name)
1499
1960
  }));
1500
1961
  }
1501
- #withdraw(tool) {
1502
- if (this.#vocabulary.has(tool.name)) return;
1503
- const current = this.#lease?.value.toolset.tools.tool(tool.name);
1962
+ #withdraw(lease, tool) {
1963
+ if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
1964
+ const current = lease.generation.toolset.tools.tool(tool.name);
1504
1965
  if (current === void 0) this.#tools.remove(tool.name);
1505
- else this.#mirror(current);
1966
+ else this.#mirror(lease, current);
1506
1967
  }
1507
- #clear(tools) {
1508
- for (const tool of tools) this.#withdraw(tool);
1968
+ #clear(lease, tools) {
1969
+ for (const tool of tools) this.#withdraw(lease, tool);
1509
1970
  }
1510
1971
  #unmirror() {
1511
- const mirrored = this.#lease?.value.toolset.tools;
1972
+ const mirrored = this.#mirrored;
1512
1973
  if (mirrored === void 0) return;
1513
- for (const tool of mirrored.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1514
- mirrored.emitter.off("add", this.#added);
1515
- mirrored.emitter.off("remove", this.#removed);
1516
- mirrored.emitter.off("clear", this.#cleared);
1974
+ this.#mirrored = void 0;
1975
+ const tools = mirrored.lease.generation.toolset.tools;
1976
+ for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1977
+ tools.emitter.off("add", mirrored.added);
1978
+ tools.emitter.off("remove", mirrored.removed);
1979
+ tools.emitter.off("clear", mirrored.cleared);
1517
1980
  }
1518
1981
  #ended() {
1519
1982
  return new BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
@@ -1940,30 +2403,62 @@ var FileBrowserJourneyStore = class {
1940
2403
  /**
1941
2404
  * Persists runs and captures only in directories allocated by this instance.
1942
2405
  * @remarks Requires an existing root. Reopened stores can read and delete saved runs.
2406
+ * Allocations of one journey share a process-local queue across instances; the filesystem lock
2407
+ * still refuses conflicting mutations and allocations from another process.
1943
2408
  * @example
1944
2409
  * const store = new FileBrowserRunStore({ root: directory })
1945
2410
  * const slot = await store.open('check-ready')
1946
2411
  */
1947
- var FileBrowserRunStore = class {
2412
+ var FileBrowserRunStore = class FileBrowserRunStore {
2413
+ static #allocations = /* @__PURE__ */ new Map();
1948
2414
  #files;
1949
2415
  #slots = /* @__PURE__ */ new WeakMap();
1950
2416
  #directories = /* @__PURE__ */ new Set();
1951
2417
  constructor(options) {
1952
2418
  this.#files = new FileBrowserStore(options);
1953
2419
  }
2420
+ async snapshot(bytes, options) {
2421
+ options?.signal?.throwIfAborted();
2422
+ const owned = new Uint8Array(bytes);
2423
+ const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
2424
+ const path = this.#files.resolvePath(directory.path, "view.png");
2425
+ try {
2426
+ await this.#files.check(path, options);
2427
+ options?.signal?.throwIfAborted();
2428
+ await writeFile(path, owned, {
2429
+ flag: "wx",
2430
+ signal: options?.signal
2431
+ });
2432
+ return path;
2433
+ } catch (error) {
2434
+ options?.signal?.throwIfAborted();
2435
+ throw this.#files.translateError(path, error);
2436
+ }
2437
+ }
1954
2438
  async open(name, options) {
1955
2439
  options?.signal?.throwIfAborted();
1956
2440
  this.#files.validateName(name);
1957
- return this.#files.lock(this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
1958
- const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
1959
- const slot = Object.freeze({
1960
- id: directory.id,
1961
- directory: directory.path
1962
- });
1963
- this.#directories.add(directory.path);
1964
- this.#slots.set(slot, directory.path);
1965
- return slot;
1966
- }, options);
2441
+ const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
2442
+ const previous = FileBrowserRunStore.#allocations.get(path);
2443
+ const allocation = Promise.withResolvers();
2444
+ FileBrowserRunStore.#allocations.set(path, allocation.promise);
2445
+ try {
2446
+ await previous;
2447
+ options?.signal?.throwIfAborted();
2448
+ return await this.#files.lock(path, async () => {
2449
+ const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), generateBrowserRunId, options);
2450
+ const slot = Object.freeze({
2451
+ id: directory.id,
2452
+ directory: directory.path
2453
+ });
2454
+ this.#directories.add(directory.path);
2455
+ this.#slots.set(slot, directory.path);
2456
+ return slot;
2457
+ }, options);
2458
+ } finally {
2459
+ allocation.resolve();
2460
+ if (FileBrowserRunStore.#allocations.get(path) === allocation.promise) FileBrowserRunStore.#allocations.delete(path);
2461
+ }
1967
2462
  }
1968
2463
  async get(name, id, options) {
1969
2464
  options?.signal?.throwIfAborted();
@@ -2173,6 +2668,7 @@ var Browser = class {
2173
2668
  #servingPid;
2174
2669
  #profile;
2175
2670
  #contexts = [];
2671
+ #isolating = /* @__PURE__ */ new Set();
2176
2672
  #destroyed = false;
2177
2673
  #connecting = new BrowserTransition();
2178
2674
  #disconnecting = new BrowserTransition();
@@ -2259,33 +2755,13 @@ var Browser = class {
2259
2755
  return [...this.#contexts];
2260
2756
  }
2261
2757
  async isolate(options) {
2262
- if (this.#destroyed) throw new BrowserDestroyedError();
2263
- const client = this.#client;
2264
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2265
- validateBrowserContextOptions(options);
2266
- const params = { disposeOnDetach: false };
2267
- if (options?.proxy !== void 0) {
2268
- params["proxyServer"] = options.proxy.server;
2269
- if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
2270
- }
2271
- if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2272
- const result = await client.send("Target.createBrowserContext", params);
2273
- if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
2274
- const id = result["browserContextId"];
2275
- const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2758
+ const attempt = this.#isolate(options);
2759
+ this.#isolating.add(attempt);
2276
2760
  try {
2277
- if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2278
- behavior: options.downloads.named === true ? "allowAndName" : "allow",
2279
- browserContextId: id,
2280
- downloadPath: options.downloads.path,
2281
- eventsEnabled: true
2282
- });
2283
- } catch (error) {
2284
- await context.close().catch(() => void 0);
2285
- throw error;
2761
+ return await attempt;
2762
+ } finally {
2763
+ this.#isolating.delete(attempt);
2286
2764
  }
2287
- this.#registerContext(context);
2288
- return context;
2289
2765
  }
2290
2766
  async create(options) {
2291
2767
  if (this.#destroyed) throw new BrowserDestroyedError();
@@ -2320,6 +2796,37 @@ var Browser = class {
2320
2796
  this.#shutdown = shutdown;
2321
2797
  return shutdown;
2322
2798
  }
2799
+ async #isolate(options) {
2800
+ if (this.#destroyed) throw new BrowserDestroyedError();
2801
+ const client = this.#client;
2802
+ if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2803
+ validateBrowserContextOptions(options);
2804
+ const params = { disposeOnDetach: false };
2805
+ if (options?.proxy !== void 0) {
2806
+ params["proxyServer"] = options.proxy.server;
2807
+ if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
2808
+ }
2809
+ if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2810
+ const result = await client.send("Target.createBrowserContext", params);
2811
+ if (!isRecord(result) || !isString(result["browserContextId"])) throw new BrowserConnectionError("Failed to create isolated browser context");
2812
+ const id = result["browserContextId"];
2813
+ const context = new BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2814
+ try {
2815
+ if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2816
+ if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2817
+ behavior: options.downloads.named === true ? "allowAndName" : "allow",
2818
+ browserContextId: id,
2819
+ downloadPath: options.downloads.path,
2820
+ eventsEnabled: true
2821
+ });
2822
+ if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2823
+ } catch (error) {
2824
+ await context.close().catch(() => void 0);
2825
+ throw error;
2826
+ }
2827
+ this.#registerContext(context);
2828
+ return context;
2829
+ }
2323
2830
  async #establish() {
2324
2831
  const disconnecting = this.#disconnecting.pending;
2325
2832
  if (disconnecting !== void 0) await disconnecting;
@@ -2793,6 +3300,7 @@ var Browser = class {
2793
3300
  }
2794
3301
  }
2795
3302
  async #settle() {
3303
+ await Promise.allSettled(this.#isolating);
2796
3304
  await this.#connecting.pending?.catch(() => void 0);
2797
3305
  await this.#disconnecting.pending?.catch(() => void 0);
2798
3306
  await this.#settleExit();
@@ -2981,6 +3489,6 @@ var Browser = class {
2981
3489
  }
2982
3490
  };
2983
3491
  //#endregion
2984
- export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserDestroyedError, BrowserMCPServer, BrowserNotConnectedError, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createBrowserWriter, createCDPTransport, createFileBrowserJourneyStore, createFileBrowserRunStore, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, formatBrowserLockEntry, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
3492
+ export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_DEVTOOLS_PATTERN, BROWSER_DRAIN_INTERVAL_MS, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_FILE_STORE_LIMIT, BROWSER_HEADLESS_ARG, BROWSER_JOURNEY_LOCK_ATTEMPTS, BROWSER_JOURNEY_LOCK_DIRECTORY, BROWSER_JOURNEY_REVISION_FILE, BROWSER_JOURNEY_SNAPSHOT_FILE, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_PROFILE_PREFIX, BROWSER_RUN_DIRECTORY, BROWSER_RUN_FILE, BROWSER_SERVER_BUSY, BROWSER_SERVER_CONTEXTS, BROWSER_SERVER_CONTEXTS_LIMIT, BROWSER_SERVER_COPY, BROWSER_SERVER_CRASH, BROWSER_SERVER_EXHAUSTED, BROWSER_SERVER_HOLDER, BROWSER_SERVER_LAUNCH, BROWSER_SERVER_OPTIONS, BROWSER_SERVER_POOL_LIMIT, BROWSER_SERVER_POOL_SIZE, BROWSER_SERVER_RECORD, BROWSER_SERVER_RESTARTS, BROWSER_SERVER_SWEEP, BROWSER_SERVER_TEARDOWN, BROWSER_SERVER_UNAVAILABLE, BROWSER_SERVER_UNRESOLVED, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserDestroyedError, BrowserMCPServer, BrowserNotConnectedError, FileBrowserJourneyStore, FileBrowserRunStore, FileBrowserWriter, WebSocketCDPTransport, browserToEngine, buildInstallPaths, buildStoreBases, buildWindowsRoots, createBrowser, createBrowserMCPServer, createBrowserProfile, createBrowserWriter, createCDPTransport, createFileBrowserJourneyStore, createFileBrowserRunStore, describeBrowserServerLoss, fetchCDPTargets, findEnvOverrides, findInstallPaths, findStorePaths, findSystemBrowser, findSystemBrowsers, formatBrowserLockEntry, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, parseBrowserLockEntry, parseBrowserProfileRecord, parseBrowserViewport, probePathNames, probeProcess, readBrowserEndpoint, readFirstLine, removeBrowserProfile };
2985
3493
 
2986
3494
  //# sourceMappingURL=index.js.map