@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.
@@ -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. */
@@ -237,6 +251,84 @@ var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
237
251
  var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
238
252
  /** Names an interrupted call whose outcome is unknown. */
239
253
  var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
254
+ /** Names the refusal when every browser is admitted to a holder. */
255
+ var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
256
+ /** Names the refusal for an unknown or ended holder. */
257
+ var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
258
+ /** Defines the holder tools independently of the shared browser's vocabulary. */
259
+ var BROWSER_SERVER_COPY = Object.freeze({
260
+ acquire: Object.freeze({
261
+ name: "acquire",
262
+ description: "Acquires an independent browser and returns its holder id and tool catalog. Destroy the holder when its work ends.",
263
+ parameters: Object.freeze({
264
+ type: "object",
265
+ properties: Object.freeze({ purpose: Object.freeze({
266
+ type: "string",
267
+ description: "The work this browser will serve."
268
+ }) }),
269
+ required: Object.freeze(["purpose"])
270
+ })
271
+ }),
272
+ execute: Object.freeze({
273
+ name: "execute",
274
+ description: "Runs a tool on the selected holder, with that tool’s arguments inside arguments. Get its available tools with tools.",
275
+ parameters: Object.freeze({
276
+ type: "object",
277
+ properties: Object.freeze({
278
+ holder: Object.freeze({
279
+ type: "string",
280
+ description: "The holder id returned by acquire."
281
+ }),
282
+ name: Object.freeze({
283
+ type: "string",
284
+ description: "The tool name from this holder’s catalog."
285
+ }),
286
+ arguments: Object.freeze({
287
+ type: "object",
288
+ description: "The arguments for the selected tool."
289
+ })
290
+ }),
291
+ required: Object.freeze([
292
+ "holder",
293
+ "name",
294
+ "arguments"
295
+ ])
296
+ }),
297
+ annotations: Object.freeze({
298
+ pure: false,
299
+ untrusted: true
300
+ })
301
+ }),
302
+ tools: Object.freeze({
303
+ name: "tools",
304
+ description: "Lists the selected holder’s tools, including tools supplied by its page.",
305
+ parameters: Object.freeze({
306
+ type: "object",
307
+ properties: Object.freeze({ holder: Object.freeze({
308
+ type: "string",
309
+ description: "The holder id returned by acquire."
310
+ }) }),
311
+ required: Object.freeze(["holder"])
312
+ }),
313
+ annotations: Object.freeze({
314
+ pure: true,
315
+ untrusted: true
316
+ })
317
+ }),
318
+ destroy: Object.freeze({
319
+ name: "destroy",
320
+ description: "Ends the selected holder and destroys its browser. Calls still running on that holder report an unresolved outcome.",
321
+ parameters: Object.freeze({
322
+ type: "object",
323
+ properties: Object.freeze({ holder: Object.freeze({
324
+ type: "string",
325
+ description: "The holder id returned by acquire."
326
+ }) }),
327
+ required: Object.freeze(["holder"])
328
+ }),
329
+ annotations: Object.freeze({ pure: false })
330
+ })
331
+ });
240
332
  //#endregion
241
333
  //#region src/server/errors.ts
242
334
  /**
@@ -280,6 +372,25 @@ function isBrowserDestroyedError(value) {
280
372
  //#endregion
281
373
  //#region src/server/helpers.ts
282
374
  /**
375
+ * Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
376
+ * @param value - The viewport environment variable, or undefined when unset
377
+ * @returns The viewport, or undefined when absent or malformed
378
+ * @example
379
+ * parseBrowserViewport('1280x720') // { width: 1280, height: 720 }
380
+ */
381
+ function parseBrowserViewport(value) {
382
+ if (value === void 0) return void 0;
383
+ const match = /^([0-9]+)x([0-9]+)$/u.exec(value);
384
+ if (match === null || match[0] !== value) return void 0;
385
+ const width = (0, _orkestrel_contract.parseInteger)(match[1]);
386
+ const height = (0, _orkestrel_contract.parseInteger)(match[2]);
387
+ if (width === void 0 || height === void 0 || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
388
+ return {
389
+ width,
390
+ height
391
+ };
392
+ }
393
+ /**
283
394
  * Probes whether a process has not been confirmed absent.
284
395
  * @param pid - Process identifier to probe with signal zero
285
396
  * @returns True if the host has not confirmed absence with ESRCH; false otherwise
@@ -945,7 +1056,7 @@ var FileBrowserWriter = class {
945
1056
  };
946
1057
  //#endregion
947
1058
  //#region package.json
948
- var version = "0.0.23";
1059
+ var version = "0.0.25";
949
1060
  //#endregion
950
1061
  //#region src/server/BrowserMCPServer.ts
951
1062
  /**
@@ -960,21 +1071,28 @@ var version = "0.0.23";
960
1071
  * absent. After the launch, a tool the toolset's manager adds under another name, such as a page
961
1072
  * tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
962
1073
  * removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
963
- * client is notified. Startup warms the pool and leases one validated browser. The legacy
964
- * handshake and every tool call await that setup; concurrent callers share a replacement lease.
1074
+ * client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
1075
+ * `BROWSER_SERVER_COPY`. Startup warms the pool and assigns a prepared context to the shared holder. The legacy
1076
+ * handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
1077
+ * and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
1078
+ * closes its context and releases its lease, and admission counts that holder until disposal settles.
965
1079
  * A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
966
1080
  * existing endpoint, opens one page in an isolated context, and constructs the page toolset with
967
1081
  * that context and the file journey and run stores under the root, on a manager the toolset owns.
968
1082
  * A dispatcher forwards its arguments and the call's context, signal included, to the toolset's
969
1083
  * manager and returns that manager's value, or rejects with its failure message; a text value
970
1084
  * reaches the client as one text block rather than as its JSON text.
971
- * 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;
972
1087
  * loss makes its outcome unresolved, while a known success remains successful. Pending losses
973
1088
  * prefix the next outcome on a successor, or a refusal, and calls are never repeated.
1089
+ * Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
1090
+ * its deadline and records loss for the next call.
974
1091
  *
975
1092
  * `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
976
1093
  * `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
977
- * 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
978
1096
  * profile — and rechecks the profile folders this server made or kept.
979
1097
  *
980
1098
  * Chromium on Linux refuses to start as the root user unless its sandbox is disabled, so a server
@@ -994,6 +1112,7 @@ var BrowserMCPServer = class {
994
1112
  #root;
995
1113
  #headless;
996
1114
  #executable;
1115
+ #viewport;
997
1116
  #readonly;
998
1117
  #launcher;
999
1118
  #input;
@@ -1002,22 +1121,40 @@ var BrowserMCPServer = class {
1002
1121
  #abort = new AbortController();
1003
1122
  #signal;
1004
1123
  #vocabulary;
1005
- #added;
1006
- #removed;
1007
- #cleared;
1124
+ #mirrored;
1008
1125
  #pool;
1009
1126
  #log;
1010
1127
  #folders = /* @__PURE__ */ new Set();
1011
1128
  #faults = /* @__PURE__ */ new Set();
1012
1129
  #losses = /* @__PURE__ */ new WeakMap();
1130
+ #departures = /* @__PURE__ */ new WeakMap();
1131
+ #pings = /* @__PURE__ */ new WeakMap();
1013
1132
  #watches = /* @__PURE__ */ new Map();
1014
- #notice = /* @__PURE__ */ new Set();
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();
1140
+ #shared = {
1141
+ id: "shared",
1142
+ purpose: "Shared browser",
1143
+ abort: this.#abort
1144
+ };
1145
+ #holders = /* @__PURE__ */ new Map([[this.#shared.id, this.#shared]]);
1146
+ #leases = /* @__PURE__ */ new Map();
1147
+ #grants = /* @__PURE__ */ new Map();
1148
+ #notices = /* @__PURE__ */ new Map();
1149
+ #disposals = /* @__PURE__ */ new Map();
1150
+ #drains = /* @__PURE__ */ new Map();
1151
+ #readers = /* @__PURE__ */ new Map();
1152
+ #writers = /* @__PURE__ */ new Set();
1153
+ #admission;
1015
1154
  #reference;
1016
1155
  #references = 0;
1017
1156
  #failure;
1018
1157
  #stranded;
1019
- #lease;
1020
- #granting;
1021
1158
  #starting;
1022
1159
  #sweeping;
1023
1160
  #closing;
@@ -1028,30 +1165,51 @@ var BrowserMCPServer = class {
1028
1165
  */
1029
1166
  constructor(options) {
1030
1167
  const size = options?.pool?.size ?? 1;
1168
+ const contexts = options?.pool?.contexts ?? 2;
1031
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);
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;
1032
1172
  this.#log = options?.log ?? process.stderr;
1033
1173
  this.#reference = this.#issue.bind(this);
1034
1174
  this.#pool = (0, _orkestrel_pool.createPool)({
1035
1175
  create: this.#warm.bind(this),
1036
- destroy: (slot) => this.#destroySlot(slot.profile, slot.browser, slot.toolset, this.#losses.get(slot)?.cause),
1176
+ destroy: this.#destroyRecord.bind(this),
1037
1177
  validate: this.#validate.bind(this),
1038
1178
  watch: this.#watch.bind(this),
1039
1179
  error: this.#fault.bind(this),
1040
1180
  min: size,
1181
+ capacity: contexts,
1041
1182
  restarts: 1
1042
1183
  });
1043
1184
  this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
1044
1185
  this.#headless = options?.headless ?? true;
1045
1186
  this.#executable = options?.executable;
1187
+ this.#viewport = options?.viewport;
1046
1188
  this.#readonly = options?.readonly ?? false;
1047
1189
  this.#launcher = options?.launch ?? createBrowser;
1048
1190
  this.#input = options?.stdio?.input ?? process.stdin;
1049
1191
  this.#tools = (0, _orkestrel_tool.createToolManager)();
1050
1192
  const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
1051
- this.#vocabulary = new Set(names);
1193
+ this.#vocabulary = /* @__PURE__ */ new Set([...names, ...Object.keys(BROWSER_SERVER_COPY)]);
1052
1194
  for (const name of names) this.#tools.add((0, _orkestrel_tool.createTool)({
1053
1195
  ..._src_core.BROWSER_TOOL_COPY[name],
1054
- execute: this.#forward.bind(this, name)
1196
+ execute: this.#forward.bind(this, this.#shared, name)
1197
+ }));
1198
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1199
+ ...BROWSER_SERVER_COPY.acquire,
1200
+ execute: this.#acquire.bind(this)
1201
+ }));
1202
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1203
+ ...BROWSER_SERVER_COPY.execute,
1204
+ execute: this.#dispatch.bind(this)
1205
+ }));
1206
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1207
+ ...BROWSER_SERVER_COPY.tools,
1208
+ execute: this.#catalog.bind(this)
1209
+ }));
1210
+ this.#tools.add((0, _orkestrel_tool.createTool)({
1211
+ ...BROWSER_SERVER_COPY.destroy,
1212
+ execute: this.#dispose.bind(this)
1055
1213
  }));
1056
1214
  const server = (0, _orkestrel_mcp.createMCPServer)({
1057
1215
  identity: {
@@ -1067,9 +1225,6 @@ var BrowserMCPServer = class {
1067
1225
  ...options?.stdio?.output === void 0 ? {} : { output: options.stdio.output }
1068
1226
  });
1069
1227
  this.#signal = this.#end.bind(this);
1070
- this.#added = this.#mirror.bind(this);
1071
- this.#removed = this.#withdraw.bind(this);
1072
- this.#cleared = this.#clear.bind(this);
1073
1228
  }
1074
1229
  async start() {
1075
1230
  if (this.#closing !== void 0) throw this.#ended();
@@ -1090,7 +1245,7 @@ var BrowserMCPServer = class {
1090
1245
  if (this.#closing !== void 0) return;
1091
1246
  this.#sweeping = this.#sweep();
1092
1247
  const warming = this.#pool.start().then(void 0, (error) => error);
1093
- await this.#grant(this.#abort.signal);
1248
+ await this.#grant(this.#shared, this.#abort.signal);
1094
1249
  if (this.#closing !== void 0) return;
1095
1250
  warming.then(this.#exhaust.bind(this));
1096
1251
  } catch (error) {
@@ -1106,13 +1261,18 @@ var BrowserMCPServer = class {
1106
1261
  this.#transport.stop();
1107
1262
  for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
1108
1263
  this.#unmirror();
1109
- this.#lease = void 0;
1264
+ for (const generation of this.#observed.keys()) this.#untrack(generation);
1265
+ this.#leases.clear();
1266
+ for (const holder of this.#holders.values()) holder.abort.abort();
1110
1267
  this.#abort.abort();
1111
1268
  const barrier = this.#pool.destroy().catch((error) => {
1112
1269
  for (const failure of (0, _orkestrel_pool.isPoolError)(error) ? error.context?.failures ?? [error] : [error]) this.#faults.add(failure);
1113
1270
  });
1114
1271
  await this.#starting?.catch(() => void 0);
1115
1272
  await barrier;
1273
+ await Promise.allSettled(this.#grants.values());
1274
+ await Promise.allSettled(this.#disposals.values());
1275
+ this.#holders.clear();
1116
1276
  await this.#sweeping;
1117
1277
  if (this.#stranded !== void 0) this.#faults.add(this.#stranded);
1118
1278
  for (const folder of this.#folders) {
@@ -1157,22 +1317,143 @@ var BrowserMCPServer = class {
1157
1317
  process.exitCode = 1;
1158
1318
  this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
1159
1319
  }
1160
- async #forward(name, args, context) {
1161
- let slot;
1320
+ async #acquire(args, context) {
1321
+ context.signal.throwIfAborted();
1322
+ if (this.#closing !== void 0) throw this.#ended();
1323
+ const purpose = args["purpose"];
1324
+ if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
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);
1326
+ const holder = {
1327
+ id: (0, node_crypto.randomUUID)(),
1328
+ purpose,
1329
+ abort: new AbortController()
1330
+ };
1331
+ this.#holders.set(holder.id, holder);
1332
+ const listener = (0, node_events.addAbortListener)(context.signal, () => holder.abort.abort(context.signal.reason));
1333
+ try {
1334
+ const lease = await this.#serve(holder, context.signal);
1335
+ context.signal.throwIfAborted();
1336
+ if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
1337
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1338
+ const catalog = {
1339
+ holder: holder.id,
1340
+ tools: lease.generation.toolset.tools.definitions()
1341
+ };
1342
+ return JSON.stringify(catalog);
1343
+ } catch (error) {
1344
+ await this.#retire(holder).catch(this.#fault.bind(this));
1345
+ throw error;
1346
+ } finally {
1347
+ listener[Symbol.dispose]();
1348
+ }
1349
+ }
1350
+ #holder(value) {
1351
+ const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
1352
+ if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder`, BROWSER_SERVER_HOLDER);
1353
+ return holder;
1354
+ }
1355
+ async #dispatch(args, context) {
1356
+ const holder = this.#holder(args["holder"]);
1357
+ const name = args["name"];
1358
+ const parameters = args["arguments"];
1359
+ if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
1360
+ return this.#forward(holder, name, parameters, context);
1361
+ }
1362
+ async #catalog(args, context) {
1363
+ const holder = this.#holder(args["holder"]);
1364
+ const signal = AbortSignal.any([context.signal, holder.abort.signal]);
1365
+ try {
1366
+ const lease = await this.#serve(holder, signal);
1367
+ signal.throwIfAborted();
1368
+ if (this.#leases.get(holder) !== lease) throw this.#unavailable(this.#departures.get(lease)?.cause);
1369
+ const catalog = {
1370
+ holder: holder.id,
1371
+ tools: lease.generation.toolset.tools.definitions()
1372
+ };
1373
+ return this.#annotate(holder, JSON.stringify(catalog));
1374
+ } catch (error) {
1375
+ context.signal.throwIfAborted();
1376
+ if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
1377
+ if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1378
+ throw error;
1379
+ }
1380
+ }
1381
+ async #dispose(args) {
1382
+ const id = args["holder"];
1383
+ await (((0, _orkestrel_contract.isString)(id) ? this.#disposals.get(id) : void 0) ?? this.#retire(this.#holder(id)));
1384
+ return "Holder destroyed.";
1385
+ }
1386
+ #retire(holder) {
1387
+ const pending = this.#disposals.get(holder.id);
1388
+ if (pending !== void 0) return pending;
1389
+ holder.abort.abort();
1390
+ const lease = this.#leases.get(holder);
1391
+ this.#leases.delete(holder);
1392
+ if (lease !== void 0) this.#untrack(lease.generation);
1393
+ const disposal = Promise.resolve().then(async () => {
1394
+ try {
1395
+ await this.#grants.get(holder)?.catch(() => void 0);
1396
+ if (lease !== void 0) await this.#release(holder, lease);
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));
1399
+ } catch (error) {
1400
+ this.#faults.add(error);
1401
+ } finally {
1402
+ if (this.#holders.get(holder.id) === holder && !this.#retentions.has(holder)) this.#holders.delete(holder.id);
1403
+ this.#notices.delete(holder);
1404
+ this.#drains.delete(holder);
1405
+ }
1406
+ });
1407
+ this.#disposals.set(holder.id, disposal);
1408
+ return disposal;
1409
+ }
1410
+ async #forward(holder, name, args, context) {
1411
+ context.signal.throwIfAborted();
1412
+ const journey = args["journey"];
1413
+ if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
1414
+ if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError(`BROWSER_JOURNEY_LOCKED: Journey ${journey} is locked; call ${name} again.`, "BROWSER_JOURNEY_LOCKED", { name: journey });
1415
+ const reader = Symbol(journey);
1416
+ if (name === "forget") this.#writers.add(journey);
1417
+ else {
1418
+ const readers = this.#readers.get(journey) ?? /* @__PURE__ */ new Set();
1419
+ readers.add(reader);
1420
+ this.#readers.set(journey, readers);
1421
+ }
1422
+ try {
1423
+ return await this.#perform(holder, name, args, context);
1424
+ } finally {
1425
+ if (name === "forget") this.#writers.delete(journey);
1426
+ else {
1427
+ const readers = this.#readers.get(journey);
1428
+ readers?.delete(reader);
1429
+ if (readers?.size === 0) this.#readers.delete(journey);
1430
+ }
1431
+ }
1432
+ }
1433
+ async #perform(holder, name, args, context) {
1434
+ const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
1435
+ let lease;
1162
1436
  try {
1163
- slot = await this.#serve(context.signal);
1437
+ lease = await this.#serve(holder, signal);
1164
1438
  } catch (error) {
1165
1439
  context.signal.throwIfAborted();
1166
- if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(`${error.code}: ${error.message}`), error.code);
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);
1441
+ if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
1167
1442
  throw error;
1168
1443
  }
1169
1444
  let result;
1170
1445
  try {
1171
- result = await slot.toolset.tools.execute({
1446
+ const execution = lease.generation.toolset.tools.execute({
1172
1447
  id: name,
1173
1448
  name,
1174
1449
  arguments: args
1175
- }, context);
1450
+ }, {
1451
+ ...context,
1452
+ signal
1453
+ });
1454
+ const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
1455
+ if (outcome === void 0) throw this.#ended();
1456
+ result = outcome;
1176
1457
  } catch (error) {
1177
1458
  result = {
1178
1459
  id: name,
@@ -1182,28 +1463,27 @@ var BrowserMCPServer = class {
1182
1463
  };
1183
1464
  }
1184
1465
  context.signal.throwIfAborted();
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);
1185
1467
  if (!result.success) {
1186
- if (this.#lease?.value === slot && !context.signal.aborted) try {
1187
- await slot.browser.ping({ signal: AbortSignal.any([context.signal, this.#abort.signal]) });
1188
- } catch (error) {
1189
- if (!context.signal.aborted && !this.#abort.signal.aborted) this.#lose(slot, error);
1190
- }
1468
+ if (this.#leases.get(holder) === lease && !context.signal.aborted) try {
1469
+ await this.#race(this.#ping(lease.token.value), context.signal);
1470
+ } catch {}
1191
1471
  context.signal.throwIfAborted();
1192
- if (this.#lease?.value !== slot) {
1193
- const loss = this.#losses.get(slot);
1194
- throw new _src_core.BrowserError(this.#annotate(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);
1195
1475
  }
1196
- throw new _src_core.BrowserError(this.#annotate(result.error, slot));
1476
+ throw new _src_core.BrowserError(this.#annotate(holder, result.error, lease));
1197
1477
  }
1198
- return typeof result.value === "string" ? this.#annotate(result.value, slot) : result.value;
1478
+ return typeof result.value === "string" ? this.#annotate(holder, result.value, lease) : result.value;
1199
1479
  }
1200
- #annotate(value, slot) {
1480
+ #annotate(holder, value, lease) {
1201
1481
  const notes = [];
1202
- for (const lost of this.#notice) {
1203
- if (lost === slot) continue;
1204
- const loss = this.#losses.get(lost);
1482
+ for (const lost of this.#notices.get(holder) ?? []) {
1483
+ if (lost === lease) continue;
1484
+ const loss = this.#departures.get(lost);
1205
1485
  notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
1206
- this.#notice.delete(lost);
1486
+ this.#notices.get(holder)?.delete(lost);
1207
1487
  }
1208
1488
  return [...notes, value].join("\n");
1209
1489
  }
@@ -1227,52 +1507,95 @@ var BrowserMCPServer = class {
1227
1507
  listener[Symbol.dispose]();
1228
1508
  }
1229
1509
  }
1230
- async #grant(signal) {
1231
- if (this.#lease !== void 0) return this.#lease;
1232
- this.#granting ??= this.#pool.acquire(this.#abort.signal).then(this.#hold.bind(this), this.#refuse.bind(this));
1233
- const token = await this.#race(this.#granting, signal);
1234
- if (token === void 0) throw this.#ended();
1235
- return token;
1510
+ async #grant(holder, signal) {
1511
+ const held = this.#leases.get(holder);
1512
+ if (held !== void 0) return held;
1513
+ let granting = this.#grants.get(holder);
1514
+ if (granting === void 0) {
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
+ });
1521
+ this.#grants.set(holder, granting);
1522
+ granting.finally(() => {
1523
+ if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
1524
+ }).catch(() => void 0);
1525
+ }
1526
+ const lease = await this.#race(granting, signal);
1527
+ if (lease === void 0) throw this.#ended();
1528
+ return lease;
1529
+ }
1530
+ async #hold(holder, token) {
1531
+ const slot = token.value;
1532
+ if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
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();
1539
+ throw this.#ended();
1540
+ }
1541
+ if (this.#losses.has(slot)) {
1542
+ token.release();
1543
+ throw this.#unavailable(this.#losses.get(slot)?.cause);
1544
+ }
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);
1564
+ if (holder === this.#shared) {
1565
+ const tools = generation.toolset.tools;
1566
+ const mirrored = {
1567
+ lease,
1568
+ added: this.#mirror.bind(this, lease),
1569
+ removed: this.#withdraw.bind(this, lease),
1570
+ cleared: this.#clear.bind(this, lease)
1571
+ };
1572
+ this.#mirrored = mirrored;
1573
+ for (const tool of tools.tools()) this.#mirror(lease, tool);
1574
+ tools.emitter.on("add", mirrored.added);
1575
+ tools.emitter.on("remove", mirrored.removed);
1576
+ tools.emitter.on("clear", mirrored.cleared);
1577
+ }
1578
+ return lease;
1236
1579
  }
1237
- #hold(token) {
1238
- this.#granting = void 0;
1239
- if (this.#closing !== void 0) throw this.#ended();
1240
- if (this.#losses.has(token.value)) {
1241
- token.destroy().catch(() => void 0);
1242
- throw this.#unavailable(this.#losses.get(token.value)?.cause);
1243
- }
1244
- this.#lease = token;
1245
- const tools = token.value.toolset.tools;
1246
- for (const tool of tools.tools()) this.#mirror(tool);
1247
- tools.emitter.on("add", this.#added);
1248
- tools.emitter.on("remove", this.#removed);
1249
- tools.emitter.on("clear", this.#cleared);
1250
- return token;
1251
- }
1252
- #refuse(error) {
1253
- this.#granting = void 0;
1254
- throw error;
1255
- }
1256
- async #serve(signal) {
1257
- if (this.#closing !== void 0) throw this.#ended();
1580
+ async #serve(holder, signal) {
1581
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1258
1582
  await this.#race(this.#starting, signal);
1259
- if (this.#closing !== void 0) throw this.#ended();
1260
- const held = this.#lease;
1583
+ if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
1584
+ const held = this.#leases.get(holder);
1261
1585
  if (held !== void 0) try {
1262
- await held.value.browser.ping({ signal: AbortSignal.any([signal, this.#abort.signal]) });
1263
- if (this.#lease === held) return held.value;
1264
- } catch (error) {
1586
+ await this.#race(this.#ping(held.token.value), signal);
1587
+ if (this.#leases.get(holder) === held) return held;
1588
+ } catch {
1265
1589
  signal.throwIfAborted();
1266
- if (this.#abort.signal.aborted) throw this.#ended();
1267
- this.#lose(held.value, error);
1590
+ if (holder.abort.signal.aborted) throw this.#ended();
1268
1591
  }
1269
1592
  try {
1270
- const token = await this.#grant(signal);
1271
- if (this.#lease !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
1272
- 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;
1273
1596
  } catch (error) {
1274
1597
  signal.throwIfAborted();
1275
- if (this.#abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
1598
+ if (holder.abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
1276
1599
  if ((0, _orkestrel_pool.isPoolError)(error) && (error.code === "create" || error.code === "cleanup")) {
1277
1600
  this.#exhaust(error);
1278
1601
  throw this.#unavailable(error.cause ?? this.#failure);
@@ -1280,6 +1603,15 @@ var BrowserMCPServer = class {
1280
1603
  throw error;
1281
1604
  }
1282
1605
  }
1606
+ #ping(slot) {
1607
+ const pending = this.#pings.get(slot);
1608
+ if (pending !== void 0) return pending;
1609
+ const ping = slot.browser.ping({ signal: this.#abort.signal }).catch((error) => {
1610
+ if (!this.#abort.signal.aborted) this.#lose(slot, error);
1611
+ }).finally(() => this.#pings.delete(slot));
1612
+ this.#pings.set(slot, ping);
1613
+ return ping;
1614
+ }
1283
1615
  async #validate(slot) {
1284
1616
  try {
1285
1617
  await slot.browser.ping({ signal: this.#abort.signal });
@@ -1291,71 +1623,211 @@ var BrowserMCPServer = class {
1291
1623
  }
1292
1624
  #lose(slot, cause) {
1293
1625
  if (this.#closing !== void 0 || this.#losses.has(slot)) return;
1294
- this.#losses.set(slot, {
1626
+ this.#losses.set(slot, { cause });
1627
+ this.#failure = cause;
1628
+ for (const [holder, lease] of this.#leases) {
1629
+ if (lease.token.value !== slot) continue;
1630
+ this.#detach(holder, lease, cause);
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, {
1295
1639
  cause,
1296
- url: slot.toolset.view.url
1640
+ url: lease.generation.toolset.view.url
1297
1641
  });
1298
- this.#failure = cause;
1299
- if (this.#lease?.value !== slot) return;
1300
- const token = this.#lease;
1301
- this.#unmirror();
1302
- this.#lease = void 0;
1303
- this.#notice.add(slot);
1304
- token.destroy().catch(() => void 0);
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);
1305
1648
  }
1306
1649
  #watch(slot, signal) {
1307
1650
  const loss = Promise.withResolvers();
1308
1651
  const disconnect = this.#disconnect.bind(this, slot);
1309
- const page = this.#track.bind(this, slot);
1310
1652
  const subscription = (0, node_events.addAbortListener)(signal, this.#unwatch.bind(this, slot));
1311
1653
  this.#watches.set(slot, {
1312
1654
  resolve: loss.resolve,
1313
1655
  disconnect,
1314
- page,
1315
- subscription,
1316
- crashes: /* @__PURE__ */ new Map()
1656
+ subscription
1317
1657
  });
1318
1658
  slot.browser.emitter.on("disconnect", disconnect);
1319
- slot.context.emitter.on("page", page);
1320
- for (const current of slot.context.pages()) this.#track(slot, current);
1321
1659
  if (signal.aborted) this.#unwatch(slot);
1322
1660
  else if (slot.browser.status !== "connected") this.#disconnect(slot);
1323
1661
  return loss.promise;
1324
1662
  }
1325
- #track(slot, page) {
1326
- 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);
1327
1675
  if (watch === void 0 || watch.crashes.has(page)) return;
1328
- const crash = this.#createCrash(slot, page);
1676
+ const crash = this.#crash.bind(this, slot, generation, page);
1329
1677
  page.emitter.on("crash", crash);
1330
- this.#watches.set(slot, {
1678
+ this.#observed.set(generation, {
1331
1679
  ...watch,
1332
1680
  crashes: new Map([...watch.crashes, [page, crash]])
1333
1681
  });
1334
1682
  }
1335
- #createCrash(slot, page) {
1336
- return () => {
1337
- if (page === slot.toolset.view) this.#report(slot, /* @__PURE__ */ new Error("The current page renderer crashed"));
1338
- };
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);
1339
1706
  }
1340
1707
  #disconnect(slot) {
1341
- this.#report(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1342
- }
1343
- #report(slot, cause) {
1344
- const watch = this.#watches.get(slot);
1345
- if (watch === void 0) return;
1346
- this.#unwatch(slot);
1347
- this.#lose(slot, cause);
1348
- watch.resolve(cause);
1708
+ this.#lose(slot, /* @__PURE__ */ new Error(slot.browser.pid === void 0 ? "The browser process exited" : "The browser transport disconnected"));
1349
1709
  }
1350
1710
  #unwatch(slot) {
1351
1711
  const watch = this.#watches.get(slot);
1352
1712
  if (watch === void 0) return;
1353
1713
  this.#watches.delete(slot);
1354
1714
  slot.browser.emitter.off("disconnect", watch.disconnect);
1355
- slot.context.emitter.off("page", watch.page);
1356
- for (const [page, crash] of watch.crashes) page.emitter.off("crash", crash);
1357
1715
  watch.subscription[Symbol.dispose]();
1358
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
+ }
1359
1831
  #fault(error) {
1360
1832
  this.#faults.add(error);
1361
1833
  }
@@ -1364,7 +1836,7 @@ var BrowserMCPServer = class {
1364
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);
1365
1837
  const profile = (0, node_path.join)(this.#root, ".profiles", formatBrowserLockEntry(process.pid, (0, node_crypto.randomUUID)()));
1366
1838
  let browser;
1367
- let toolset;
1839
+ let slot;
1368
1840
  try {
1369
1841
  await (0, node_fs_promises.mkdir)(profile);
1370
1842
  this.#folders.add(profile);
@@ -1374,6 +1846,7 @@ var BrowserMCPServer = class {
1374
1846
  cdp: { discover: false },
1375
1847
  signal: this.#abort.signal,
1376
1848
  ...this.#executable === void 0 ? {} : { executable: this.#executable },
1849
+ ...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
1377
1850
  ...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
1378
1851
  });
1379
1852
  await browser.connect();
@@ -1385,38 +1858,26 @@ var BrowserMCPServer = class {
1385
1858
  }));
1386
1859
  await (0, node_fs_promises.rename)(`${record}.tmp`, record);
1387
1860
  }
1388
- const context = await browser.isolate({ reference: this.#reference });
1389
- const page = await context.create();
1390
- toolset = (0, _src_core.createBrowserToolset)(page, {
1391
- context,
1392
- journeys: {
1393
- store: createFileBrowserJourneyStore({ root: this.#root }),
1394
- runs: createFileBrowserRunStore({ root: this.#root }),
1395
- readonly: this.#readonly
1396
- }
1397
- });
1398
- await toolset.start();
1399
- return {
1861
+ slot = {
1400
1862
  browser,
1401
- profile,
1402
- context,
1403
- toolset
1863
+ profile
1404
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;
1405
1872
  } catch (error) {
1406
- 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) => {
1407
1874
  this.#stranded = failure;
1408
1875
  });
1409
1876
  if (this.#closing === void 0) this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_LAUNCH, error)}`);
1410
1877
  throw error;
1411
1878
  }
1412
1879
  }
1413
- async #destroySlot(profile, browser, toolset, cause) {
1414
- if ((0, _src_core.isCDPTimeoutError)(cause) && browser?.pid !== void 0) try {
1415
- process.kill(browser.pid, "SIGKILL");
1416
- } catch (error) {
1417
- if (!(0, _orkestrel_contract.isError)(error) || !("code" in error) || error.code !== "ESRCH") this.#faults.add(error);
1418
- }
1419
- await toolset?.destroy().catch((error) => this.#faults.add(error));
1880
+ async #destroySlot(profile, browser) {
1420
1881
  try {
1421
1882
  await browser?.destroy();
1422
1883
  } catch (error) {
@@ -1492,29 +1953,31 @@ var BrowserMCPServer = class {
1492
1953
  #issue() {
1493
1954
  return `e${++this.#references}`;
1494
1955
  }
1495
- #mirror(tool) {
1496
- if (this.#vocabulary.has(tool.name)) return;
1956
+ #mirror(lease, tool) {
1957
+ if (this.#leases.get(this.#shared) !== lease || this.#vocabulary.has(tool.name)) return;
1497
1958
  this.#tools.add((0, _orkestrel_tool.createTool)({
1498
1959
  ...(0, _orkestrel_tool.toolToDefinition)(tool),
1499
- execute: this.#forward.bind(this, tool.name)
1960
+ execute: this.#forward.bind(this, this.#shared, tool.name)
1500
1961
  }));
1501
1962
  }
1502
- #withdraw(tool) {
1503
- if (this.#vocabulary.has(tool.name)) return;
1504
- const current = this.#lease?.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);
1505
1966
  if (current === void 0) this.#tools.remove(tool.name);
1506
- else this.#mirror(current);
1967
+ else this.#mirror(lease, current);
1507
1968
  }
1508
- #clear(tools) {
1509
- for (const tool of tools) this.#withdraw(tool);
1969
+ #clear(lease, tools) {
1970
+ for (const tool of tools) this.#withdraw(lease, tool);
1510
1971
  }
1511
1972
  #unmirror() {
1512
- const mirrored = this.#lease?.value.toolset.tools;
1973
+ const mirrored = this.#mirrored;
1513
1974
  if (mirrored === void 0) return;
1514
- for (const tool of mirrored.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1515
- mirrored.emitter.off("add", this.#added);
1516
- mirrored.emitter.off("remove", this.#removed);
1517
- mirrored.emitter.off("clear", this.#cleared);
1975
+ this.#mirrored = void 0;
1976
+ const tools = mirrored.lease.generation.toolset.tools;
1977
+ for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
1978
+ tools.emitter.off("add", mirrored.added);
1979
+ tools.emitter.off("remove", mirrored.removed);
1980
+ tools.emitter.off("clear", mirrored.cleared);
1518
1981
  }
1519
1982
  #ended() {
1520
1983
  return new _src_core.BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
@@ -1941,30 +2404,62 @@ var FileBrowserJourneyStore = class {
1941
2404
  /**
1942
2405
  * Persists runs and captures only in directories allocated by this instance.
1943
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.
1944
2409
  * @example
1945
2410
  * const store = new FileBrowserRunStore({ root: directory })
1946
2411
  * const slot = await store.open('check-ready')
1947
2412
  */
1948
- var FileBrowserRunStore = class {
2413
+ var FileBrowserRunStore = class FileBrowserRunStore {
2414
+ static #allocations = /* @__PURE__ */ new Map();
1949
2415
  #files;
1950
2416
  #slots = /* @__PURE__ */ new WeakMap();
1951
2417
  #directories = /* @__PURE__ */ new Set();
1952
2418
  constructor(options) {
1953
2419
  this.#files = new FileBrowserStore(options);
1954
2420
  }
2421
+ async snapshot(bytes, options) {
2422
+ options?.signal?.throwIfAborted();
2423
+ const owned = new Uint8Array(bytes);
2424
+ const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
2425
+ const path = this.#files.resolvePath(directory.path, "view.png");
2426
+ try {
2427
+ await this.#files.check(path, options);
2428
+ options?.signal?.throwIfAborted();
2429
+ await (0, node_fs_promises.writeFile)(path, owned, {
2430
+ flag: "wx",
2431
+ signal: options?.signal
2432
+ });
2433
+ return path;
2434
+ } catch (error) {
2435
+ options?.signal?.throwIfAborted();
2436
+ throw this.#files.translateError(path, error);
2437
+ }
2438
+ }
1955
2439
  async open(name, options) {
1956
2440
  options?.signal?.throwIfAborted();
1957
2441
  this.#files.validateName(name);
1958
- return this.#files.lock(this.#files.resolvePath(name, BROWSER_JOURNEY_LOCK_DIRECTORY), async () => {
1959
- const directory = await this.#files.allocate(this.#files.resolvePath(name, BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
1960
- const slot = Object.freeze({
1961
- id: directory.id,
1962
- directory: directory.path
1963
- });
1964
- this.#directories.add(directory.path);
1965
- this.#slots.set(slot, directory.path);
1966
- return slot;
1967
- }, 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
+ }
1968
2463
  }
1969
2464
  async get(name, id, options) {
1970
2465
  options?.signal?.throwIfAborted();
@@ -2174,6 +2669,7 @@ var Browser = class {
2174
2669
  #servingPid;
2175
2670
  #profile;
2176
2671
  #contexts = [];
2672
+ #isolating = /* @__PURE__ */ new Set();
2177
2673
  #destroyed = false;
2178
2674
  #connecting = new _src_core.BrowserTransition();
2179
2675
  #disconnecting = new _src_core.BrowserTransition();
@@ -2260,33 +2756,13 @@ var Browser = class {
2260
2756
  return [...this.#contexts];
2261
2757
  }
2262
2758
  async isolate(options) {
2263
- if (this.#destroyed) throw new BrowserDestroyedError();
2264
- const client = this.#client;
2265
- if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
2266
- (0, _src_core.validateBrowserContextOptions)(options);
2267
- const params = { disposeOnDetach: false };
2268
- if (options?.proxy !== void 0) {
2269
- params["proxyServer"] = options.proxy.server;
2270
- if (options.proxy.bypass !== void 0) params["proxyBypassList"] = options.proxy.bypass.join(",");
2271
- }
2272
- if (options?.origins !== void 0) params["originsWithUniversalNetworkAccess"] = [...options.origins];
2273
- const result = await client.send("Target.createBrowserContext", params);
2274
- if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isString)(result["browserContextId"])) throw new _src_core.BrowserConnectionError("Failed to create isolated browser context");
2275
- const id = result["browserContextId"];
2276
- 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);
2277
2761
  try {
2278
- if (options?.downloads !== void 0) await client.send("Browser.setDownloadBehavior", {
2279
- behavior: options.downloads.named === true ? "allowAndName" : "allow",
2280
- browserContextId: id,
2281
- downloadPath: options.downloads.path,
2282
- eventsEnabled: true
2283
- });
2284
- } catch (error) {
2285
- await context.close().catch(() => void 0);
2286
- throw error;
2762
+ return await attempt;
2763
+ } finally {
2764
+ this.#isolating.delete(attempt);
2287
2765
  }
2288
- this.#registerContext(context);
2289
- return context;
2290
2766
  }
2291
2767
  async create(options) {
2292
2768
  if (this.#destroyed) throw new BrowserDestroyedError();
@@ -2321,6 +2797,37 @@ var Browser = class {
2321
2797
  this.#shutdown = shutdown;
2322
2798
  return shutdown;
2323
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
+ }
2324
2831
  async #establish() {
2325
2832
  const disconnecting = this.#disconnecting.pending;
2326
2833
  if (disconnecting !== void 0) await disconnecting;
@@ -2794,6 +3301,7 @@ var Browser = class {
2794
3301
  }
2795
3302
  }
2796
3303
  async #settle() {
3304
+ await Promise.allSettled(this.#isolating);
2797
3305
  await this.#connecting.pending?.catch(() => void 0);
2798
3306
  await this.#disconnecting.pending?.catch(() => void 0);
2799
3307
  await this.#settleExit();
@@ -3006,8 +3514,13 @@ exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
3006
3514
  exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
3007
3515
  exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
3008
3516
  exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
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;
3520
+ exports.BROWSER_SERVER_COPY = BROWSER_SERVER_COPY;
3009
3521
  exports.BROWSER_SERVER_CRASH = BROWSER_SERVER_CRASH;
3010
3522
  exports.BROWSER_SERVER_EXHAUSTED = BROWSER_SERVER_EXHAUSTED;
3523
+ exports.BROWSER_SERVER_HOLDER = BROWSER_SERVER_HOLDER;
3011
3524
  exports.BROWSER_SERVER_LAUNCH = BROWSER_SERVER_LAUNCH;
3012
3525
  exports.BROWSER_SERVER_OPTIONS = BROWSER_SERVER_OPTIONS;
3013
3526
  exports.BROWSER_SERVER_POOL_LIMIT = BROWSER_SERVER_POOL_LIMIT;
@@ -3061,6 +3574,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
3061
3574
  exports.parseBrowserEngine = parseBrowserEngine;
3062
3575
  exports.parseBrowserLockEntry = parseBrowserLockEntry;
3063
3576
  exports.parseBrowserProfileRecord = parseBrowserProfileRecord;
3577
+ exports.parseBrowserViewport = parseBrowserViewport;
3064
3578
  exports.probePathNames = probePathNames;
3065
3579
  exports.probeProcess = probeProcess;
3066
3580
  exports.readBrowserEndpoint = readBrowserEndpoint;