@mind-harness/mh-browser 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -33,6 +33,146 @@ function defaultProfileDir(profile, home = resolveDshHome()) {
33
33
  return profile === void 0 ? join(home, "mh", "browser") : join(home, "mh", "profiles", profile.name, "browser");
34
34
  }
35
35
  //#endregion
36
+ //#region lib/types/types.js
37
+ var MhBrowserError = class extends Error {
38
+ code;
39
+ page;
40
+ constructor(code, message, page) {
41
+ super(message);
42
+ this.name = "MhBrowserError";
43
+ this.code = code;
44
+ if (page !== void 0) this.page = page;
45
+ }
46
+ };
47
+ /** Owner of the person's own window. It belongs to no Session. */
48
+ const PERSON_OWNER = "person";
49
+ const UNKNOWN_TAB = "UNKNOWN_TAB";
50
+ const UNKNOWN_TAB_REASON = "no such tab in this session";
51
+ /**
52
+ * Internal: a wait ended because its signal aborted. A tool never reports it:
53
+ * a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
54
+ */
55
+ const INTERRUPTED = "INTERRUPTED";
56
+ /** The browser did not start or could not be restarted. */
57
+ const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
58
+ function unknownTab() {
59
+ return {
60
+ url: "",
61
+ reason: UNKNOWN_TAB_REASON
62
+ };
63
+ }
64
+ //#endregion
65
+ //#region lib/types/service/abortable.js
66
+ const WAITING_STOPPED$1 = "stopped while waiting";
67
+ /** A wait ended because its signal aborted, including a deadline synthesized here. */
68
+ function stopped$1() {
69
+ return new MhBrowserError(INTERRUPTED, WAITING_STOPPED$1);
70
+ }
71
+ function isInterrupted(error) {
72
+ return error instanceof MhBrowserError && error.code === "INTERRUPTED";
73
+ }
74
+ /**
75
+ * `promise`, or a rejection when `signal` aborts. The original promise is
76
+ * always observed, so abandoning it leaves no unhandled rejection.
77
+ */
78
+ function untilAborted(promise, signal) {
79
+ const settled = promise.then((value) => ({
80
+ ok: true,
81
+ value
82
+ }), (error) => ({
83
+ ok: false,
84
+ error
85
+ }));
86
+ if (signal === void 0) return unwrap(settled);
87
+ return new Promise((resolve, reject) => {
88
+ let done = false;
89
+ const finish = (run) => {
90
+ if (done) return;
91
+ done = true;
92
+ signal.removeEventListener("abort", onAbort);
93
+ run();
94
+ };
95
+ const onAbort = () => {
96
+ finish(() => reject(stopped$1()));
97
+ };
98
+ if (signal.aborted) {
99
+ finish(() => reject(stopped$1()));
100
+ return;
101
+ }
102
+ signal.addEventListener("abort", onAbort, { once: true });
103
+ settled.then((result) => {
104
+ finish(() => {
105
+ if (result.ok) resolve(result.value);
106
+ else reject(result.error);
107
+ });
108
+ });
109
+ });
110
+ }
111
+ /** `signal`, a deadline of `ms`, or whichever of the two aborts first. */
112
+ function capped(signal, ms) {
113
+ const limit = AbortSignal.timeout(ms);
114
+ if (signal === void 0) return limit;
115
+ return AbortSignal.any([signal, limit]);
116
+ }
117
+ /**
118
+ * Run `op` once `previous` settles. Aborting `signal` while waiting rejects
119
+ * `run` and does not call `op`. `tail` still follows `previous`, and `op`
120
+ * when `op` has started, so a later caller cannot overlap the work.
121
+ */
122
+ function enqueue(previous, op, signal) {
123
+ let started = Promise.resolve();
124
+ let began = false;
125
+ return {
126
+ run: new Promise((resolve, reject) => {
127
+ let settled = false;
128
+ const fail = () => {
129
+ if (settled) return;
130
+ settled = true;
131
+ reject(stopped$1());
132
+ };
133
+ const onAbort = () => {
134
+ if (began) return;
135
+ fail();
136
+ };
137
+ const start = () => {
138
+ signal?.removeEventListener("abort", onAbort);
139
+ if (settled || signal?.aborted === true) {
140
+ fail();
141
+ return;
142
+ }
143
+ let work;
144
+ try {
145
+ began = true;
146
+ work = op();
147
+ } catch (error) {
148
+ started = Promise.reject(error);
149
+ started.catch(() => void 0);
150
+ if (!settled) {
151
+ settled = true;
152
+ reject(error);
153
+ }
154
+ return;
155
+ }
156
+ started = work;
157
+ resolve(work);
158
+ };
159
+ if (signal?.aborted === true) {
160
+ fail();
161
+ return;
162
+ }
163
+ if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
164
+ previous.then(start, start);
165
+ }),
166
+ tail: previous.then(() => started, () => started).then(() => void 0, () => void 0)
167
+ };
168
+ }
169
+ function unwrap(settled) {
170
+ return settled.then((result) => {
171
+ if (result.ok) return result.value;
172
+ throw result.error;
173
+ });
174
+ }
175
+ //#endregion
36
176
  //#region lib/types/representation/agent-view.js
37
177
  const AGENT_VIEW_VERSION = "0.1";
38
178
  const AGENT_VIEW_ACCEPT = "text/markdown; variant=agent-view";
@@ -446,35 +586,6 @@ function formatSeconds(seconds) {
446
586
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
447
587
  }
448
588
  //#endregion
449
- //#region lib/types/types.js
450
- var MhBrowserError = class extends Error {
451
- code;
452
- page;
453
- constructor(code, message, page) {
454
- super(message);
455
- this.name = "MhBrowserError";
456
- this.code = code;
457
- if (page !== void 0) this.page = page;
458
- }
459
- };
460
- /** Owner of the person's own window. It belongs to no Session. */
461
- const PERSON_OWNER = "person";
462
- const UNKNOWN_TAB = "UNKNOWN_TAB";
463
- const UNKNOWN_TAB_REASON = "no such tab in this session";
464
- /**
465
- * Internal: a wait ended because its signal aborted. A tool never reports it:
466
- * a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
467
- */
468
- const INTERRUPTED = "INTERRUPTED";
469
- /** The browser did not start or could not be restarted. */
470
- const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
471
- function unknownTab() {
472
- return {
473
- url: "",
474
- reason: UNKNOWN_TAB_REASON
475
- };
476
- }
477
- //#endregion
478
589
  //#region lib/types/tools/session.js
479
590
  /** dsh shows the model only the message, so the message starts with the code. */
480
591
  var BrowserToolError = class extends HarnessError {
@@ -694,6 +805,18 @@ function visit(value, depth, found) {
694
805
  for (const item of Object.values(row)) visit(item, depth + 1, found);
695
806
  }
696
807
  //#endregion
808
+ //#region lib/types/tools/given.js
809
+ /** A string the caller gave. Blank is the same as omitting it; the text itself is kept. */
810
+ function givenString(value) {
811
+ if (typeof value !== "string" || value.trim() === "") return void 0;
812
+ return value;
813
+ }
814
+ /** Seconds to wait. Zero is the same as omitting it; any other number is kept. */
815
+ function givenTime(value) {
816
+ if (typeof value !== "number" || value === 0) return void 0;
817
+ return value;
818
+ }
819
+ //#endregion
697
820
  //#region lib/types/tools/delivery.js
698
821
  /** Every mh-browser tool result's `meta.by`; a result without it is not a delivery. */
699
822
  const META_BY = "mh-browser";
@@ -1370,9 +1493,9 @@ const OPS = [
1370
1493
  },
1371
1494
  build: async (args) => ({
1372
1495
  kind: "wait_for",
1373
- ...typeof args.text === "string" ? { text: args.text } : {},
1374
- ...typeof args.textGone === "string" ? { textGone: args.textGone } : {},
1375
- ...typeof args.time === "number" ? { time: args.time } : {}
1496
+ ...stringField("text", args.text),
1497
+ ...stringField("textGone", args.textGone),
1498
+ ...timeField(args.time)
1376
1499
  })
1377
1500
  },
1378
1501
  {
@@ -1399,7 +1522,7 @@ const OPS = [
1399
1522
  filename: true,
1400
1523
  build: async (args) => ({
1401
1524
  kind: "snapshot",
1402
- ...typeof args.target === "string" ? { target: args.target } : {},
1525
+ ...stringField("target", args.target),
1403
1526
  ...typeof args.depth === "number" ? { depth: args.depth } : {},
1404
1527
  ...args.boxes === true ? { boxes: true } : {}
1405
1528
  })
@@ -1428,8 +1551,8 @@ const OPS = [
1428
1551
  filename: true,
1429
1552
  build: async (args) => ({
1430
1553
  kind: "find",
1431
- ...typeof args.text === "string" ? { text: args.text } : {},
1432
- ...typeof args.regex === "string" ? { regex: args.regex } : {},
1554
+ ...stringField("text", args.text),
1555
+ ...stringField("regex", args.regex),
1433
1556
  ...typeof args.maxResults === "number" ? { maxResults: args.maxResults } : {}
1434
1557
  })
1435
1558
  },
@@ -1473,7 +1596,7 @@ const OPS = [
1473
1596
  build: async (args) => ({
1474
1597
  kind: "take_screenshot",
1475
1598
  ...typeof args.element === "string" ? { element: args.element } : {},
1476
- ...typeof args.target === "string" ? { target: args.target } : {},
1599
+ ...stringField("target", args.target),
1477
1600
  ...args.type === "png" || args.type === "jpeg" || args.type === "webp" ? { type: args.type } : {},
1478
1601
  ...args.fullPage === true ? { fullPage: true } : {},
1479
1602
  ...args.scale === "css" || args.scale === "device" ? { scale: args.scale } : {}
@@ -1507,7 +1630,7 @@ const OPS = [
1507
1630
  function: string(args.function),
1508
1631
  callId: callIdOf(exec),
1509
1632
  ...typeof args.element === "string" ? { element: args.element } : {},
1510
- ...typeof args.target === "string" ? { target: args.target } : {}
1633
+ ...stringField("target", args.target)
1511
1634
  })
1512
1635
  }
1513
1636
  ];
@@ -1542,7 +1665,7 @@ function defineOperation(ctx, reader, spec) {
1542
1665
  const target = writeTarget(ctx, session);
1543
1666
  ctx.mhBrowser.noteWorkspace(session.id, target);
1544
1667
  const input = args;
1545
- const filename = spec.filename === true && typeof input.filename === "string" ? input.filename : void 0;
1668
+ const filename = spec.filename === true ? givenString(input.filename) : void 0;
1546
1669
  if (filename !== void 0) await assertInside(target, filename);
1547
1670
  try {
1548
1671
  const tab = tabOf(reader, session, args.tab);
@@ -1600,6 +1723,18 @@ function string(value) {
1600
1723
  function stringList$1(value) {
1601
1724
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1602
1725
  }
1726
+ function stringField(key, value) {
1727
+ const given = givenString(value);
1728
+ if (given === void 0) return {};
1729
+ if (key === "target") return { target: given };
1730
+ if (key === "text") return { text: given };
1731
+ if (key === "textGone") return { textGone: given };
1732
+ return { regex: given };
1733
+ }
1734
+ function timeField(value) {
1735
+ const given = givenTime(value);
1736
+ return given === void 0 ? {} : { time: given };
1737
+ }
1603
1738
  function optionalString(args, key) {
1604
1739
  return typeof args[key] === "string" ? { element: args[key] } : {};
1605
1740
  }
@@ -1911,6 +2046,8 @@ function takeoverText(taken) {
1911
2046
  }
1912
2047
  //#endregion
1913
2048
  //#region lib/types/presence/register.js
2049
+ /** A presence read that stops answering. One stuck page must not hold the next step. */
2050
+ const CHECK_MS = 6e4;
1914
2051
  const BROWSER_TOOLS = /* @__PURE__ */ new Set([
1915
2052
  "browser_open",
1916
2053
  "browser_invoke",
@@ -1931,14 +2068,17 @@ function registerPresence(ctx, reader) {
1931
2068
  ctx.effect(() => () => {
1932
2069
  lifetime.abort();
1933
2070
  });
1934
- const waitForChecks = async (agent) => {
2071
+ const waitForChecks = async (agent, signal) => {
1935
2072
  let pending;
1936
- while ((pending = tails.get(agent)) !== void 0) await pending;
2073
+ while ((pending = tails.get(agent)) !== void 0) {
2074
+ await untilAborted(pending, signal).catch(() => void 0);
2075
+ if (signal.aborted) return;
2076
+ }
1937
2077
  };
1938
2078
  const queue = (agent, urls) => {
1939
2079
  const current = (tails.get(agent) ?? Promise.resolve()).then(async () => {
1940
2080
  if (lifetime.signal.aborted) return;
1941
- const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: lifetime.signal });
2081
+ const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: AbortSignal.any([lifetime.signal, AbortSignal.timeout(CHECK_MS)]) });
1942
2082
  if (reminder === void 0 || lifetime.signal.aborted) return;
1943
2083
  syncInbox(agent, reader.presence(agent.session), [], reminderMessage(reminder));
1944
2084
  }).catch((error) => {
@@ -2015,7 +2155,7 @@ function registerPresence(ctx, reader) {
2015
2155
  agents.set(agent.session.id, agent);
2016
2156
  const handed = messages.some((message) => message.source.kind === "user") ? ctx.mhBrowser.takePersonHandback(agent.session.id) : void 0;
2017
2157
  if (handed !== void 0 && handed.urls.length > 0) queue(agent, handed.urls);
2018
- await waitForChecks(agent);
2158
+ await waitForChecks(agent, signal);
2019
2159
  signal.throwIfAborted();
2020
2160
  const desired = await compose(ctx.mhBrowser, agent, reader.presence(agent.session), messages, signal);
2021
2161
  signal.throwIfAborted();
@@ -2159,11 +2299,10 @@ function record(value) {
2159
2299
  return {};
2160
2300
  }
2161
2301
  function checkOptions(signal, extra) {
2162
- const options = {};
2302
+ const options = { signal: capped(signal, CHECK_MS) };
2163
2303
  if (extra?.baseline === true) options.baseline = true;
2164
2304
  if (extra?.replaces === true) options.replaces = true;
2165
2305
  if (extra?.force === true) options.force = true;
2166
- if (signal !== void 0) options.signal = signal;
2167
2306
  return options;
2168
2307
  }
2169
2308
  function canonical$1(url) {
@@ -3684,6 +3823,8 @@ async function dispatch(page, op, snapshot, signal) {
3684
3823
  //#region lib/types/service/evaluate.js
3685
3824
  /** How long requests the function started are still collected after it returns. */
3686
3825
  const SETTLE_MS$1 = 50;
3826
+ /** A debugger call or a detach the page no longer answers. */
3827
+ const CALL_MS$2 = 15e3;
3687
3828
  /**
3688
3829
  * Run the expression in the page and collect write requests.
3689
3830
  * A request is from this call when its initiator stack, including parents,
@@ -3693,7 +3834,8 @@ const SETTLE_MS$1 = 50;
3693
3834
  */
3694
3835
  async function evaluateMarked(page, source, target, callId, signal, dialog) {
3695
3836
  const marker = `mh-evaluate-${callId}-${randomUUID().slice(0, 8)}`;
3696
- const client = await page.context().newCDPSession(page);
3837
+ const limit = capped(signal, CALL_MS$2);
3838
+ const client = await untilAborted(page.context().newCDPSession(page), limit);
3697
3839
  const notes = /* @__PURE__ */ new Map();
3698
3840
  const onRequest = (event) => {
3699
3841
  const method = event.request?.method?.toUpperCase();
@@ -3713,10 +3855,10 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
3713
3855
  const stop = whenAborted(signal);
3714
3856
  let dialogOpen = false;
3715
3857
  try {
3716
- await client.send("Network.enable");
3717
- await client.send("Debugger.enable");
3718
- await client.send("Debugger.setSkipAllPauses", { skip: true });
3719
- await client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 });
3858
+ await untilAborted(client.send("Network.enable"), limit);
3859
+ await untilAborted(client.send("Debugger.enable"), limit);
3860
+ await untilAborted(client.send("Debugger.setSkipAllPauses", { skip: true }), limit);
3861
+ await untilAborted(client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 }), limit);
3720
3862
  const evaluation = (target === void 0 ? page.evaluate(runSource, {
3721
3863
  expr: source,
3722
3864
  marker
@@ -3768,7 +3910,7 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
3768
3910
  stop.dispose();
3769
3911
  client.off("Network.requestWillBeSent", onRequest);
3770
3912
  const detached = client.detach().catch(() => void 0);
3771
- if (!dialogOpen) await detached;
3913
+ if (!dialogOpen) await untilAborted(detached, capped(signal, CALL_MS$2)).catch(() => void 0);
3772
3914
  }
3773
3915
  }
3774
3916
  /** One line per group: `POST https://site × 2 (this call)`. */
@@ -4166,6 +4308,8 @@ const REPLIED = "继续";
4166
4308
  //#region lib/types/service/extension.js
4167
4309
  const PACKAGE = "@mind-harness/mh-browser";
4168
4310
  const WORKER_MS = 15e3;
4311
+ /** A service-worker call that does not answer. The caller's signal can end it sooner. */
4312
+ const CALL_MS$1 = 15e3;
4169
4313
  /**
4170
4314
  * Directory of the unpacked extension shipped with this package.
4171
4315
  * Walks up from this module: sources live in `src/`, the bundle in `lib/`.
@@ -4205,8 +4349,9 @@ var Extension = class Extension {
4205
4349
  return `chrome-extension://${this.id}/${name}`;
4206
4350
  }
4207
4351
  /** `chrome.tabs.create` in the named window. Returns the extension's tab id. */
4208
- async createTab(windowId, url) {
4209
- return (await this.worker()).evaluate(async (arg) => {
4352
+ async createTab(windowId, url, signal) {
4353
+ const worker = await this.worker(signal);
4354
+ return this.answer(worker.evaluate(async (arg) => {
4210
4355
  const tab = await globalThis.chrome.tabs.create({
4211
4356
  windowId: arg.windowId,
4212
4357
  url: arg.url,
@@ -4217,23 +4362,25 @@ var Extension = class Extension {
4217
4362
  }, {
4218
4363
  windowId,
4219
4364
  url
4220
- });
4365
+ }), signal);
4221
4366
  }
4222
- async tabIdsIn(windowId) {
4223
- return (await this.worker()).evaluate(async (target) => {
4367
+ async tabIdsIn(windowId, signal) {
4368
+ const worker = await this.worker(signal);
4369
+ return this.answer(worker.evaluate(async (target) => {
4224
4370
  return (await globalThis.chrome.tabs.query({ windowId: target })).flatMap((tab) => tab.id === void 0 ? [] : [tab.id]);
4225
- }, windowId);
4371
+ }, windowId), signal);
4226
4372
  }
4227
- async activate(tabId) {
4228
- await (await this.worker()).evaluate(async (id) => {
4373
+ async activate(tabId, signal) {
4374
+ const worker = await this.worker(signal);
4375
+ await this.answer(worker.evaluate(async (id) => {
4229
4376
  await globalThis.chrome.tabs.update(id, { active: true });
4230
- }, tabId);
4377
+ }, tabId), signal);
4231
4378
  }
4232
- async group(tabId, target) {
4233
- const worker = await this.worker();
4379
+ async group(tabId, target, signal) {
4380
+ const worker = await this.worker(signal);
4234
4381
  if ("groupId" in target) {
4235
4382
  const groupId = target.groupId;
4236
- return worker.evaluate(async (arg) => {
4383
+ return this.answer(worker.evaluate(async (arg) => {
4237
4384
  return globalThis.chrome.tabs.group({
4238
4385
  tabIds: [arg.tabId],
4239
4386
  groupId: arg.groupId
@@ -4241,10 +4388,10 @@ var Extension = class Extension {
4241
4388
  }, {
4242
4389
  tabId,
4243
4390
  groupId
4244
- });
4391
+ }), signal);
4245
4392
  }
4246
4393
  const windowId = target.windowId;
4247
- return worker.evaluate(async (arg) => {
4394
+ return this.answer(worker.evaluate(async (arg) => {
4248
4395
  return globalThis.chrome.tabs.group({
4249
4396
  tabIds: [arg.tabId],
4250
4397
  createProperties: { windowId: arg.windowId }
@@ -4252,10 +4399,11 @@ var Extension = class Extension {
4252
4399
  }, {
4253
4400
  tabId,
4254
4401
  windowId
4255
- });
4402
+ }), signal);
4256
4403
  }
4257
- async styleGroup(groupId, title, color) {
4258
- await (await this.worker()).evaluate(async (arg) => {
4404
+ async styleGroup(groupId, title, color, signal) {
4405
+ const worker = await this.worker(signal);
4406
+ await this.answer(worker.evaluate(async (arg) => {
4259
4407
  await globalThis.chrome.tabGroups.update(arg.groupId, {
4260
4408
  title: arg.title,
4261
4409
  color: arg.color
@@ -4264,17 +4412,21 @@ var Extension = class Extension {
4264
4412
  groupId,
4265
4413
  title,
4266
4414
  color
4267
- });
4415
+ }), signal);
4268
4416
  }
4269
- async ungroup(tabId) {
4270
- await (await this.worker()).evaluate(async (id) => {
4417
+ async ungroup(tabId, signal) {
4418
+ const worker = await this.worker(signal);
4419
+ await this.answer(worker.evaluate(async (id) => {
4271
4420
  await globalThis.chrome.tabs.ungroup(id);
4272
- }, tabId);
4421
+ }, tabId), signal);
4422
+ }
4423
+ answer(pending, signal) {
4424
+ return untilAborted(pending, capped(signal, CALL_MS$1));
4273
4425
  }
4274
- async worker() {
4426
+ async worker(signal) {
4275
4427
  const current = this.workerPage;
4276
4428
  if (current !== void 0 && this.context.serviceWorkers().includes(current)) return current;
4277
- const worker = await findWorker(this.context, this.id);
4429
+ const worker = await untilAborted(findWorker(this.context, this.id), capped(signal, CALL_MS$1));
4278
4430
  this.workerPage = worker;
4279
4431
  return worker;
4280
4432
  }
@@ -4300,6 +4452,10 @@ async function findWorker(context, id, timeoutMs = WORKER_MS) {
4300
4452
  const TAB_CAP = 8;
4301
4453
  /** How long a new window or tab may take to appear. */
4302
4454
  const TARGET_MS = 5e3;
4455
+ /** A CDP or extension call that does not answer. The caller's signal can end it sooner. */
4456
+ const CALL_MS = 15e3;
4457
+ /** Closing a page the renderer no longer answers. */
4458
+ const CLOSE_MS = 5e3;
4303
4459
  const WORLD = "mh-browser";
4304
4460
  const BINDING = "mhBrowserInput";
4305
4461
  const LISTENER = `(() => {
@@ -4379,7 +4535,10 @@ var Windows = class {
4379
4535
  if (extension !== void 0) for (const page of launched) await page.goto(extension.pageUrl("welcome.html")).catch(() => void 0);
4380
4536
  context.on("page", (page) => {
4381
4537
  for (const blank of launched.splice(0)) blank.close().catch(() => void 0);
4382
- this.ingest(page);
4538
+ this.ingest(page).catch((error) => {
4539
+ if (closedTarget(error)) return;
4540
+ throw error;
4541
+ });
4383
4542
  });
4384
4543
  }
4385
4544
  setPersonInput(fn) {
@@ -4518,10 +4677,10 @@ var Windows = class {
4518
4677
  return open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
4519
4678
  }
4520
4679
  /** Make this tab the current one in its window. Does not raise the browser. */
4521
- async activate(tab) {
4680
+ async activate(tab, signal) {
4522
4681
  const extension = this.extension;
4523
4682
  if (extension === void 0 || tab.chromeTabId === void 0) return;
4524
- await extension.activate(tab.chromeTabId);
4683
+ await extension.activate(tab.chromeTabId, signal);
4525
4684
  }
4526
4685
  /** Group title for the person. Empty until a title is known. */
4527
4686
  setTitle(owner, title) {
@@ -4548,54 +4707,70 @@ var Windows = class {
4548
4707
  * use the extension. Without a window, later pages use `window.open` from
4549
4708
  * an isolated world so they stay in that window.
4550
4709
  */
4551
- openBlank(owner) {
4710
+ openBlank(owner, signal) {
4552
4711
  this.pendingCreates.set(owner, (this.pendingCreates.get(owner) ?? 0) + 1);
4553
- const create = () => this.createTab(owner);
4554
- const run = this.creating.then(create, create).finally(() => {
4712
+ const queued = enqueue(this.creating, () => this.createTab(owner, signal), signal);
4713
+ const run = queued.run.finally(() => {
4555
4714
  const left = (this.pendingCreates.get(owner) ?? 1) - 1;
4556
4715
  if (left <= 0) this.pendingCreates.delete(owner);
4557
4716
  else this.pendingCreates.set(owner, left);
4558
4717
  });
4559
- this.creating = run.catch(() => void 0);
4718
+ this.creating = queued.tail;
4560
4719
  return run;
4561
4720
  }
4562
- async createTab(owner) {
4721
+ async createTab(owner, signal) {
4563
4722
  const extension = this.extension;
4564
4723
  if (extension === void 0) {
4565
- const opener = await this.openerFor(owner);
4566
- const page = await this.openTabIn(opener);
4567
- return this.keep(owner, page, void 0);
4724
+ const opener = await this.openerFor(owner, signal);
4725
+ const page = await this.openTabIn(opener, signal);
4726
+ return this.keep(owner, page, void 0, void 0, signal);
4568
4727
  }
4569
4728
  const open = this.owned(owner);
4570
4729
  if (open.length === 0) {
4571
- const page = await this.openWindow();
4572
- const windowId = await this.windowIdOf(page);
4573
- const chromeTabId = await this.extensionTabIn(extension, windowId);
4574
- const tab = await this.keep(owner, page, chromeTabId);
4575
- this.groupTab(tab);
4730
+ const page = await this.openWindow(signal);
4731
+ const windowId = await this.windowIdOf(page, signal);
4732
+ const chromeTabId = await this.extensionTabIn(extension, windowId, signal);
4733
+ const tab = await this.keep(owner, page, chromeTabId, void 0, signal);
4734
+ this.groupTab(tab, signal);
4576
4735
  return tab;
4577
4736
  }
4578
4737
  const anchor = open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
4579
- const opened = await this.openTabViaExtension(anchor.windowId);
4580
- const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId);
4581
- this.groupTab(tab);
4738
+ const opened = await this.openTabViaExtension(anchor.windowId, signal);
4739
+ const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId, signal);
4740
+ this.groupTab(tab, signal);
4582
4741
  return tab;
4583
4742
  }
4584
- async keep(owner, page, chromeTabId, windowId) {
4585
- const tab = {
4586
- id: await this.allocate(),
4587
- owner,
4588
- page,
4589
- targetId: await this.targetIdOf(page),
4590
- windowId: windowId ?? await this.windowIdOf(page),
4591
- chromeTabId,
4592
- url: page.url(),
4593
- touchedAt: Date.now(),
4594
- ...idleTab()
4595
- };
4596
- this.remember(tab);
4597
- await this.arm(page, owner, tab.id);
4598
- return tab;
4743
+ async keep(owner, page, chromeTabId, windowId, signal) {
4744
+ let tab;
4745
+ try {
4746
+ tab = {
4747
+ id: await this.allocate(),
4748
+ owner,
4749
+ page,
4750
+ targetId: await this.targetIdOf(page, signal),
4751
+ windowId: windowId ?? await this.windowIdOf(page, signal),
4752
+ chromeTabId,
4753
+ url: page.url(),
4754
+ touchedAt: Date.now(),
4755
+ ...idleTab()
4756
+ };
4757
+ this.remember(tab);
4758
+ await this.arm(page, owner, tab.id, signal);
4759
+ return tab;
4760
+ } catch (error) {
4761
+ if (tab !== void 0 && this.byPage.get(page) === tab) await this.closePage(tab);
4762
+ else await this.shutdown(page);
4763
+ throw error;
4764
+ }
4765
+ }
4766
+ /** The page from a create, or close it when this wait is abandoned. */
4767
+ async claimedPage(pending, signal) {
4768
+ try {
4769
+ return await untilAborted(pending, capped(signal, TARGET_MS));
4770
+ } catch (error) {
4771
+ pending.then((page) => this.shutdown(page)).catch(() => void 0);
4772
+ throw error;
4773
+ }
4599
4774
  }
4600
4775
  /**
4601
4776
  * Close one tab. `note` is listed in the next `<tabs>` block. A tab the
@@ -4609,9 +4784,12 @@ var Windows = class {
4609
4784
  }
4610
4785
  await this.closePage(tab);
4611
4786
  }
4612
- async closeOwner(owner) {
4613
- for (const tab of this.owned(owner)) await this.closePage(tab);
4614
- await this.closeOpener(owner);
4787
+ async closeOwner(owner, signal) {
4788
+ for (const tab of this.owned(owner)) {
4789
+ if (signal?.aborted === true) break;
4790
+ await this.closePage(tab);
4791
+ }
4792
+ if (signal?.aborted !== true) await this.closeOpener(owner);
4615
4793
  this.closedNotes.delete(owner);
4616
4794
  for (const [id, last] of this.lastUrls) if (last.owner === owner) this.lastUrls.delete(id);
4617
4795
  this.groups.delete(owner);
@@ -4629,7 +4807,7 @@ var Windows = class {
4629
4807
  }
4630
4808
  this.serviceClose.add(tab.page);
4631
4809
  this.forget(tab);
4632
- if (!tab.page.isClosed()) await tab.page.close().catch(() => void 0);
4810
+ if (!tab.page.isClosed()) await this.shutdown(tab.page);
4633
4811
  }
4634
4812
  /** A tab leaves the registry. The window closes with its last tab. */
4635
4813
  forget(tab) {
@@ -4657,7 +4835,7 @@ var Windows = class {
4657
4835
  session?.detach().catch(() => void 0);
4658
4836
  if (opener.isClosed()) return;
4659
4837
  this.serviceClose.add(opener);
4660
- await opener.close().catch(() => void 0);
4838
+ await this.shutdown(opener);
4661
4839
  }
4662
4840
  /** Close least-recently touched idle tabs until the owner is within the cap. */
4663
4841
  async enforceCap(owner) {
@@ -4704,57 +4882,58 @@ var Windows = class {
4704
4882
  await writeCounter(this.profileDir, this.nextId);
4705
4883
  return id;
4706
4884
  }
4707
- async openerFor(owner) {
4885
+ async openerFor(owner, signal) {
4708
4886
  const existing = this.openers.get(owner);
4709
4887
  if (existing !== void 0 && !existing.isClosed()) return existing;
4710
- const opener = await this.openWindow();
4888
+ const opener = await this.openWindow(signal);
4711
4889
  this.openers.set(owner, opener);
4712
- await this.arm(opener, owner, void 0);
4890
+ await this.arm(opener, owner, void 0, signal);
4713
4891
  return opener;
4714
4892
  }
4715
- async openWindow() {
4893
+ async openWindow(signal) {
4716
4894
  const session = this.browserSession;
4717
4895
  if (session === void 0) throw new Error("browser is not running");
4718
- const created = await this.createWindowTarget(session);
4719
- return this.waitForTarget(created.targetId);
4896
+ const created = await this.createWindowTarget(session, signal);
4897
+ const pending = this.waitForTarget(created.targetId);
4898
+ return this.claimedPage(pending, signal);
4720
4899
  }
4721
4900
  /** Headed windows pass `focus: false`. A browser that rejects it is opened without the flag. */
4722
- async createWindowTarget(session) {
4901
+ async createWindowTarget(session, signal) {
4723
4902
  const base = {
4724
4903
  url: "about:blank",
4725
4904
  newWindow: true
4726
4905
  };
4727
- if (!this.headed) return await session.send("Target.createTarget", base);
4906
+ if (!this.headed) return this.cdp(session, "Target.createTarget", base, signal);
4728
4907
  try {
4729
- return await session.send("Target.createTarget", {
4908
+ return await this.cdp(session, "Target.createTarget", {
4730
4909
  ...base,
4731
4910
  focus: false
4732
- });
4733
- } catch {
4734
- return await session.send("Target.createTarget", base);
4911
+ }, signal);
4912
+ } catch (error) {
4913
+ if (signal?.aborted === true || isInterrupted(error)) throw error;
4914
+ return this.cdp(session, "Target.createTarget", base, signal);
4735
4915
  }
4736
4916
  }
4737
- async openTabIn(anchor) {
4738
- const context = this.context;
4739
- if (context === void 0) throw new Error("browser is not running");
4917
+ async openTabIn(anchor, signal) {
4918
+ if (this.context === void 0) throw new Error("browser is not running");
4740
4919
  const claim = this.claimBlank();
4741
4920
  try {
4742
- const session = await context.newCDPSession(anchor);
4921
+ const session = await this.pageSession(anchor, signal);
4743
4922
  try {
4744
- const tree = await session.send("Page.getFrameTree");
4745
- const world = await session.send("Page.createIsolatedWorld", {
4923
+ const tree = await this.cdp(session, "Page.getFrameTree", void 0, signal);
4924
+ const world = await this.cdp(session, "Page.createIsolatedWorld", {
4746
4925
  frameId: tree.frameTree.frame.id,
4747
4926
  worldName: WORLD,
4748
4927
  grantUniveralAccess: true
4749
- });
4750
- await session.send("Runtime.evaluate", {
4928
+ }, signal);
4929
+ await this.cdp(session, "Runtime.evaluate", {
4751
4930
  expression: `window.open(${JSON.stringify(claim.url)},'_blank','noopener,noreferrer')`,
4752
4931
  contextId: world.executionContextId
4753
- });
4932
+ }, signal);
4754
4933
  } finally {
4755
- await session.detach().catch(() => void 0);
4934
+ await this.shutdownSession(session);
4756
4935
  }
4757
- return await claim.page;
4936
+ return await this.claimedPage(claim.page, signal);
4758
4937
  } finally {
4759
4938
  claim.drop();
4760
4939
  }
@@ -4797,22 +4976,23 @@ var Windows = class {
4797
4976
  };
4798
4977
  }
4799
4978
  /** The extension's id for the only tab in a window just opened. */
4800
- async extensionTabIn(extension, windowId) {
4979
+ async extensionTabIn(extension, windowId, signal) {
4801
4980
  for (let attempt = 0; attempt < 5; attempt += 1) {
4802
- const id = (await extension.tabIdsIn(windowId))[0];
4981
+ if (signal?.aborted === true) return void 0;
4982
+ const id = (await extension.tabIdsIn(windowId, signal))[0];
4803
4983
  if (id !== void 0) return id;
4804
- await new Promise((resolve) => setTimeout(resolve, 100));
4984
+ await untilAborted(new Promise((resolve) => setTimeout(resolve, 100)), signal).catch(() => void 0);
4805
4985
  }
4806
4986
  }
4807
4987
  /** Ask the extension to open a blank tab in `windowId`, and claim it. */
4808
- async openTabViaExtension(windowId) {
4988
+ async openTabViaExtension(windowId, signal) {
4809
4989
  const extension = this.extension;
4810
4990
  if (extension === void 0) throw new Error("browser is not running");
4811
4991
  const claim = this.claimBlank();
4812
4992
  try {
4813
- const chromeTabId = await extension.createTab(windowId, claim.url);
4993
+ const chromeTabId = await extension.createTab(windowId, claim.url, signal);
4814
4994
  return {
4815
- page: await claim.page,
4995
+ page: await this.claimedPage(claim.page, signal),
4816
4996
  chromeTabId
4817
4997
  };
4818
4998
  } finally {
@@ -4820,28 +5000,29 @@ var Windows = class {
4820
5000
  }
4821
5001
  }
4822
5002
  /** Put a Session's tab in its group. The person's window is not a Session and stays ungrouped. */
4823
- async groupTab(tab) {
5003
+ async groupTab(tab, signal) {
4824
5004
  const extension = this.extension;
4825
5005
  const chromeTabId = tab.chromeTabId;
4826
5006
  if (extension === void 0 || chromeTabId === void 0 || tab.owner === "person") return;
4827
5007
  try {
4828
5008
  const known = this.groups.get(tab.owner);
4829
- if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }));
5009
+ if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
4830
5010
  else try {
4831
- await extension.group(chromeTabId, { groupId: known });
4832
- } catch {
4833
- this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }));
5011
+ await extension.group(chromeTabId, { groupId: known }, signal);
5012
+ } catch (error) {
5013
+ if (isInterrupted(error)) return;
5014
+ this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
4834
5015
  }
4835
- await this.applyGroupStyle(tab.owner);
5016
+ await this.applyGroupStyle(tab.owner, signal);
4836
5017
  } catch {}
4837
5018
  }
4838
- async applyGroupStyle(owner) {
5019
+ async applyGroupStyle(owner, signal) {
4839
5020
  const extension = this.extension;
4840
5021
  const groupId = this.groups.get(owner);
4841
5022
  if (extension === void 0 || groupId === void 0) return;
4842
5023
  const title = this.titles.get(owner) ?? "";
4843
5024
  const waiting = this.waiting.has(owner);
4844
- await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue");
5025
+ await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue", signal);
4845
5026
  }
4846
5027
  async ingest(page) {
4847
5028
  if (this.byPage.has(page)) return;
@@ -4863,6 +5044,10 @@ var Windows = class {
4863
5044
  slot.accept(page);
4864
5045
  return;
4865
5046
  }
5047
+ if (page.url().startsWith("about:blank#mh-")) {
5048
+ await this.shutdown(page);
5049
+ return;
5050
+ }
4866
5051
  const opener = await page.opener().catch(() => null);
4867
5052
  const fromOpener = opener === null ? void 0 : this.byPage.get(opener);
4868
5053
  if (fromOpener !== void 0) {
@@ -4904,10 +5089,9 @@ var Windows = class {
4904
5089
  * or a close the service did not start counts as well. A page the content
4905
5090
  * tab opened itself is not counted for its first navigation or its close.
4906
5091
  */
4907
- async arm(page, owner, tab) {
5092
+ async arm(page, owner, tab, signal) {
4908
5093
  if (this.armed.has(page)) return;
4909
- const context = this.context;
4910
- if (context === void 0) return;
5094
+ if (this.context === void 0) return;
4911
5095
  this.armed.add(page);
4912
5096
  page.on("close", () => {
4913
5097
  const live = this.context;
@@ -4923,19 +5107,29 @@ var Windows = class {
4923
5107
  const known = this.byPage.get(page);
4924
5108
  if (known !== void 0) this.forget(known);
4925
5109
  });
4926
- const session = await context.newCDPSession(page);
4927
- this.sessions.set(page, session);
4928
- await session.send("Runtime.enable");
4929
- await session.send("Page.enable");
4930
- await session.send("Runtime.addBinding", {
4931
- name: BINDING,
4932
- executionContextName: WORLD
4933
- });
4934
- await session.send("Page.addScriptToEvaluateOnNewDocument", {
4935
- source: LISTENER,
4936
- worldName: WORLD,
4937
- runImmediately: true
4938
- });
5110
+ let session;
5111
+ try {
5112
+ session = await this.pageSession(page, signal);
5113
+ this.sessions.set(page, session);
5114
+ await this.cdp(session, "Runtime.enable", void 0, signal);
5115
+ await this.cdp(session, "Page.enable", void 0, signal);
5116
+ await this.cdp(session, "Runtime.addBinding", {
5117
+ name: BINDING,
5118
+ executionContextName: WORLD
5119
+ }, signal);
5120
+ await this.cdp(session, "Page.addScriptToEvaluateOnNewDocument", {
5121
+ source: LISTENER,
5122
+ worldName: WORLD,
5123
+ runImmediately: true
5124
+ }, signal);
5125
+ } catch (error) {
5126
+ if (closedTarget(error)) {
5127
+ const known = this.byPage.get(page);
5128
+ if (known !== void 0) this.forget(known);
5129
+ else this.dropSession(page);
5130
+ }
5131
+ throw error;
5132
+ }
4939
5133
  session.on("Runtime.bindingCalled", () => {
4940
5134
  this.report(owner, tab, page);
4941
5135
  });
@@ -4979,23 +5173,46 @@ var Windows = class {
4979
5173
  });
4980
5174
  });
4981
5175
  }
4982
- async targetIdOf(page) {
4983
- const context = this.context;
4984
- if (context === void 0) throw new Error("browser is not running");
4985
- const session = await context.newCDPSession(page);
5176
+ dropSession(page) {
5177
+ const session = this.sessions.get(page);
5178
+ this.sessions.delete(page);
5179
+ session?.detach().catch(() => void 0);
5180
+ }
5181
+ async targetIdOf(page, signal) {
5182
+ const session = await this.pageSession(page, signal);
4986
5183
  try {
4987
- return (await session.send("Target.getTargetInfo")).targetInfo.targetId;
5184
+ return (await this.cdp(session, "Target.getTargetInfo", void 0, signal)).targetInfo.targetId;
4988
5185
  } finally {
4989
- await session.detach().catch(() => void 0);
5186
+ await this.shutdownSession(session);
4990
5187
  }
4991
5188
  }
4992
- async windowIdOf(page) {
5189
+ async windowIdOf(page, signal) {
4993
5190
  const session = this.browserSession;
4994
5191
  if (session === void 0) throw new Error("browser is not running");
4995
- const targetId = await this.targetIdOf(page);
4996
- return (await session.send("Browser.getWindowForTarget", { targetId })).windowId;
5192
+ const targetId = await this.targetIdOf(page, signal);
5193
+ return (await this.cdp(session, "Browser.getWindowForTarget", { targetId }, signal)).windowId;
5194
+ }
5195
+ pageSession(page, signal) {
5196
+ const context = this.context;
5197
+ if (context === void 0) return Promise.reject(/* @__PURE__ */ new Error("browser is not running"));
5198
+ return untilAborted(context.newCDPSession(page), capped(signal, CALL_MS));
5199
+ }
5200
+ cdp(session, method, params, signal) {
5201
+ const send = session.send.bind(session);
5202
+ return untilAborted(params === void 0 ? send(method) : send(method, params), capped(signal, CALL_MS));
5203
+ }
5204
+ /** Close a page that may no longer answer. A stall ends at `CLOSE_MS`. */
5205
+ shutdown(page) {
5206
+ return untilAborted(page.close(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
5207
+ }
5208
+ shutdownSession(session) {
5209
+ return untilAborted(session.detach(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
4997
5210
  }
4998
5211
  };
5212
+ /** Playwright's error once the page, its context, or the browser has gone. */
5213
+ function closedTarget(error) {
5214
+ return error instanceof Error && (error.message.includes("has been closed") || error.message.includes("Target closed"));
5215
+ }
4999
5216
  function idleTab() {
5000
5217
  return {
5001
5218
  title: "",
@@ -5082,6 +5299,10 @@ const RELAUNCH_WINDOW_MS = 6e4;
5082
5299
  const DOCUMENT_CAP = 64;
5083
5300
  const OPEN_ADDRESS_REASON = "only http and https addresses are opened";
5084
5301
  const STOP_WAIT_MS = 5e3;
5302
+ /** A page read the renderer no longer answers. The caller's signal can end it sooner. */
5303
+ const READ_MS = 15e3;
5304
+ /** Closing one Session's windows must not wait on a renderer that stopped answering. */
5305
+ const DISPOSE_MS = 3e4;
5085
5306
  const WAITING_STOPPED = "stopped while waiting";
5086
5307
  const STOPPED = Symbol("stopped");
5087
5308
  const NAVIGATED_DURING_EVALUATE = "The page navigated before the function returned.";
@@ -5162,7 +5383,7 @@ let MhBrowserService = (() => {
5162
5383
  ctx.on("session/disposed", (session) => {
5163
5384
  const owner = session.id;
5164
5385
  if (owner === void 0) return;
5165
- this.exclusive(owner, () => this.windows.closeOwner(owner)).catch((error) => {
5386
+ this.exclusive(owner, () => this.windows.closeOwner(owner, AbortSignal.timeout(DISPOSE_MS)), AbortSignal.timeout(DISPOSE_MS)).catch((error) => {
5166
5387
  ctx.logger.warn("closing the windows of %s failed: %o", owner, error);
5167
5388
  });
5168
5389
  this.book.forget(owner);
@@ -5184,22 +5405,30 @@ let MhBrowserService = (() => {
5184
5405
  }
5185
5406
  async open(owner, items, signal, options) {
5186
5407
  const reveal = options?.reveal === true;
5187
- return this.exclusive(owner, async () => {
5188
- await this.up(owner === PERSON_OWNER);
5189
- const views = [];
5190
- for (const item of items) {
5191
- if (signal?.aborted) {
5192
- views.push({
5193
- url: item.url,
5194
- reason: WAITING_STOPPED
5195
- });
5196
- continue;
5408
+ try {
5409
+ return await this.exclusive(owner, async () => {
5410
+ await this.up(owner === PERSON_OWNER);
5411
+ const views = [];
5412
+ for (const item of items) {
5413
+ if (signal?.aborted) {
5414
+ views.push({
5415
+ url: item.url,
5416
+ reason: WAITING_STOPPED
5417
+ });
5418
+ continue;
5419
+ }
5420
+ views.push(await this.openAddress(owner, item, signal, reveal));
5197
5421
  }
5198
- views.push(await this.openAddress(owner, item, signal, reveal));
5199
- }
5200
- await this.enforceCap(owner);
5201
- return views;
5202
- });
5422
+ await this.enforceCap(owner);
5423
+ return views;
5424
+ }, signal);
5425
+ } catch (error) {
5426
+ if (!isInterrupted(error)) throw error;
5427
+ return items.map((item) => ({
5428
+ url: item.url,
5429
+ reason: WAITING_STOPPED
5430
+ }));
5431
+ }
5203
5432
  }
5204
5433
  async view(owner, tab, signal) {
5205
5434
  return this.exclusive(owner, async () => {
@@ -5211,8 +5440,8 @@ let MhBrowserService = (() => {
5211
5440
  url: live.url,
5212
5441
  reason: WAITING_STOPPED
5213
5442
  };
5214
- return this.refreshView(live);
5215
- });
5443
+ return this.refreshView(live, signal);
5444
+ }, signal);
5216
5445
  }
5217
5446
  /** Pages whose current representation is already in the model's presence. */
5218
5447
  rememberPresence(owner, urls) {
@@ -5278,7 +5507,7 @@ let MhBrowserService = (() => {
5278
5507
  text: planned.text,
5279
5508
  record: planned.record
5280
5509
  };
5281
- });
5510
+ }, opts?.signal);
5282
5511
  }
5283
5512
  /**
5284
5513
  * Where a download the page starts may be written: the session's writable
@@ -5320,7 +5549,7 @@ let MhBrowserService = (() => {
5320
5549
  kind: "response",
5321
5550
  finalUrl: response.url(),
5322
5551
  status: response.status(),
5323
- body: new Uint8Array(await response.body())
5552
+ body: new Uint8Array(await untilAborted(response.body(), capped(opts?.signal, READ_MS)))
5324
5553
  };
5325
5554
  const mediaType = headerMap["content-type"];
5326
5555
  const etag = headerMap.etag;
@@ -5374,7 +5603,7 @@ let MhBrowserService = (() => {
5374
5603
  try {
5375
5604
  if (live.dialogHeld && op.kind !== "handle_dialog") throw new MhBrowserError("MODAL_STATE", "a dialog is open; use browser_handle_dialog", await this.blockedView(live));
5376
5605
  if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
5377
- const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" });
5606
+ const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal);
5378
5607
  blocked.dialog = {
5379
5608
  type: "filechooser",
5380
5609
  message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
@@ -5387,8 +5616,8 @@ let MhBrowserService = (() => {
5387
5616
  else this.windows.holdInput(live);
5388
5617
  } else if (outcome.navigated) this.windows.onNavigated(live);
5389
5618
  this.windows.touch(live);
5390
- const shown = outcome.navigated ? await this.discover(live, live.page.url(), null) : await this.refreshSnapshot(live);
5391
- const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op) : void 0;
5619
+ const shown = outcome.navigated ? await this.discover(live, live.page.url(), null, signal) : await this.refreshSnapshot(live, signal);
5620
+ const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op, signal) : void 0;
5392
5621
  this.shown.set(live.id, shown);
5393
5622
  this.windows.noteUrl(live, live.page.url());
5394
5623
  const view = await this.pageFromShown(live, shown, {
@@ -5396,7 +5625,7 @@ let MhBrowserService = (() => {
5396
5625
  ...outcome.dialog !== void 0 ? { dialog: outcome.dialog } : {},
5397
5626
  ...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
5398
5627
  ...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
5399
- });
5628
+ }, signal);
5400
5629
  if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
5401
5630
  ...view.representation,
5402
5631
  text: scoped
@@ -5413,12 +5642,12 @@ let MhBrowserService = (() => {
5413
5642
  } catch (error) {
5414
5643
  if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
5415
5644
  if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
5416
- if (error instanceof MhBrowserError && error.code === "STALE_REF" && error.page === void 0) throw new MhBrowserError(error.code, error.message, await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }));
5645
+ if (error instanceof MhBrowserError && error.code === "STALE_REF" && error.page === void 0) throw new MhBrowserError(error.code, error.message, await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal));
5417
5646
  throw error;
5418
5647
  } finally {
5419
5648
  linked.release();
5420
5649
  }
5421
- });
5650
+ }, signal);
5422
5651
  }
5423
5652
  holdHandover(owner, tab) {
5424
5653
  const live = this.windows.get(owner, tab);
@@ -5436,15 +5665,16 @@ let MhBrowserService = (() => {
5436
5665
  const live = this.windows.get(owner, tab);
5437
5666
  if (live === void 0 || live.dialogHeld) return void 0;
5438
5667
  try {
5439
- const bytes = await live.page.screenshot({
5668
+ const limit = capped(signal, READ_MS);
5669
+ const bytes = await untilAborted(live.page.screenshot({
5440
5670
  type: "png",
5441
- ...signal !== void 0 ? { signal } : {}
5442
- });
5671
+ signal: limit
5672
+ }), limit);
5443
5673
  return new Uint8Array(bytes);
5444
5674
  } catch {
5445
5675
  return;
5446
5676
  }
5447
- });
5677
+ }, signal);
5448
5678
  }
5449
5679
  invoke(owner, _page, call, opts) {
5450
5680
  return this.exclusive(owner, async () => {
@@ -5461,7 +5691,7 @@ let MhBrowserService = (() => {
5461
5691
  }, call);
5462
5692
  await this.enforceCap(owner);
5463
5693
  return sent;
5464
- });
5694
+ }, opts?.signal);
5465
5695
  }
5466
5696
  /**
5467
5697
  * Not queued: it opens no tab, and a long body must not hold up the
@@ -5493,11 +5723,11 @@ let MhBrowserService = (() => {
5493
5723
  const live = this.windows.get(owner, req.tab);
5494
5724
  if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
5495
5725
  this.windows.holdHandover(live);
5496
- await this.windows.activate(live).catch(() => void 0);
5726
+ await this.windows.activate(live, signal).catch(() => void 0);
5497
5727
  }
5498
5728
  this.control.handover(owner, req.tab);
5499
5729
  this.windows.markWaiting(owner, true);
5500
- });
5730
+ }, signal);
5501
5731
  }
5502
5732
  /** Remember the Session title for its tab group. Empty when the host has none. */
5503
5733
  noteSessionTitle(owner, title) {
@@ -5508,7 +5738,7 @@ let MhBrowserService = (() => {
5508
5738
  await this.ready;
5509
5739
  const live = this.windows.latest(owner);
5510
5740
  if (live === void 0) return false;
5511
- await live.page.bringToFront();
5741
+ await untilAborted(live.page.bringToFront(), AbortSignal.timeout(READ_MS));
5512
5742
  return true;
5513
5743
  }
5514
5744
  /** The person asked to see the page this Session handed over. */
@@ -5519,11 +5749,12 @@ let MhBrowserService = (() => {
5519
5749
  const named = this.control.namedTab(sessionId);
5520
5750
  const live = (named === void 0 ? void 0 : this.windows.get(sessionId, named)) ?? this.windows.latest(sessionId);
5521
5751
  if (live === void 0) return { status: "no-tab" };
5522
- await live.page.bringToFront();
5752
+ await untilAborted(live.page.bringToFront(), capped(signal, READ_MS));
5523
5753
  return { status: "revealed" };
5524
5754
  }
5525
5755
  /** The person answered. Returns the note, the operation count, and the named tab. */
5526
5756
  async handBack(owner, note) {
5757
+ const signal = capped(void 0, READ_MS);
5527
5758
  return this.exclusive(owner, async () => {
5528
5759
  await this.ready;
5529
5760
  const taken = this.control.finish(owner);
@@ -5532,14 +5763,14 @@ let MhBrowserService = (() => {
5532
5763
  this.windows.markWaiting(owner, false);
5533
5764
  const urls = this.presenceUrls(owner, taken.tabs);
5534
5765
  for (const touch of this.personTouch) touch(owner, urls);
5535
- const page = await this.snapshotNamed(owner, taken.tab);
5766
+ const page = await this.snapshotNamed(owner, taken.tab, signal);
5536
5767
  return {
5537
5768
  ...taken,
5538
5769
  urls,
5539
5770
  ...note !== void 0 && note !== "" ? { note } : {},
5540
5771
  ...page !== void 0 ? { page } : {}
5541
5772
  };
5542
- });
5773
+ }, signal);
5543
5774
  }
5544
5775
  /** A stopped handover gives control back. A finished hand-back is left alone. */
5545
5776
  releaseIfHeld(owner) {
@@ -5581,7 +5812,7 @@ let MhBrowserService = (() => {
5581
5812
  }
5582
5813
  const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
5583
5814
  this.keepAgentView(opened);
5584
- if (!wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
5815
+ if (opened.live !== void 0 && !wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
5585
5816
  await this.windows.closeTab(opened.live);
5586
5817
  const view = { ...opened.view };
5587
5818
  delete view.tab;
@@ -5607,6 +5838,7 @@ let MhBrowserService = (() => {
5607
5838
  }
5608
5839
  /** A readable Agent View puts its origin on the list and keeps the document for the next read. */
5609
5840
  keepAgentView(opened) {
5841
+ if (opened.live === void 0) return;
5610
5842
  if (!("representation" in opened.view)) return;
5611
5843
  const representation = opened.view.representation;
5612
5844
  if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
@@ -5738,7 +5970,7 @@ let MhBrowserService = (() => {
5738
5970
  kind: "response",
5739
5971
  finalUrl: current,
5740
5972
  status,
5741
- body: readsAsText(mediaType) ? new Uint8Array(await response.body()) : /* @__PURE__ */ new Uint8Array()
5973
+ body: readsAsText(mediaType) ? new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))) : /* @__PURE__ */ new Uint8Array()
5742
5974
  };
5743
5975
  const etag = headerMap.etag;
5744
5976
  const link = headerMap.link;
@@ -5751,8 +5983,20 @@ let MhBrowserService = (() => {
5751
5983
  async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
5752
5984
  assertWaitFor(waitFor);
5753
5985
  const reused = this.idleTabOn(owner, url);
5754
- const live = reused ?? await this.windows.openBlank(owner);
5755
5986
  const fresh = reused === void 0;
5987
+ let live;
5988
+ try {
5989
+ live = reused ?? await this.windows.openBlank(owner, signal);
5990
+ } catch (error) {
5991
+ if (signal?.aborted === true || isInterrupted(error)) return {
5992
+ view: {
5993
+ url,
5994
+ reason: WAITING_STOPPED
5995
+ },
5996
+ fresh
5997
+ };
5998
+ throw error;
5999
+ }
5756
6000
  const linked = this.control.link(owner, signal, live.id);
5757
6001
  if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
5758
6002
  const nextDialog = this.windows.nextDialog(live);
@@ -5775,7 +6019,7 @@ let MhBrowserService = (() => {
5775
6019
  nextDialog.cancel();
5776
6020
  if (winner === "failed" && seenDialog === void 0) {
5777
6021
  if (isDownloadStart(navigationError) || live.pendingDownload !== void 0) {
5778
- const view = await this.pageFromShown(live, this.shown.get(live.id));
6022
+ const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
5779
6023
  if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
5780
6024
  this.applyDocumentType(live, view);
5781
6025
  return {
@@ -5826,8 +6070,8 @@ let MhBrowserService = (() => {
5826
6070
  ...optionalFragment(address),
5827
6071
  snapshot: "",
5828
6072
  relocate: false,
5829
- fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
5830
- }) : await this.discover(live, address, response);
6073
+ fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
6074
+ }) : await this.discover(live, address, response, signal);
5831
6075
  this.shown.set(live.id, shown);
5832
6076
  const view = await this.pageFromShown(live, shown, {
5833
6077
  ...snapshot ? { surface: "snapshot" } : {},
@@ -5837,7 +6081,7 @@ let MhBrowserService = (() => {
5837
6081
  } },
5838
6082
  ...load.dialog !== void 0 ? { dialog: load.dialog } : {},
5839
6083
  ...waited !== void 0 ? { waited } : {}
5840
- });
6084
+ }, signal);
5841
6085
  return {
5842
6086
  view: await this.attachDownload(live, view, false),
5843
6087
  fresh,
@@ -5872,7 +6116,7 @@ let MhBrowserService = (() => {
5872
6116
  if (op.kind === "snapshot") return { navigated: false };
5873
6117
  if (op.kind === "find") return {
5874
6118
  navigated: false,
5875
- summary: findInSnapshot(await this.aria(live), op)
6119
+ summary: findInSnapshot(await this.aria(live, signal), op)
5876
6120
  };
5877
6121
  if (op.kind === "evaluate") {
5878
6122
  const snapshot = this.shown.get(live.id)?.snapshot ?? "";
@@ -6064,15 +6308,16 @@ let MhBrowserService = (() => {
6064
6308
  };
6065
6309
  }
6066
6310
  /** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
6067
- async scopedSnapshot(tab, op) {
6311
+ async scopedSnapshot(tab, op, signal) {
6068
6312
  const options = {
6069
6313
  mode: "ai",
6070
6314
  ...op.depth !== void 0 ? { depth: op.depth } : {},
6071
6315
  ...op.boxes === true ? { boxes: true } : {}
6072
6316
  };
6073
6317
  const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
6074
- if (op.target !== void 0) return (await locate(tab.page, op.target, snapshot)).ariaSnapshot(options);
6075
- return tab.page.ariaSnapshot(options);
6318
+ const limit = capped(signal, READ_MS);
6319
+ if (op.target !== void 0) return untilAborted((await locate(tab.page, op.target, snapshot)).ariaSnapshot(options), limit);
6320
+ return untilAborted(tab.page.ariaSnapshot(options), limit);
6076
6321
  }
6077
6322
  /** A file chooser the action opened. The tab stays in use until it navigates. */
6078
6323
  chooserHeld(live, chooser) {
@@ -6083,9 +6328,9 @@ let MhBrowserService = (() => {
6083
6328
  message: chooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
6084
6329
  };
6085
6330
  }
6086
- async refreshSnapshot(tab) {
6331
+ async refreshSnapshot(tab, signal) {
6087
6332
  const previous = this.shown.get(tab.id);
6088
- const snapshot = tab.dialogHeld ? "" : await this.aria(tab);
6333
+ const snapshot = tab.dialogHeld ? "" : await this.aria(tab, signal);
6089
6334
  if (previous === void 0) return {
6090
6335
  pageUrl: canonical(tab.page.url()),
6091
6336
  digest: "",
@@ -6120,20 +6365,20 @@ let MhBrowserService = (() => {
6120
6365
  this.windows.endDispatch(page);
6121
6366
  }
6122
6367
  }
6123
- async discover(tab, address, response) {
6124
- const main = response === null ? void 0 : await mainDocument(response);
6368
+ async discover(tab, address, response, signal) {
6369
+ const main = response === null ? void 0 : await mainDocument(response, signal);
6125
6370
  const relocated = main !== void 0 && !samePage(main.finalUrl, address);
6126
- const domLinks = relocated ? [] : await readDomLinks(tab.page);
6371
+ const domLinks = relocated ? [] : await readDomLinks(tab.page, signal);
6127
6372
  const previous = this.shown.get(tab.id);
6128
6373
  return loadShown({
6129
6374
  pageUrl: canonical(address),
6130
6375
  ...optionalFragment(address),
6131
- snapshot: await this.aria(tab),
6376
+ snapshot: await this.aria(tab, signal),
6132
6377
  ...main !== void 0 && !relocated ? { main } : {},
6133
6378
  ...domLinks.length > 0 ? { domLinks } : {},
6134
6379
  ...previous !== void 0 ? { previous } : {},
6135
6380
  relocate: relocated,
6136
- fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
6381
+ fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
6137
6382
  });
6138
6383
  }
6139
6384
  fetchForRead(url, accept, ifNoneMatch, signal) {
@@ -6143,13 +6388,13 @@ let MhBrowserService = (() => {
6143
6388
  ...signal !== void 0 ? { signal } : {}
6144
6389
  });
6145
6390
  }
6146
- async aria(tab) {
6147
- return tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
6391
+ async aria(tab, signal) {
6392
+ return untilAborted(tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
6148
6393
  this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
6149
6394
  return "";
6150
- });
6395
+ }), capped(signal, READ_MS));
6151
6396
  }
6152
- async pageFromShown(tab, shown, extra) {
6397
+ async pageFromShown(tab, shown, extra, signal) {
6153
6398
  const current = shown ?? {
6154
6399
  pageUrl: canonical(tab.url),
6155
6400
  digest: "",
@@ -6157,10 +6402,10 @@ let MhBrowserService = (() => {
6157
6402
  text: "",
6158
6403
  snapshot: ""
6159
6404
  };
6160
- const title = tab.dialogHeld ? "" : await tab.page.title().catch((error) => {
6405
+ const title = tab.dialogHeld ? "" : await untilAborted(tab.page.title().catch((error) => {
6161
6406
  this.ctx.logger.debug("page title failed: %s", errorText(error));
6162
6407
  return "";
6163
- });
6408
+ }), capped(signal, READ_MS));
6164
6409
  this.windows.noteTitle(tab, title);
6165
6410
  const view = {
6166
6411
  tab: tab.id,
@@ -6205,7 +6450,7 @@ let MhBrowserService = (() => {
6205
6450
  if (via !== void 0) return observationOf(url, via);
6206
6451
  }
6207
6452
  const existing = this.tabOn(owner, url);
6208
- if (existing !== void 0) return observationOf(url, await this.refreshView(existing));
6453
+ if (existing !== void 0) return observationOf(url, await this.refreshView(existing, signal));
6209
6454
  const opened = await this.openOne(owner, url, void 0, signal, false);
6210
6455
  this.keepAgentView(opened);
6211
6456
  const view = opened.view;
@@ -6229,22 +6474,22 @@ let MhBrowserService = (() => {
6229
6474
  tabs.sort((left, right) => right.touchedAt - left.touchedAt);
6230
6475
  return tabs[0];
6231
6476
  }
6232
- async refreshView(live) {
6477
+ async refreshView(live, signal) {
6233
6478
  this.windows.touch(live);
6234
- if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id));
6479
+ if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
6235
6480
  const address = live.page.url();
6236
6481
  const previous = this.shown.get(live.id);
6237
6482
  const shown = await loadShown({
6238
6483
  pageUrl: canonical(address),
6239
6484
  ...optionalFragment(address),
6240
- snapshot: await this.aria(live),
6485
+ snapshot: await this.aria(live, signal),
6241
6486
  ...previous !== void 0 ? { previous } : {},
6242
6487
  relocate: previous !== void 0 && !samePage(previous.pageUrl, address),
6243
- fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch)
6488
+ fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch, signal)
6244
6489
  });
6245
6490
  this.shown.set(live.id, shown);
6246
6491
  this.windows.noteUrl(live, address);
6247
- return this.pageFromShown(live, shown);
6492
+ return this.pageFromShown(live, shown, void 0, signal);
6248
6493
  }
6249
6494
  /** The most recently touched idle tab on this address. A tab the person holds is left alone. */
6250
6495
  idleTabOn(owner, url) {
@@ -6277,7 +6522,7 @@ let MhBrowserService = (() => {
6277
6522
  const load = await waitForLoad(live.page, signal);
6278
6523
  if (load.dialog !== void 0) this.windows.holdDialog(live);
6279
6524
  this.windows.touch(live);
6280
- const shown = await this.discover(live, live.page.url(), response);
6525
+ const shown = await this.discover(live, live.page.url(), response, signal);
6281
6526
  this.shown.set(live.id, shown);
6282
6527
  const view = await this.pageFromShown(live, shown, {
6283
6528
  ...load.load ? {} : { incomplete: {
@@ -6285,11 +6530,11 @@ let MhBrowserService = (() => {
6285
6530
  pendingRequests: load.pendingRequests
6286
6531
  } },
6287
6532
  ...load.dialog !== void 0 ? { dialog: load.dialog } : {}
6288
- });
6533
+ }, signal);
6289
6534
  return this.attachDownload(live, view, false);
6290
6535
  } catch (error) {
6291
6536
  if (isDownloadStart(error) || live.pendingDownload !== void 0) {
6292
- const view = await this.pageFromShown(live, this.shown.get(live.id));
6537
+ const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
6293
6538
  if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
6294
6539
  this.applyDocumentType(live, view);
6295
6540
  return this.attachDownload(live, view, true);
@@ -6321,7 +6566,7 @@ let MhBrowserService = (() => {
6321
6566
  return {
6322
6567
  kind: "response",
6323
6568
  status: response.status(),
6324
- body: new Uint8Array(await response.body()),
6569
+ body: new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))),
6325
6570
  ...location === void 0 || location === "" ? {} : { location },
6326
6571
  ...mediaType === void 0 ? {} : { mediaType }
6327
6572
  };
@@ -6515,22 +6760,21 @@ let MhBrowserService = (() => {
6515
6760
  }
6516
6761
  return urls;
6517
6762
  }
6518
- async snapshotNamed(owner, tab) {
6763
+ async snapshotNamed(owner, tab, signal) {
6519
6764
  if (tab === void 0) return void 0;
6520
6765
  const live = this.windows.get(owner, tab);
6521
6766
  if (live === void 0) return void 0;
6522
- const shown = await this.refreshSnapshot(live);
6767
+ const shown = await this.refreshSnapshot(live, signal);
6523
6768
  this.shown.set(live.id, shown);
6524
- return this.pageFromShown(live, shown, { surface: "snapshot" });
6525
- }
6526
- exclusive(owner, op) {
6527
- const run = (this.queues.get(owner) ?? Promise.resolve()).then(op, op);
6528
- const tail = run.then(() => void 0, () => void 0);
6529
- this.queues.set(owner, tail);
6530
- tail.then(() => {
6531
- if (this.queues.get(owner) === tail) this.queues.delete(owner);
6769
+ return this.pageFromShown(live, shown, { surface: "snapshot" }, signal);
6770
+ }
6771
+ exclusive(owner, op, signal) {
6772
+ const queued = enqueue(this.queues.get(owner) ?? Promise.resolve(), op, signal);
6773
+ this.queues.set(owner, queued.tail);
6774
+ queued.tail.then(() => {
6775
+ if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
6532
6776
  });
6533
- return run;
6777
+ return queued.run;
6534
6778
  }
6535
6779
  };
6536
6780
  })();
@@ -6578,24 +6822,24 @@ function fragmentOf(url) {
6578
6822
  return;
6579
6823
  }
6580
6824
  }
6581
- async function mainDocument(response) {
6825
+ async function mainDocument(response, signal) {
6582
6826
  const headers = response.headers();
6583
6827
  const mediaType = headers["content-type"];
6584
6828
  const link = headers.link;
6585
6829
  return {
6586
6830
  status: response.status(),
6587
6831
  finalUrl: response.url(),
6588
- body: await response.text(),
6832
+ body: await untilAborted(response.text(), capped(signal, READ_MS)),
6589
6833
  ...mediaType !== void 0 ? { mediaType } : {},
6590
6834
  ...link !== void 0 ? { link } : {}
6591
6835
  };
6592
6836
  }
6593
- async function readDomLinks(page) {
6594
- return (await page.locator("link").evaluateAll((elements) => elements.map((element) => ({
6837
+ async function readDomLinks(page, signal) {
6838
+ return (await untilAborted(page.locator("link").evaluateAll((elements) => elements.map((element) => ({
6595
6839
  rel: element.getAttribute("rel") ?? "",
6596
6840
  href: element.getAttribute("href") ?? "",
6597
6841
  title: element.getAttribute("title") ?? ""
6598
- }))).catch(() => [])).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
6842
+ }))).catch(() => []), capped(signal, READ_MS))).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
6599
6843
  rel: row.rel,
6600
6844
  href: row.href,
6601
6845
  ...row.title !== "" ? { title: row.title } : {}
@@ -6868,9 +7112,10 @@ function browserHandoverTool(ctx, reader, headed = true) {
6868
7112
  if (exec.signal.aborted) return quiet();
6869
7113
  const session = sessionFor(ctx, exec);
6870
7114
  try {
7115
+ const tab = givenString(args.tab);
6871
7116
  await ctx.mhBrowser.handover(session.id, {
6872
7117
  reason: args.reason,
6873
- ...args.tab === void 0 ? {} : { tab: args.tab }
7118
+ ...tab === void 0 ? {} : { tab }
6874
7119
  }, exec.signal);
6875
7120
  const answer = await askUntilAnswered(ctx, args.reason, exec, headed);
6876
7121
  if (answer === ABORT || exec.signal.aborted) return quiet();
@@ -7014,6 +7259,7 @@ function browserInvokeTool(ctx, reader) {
7014
7259
  if (resolved === void 0) throw new BrowserToolError("NOT_DELIVERED", `${NOT_DELIVERED_REASON}: ${args.page}`);
7015
7260
  if ("ambiguous" in resolved) throw new BrowserToolError("INVALID_ARGS", `${args.page} is the id of more than one page shown to you; give the url of one: ${resolved.ambiguous.join(", ")}`);
7016
7261
  const input = args.input === void 0 ? void 0 : asObject(args.input);
7262
+ const item = givenString(args.item);
7017
7263
  try {
7018
7264
  const sent = await ctx.mhBrowser.invoke(session.id, resolved.url, {
7019
7265
  name: args.name,
@@ -7021,7 +7267,7 @@ function browserInvokeTool(ctx, reader) {
7021
7267
  url: resolved.url,
7022
7268
  text: resolved.text
7023
7269
  },
7024
- ...args.item === void 0 ? {} : { item: args.item },
7270
+ ...item === void 0 ? {} : { item },
7025
7271
  ...input === void 0 ? {} : { input }
7026
7272
  }, {
7027
7273
  signal: exec.signal,
@@ -7146,18 +7392,19 @@ function browserOpenTool(ctx, reader) {
7146
7392
  }),
7147
7393
  async execute(args, exec) {
7148
7394
  if (args.pages.length < 1 || args.pages.length > 8) throw new BrowserToolError("INVALID_ARGS", "pages takes 1 to 8 entries");
7149
- for (const page of args.pages) {
7150
- const waitFor = page.waitFor;
7395
+ const items = args.pages.map((page) => {
7396
+ const waitFor = page.waitFor === void 0 ? void 0 : compactWait(page.waitFor);
7151
7397
  if (waitFor !== void 0 && waitFor.text === void 0 && waitFor.textGone === void 0 && waitFor.time === void 0) throw new BrowserToolError("INVALID_ARGS", "waitFor needs text, textGone, or time");
7152
- }
7398
+ return {
7399
+ url: page.url,
7400
+ ...page.snapshot === true ? { snapshot: true } : {},
7401
+ ...waitFor === void 0 ? {} : { waitFor }
7402
+ };
7403
+ });
7153
7404
  const session = sessionFor(ctx, exec);
7154
7405
  ctx.mhBrowser.noteWorkspace(session.id, writeTarget(ctx, session));
7155
7406
  try {
7156
- const pages = await ctx.mhBrowser.open(session.id, args.pages.map((page) => ({
7157
- url: page.url,
7158
- ...page.snapshot === true ? { snapshot: true } : {},
7159
- ...page.waitFor === void 0 ? {} : { waitFor: compactWait(page.waitFor) }
7160
- })), exec.signal);
7407
+ const pages = await ctx.mhBrowser.open(session.id, items, exec.signal);
7161
7408
  const block = tabBlock(ctx, reader, session);
7162
7409
  return {
7163
7410
  text: `${renderOutcome(pages, void 0, true)}${block.suffix}`,
@@ -7174,10 +7421,13 @@ function browserOpenTool(ctx, reader) {
7174
7421
  });
7175
7422
  }
7176
7423
  function compactWait(waitFor) {
7424
+ const text = givenString(waitFor.text);
7425
+ const textGone = givenString(waitFor.textGone);
7426
+ const time = givenTime(waitFor.time);
7177
7427
  return {
7178
- ...waitFor.text !== void 0 ? { text: waitFor.text } : {},
7179
- ...waitFor.textGone !== void 0 ? { textGone: waitFor.textGone } : {},
7180
- ...waitFor.time !== void 0 ? { time: waitFor.time } : {}
7428
+ ...text === void 0 ? {} : { text },
7429
+ ...textGone === void 0 ? {} : { textGone },
7430
+ ...time === void 0 ? {} : { time }
7181
7431
  };
7182
7432
  }
7183
7433
  function titleOf(name, url) {