@orkestrel/browser 0.0.24 → 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.
@@ -38,8 +38,18 @@ var BROWSER_CDP_VERSION_PATH = "/json/version";
38
38
  * other target category Chromium reports — `'/json/list'`.
39
39
  */
40
40
  var BROWSER_CDP_LIST_PATH = "/json/list";
41
- /** Lists the flags always passed to a launched browser process, alongside the caller's own. */
42
- var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
41
+ /**
42
+ * Lists the flags always passed to a launched browser process, alongside the caller's own.
43
+ *
44
+ * @remarks
45
+ * Disables account sync so extension installs cannot reset URL loader factories during page imports.
46
+ * Edge can still sign a fresh Windows profile in to the operating-system account.
47
+ */
48
+ var BROWSER_LAUNCH_ARGS = Object.freeze([
49
+ "--no-first-run",
50
+ "--no-default-browser-check",
51
+ "--disable-sync"
52
+ ]);
43
53
  /**
44
54
  * Names the flag that enables headless mode on a launched browser process, `'--headless=new'`.
45
55
  */
@@ -219,6 +229,10 @@ var BROWSER_SERVER_UNAVAILABLE = "BROWSER_SERVER_UNAVAILABLE";
219
229
  var BROWSER_SERVER_LAUNCH = "BROWSER_SERVER_LAUNCH";
220
230
  /** Sets the default number of warm browsers to 1. */
221
231
  var BROWSER_SERVER_POOL_SIZE = 1;
232
+ /** Sets the default context capacity per browser, including the shared holder. */
233
+ var BROWSER_SERVER_CONTEXTS = 2;
234
+ /** Limits context capacity to the largest measured per-browser count. */
235
+ var BROWSER_SERVER_CONTEXTS_LIMIT = 4;
222
236
  /** Limits the number of warm browsers to 3. */
223
237
  var BROWSER_SERVER_POOL_LIMIT = 3;
224
238
  /** Permits one failed refill before the next failure spends the bound. */
@@ -1042,7 +1056,7 @@ var FileBrowserWriter = class {
1042
1056
  };
1043
1057
  //#endregion
1044
1058
  //#region package.json
1045
- var version = "0.0.24";
1059
+ var version = "0.0.25";
1046
1060
  //#endregion
1047
1061
  //#region src/server/BrowserMCPServer.ts
1048
1062
  /**
@@ -1058,17 +1072,18 @@ var version = "0.0.24";
1058
1072
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
1059
1073
  * removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
1060
1074
  * client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
1061
- * `BROWSER_SERVER_COPY`. Startup warms the pool and leases the shared browser. The legacy
1075
+ * `BROWSER_SERVER_COPY`. Startup warms the pool and assigns a prepared context to the shared holder. The legacy
1062
1076
  * handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
1063
1077
  * and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
1064
- * destroys its token, and admission counts that holder until disposal settles.
1078
+ * closes its context and releases its lease, and admission counts that holder until disposal settles.
1065
1079
  * A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
1066
1080
  * existing endpoint, opens one page in an isolated context, and constructs the page toolset with
1067
1081
  * that context and the file journey and run stores under the root, on a manager the toolset owns.
1068
1082
  * A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
1069
1083
  * manager and returns that manager's value, or rejects with its failure message; a text value
1070
1084
  * reaches the client as one text block rather than as its JSON text.
1071
- * A disconnect or current-page crash retires its slot. A failed call receives a liveness ping;
1085
+ * A disconnect retires its browser and every hosted generation. A current-page crash retires only
1086
+ * that holder's context. A failed call receives a liveness ping;
1072
1087
  * loss makes its outcome unresolved, while a known success remains successful. Pending losses
1073
1088
  * prefix the next outcome on a successor, or a refusal, and calls are never repeated.
1074
1089
  * Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
@@ -1076,7 +1091,8 @@ var version = "0.0.24";
1076
1091
  *
1077
1092
  * `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
1078
1093
  * `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
1079
- * slot through the pool — its toolset, which aborts the active replay, then its browser, then its
1094
+ * slot through the pool — each toolset, which aborts its active replay, then its context and downloads,
1095
+ * then its browser and its
1080
1096
  * profile — and rechecks the profile folders this server made or kept.
1081
1097
  *
1082
1098
  * Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
@@ -1111,8 +1127,16 @@ var BrowserMCPServer = class {
1111
1127
  #folders = /* @__PURE__ */ new Set();
1112
1128
  #faults = /* @__PURE__ */ new Set();
1113
1129
  #losses = /* @__PURE__ */ new WeakMap();
1130
+ #departures = /* @__PURE__ */ new WeakMap();
1114
1131
  #pings = /* @__PURE__ */ new WeakMap();
1115
1132
  #watches = /* @__PURE__ */ new Map();
1133
+ #observed = /* @__PURE__ */ new Map();
1134
+ #prepared = /* @__PURE__ */ new Map();
1135
+ #owned = /* @__PURE__ */ new Map();
1136
+ #building = /* @__PURE__ */ new Map();
1137
+ #cleaning = /* @__PURE__ */ new WeakMap();
1138
+ #endings = /* @__PURE__ */ new WeakMap();
1139
+ #retentions = /* @__PURE__ */ new Map();
1116
1140
  #shared = {
1117
1141
  id: "shared",
1118
1142
  purpose: "Shared browser",
@@ -1126,7 +1150,7 @@ var BrowserMCPServer = class {
1126
1150
  #drains = /* @__PURE__ */ new Map();
1127
1151
  #readers = /* @__PURE__ */ new Map();
1128
1152
  #writers = /* @__PURE__ */ new Set();
1129
- #size;
1153
+ #admission;
1130
1154
  #reference;
1131
1155
  #references = 0;
1132
1156
  #failure;
@@ -1141,17 +1165,20 @@ var BrowserMCPServer = class {
1141
1165
  */
1142
1166
  constructor(options) {
1143
1167
  const size = options?.pool?.size ?? 1;
1168
+ const contexts = options?.pool?.contexts ?? 2;
1144
1169
  if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
1145
- this.#size = size;
1170
+ if (!(0, _orkestrel_contract.isInteger)(contexts) || contexts < 1 || contexts > 4) throw new _src_core.BrowserError(`pool.contexts must be an integer from 1 through 4`, BROWSER_SERVER_OPTIONS);
1171
+ this.#admission = size * contexts;
1146
1172
  this.#log = options?.log ?? process.stderr;
1147
1173
  this.#reference = this.#issue.bind(this);
1148
1174
  this.#pool = (0, _orkestrel_pool.createPool)({
1149
1175
  create: this.#warm.bind(this),
1150
- destroy: (slot) => this.#destroySlot(slot.profile, slot.browser, slot.toolset, this.#losses.get(slot)?.cause),
1176
+ destroy: this.#destroyRecord.bind(this),
1151
1177
  validate: this.#validate.bind(this),
1152
1178
  watch: this.#watch.bind(this),
1153
1179
  error: this.#fault.bind(this),
1154
1180
  min: size,
1181
+ capacity: contexts,
1155
1182
  restarts: 1
1156
1183
  });
1157
1184
  this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
@@ -1234,6 +1261,7 @@ var BrowserMCPServer = class {
1234
1261
  this.#transport.stop();
1235
1262
  for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
1236
1263
  this.#unmirror();
1264
+ for (const generation of this.#observed.keys()) this.#untrack(generation);
1237
1265
  this.#leases.clear();
1238
1266
  for (const holder of this.#holders.values()) holder.abort.abort();
1239
1267
  this.#abort.abort();
@@ -1242,6 +1270,7 @@ var BrowserMCPServer = class {
1242
1270
  });
1243
1271
  await this.#starting?.catch(() => void 0);
1244
1272
  await barrier;
1273
+ await Promise.allSettled(this.#grants.values());
1245
1274
  await Promise.allSettled(this.#disposals.values());
1246
1275
  this.#holders.clear();
1247
1276
  await this.#sweeping;
@@ -1293,7 +1322,7 @@ var BrowserMCPServer = class {
1293
1322
  if (this.#closing !== void 0) throw this.#ended();
1294
1323
  const purpose = args["purpose"];
1295
1324
  if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
1296
- if (this.#holders.size >= this.#size) throw new _src_core.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
+ if (this.#holders.size >= this.#admission) throw new _src_core.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);
1297
1326
  const holder = {
1298
1327
  id: (0, node_crypto.randomUUID)(),
1299
1328
  purpose,
@@ -1302,13 +1331,13 @@ var BrowserMCPServer = class {
1302
1331
  this.#holders.set(holder.id, holder);
1303
1332
  const listener = (0, node_events.addAbortListener)(context.signal, () => holder.abort.abort(context.signal.reason));
1304
1333
  try {
1305
- const slot = await this.#serve(holder, context.signal);
1334
+ const lease = await this.#serve(holder, context.signal);
1306
1335
  context.signal.throwIfAborted();
1307
1336
  if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
1308
- if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1337
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1309
1338
  const catalog = {
1310
1339
  holder: holder.id,
1311
- tools: slot.toolset.tools.definitions()
1340
+ tools: lease.generation.toolset.tools.definitions()
1312
1341
  };
1313
1342
  return JSON.stringify(catalog);
1314
1343
  } catch (error) {
@@ -1334,12 +1363,12 @@ var BrowserMCPServer = class {
1334
1363
  const holder = this.#holder(args["holder"]);
1335
1364
  const signal = AbortSignal.any([context.signal, holder.abort.signal]);
1336
1365
  try {
1337
- const slot = await this.#serve(holder, signal);
1366
+ const lease = await this.#serve(holder, signal);
1338
1367
  signal.throwIfAborted();
1339
- if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
1368
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1340
1369
  const catalog = {
1341
1370
  holder: holder.id,
1342
- tools: slot.toolset.tools.definitions()
1371
+ tools: lease.generation.toolset.tools.definitions()
1343
1372
  };
1344
1373
  return this.#annotate(holder, JSON.stringify(catalog));
1345
1374
  } catch (error) {
@@ -1358,17 +1387,19 @@ var BrowserMCPServer = class {
1358
1387
  const pending = this.#disposals.get(holder.id);
1359
1388
  if (pending !== void 0) return pending;
1360
1389
  holder.abort.abort();
1361
- const token = this.#leases.get(holder);
1390
+ const lease = this.#leases.get(holder);
1362
1391
  this.#leases.delete(holder);
1392
+ if (lease !== void 0) this.#untrack(lease.generation);
1363
1393
  const disposal = Promise.resolve().then(async () => {
1364
1394
  try {
1365
1395
  await this.#grants.get(holder)?.catch(() => void 0);
1366
- await token?.destroy();
1396
+ if (lease !== void 0) await this.#release(holder, lease);
1367
1397
  await Promise.all(this.#drains.get(holder) ?? []);
1398
+ await Promise.all([...this.#notices.get(holder) ?? []].filter((lost) => this.#losses.has(lost.token.value)).map((lost) => this.#endings.get(lost.token.value)?.promise));
1368
1399
  } catch (error) {
1369
1400
  this.#faults.add(error);
1370
1401
  } finally {
1371
- if (this.#holders.get(holder.id) === holder) this.#holders.delete(holder.id);
1402
+ if (this.#holders.get(holder.id) === holder && !this.#retentions.has(holder)) this.#holders.delete(holder.id);
1372
1403
  this.#notices.delete(holder);
1373
1404
  this.#drains.delete(holder);
1374
1405
  }
@@ -1401,9 +1432,9 @@ var BrowserMCPServer = class {
1401
1432
  }
1402
1433
  async #perform(holder, name, args, context) {
1403
1434
  const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
1404
- let slot;
1435
+ let lease;
1405
1436
  try {
1406
- slot = await this.#serve(holder, signal);
1437
+ lease = await this.#serve(holder, signal);
1407
1438
  } catch (error) {
1408
1439
  context.signal.throwIfAborted();
1409
1440
  if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
@@ -1412,7 +1443,7 @@ var BrowserMCPServer = class {
1412
1443
  }
1413
1444
  let result;
1414
1445
  try {
1415
- const execution = slot.toolset.tools.execute({
1446
+ const execution = lease.generation.toolset.tools.execute({
1416
1447
  id: name,
1417
1448
  name,
1418
1449
  arguments: args
@@ -1434,23 +1465,23 @@ var BrowserMCPServer = class {
1434
1465
  context.signal.throwIfAborted();
1435
1466
  if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
1436
1467
  if (!result.success) {
1437
- if (this.#leases.get(holder)?.value === slot && !context.signal.aborted) try {
1438
- await this.#race(this.#ping(slot), context.signal);
1468
+ if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1469
+ await this.#race(this.#ping(lease.token.value), context.signal);
1439
1470
  } catch {}
1440
1471
  context.signal.throwIfAborted();
1441
- if (this.#leases.get(holder)?.value !== slot) {
1442
- const loss = this.#losses.get(slot);
1443
- throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
1472
+ if (this.#leases.get(holder) !== lease) {
1473
+ const loss = this.#departures.get(lease);
1474
+ throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), lease), BROWSER_SERVER_UNRESOLVED);
1444
1475
  }
1445
- throw new _src_core.BrowserError(this.#annotate(holder, result.error, slot));
1476
+ throw new _src_core.BrowserError(this.#annotate(holder, result.error, lease));
1446
1477
  }
1447
- return typeof result.value === "string" ? this.#annotate(holder, result.value, slot) : result.value;
1478
+ return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1448
1479
  }
1449
- #annotate(holder, value, slot) {
1480
+ #annotate(holder, value, lease) {
1450
1481
  const notes = [];
1451
1482
  for (const lost of this.#notices.get(holder) ?? []) {
1452
- if (lost === slot) continue;
1453
- const loss = this.#losses.get(lost);
1483
+ if (lost === lease) continue;
1484
+ const loss = this.#departures.get(lost);
1454
1485
  notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
1455
1486
  this.#notices.get(holder)?.delete(lost);
1456
1487
  }
@@ -1481,41 +1512,70 @@ var BrowserMCPServer = class {
1481
1512
  if (held !== void 0) return held;
1482
1513
  let granting = this.#grants.get(holder);
1483
1514
  if (granting === void 0) {
1484
- granting = this.#pool.acquire(holder.abort.signal).then(this.#hold.bind(this, holder));
1515
+ granting = Promise.resolve().then(async () => {
1516
+ await Promise.allSettled(this.#drains.get(holder) ?? []);
1517
+ if (this.#retentions.has(holder)) throw this.#unavailable(this.#retentions.get(holder));
1518
+ const token = await this.#pool.acquire(holder.abort.signal);
1519
+ return this.#hold(holder, token);
1520
+ });
1485
1521
  this.#grants.set(holder, granting);
1486
1522
  granting.finally(() => {
1487
1523
  if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1488
1524
  }).catch(() => void 0);
1489
1525
  }
1490
- const token = await this.#race(granting, signal);
1491
- if (token === void 0) throw this.#ended();
1492
- return token;
1526
+ const lease = await this.#race(granting, signal);
1527
+ if (lease === void 0) throw this.#ended();
1528
+ return lease;
1493
1529
  }
1494
1530
  async #hold(holder, token) {
1531
+ const slot = token.value;
1495
1532
  if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
1496
- await token.destroy();
1533
+ token.release();
1534
+ throw this.#ended();
1535
+ }
1536
+ if (!this.#prepared.has(slot)) await this.#ping(slot);
1537
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) {
1538
+ token.release();
1497
1539
  throw this.#ended();
1498
1540
  }
1499
- if (this.#losses.has(token.value)) {
1500
- await token.destroy().catch(() => void 0);
1501
- throw this.#unavailable(this.#losses.get(token.value)?.cause);
1541
+ if (this.#losses.has(slot)) {
1542
+ token.release();
1543
+ throw this.#unavailable(this.#losses.get(slot)?.cause);
1502
1544
  }
1503
- this.#leases.set(holder, token);
1545
+ let generation = this.#prepared.get(slot);
1546
+ this.#prepared.delete(slot);
1547
+ try {
1548
+ generation ??= await this.#build(slot, token);
1549
+ } catch (error) {
1550
+ if (this.#losses.has(slot)) await this.#endings.get(slot)?.promise;
1551
+ else token.release();
1552
+ throw this.#unavailable(error);
1553
+ }
1554
+ const lease = {
1555
+ token,
1556
+ generation
1557
+ };
1558
+ if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted || this.#losses.has(slot)) {
1559
+ await this.#release(holder, lease);
1560
+ throw this.#ended();
1561
+ }
1562
+ this.#leases.set(holder, lease);
1563
+ this.#observe(slot, generation);
1504
1564
  if (holder === this.#shared) {
1505
- const tools = token.value.toolset.tools;
1565
+ const tools = generation.toolset.tools;
1506
1566
  const mirrored = {
1507
- token,
1508
- added: this.#mirror.bind(this, token),
1509
- removed: this.#withdraw.bind(this, token),
1510
- cleared: this.#clear.bind(this, token)
1567
+ lease,
1568
+ added: this.#mirror.bind(this, lease),
1569
+ removed: this.#withdraw.bind(this, lease),
1570
+ cleared: this.#clear.bind(this, lease)
1511
1571
  };
1512
1572
  this.#mirrored = mirrored;
1513
- for (const tool of tools.tools()) this.#mirror(token, tool);
1573
+ for (const tool of tools.tools()) this.#mirror(lease, tool);
1514
1574
  tools.emitter.on("add", mirrored.added);
1515
1575
  tools.emitter.on("remove", mirrored.removed);
1516
1576
  tools.emitter.on("clear", mirrored.cleared);
1517
1577
  }
1518
- return token;
1578
+ return lease;
1519
1579
  }
1520
1580
  async #serve(holder, signal) {
1521
1581
  if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
@@ -1523,16 +1583,16 @@ var BrowserMCPServer = class {
1523
1583
  if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1524
1584
  const held = this.#leases.get(holder);
1525
1585
  if (held !== void 0) try {
1526
- await this.#race(this.#ping(held.value), signal);
1527
- if (this.#leases.get(holder) === held) return held.value;
1586
+ await this.#race(this.#ping(held.token.value), signal);
1587
+ if (this.#leases.get(holder) === held) return held;
1528
1588
  } catch {
1529
1589
  signal.throwIfAborted();
1530
1590
  if (holder.abort.signal.aborted) throw this.#ended();
1531
1591
  }
1532
1592
  try {
1533
- const token = await this.#grant(holder, signal);
1534
- if (this.#leases.get(holder) !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1535
- return token.value;
1593
+ const lease = await this.#grant(holder, signal);
1594
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1595
+ return lease;
1536
1596
  } catch (error) {
1537
1597
  signal.throwIfAborted();
1538
1598
  if (holder.abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
@@ -1563,78 +1623,211 @@ var BrowserMCPServer = class {
1563
1623
  }
1564
1624
  #lose(slot, cause) {
1565
1625
  if (this.#closing !== void 0 || this.#losses.has(slot)) return;
1566
- this.#losses.set(slot, {
1567
- cause,
1568
- url: slot.toolset.view.url
1569
- });
1626
+ this.#losses.set(slot, { cause });
1570
1627
  this.#failure = cause;
1571
- for (const [holder, token] of this.#leases) {
1572
- if (token.value !== slot || this.#holders.get(holder.id) !== holder) continue;
1573
- if (holder === this.#shared) this.#unmirror();
1574
- this.#leases.delete(holder);
1575
- const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
1576
- notices.add(slot);
1577
- this.#notices.set(holder, notices);
1578
- const drain = token.destroy();
1579
- const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
1580
- drains.add(drain);
1581
- this.#drains.set(holder, drains);
1582
- drain.finally(() => drains.delete(drain)).catch(() => void 0);
1628
+ for (const [holder, lease] of this.#leases) {
1629
+ if (lease.token.value !== slot) continue;
1630
+ this.#detach(holder, lease, cause);
1583
1631
  }
1632
+ const watch = this.#watches.get(slot);
1633
+ this.#unwatch(slot);
1634
+ watch?.resolve(cause);
1635
+ }
1636
+ #detach(holder, lease, cause) {
1637
+ if (this.#leases.get(holder) !== lease || this.#holders.get(holder.id) !== holder) return;
1638
+ this.#departures.set(lease, {
1639
+ cause,
1640
+ url: lease.generation.toolset.view.url
1641
+ });
1642
+ if (holder === this.#shared) this.#unmirror();
1643
+ this.#leases.delete(holder);
1644
+ this.#untrack(lease.generation);
1645
+ const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
1646
+ notices.add(lease);
1647
+ this.#notices.set(holder, notices);
1584
1648
  }
1585
1649
  #watch(slot, signal) {
1586
1650
  const loss = Promise.withResolvers();
1587
1651
  const disconnect = this.#disconnect.bind(this, slot);
1588
- const page = this.#track.bind(this, slot);
1589
1652
  const subscription = (0, node_events.addAbortListener)(signal, this.#unwatch.bind(this, slot));
1590
1653
  this.#watches.set(slot, {
1591
1654
  resolve: loss.resolve,
1592
1655
  disconnect,
1593
- page,
1594
- subscription,
1595
- crashes: /* @__PURE__ */ new Map()
1656
+ subscription
1596
1657
  });
1597
1658
  slot.browser.emitter.on("disconnect", disconnect);
1598
- slot.context.emitter.on("page", page);
1599
- for (const current of slot.context.pages()) this.#track(slot, current);
1600
1659
  if (signal.aborted) this.#unwatch(slot);
1601
1660
  else if (slot.browser.status !== "connected") this.#disconnect(slot);
1602
1661
  return loss.promise;
1603
1662
  }
1604
- #track(slot, page) {
1605
- const watch = this.#watches.get(slot);
1663
+ #observe(slot, generation) {
1664
+ if (this.#observed.has(generation)) return;
1665
+ const page = this.#track.bind(this, slot, generation);
1666
+ this.#observed.set(generation, {
1667
+ page,
1668
+ crashes: /* @__PURE__ */ new Map()
1669
+ });
1670
+ generation.context.emitter.on("page", page);
1671
+ for (const current of generation.context.pages()) this.#track(slot, generation, current);
1672
+ }
1673
+ #track(slot, generation, page) {
1674
+ const watch = this.#observed.get(generation);
1606
1675
  if (watch === void 0 || watch.crashes.has(page)) return;
1607
- const crash = this.#createCrash(slot, page);
1676
+ const crash = this.#crash.bind(this, slot, generation, page);
1608
1677
  page.emitter.on("crash", crash);
1609
- this.#watches.set(slot, {
1678
+ this.#observed.set(generation, {
1610
1679
  ...watch,
1611
1680
  crashes: new Map([...watch.crashes, [page, crash]])
1612
1681
  });
1613
1682
  }
1614
- #createCrash(slot, page) {
1615
- return () => {
1616
- if (page === slot.toolset.view) this.#report(slot, /* @__PURE__ */ new Error("The current page renderer crashed"));
1617
- };
1683
+ #crash(slot, generation, page) {
1684
+ if (this.#prepared.get(slot) === generation) {
1685
+ this.#prepared.delete(slot);
1686
+ this.#untrack(generation);
1687
+ this.#clean(slot, generation).catch(this.#fault.bind(this));
1688
+ return;
1689
+ }
1690
+ const held = [...this.#leases].find(([, lease]) => lease.generation === generation);
1691
+ if (held === void 0 || page !== generation.toolset.view) return;
1692
+ const [holder, lease] = held;
1693
+ this.#detach(holder, lease, /* @__PURE__ */ new Error("The current page renderer crashed"));
1694
+ const drain = this.#release(holder, lease);
1695
+ const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
1696
+ drains.add(drain);
1697
+ this.#drains.set(holder, drains);
1698
+ drain.finally(() => drains.delete(drain)).catch(this.#fault.bind(this));
1699
+ }
1700
+ #untrack(generation) {
1701
+ const watch = this.#observed.get(generation);
1702
+ if (watch === void 0) return;
1703
+ this.#observed.delete(generation);
1704
+ generation.context.emitter.off("page", watch.page);
1705
+ for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
1618
1706
  }
1619
1707
  #disconnect(slot) {
1620
- this.#report(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1621
- }
1622
- #report(slot, cause) {
1623
- const watch = this.#watches.get(slot);
1624
- if (watch === void 0) return;
1625
- this.#unwatch(slot);
1626
- this.#lose(slot, cause);
1627
- watch.resolve(cause);
1708
+ this.#lose(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1628
1709
  }
1629
1710
  #unwatch(slot) {
1630
1711
  const watch = this.#watches.get(slot);
1631
1712
  if (watch === void 0) return;
1632
1713
  this.#watches.delete(slot);
1633
1714
  slot.browser.emitter.off("disconnect", watch.disconnect);
1634
- slot.context.emitter.off("page", watch.page);
1635
- for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
1636
1715
  watch.subscription[Symbol.dispose]();
1637
1716
  }
1717
+ async #release(holder, lease) {
1718
+ const slot = lease.token.value;
1719
+ const removed = await this.#clean(slot, lease.generation);
1720
+ const disposal = lease.generation.context.disposal;
1721
+ if (!removed) this.#retentions.set(holder, /* @__PURE__ */ new Error("The context downloads directory remains owned"));
1722
+ if (disposal?.confirmed !== true && !this.#losses.has(slot) && this.#closing === void 0) {
1723
+ if (![...this.#leases.values()].some((held) => held.token.value === slot && held !== lease) && (this.#building.get(slot)?.size ?? 0) === 0) lease.token.release();
1724
+ this.#lose(slot, disposal?.error ?? /* @__PURE__ */ new Error("The context disposal is unconfirmed"));
1725
+ await this.#endings.get(slot)?.promise;
1726
+ } else if (removed) lease.token.release();
1727
+ }
1728
+ #clean(slot, generation) {
1729
+ let cleaning = this.#cleaning.get(generation);
1730
+ if (cleaning !== void 0) return cleaning;
1731
+ cleaning = Promise.resolve().then(async () => {
1732
+ await generation.toolset.destroy().catch(this.#fault.bind(this));
1733
+ await generation.context.close().catch((error) => {
1734
+ if (!this.#losses.has(slot) && slot.browser.status === "connected") this.#fault(error);
1735
+ });
1736
+ try {
1737
+ await (0, node_fs_promises.rm)(generation.directory, {
1738
+ recursive: true,
1739
+ force: true
1740
+ });
1741
+ this.#owned.get(slot)?.delete(generation);
1742
+ return true;
1743
+ } catch (error) {
1744
+ this.#fault(error);
1745
+ return false;
1746
+ }
1747
+ });
1748
+ this.#cleaning.set(generation, cleaning);
1749
+ return cleaning;
1750
+ }
1751
+ #build(slot, token) {
1752
+ const construction = this.#construct(slot, token);
1753
+ const building = this.#building.get(slot) ?? /* @__PURE__ */ new Set();
1754
+ building.add(construction);
1755
+ this.#building.set(slot, building);
1756
+ construction.finally(() => building.delete(construction)).catch(() => void 0);
1757
+ return construction;
1758
+ }
1759
+ async #construct(slot, token) {
1760
+ const directory = (0, node_path.join)(slot.profile, "contexts", (0, node_crypto.randomUUID)(), "downloads");
1761
+ let context;
1762
+ let toolset;
1763
+ try {
1764
+ await (0, node_fs_promises.mkdir)(directory, { recursive: true });
1765
+ let page;
1766
+ try {
1767
+ context = await slot.browser.isolate({
1768
+ reference: this.#reference,
1769
+ downloads: { path: directory },
1770
+ ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1771
+ });
1772
+ page = await context.create();
1773
+ } catch (error) {
1774
+ if (token !== void 0) {
1775
+ if (![...this.#leases.values()].some((lease) => lease.token.value === slot) && (this.#building.get(slot)?.size ?? 0) === 1) token.release();
1776
+ this.#lose(slot, error);
1777
+ }
1778
+ throw error;
1779
+ }
1780
+ toolset = (0, _src_core.createBrowserToolset)(page, {
1781
+ context,
1782
+ journeys: {
1783
+ store: createFileBrowserJourneyStore({ root: this.#root }),
1784
+ runs: createFileBrowserRunStore({ root: this.#root }),
1785
+ readonly: this.#readonly
1786
+ }
1787
+ });
1788
+ await toolset.start();
1789
+ const generation = {
1790
+ context,
1791
+ toolset,
1792
+ directory
1793
+ };
1794
+ const owned = this.#owned.get(slot) ?? /* @__PURE__ */ new Set();
1795
+ owned.add(generation);
1796
+ this.#owned.set(slot, owned);
1797
+ return generation;
1798
+ } catch (error) {
1799
+ await toolset?.destroy().catch(this.#fault.bind(this));
1800
+ await context?.close().catch((failure) => {
1801
+ if (!this.#losses.has(slot) && slot.browser.status === "connected" && this.#closing === void 0 && !(0, _src_core.isCDPConnectionError)(failure)) this.#fault(failure);
1802
+ });
1803
+ await (0, node_fs_promises.rm)(directory, {
1804
+ recursive: true,
1805
+ force: true
1806
+ }).catch(this.#fault.bind(this));
1807
+ throw error;
1808
+ }
1809
+ }
1810
+ async #destroyRecord(slot) {
1811
+ if ((0, _src_core.isCDPTimeoutError)(this.#losses.get(slot)?.cause) && slot.browser.pid !== void 0) try {
1812
+ process.kill(slot.browser.pid, "SIGKILL");
1813
+ } catch (error) {
1814
+ if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
1815
+ }
1816
+ try {
1817
+ await Promise.allSettled(this.#building.get(slot) ?? []);
1818
+ const prepared = this.#prepared.get(slot);
1819
+ if (prepared !== void 0) this.#untrack(prepared);
1820
+ this.#prepared.delete(slot);
1821
+ await Promise.all([...this.#owned.get(slot) ?? []].map((generation) => this.#clean(slot, generation)));
1822
+ await this.#destroySlot(slot.profile, slot.browser);
1823
+ this.#owned.delete(slot);
1824
+ this.#building.delete(slot);
1825
+ this.#endings.get(slot)?.resolve();
1826
+ } catch (error) {
1827
+ this.#endings.get(slot)?.reject(error);
1828
+ throw error;
1829
+ }
1830
+ }
1638
1831
  #fault(error) {
1639
1832
  this.#faults.add(error);
1640
1833
  }
@@ -1643,7 +1836,7 @@ var BrowserMCPServer = class {
1643
1836
  if (this.#stranded !== void 0) throw new _src_core.BrowserError((0, _orkestrel_contract.isError)(this.#stranded) ? this.#stranded.message : String(this.#stranded), BROWSER_SERVER_TEARDOWN, (0, _src_core.isBrowserError)(this.#stranded) ? this.#stranded.context : void 0);
1644
1837
  const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
1645
1838
  let browser;
1646
- let toolset;
1839
+ let slot;
1647
1840
  try {
1648
1841
  await (0, node_fs_promises.mkdir)(profile);
1649
1842
  this.#folders.add(profile);
@@ -1665,42 +1858,26 @@ var BrowserMCPServer = class {
1665
1858
  }));
1666
1859
  await (0, node_fs_promises.rename)(`${record}.tmp`, record);
1667
1860
  }
1668
- const context = await browser.isolate({
1669
- reference: this.#reference,
1670
- downloads: { path: (0, node_path.join)(profile, "downloads") },
1671
- ...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
1672
- });
1673
- const page = await context.create();
1674
- toolset = (0, _src_core.createBrowserToolset)(page, {
1675
- context,
1676
- journeys: {
1677
- store: createFileBrowserJourneyStore({ root: this.#root }),
1678
- runs: createFileBrowserRunStore({ root: this.#root }),
1679
- readonly: this.#readonly
1680
- }
1681
- });
1682
- await toolset.start();
1683
- return {
1861
+ slot = {
1684
1862
  browser,
1685
- profile,
1686
- context,
1687
- toolset
1863
+ profile
1688
1864
  };
1865
+ const ending = Promise.withResolvers();
1866
+ ending.promise.catch(() => void 0);
1867
+ this.#endings.set(slot, ending);
1868
+ const generation = await this.#build(slot);
1869
+ this.#prepared.set(slot, generation);
1870
+ this.#observe(slot, generation);
1871
+ return slot;
1689
1872
  } catch (error) {
1690
- if (this.#folders.has(profile)) await this.#destroySlot(profile, browser, toolset).catch((failure) => {
1873
+ if (this.#folders.has(profile)) await (slot === void 0 ? this.#destroySlot(profile, browser) : this.#destroyRecord(slot)).catch((failure) => {
1691
1874
  this.#stranded = failure;
1692
1875
  });
1693
1876
  if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
1694
1877
  throw error;
1695
1878
  }
1696
1879
  }
1697
- async #destroySlot(profile, browser, toolset, cause) {
1698
- if ((0, _src_core.isCDPTimeoutError)(cause) && browser?.pid !== void 0) try {
1699
- process.kill(browser.pid, "SIGKILL");
1700
- } catch (error) {
1701
- if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
1702
- }
1703
- await toolset?.destroy().catch((error) => this.#faults.add(error));
1880
+ async #destroySlot(profile, browser) {
1704
1881
  try {
1705
1882
  await browser?.destroy();
1706
1883
  } catch (error) {
@@ -1776,27 +1953,27 @@ var BrowserMCPServer = class {
1776
1953
  #issue() {
1777
1954
  return `e${++this.#references}`;
1778
1955
  }
1779
- #mirror(token, tool) {
1780
- if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1956
+ #mirror(lease, tool) {
1957
+ if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
1781
1958
  this.#tools.add((0, _orkestrel_tool.createTool)({
1782
1959
  ...(0, _orkestrel_tool.toolToDefinition)(tool),
1783
1960
  execute: this.#forward.bind(this, this.#shared, tool.name)
1784
1961
  }));
1785
1962
  }
1786
- #withdraw(token, tool) {
1787
- if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
1788
- const current = token.value.toolset.tools.tool(tool.name);
1963
+ #withdraw(lease, tool) {
1964
+ if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
1965
+ const current = lease.generation.toolset.tools.tool(tool.name);
1789
1966
  if (current === void 0) this.#tools.remove(tool.name);
1790
- else this.#mirror(token, current);
1967
+ else this.#mirror(lease, current);
1791
1968
  }
1792
- #clear(token, tools) {
1793
- for (const tool of tools) this.#withdraw(token, tool);
1969
+ #clear(lease, tools) {
1970
+ for (const tool of tools) this.#withdraw(lease, tool);
1794
1971
  }
1795
1972
  #unmirror() {
1796
1973
  const mirrored = this.#mirrored;
1797
1974
  if (mirrored === void 0) return;
1798
1975
  this.#mirrored = void 0;
1799
- const tools = mirrored.token.value.toolset.tools;
1976
+ const tools = mirrored.lease.generation.toolset.tools;
1800
1977
  for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1801
1978
  tools.emitter.off("add", mirrored.added);
1802
1979
  tools.emitter.off("remove", mirrored.removed);
@@ -2227,11 +2404,14 @@ var FileBrowserJourneyStore = class {
2227
2404
  /**
2228
2405
  * Persists runs and captures only in directories allocated by this instance.
2229
2406
  * @remarks Requires an existing root. Reopened stores can read and delete saved runs.
2407
+ * Allocations of one journey share a process-local queue across instances; the filesystem lock
2408
+ * still refuses conflicting mutations and allocations from another process.
2230
2409
  * @example
2231
2410
  * const store = new FileBrowserRunStore({ root: directory })
2232
2411
  * const slot = await store.open('check-ready')
2233
2412
  */
2234
- var FileBrowserRunStore = class {
2413
+ var FileBrowserRunStore = class FileBrowserRunStore {
2414
+ static #allocations = /* @__PURE__ */ new Map();
2235
2415
  #files;
2236
2416
  #slots = /* @__PURE__ */ new WeakMap();
2237
2417
  #directories = /* @__PURE__ */ new Set();
@@ -2259,16 +2439,27 @@ var FileBrowserRunStore = class {
2259
2439
  async open(name, options) {
2260
2440
  options?.signal?.throwIfAborted();
2261
2441
  this.#files.validateName(name);
2262
- return this.#files.lock(this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
2263
- const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
2264
- const slot = Object.freeze({
2265
- id: directory.id,
2266
- directory: directory.path
2267
- });
2268
- this.#directories.add(directory.path);
2269
- this.#slots.set(slot, directory.path);
2270
- return slot;
2271
- }, options);
2442
+ const path = this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY);
2443
+ const previous = FileBrowserRunStore.#allocations.get(path);
2444
+ const allocation = Promise.withResolvers();
2445
+ FileBrowserRunStore.#allocations.set(path, allocation.promise);
2446
+ try {
2447
+ await previous;
2448
+ options?.signal?.throwIfAborted();
2449
+ return await this.#files.lock(path, async () => {
2450
+ const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
2451
+ const slot = Object.freeze({
2452
+ id: directory.id,
2453
+ directory: directory.path
2454
+ });
2455
+ this.#directories.add(directory.path);
2456
+ this.#slots.set(slot, directory.path);
2457
+ return slot;
2458
+ }, options);
2459
+ } finally {
2460
+ allocation.resolve();
2461
+ if (FileBrowserRunStore.#allocations.get(path) === allocation.promise) FileBrowserRunStore.#allocations.delete(path);
2462
+ }
2272
2463
  }
2273
2464
  async get(name, id, options) {
2274
2465
  options?.signal?.throwIfAborted();
@@ -2478,6 +2669,7 @@ var Browser = class {
2478
2669
  #servingPid;
2479
2670
  #profile;
2480
2671
  #contexts = [];
2672
+ #isolating = /* @__PURE__ */ new Set();
2481
2673
  #destroyed = false;
2482
2674
  #connecting = new _src_core.BrowserTransition();
2483
2675
  #disconnecting = new _src_core.BrowserTransition();
@@ -2564,33 +2756,13 @@ var Browser = class {
2564
2756
  return [...this.#contexts];
2565
2757
  }
2566
2758
  async isolate(options) {
2567
- if (this.#destroyed) throw new BrowserDestroyedError();
2568
- const client = this.#client;
2569
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2570
- (0, _src_core.validateBrowserContextOptions)(options);
2571
- const params = { disposeOnDetach: false };
2572
- if (options?.proxy !== void 0) {
2573
- params["proxyServer"] = options.proxy.server;
2574
- if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
2575
- }
2576
- if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2577
- const result = await client.send("Target.createBrowserContext", params);
2578
- if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
2579
- const id = result["browserContextId"];
2580
- const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2759
+ const attempt = this.#isolate(options);
2760
+ this.#isolating.add(attempt);
2581
2761
  try {
2582
- if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2583
- behavior: options.downloads.named === true ? "allowAndName" : "allow",
2584
- browserContextId: id,
2585
- downloadPath: options.downloads.path,
2586
- eventsEnabled: true
2587
- });
2588
- } catch (error) {
2589
- await context.close().catch(() => void 0);
2590
- throw error;
2762
+ return await attempt;
2763
+ } finally {
2764
+ this.#isolating.delete(attempt);
2591
2765
  }
2592
- this.#registerContext(context);
2593
- return context;
2594
2766
  }
2595
2767
  async create(options) {
2596
2768
  if (this.#destroyed) throw new BrowserDestroyedError();
@@ -2625,6 +2797,37 @@ var Browser = class {
2625
2797
  this.#shutdown = shutdown;
2626
2798
  return shutdown;
2627
2799
  }
2800
+ async #isolate(options) {
2801
+ if (this.#destroyed) throw new BrowserDestroyedError();
2802
+ const client = this.#client;
2803
+ if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2804
+ (0, _src_core.validateBrowserContextOptions)(options);
2805
+ const params = { disposeOnDetach: false };
2806
+ if (options?.proxy !== void 0) {
2807
+ params["proxyServer"] = options.proxy.server;
2808
+ if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
2809
+ }
2810
+ if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2811
+ const result = await client.send("Target.createBrowserContext", params);
2812
+ if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
2813
+ const id = result["browserContextId"];
2814
+ const context = new _src_core.BrowserContext(client, id, options?.emulation?.viewport ?? this.#options.viewport, createBrowserWriter(), options?.emulation, options?.downloads, options);
2815
+ try {
2816
+ if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2817
+ if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2818
+ behavior: options.downloads.named === true ? "allowAndName" : "allow",
2819
+ browserContextId: id,
2820
+ downloadPath: options.downloads.path,
2821
+ eventsEnabled: true
2822
+ });
2823
+ if (this.#destroyed || this.#client !== client) throw new BrowserDestroyedError();
2824
+ } catch (error) {
2825
+ await context.close().catch(() => void 0);
2826
+ throw error;
2827
+ }
2828
+ this.#registerContext(context);
2829
+ return context;
2830
+ }
2628
2831
  async #establish() {
2629
2832
  const disconnecting = this.#disconnecting.pending;
2630
2833
  if (disconnecting !== void 0) await disconnecting;
@@ -3098,6 +3301,7 @@ var Browser = class {
3098
3301
  }
3099
3302
  }
3100
3303
  async #settle() {
3304
+ await Promise.allSettled(this.#isolating);
3101
3305
  await this.#connecting.pending?.catch(() => void 0);
3102
3306
  await this.#disconnecting.pending?.catch(() => void 0);
3103
3307
  await this.#settleExit();
@@ -3311,6 +3515,8 @@ exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
3311
3515
  exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
3312
3516
  exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
3313
3517
  exports.BROWSER_SERVER_BUSY = BROWSER_SERVER_BUSY;
3518
+ exports.BROWSER_SERVER_CONTEXTS = BROWSER_SERVER_CONTEXTS;
3519
+ exports.BROWSER_SERVER_CONTEXTS_LIMIT = BROWSER_SERVER_CONTEXTS_LIMIT;
3314
3520
  exports.BROWSER_SERVER_COPY = BROWSER_SERVER_COPY;
3315
3521
  exports.BROWSER_SERVER_CRASH = BROWSER_SERVER_CRASH;
3316
3522
  exports.BROWSER_SERVER_EXHAUSTED = BROWSER_SERVER_EXHAUSTED;