@mind-harness/mh-browser 0.2.4 → 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 = `(() => {
@@ -4521,10 +4677,10 @@ var Windows = class {
4521
4677
  return open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
4522
4678
  }
4523
4679
  /** Make this tab the current one in its window. Does not raise the browser. */
4524
- async activate(tab) {
4680
+ async activate(tab, signal) {
4525
4681
  const extension = this.extension;
4526
4682
  if (extension === void 0 || tab.chromeTabId === void 0) return;
4527
- await extension.activate(tab.chromeTabId);
4683
+ await extension.activate(tab.chromeTabId, signal);
4528
4684
  }
4529
4685
  /** Group title for the person. Empty until a title is known. */
4530
4686
  setTitle(owner, title) {
@@ -4551,54 +4707,70 @@ var Windows = class {
4551
4707
  * use the extension. Without a window, later pages use `window.open` from
4552
4708
  * an isolated world so they stay in that window.
4553
4709
  */
4554
- openBlank(owner) {
4710
+ openBlank(owner, signal) {
4555
4711
  this.pendingCreates.set(owner, (this.pendingCreates.get(owner) ?? 0) + 1);
4556
- const create = () => this.createTab(owner);
4557
- 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(() => {
4558
4714
  const left = (this.pendingCreates.get(owner) ?? 1) - 1;
4559
4715
  if (left <= 0) this.pendingCreates.delete(owner);
4560
4716
  else this.pendingCreates.set(owner, left);
4561
4717
  });
4562
- this.creating = run.catch(() => void 0);
4718
+ this.creating = queued.tail;
4563
4719
  return run;
4564
4720
  }
4565
- async createTab(owner) {
4721
+ async createTab(owner, signal) {
4566
4722
  const extension = this.extension;
4567
4723
  if (extension === void 0) {
4568
- const opener = await this.openerFor(owner);
4569
- const page = await this.openTabIn(opener);
4570
- 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);
4571
4727
  }
4572
4728
  const open = this.owned(owner);
4573
4729
  if (open.length === 0) {
4574
- const page = await this.openWindow();
4575
- const windowId = await this.windowIdOf(page);
4576
- const chromeTabId = await this.extensionTabIn(extension, windowId);
4577
- const tab = await this.keep(owner, page, chromeTabId);
4578
- 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);
4579
4735
  return tab;
4580
4736
  }
4581
4737
  const anchor = open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
4582
- const opened = await this.openTabViaExtension(anchor.windowId);
4583
- const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId);
4584
- 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);
4585
4741
  return tab;
4586
4742
  }
4587
- async keep(owner, page, chromeTabId, windowId) {
4588
- const tab = {
4589
- id: await this.allocate(),
4590
- owner,
4591
- page,
4592
- targetId: await this.targetIdOf(page),
4593
- windowId: windowId ?? await this.windowIdOf(page),
4594
- chromeTabId,
4595
- url: page.url(),
4596
- touchedAt: Date.now(),
4597
- ...idleTab()
4598
- };
4599
- this.remember(tab);
4600
- await this.arm(page, owner, tab.id);
4601
- 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
+ }
4602
4774
  }
4603
4775
  /**
4604
4776
  * Close one tab. `note` is listed in the next `<tabs>` block. A tab the
@@ -4612,9 +4784,12 @@ var Windows = class {
4612
4784
  }
4613
4785
  await this.closePage(tab);
4614
4786
  }
4615
- async closeOwner(owner) {
4616
- for (const tab of this.owned(owner)) await this.closePage(tab);
4617
- 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);
4618
4793
  this.closedNotes.delete(owner);
4619
4794
  for (const [id, last] of this.lastUrls) if (last.owner === owner) this.lastUrls.delete(id);
4620
4795
  this.groups.delete(owner);
@@ -4632,7 +4807,7 @@ var Windows = class {
4632
4807
  }
4633
4808
  this.serviceClose.add(tab.page);
4634
4809
  this.forget(tab);
4635
- if (!tab.page.isClosed()) await tab.page.close().catch(() => void 0);
4810
+ if (!tab.page.isClosed()) await this.shutdown(tab.page);
4636
4811
  }
4637
4812
  /** A tab leaves the registry. The window closes with its last tab. */
4638
4813
  forget(tab) {
@@ -4660,7 +4835,7 @@ var Windows = class {
4660
4835
  session?.detach().catch(() => void 0);
4661
4836
  if (opener.isClosed()) return;
4662
4837
  this.serviceClose.add(opener);
4663
- await opener.close().catch(() => void 0);
4838
+ await this.shutdown(opener);
4664
4839
  }
4665
4840
  /** Close least-recently touched idle tabs until the owner is within the cap. */
4666
4841
  async enforceCap(owner) {
@@ -4707,57 +4882,58 @@ var Windows = class {
4707
4882
  await writeCounter(this.profileDir, this.nextId);
4708
4883
  return id;
4709
4884
  }
4710
- async openerFor(owner) {
4885
+ async openerFor(owner, signal) {
4711
4886
  const existing = this.openers.get(owner);
4712
4887
  if (existing !== void 0 && !existing.isClosed()) return existing;
4713
- const opener = await this.openWindow();
4888
+ const opener = await this.openWindow(signal);
4714
4889
  this.openers.set(owner, opener);
4715
- await this.arm(opener, owner, void 0);
4890
+ await this.arm(opener, owner, void 0, signal);
4716
4891
  return opener;
4717
4892
  }
4718
- async openWindow() {
4893
+ async openWindow(signal) {
4719
4894
  const session = this.browserSession;
4720
4895
  if (session === void 0) throw new Error("browser is not running");
4721
- const created = await this.createWindowTarget(session);
4722
- 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);
4723
4899
  }
4724
4900
  /** Headed windows pass `focus: false`. A browser that rejects it is opened without the flag. */
4725
- async createWindowTarget(session) {
4901
+ async createWindowTarget(session, signal) {
4726
4902
  const base = {
4727
4903
  url: "about:blank",
4728
4904
  newWindow: true
4729
4905
  };
4730
- if (!this.headed) return await session.send("Target.createTarget", base);
4906
+ if (!this.headed) return this.cdp(session, "Target.createTarget", base, signal);
4731
4907
  try {
4732
- return await session.send("Target.createTarget", {
4908
+ return await this.cdp(session, "Target.createTarget", {
4733
4909
  ...base,
4734
4910
  focus: false
4735
- });
4736
- } catch {
4737
- 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);
4738
4915
  }
4739
4916
  }
4740
- async openTabIn(anchor) {
4741
- const context = this.context;
4742
- 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");
4743
4919
  const claim = this.claimBlank();
4744
4920
  try {
4745
- const session = await context.newCDPSession(anchor);
4921
+ const session = await this.pageSession(anchor, signal);
4746
4922
  try {
4747
- const tree = await session.send("Page.getFrameTree");
4748
- 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", {
4749
4925
  frameId: tree.frameTree.frame.id,
4750
4926
  worldName: WORLD,
4751
4927
  grantUniveralAccess: true
4752
- });
4753
- await session.send("Runtime.evaluate", {
4928
+ }, signal);
4929
+ await this.cdp(session, "Runtime.evaluate", {
4754
4930
  expression: `window.open(${JSON.stringify(claim.url)},'_blank','noopener,noreferrer')`,
4755
4931
  contextId: world.executionContextId
4756
- });
4932
+ }, signal);
4757
4933
  } finally {
4758
- await session.detach().catch(() => void 0);
4934
+ await this.shutdownSession(session);
4759
4935
  }
4760
- return await claim.page;
4936
+ return await this.claimedPage(claim.page, signal);
4761
4937
  } finally {
4762
4938
  claim.drop();
4763
4939
  }
@@ -4800,22 +4976,23 @@ var Windows = class {
4800
4976
  };
4801
4977
  }
4802
4978
  /** The extension's id for the only tab in a window just opened. */
4803
- async extensionTabIn(extension, windowId) {
4979
+ async extensionTabIn(extension, windowId, signal) {
4804
4980
  for (let attempt = 0; attempt < 5; attempt += 1) {
4805
- const id = (await extension.tabIdsIn(windowId))[0];
4981
+ if (signal?.aborted === true) return void 0;
4982
+ const id = (await extension.tabIdsIn(windowId, signal))[0];
4806
4983
  if (id !== void 0) return id;
4807
- await new Promise((resolve) => setTimeout(resolve, 100));
4984
+ await untilAborted(new Promise((resolve) => setTimeout(resolve, 100)), signal).catch(() => void 0);
4808
4985
  }
4809
4986
  }
4810
4987
  /** Ask the extension to open a blank tab in `windowId`, and claim it. */
4811
- async openTabViaExtension(windowId) {
4988
+ async openTabViaExtension(windowId, signal) {
4812
4989
  const extension = this.extension;
4813
4990
  if (extension === void 0) throw new Error("browser is not running");
4814
4991
  const claim = this.claimBlank();
4815
4992
  try {
4816
- const chromeTabId = await extension.createTab(windowId, claim.url);
4993
+ const chromeTabId = await extension.createTab(windowId, claim.url, signal);
4817
4994
  return {
4818
- page: await claim.page,
4995
+ page: await this.claimedPage(claim.page, signal),
4819
4996
  chromeTabId
4820
4997
  };
4821
4998
  } finally {
@@ -4823,28 +5000,29 @@ var Windows = class {
4823
5000
  }
4824
5001
  }
4825
5002
  /** Put a Session's tab in its group. The person's window is not a Session and stays ungrouped. */
4826
- async groupTab(tab) {
5003
+ async groupTab(tab, signal) {
4827
5004
  const extension = this.extension;
4828
5005
  const chromeTabId = tab.chromeTabId;
4829
5006
  if (extension === void 0 || chromeTabId === void 0 || tab.owner === "person") return;
4830
5007
  try {
4831
5008
  const known = this.groups.get(tab.owner);
4832
- 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));
4833
5010
  else try {
4834
- await extension.group(chromeTabId, { groupId: known });
4835
- } catch {
4836
- 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));
4837
5015
  }
4838
- await this.applyGroupStyle(tab.owner);
5016
+ await this.applyGroupStyle(tab.owner, signal);
4839
5017
  } catch {}
4840
5018
  }
4841
- async applyGroupStyle(owner) {
5019
+ async applyGroupStyle(owner, signal) {
4842
5020
  const extension = this.extension;
4843
5021
  const groupId = this.groups.get(owner);
4844
5022
  if (extension === void 0 || groupId === void 0) return;
4845
5023
  const title = this.titles.get(owner) ?? "";
4846
5024
  const waiting = this.waiting.has(owner);
4847
- 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);
4848
5026
  }
4849
5027
  async ingest(page) {
4850
5028
  if (this.byPage.has(page)) return;
@@ -4866,6 +5044,10 @@ var Windows = class {
4866
5044
  slot.accept(page);
4867
5045
  return;
4868
5046
  }
5047
+ if (page.url().startsWith("about:blank#mh-")) {
5048
+ await this.shutdown(page);
5049
+ return;
5050
+ }
4869
5051
  const opener = await page.opener().catch(() => null);
4870
5052
  const fromOpener = opener === null ? void 0 : this.byPage.get(opener);
4871
5053
  if (fromOpener !== void 0) {
@@ -4907,10 +5089,9 @@ var Windows = class {
4907
5089
  * or a close the service did not start counts as well. A page the content
4908
5090
  * tab opened itself is not counted for its first navigation or its close.
4909
5091
  */
4910
- async arm(page, owner, tab) {
5092
+ async arm(page, owner, tab, signal) {
4911
5093
  if (this.armed.has(page)) return;
4912
- const context = this.context;
4913
- if (context === void 0) return;
5094
+ if (this.context === void 0) return;
4914
5095
  this.armed.add(page);
4915
5096
  page.on("close", () => {
4916
5097
  const live = this.context;
@@ -4928,19 +5109,19 @@ var Windows = class {
4928
5109
  });
4929
5110
  let session;
4930
5111
  try {
4931
- session = await context.newCDPSession(page);
5112
+ session = await this.pageSession(page, signal);
4932
5113
  this.sessions.set(page, session);
4933
- await session.send("Runtime.enable");
4934
- await session.send("Page.enable");
4935
- await session.send("Runtime.addBinding", {
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", {
4936
5117
  name: BINDING,
4937
5118
  executionContextName: WORLD
4938
- });
4939
- await session.send("Page.addScriptToEvaluateOnNewDocument", {
5119
+ }, signal);
5120
+ await this.cdp(session, "Page.addScriptToEvaluateOnNewDocument", {
4940
5121
  source: LISTENER,
4941
5122
  worldName: WORLD,
4942
5123
  runImmediately: true
4943
- });
5124
+ }, signal);
4944
5125
  } catch (error) {
4945
5126
  if (closedTarget(error)) {
4946
5127
  const known = this.byPage.get(page);
@@ -4997,21 +5178,35 @@ var Windows = class {
4997
5178
  this.sessions.delete(page);
4998
5179
  session?.detach().catch(() => void 0);
4999
5180
  }
5000
- async targetIdOf(page) {
5001
- const context = this.context;
5002
- if (context === void 0) throw new Error("browser is not running");
5003
- const session = await context.newCDPSession(page);
5181
+ async targetIdOf(page, signal) {
5182
+ const session = await this.pageSession(page, signal);
5004
5183
  try {
5005
- return (await session.send("Target.getTargetInfo")).targetInfo.targetId;
5184
+ return (await this.cdp(session, "Target.getTargetInfo", void 0, signal)).targetInfo.targetId;
5006
5185
  } finally {
5007
- await session.detach().catch(() => void 0);
5186
+ await this.shutdownSession(session);
5008
5187
  }
5009
5188
  }
5010
- async windowIdOf(page) {
5189
+ async windowIdOf(page, signal) {
5011
5190
  const session = this.browserSession;
5012
5191
  if (session === void 0) throw new Error("browser is not running");
5013
- const targetId = await this.targetIdOf(page);
5014
- 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);
5015
5210
  }
5016
5211
  };
5017
5212
  /** Playwright's error once the page, its context, or the browser has gone. */
@@ -5104,6 +5299,10 @@ const RELAUNCH_WINDOW_MS = 6e4;
5104
5299
  const DOCUMENT_CAP = 64;
5105
5300
  const OPEN_ADDRESS_REASON = "only http and https addresses are opened";
5106
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;
5107
5306
  const WAITING_STOPPED = "stopped while waiting";
5108
5307
  const STOPPED = Symbol("stopped");
5109
5308
  const NAVIGATED_DURING_EVALUATE = "The page navigated before the function returned.";
@@ -5184,7 +5383,7 @@ let MhBrowserService = (() => {
5184
5383
  ctx.on("session/disposed", (session) => {
5185
5384
  const owner = session.id;
5186
5385
  if (owner === void 0) return;
5187
- 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) => {
5188
5387
  ctx.logger.warn("closing the windows of %s failed: %o", owner, error);
5189
5388
  });
5190
5389
  this.book.forget(owner);
@@ -5206,22 +5405,30 @@ let MhBrowserService = (() => {
5206
5405
  }
5207
5406
  async open(owner, items, signal, options) {
5208
5407
  const reveal = options?.reveal === true;
5209
- return this.exclusive(owner, async () => {
5210
- await this.up(owner === PERSON_OWNER);
5211
- const views = [];
5212
- for (const item of items) {
5213
- if (signal?.aborted) {
5214
- views.push({
5215
- url: item.url,
5216
- reason: WAITING_STOPPED
5217
- });
5218
- 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));
5219
5421
  }
5220
- views.push(await this.openAddress(owner, item, signal, reveal));
5221
- }
5222
- await this.enforceCap(owner);
5223
- return views;
5224
- });
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
+ }
5225
5432
  }
5226
5433
  async view(owner, tab, signal) {
5227
5434
  return this.exclusive(owner, async () => {
@@ -5233,8 +5440,8 @@ let MhBrowserService = (() => {
5233
5440
  url: live.url,
5234
5441
  reason: WAITING_STOPPED
5235
5442
  };
5236
- return this.refreshView(live);
5237
- });
5443
+ return this.refreshView(live, signal);
5444
+ }, signal);
5238
5445
  }
5239
5446
  /** Pages whose current representation is already in the model's presence. */
5240
5447
  rememberPresence(owner, urls) {
@@ -5300,7 +5507,7 @@ let MhBrowserService = (() => {
5300
5507
  text: planned.text,
5301
5508
  record: planned.record
5302
5509
  };
5303
- });
5510
+ }, opts?.signal);
5304
5511
  }
5305
5512
  /**
5306
5513
  * Where a download the page starts may be written: the session's writable
@@ -5342,7 +5549,7 @@ let MhBrowserService = (() => {
5342
5549
  kind: "response",
5343
5550
  finalUrl: response.url(),
5344
5551
  status: response.status(),
5345
- body: new Uint8Array(await response.body())
5552
+ body: new Uint8Array(await untilAborted(response.body(), capped(opts?.signal, READ_MS)))
5346
5553
  };
5347
5554
  const mediaType = headerMap["content-type"];
5348
5555
  const etag = headerMap.etag;
@@ -5396,7 +5603,7 @@ let MhBrowserService = (() => {
5396
5603
  try {
5397
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));
5398
5605
  if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
5399
- 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);
5400
5607
  blocked.dialog = {
5401
5608
  type: "filechooser",
5402
5609
  message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
@@ -5409,8 +5616,8 @@ let MhBrowserService = (() => {
5409
5616
  else this.windows.holdInput(live);
5410
5617
  } else if (outcome.navigated) this.windows.onNavigated(live);
5411
5618
  this.windows.touch(live);
5412
- const shown = outcome.navigated ? await this.discover(live, live.page.url(), null) : await this.refreshSnapshot(live);
5413
- 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;
5414
5621
  this.shown.set(live.id, shown);
5415
5622
  this.windows.noteUrl(live, live.page.url());
5416
5623
  const view = await this.pageFromShown(live, shown, {
@@ -5418,7 +5625,7 @@ let MhBrowserService = (() => {
5418
5625
  ...outcome.dialog !== void 0 ? { dialog: outcome.dialog } : {},
5419
5626
  ...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
5420
5627
  ...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
5421
- });
5628
+ }, signal);
5422
5629
  if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
5423
5630
  ...view.representation,
5424
5631
  text: scoped
@@ -5435,12 +5642,12 @@ let MhBrowserService = (() => {
5435
5642
  } catch (error) {
5436
5643
  if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
5437
5644
  if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
5438
- 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));
5439
5646
  throw error;
5440
5647
  } finally {
5441
5648
  linked.release();
5442
5649
  }
5443
- });
5650
+ }, signal);
5444
5651
  }
5445
5652
  holdHandover(owner, tab) {
5446
5653
  const live = this.windows.get(owner, tab);
@@ -5458,15 +5665,16 @@ let MhBrowserService = (() => {
5458
5665
  const live = this.windows.get(owner, tab);
5459
5666
  if (live === void 0 || live.dialogHeld) return void 0;
5460
5667
  try {
5461
- const bytes = await live.page.screenshot({
5668
+ const limit = capped(signal, READ_MS);
5669
+ const bytes = await untilAborted(live.page.screenshot({
5462
5670
  type: "png",
5463
- ...signal !== void 0 ? { signal } : {}
5464
- });
5671
+ signal: limit
5672
+ }), limit);
5465
5673
  return new Uint8Array(bytes);
5466
5674
  } catch {
5467
5675
  return;
5468
5676
  }
5469
- });
5677
+ }, signal);
5470
5678
  }
5471
5679
  invoke(owner, _page, call, opts) {
5472
5680
  return this.exclusive(owner, async () => {
@@ -5483,7 +5691,7 @@ let MhBrowserService = (() => {
5483
5691
  }, call);
5484
5692
  await this.enforceCap(owner);
5485
5693
  return sent;
5486
- });
5694
+ }, opts?.signal);
5487
5695
  }
5488
5696
  /**
5489
5697
  * Not queued: it opens no tab, and a long body must not hold up the
@@ -5515,11 +5723,11 @@ let MhBrowserService = (() => {
5515
5723
  const live = this.windows.get(owner, req.tab);
5516
5724
  if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
5517
5725
  this.windows.holdHandover(live);
5518
- await this.windows.activate(live).catch(() => void 0);
5726
+ await this.windows.activate(live, signal).catch(() => void 0);
5519
5727
  }
5520
5728
  this.control.handover(owner, req.tab);
5521
5729
  this.windows.markWaiting(owner, true);
5522
- });
5730
+ }, signal);
5523
5731
  }
5524
5732
  /** Remember the Session title for its tab group. Empty when the host has none. */
5525
5733
  noteSessionTitle(owner, title) {
@@ -5530,7 +5738,7 @@ let MhBrowserService = (() => {
5530
5738
  await this.ready;
5531
5739
  const live = this.windows.latest(owner);
5532
5740
  if (live === void 0) return false;
5533
- await live.page.bringToFront();
5741
+ await untilAborted(live.page.bringToFront(), AbortSignal.timeout(READ_MS));
5534
5742
  return true;
5535
5743
  }
5536
5744
  /** The person asked to see the page this Session handed over. */
@@ -5541,11 +5749,12 @@ let MhBrowserService = (() => {
5541
5749
  const named = this.control.namedTab(sessionId);
5542
5750
  const live = (named === void 0 ? void 0 : this.windows.get(sessionId, named)) ?? this.windows.latest(sessionId);
5543
5751
  if (live === void 0) return { status: "no-tab" };
5544
- await live.page.bringToFront();
5752
+ await untilAborted(live.page.bringToFront(), capped(signal, READ_MS));
5545
5753
  return { status: "revealed" };
5546
5754
  }
5547
5755
  /** The person answered. Returns the note, the operation count, and the named tab. */
5548
5756
  async handBack(owner, note) {
5757
+ const signal = capped(void 0, READ_MS);
5549
5758
  return this.exclusive(owner, async () => {
5550
5759
  await this.ready;
5551
5760
  const taken = this.control.finish(owner);
@@ -5554,14 +5763,14 @@ let MhBrowserService = (() => {
5554
5763
  this.windows.markWaiting(owner, false);
5555
5764
  const urls = this.presenceUrls(owner, taken.tabs);
5556
5765
  for (const touch of this.personTouch) touch(owner, urls);
5557
- const page = await this.snapshotNamed(owner, taken.tab);
5766
+ const page = await this.snapshotNamed(owner, taken.tab, signal);
5558
5767
  return {
5559
5768
  ...taken,
5560
5769
  urls,
5561
5770
  ...note !== void 0 && note !== "" ? { note } : {},
5562
5771
  ...page !== void 0 ? { page } : {}
5563
5772
  };
5564
- });
5773
+ }, signal);
5565
5774
  }
5566
5775
  /** A stopped handover gives control back. A finished hand-back is left alone. */
5567
5776
  releaseIfHeld(owner) {
@@ -5603,7 +5812,7 @@ let MhBrowserService = (() => {
5603
5812
  }
5604
5813
  const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
5605
5814
  this.keepAgentView(opened);
5606
- 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") {
5607
5816
  await this.windows.closeTab(opened.live);
5608
5817
  const view = { ...opened.view };
5609
5818
  delete view.tab;
@@ -5629,6 +5838,7 @@ let MhBrowserService = (() => {
5629
5838
  }
5630
5839
  /** A readable Agent View puts its origin on the list and keeps the document for the next read. */
5631
5840
  keepAgentView(opened) {
5841
+ if (opened.live === void 0) return;
5632
5842
  if (!("representation" in opened.view)) return;
5633
5843
  const representation = opened.view.representation;
5634
5844
  if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
@@ -5760,7 +5970,7 @@ let MhBrowserService = (() => {
5760
5970
  kind: "response",
5761
5971
  finalUrl: current,
5762
5972
  status,
5763
- 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()
5764
5974
  };
5765
5975
  const etag = headerMap.etag;
5766
5976
  const link = headerMap.link;
@@ -5773,8 +5983,20 @@ let MhBrowserService = (() => {
5773
5983
  async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
5774
5984
  assertWaitFor(waitFor);
5775
5985
  const reused = this.idleTabOn(owner, url);
5776
- const live = reused ?? await this.windows.openBlank(owner);
5777
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
+ }
5778
6000
  const linked = this.control.link(owner, signal, live.id);
5779
6001
  if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
5780
6002
  const nextDialog = this.windows.nextDialog(live);
@@ -5797,7 +6019,7 @@ let MhBrowserService = (() => {
5797
6019
  nextDialog.cancel();
5798
6020
  if (winner === "failed" && seenDialog === void 0) {
5799
6021
  if (isDownloadStart(navigationError) || live.pendingDownload !== void 0) {
5800
- 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);
5801
6023
  if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
5802
6024
  this.applyDocumentType(live, view);
5803
6025
  return {
@@ -5848,8 +6070,8 @@ let MhBrowserService = (() => {
5848
6070
  ...optionalFragment(address),
5849
6071
  snapshot: "",
5850
6072
  relocate: false,
5851
- fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
5852
- }) : 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);
5853
6075
  this.shown.set(live.id, shown);
5854
6076
  const view = await this.pageFromShown(live, shown, {
5855
6077
  ...snapshot ? { surface: "snapshot" } : {},
@@ -5859,7 +6081,7 @@ let MhBrowserService = (() => {
5859
6081
  } },
5860
6082
  ...load.dialog !== void 0 ? { dialog: load.dialog } : {},
5861
6083
  ...waited !== void 0 ? { waited } : {}
5862
- });
6084
+ }, signal);
5863
6085
  return {
5864
6086
  view: await this.attachDownload(live, view, false),
5865
6087
  fresh,
@@ -5894,7 +6116,7 @@ let MhBrowserService = (() => {
5894
6116
  if (op.kind === "snapshot") return { navigated: false };
5895
6117
  if (op.kind === "find") return {
5896
6118
  navigated: false,
5897
- summary: findInSnapshot(await this.aria(live), op)
6119
+ summary: findInSnapshot(await this.aria(live, signal), op)
5898
6120
  };
5899
6121
  if (op.kind === "evaluate") {
5900
6122
  const snapshot = this.shown.get(live.id)?.snapshot ?? "";
@@ -6086,15 +6308,16 @@ let MhBrowserService = (() => {
6086
6308
  };
6087
6309
  }
6088
6310
  /** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
6089
- async scopedSnapshot(tab, op) {
6311
+ async scopedSnapshot(tab, op, signal) {
6090
6312
  const options = {
6091
6313
  mode: "ai",
6092
6314
  ...op.depth !== void 0 ? { depth: op.depth } : {},
6093
6315
  ...op.boxes === true ? { boxes: true } : {}
6094
6316
  };
6095
6317
  const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
6096
- if (op.target !== void 0) return (await locate(tab.page, op.target, snapshot)).ariaSnapshot(options);
6097
- 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);
6098
6321
  }
6099
6322
  /** A file chooser the action opened. The tab stays in use until it navigates. */
6100
6323
  chooserHeld(live, chooser) {
@@ -6105,9 +6328,9 @@ let MhBrowserService = (() => {
6105
6328
  message: chooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
6106
6329
  };
6107
6330
  }
6108
- async refreshSnapshot(tab) {
6331
+ async refreshSnapshot(tab, signal) {
6109
6332
  const previous = this.shown.get(tab.id);
6110
- const snapshot = tab.dialogHeld ? "" : await this.aria(tab);
6333
+ const snapshot = tab.dialogHeld ? "" : await this.aria(tab, signal);
6111
6334
  if (previous === void 0) return {
6112
6335
  pageUrl: canonical(tab.page.url()),
6113
6336
  digest: "",
@@ -6142,20 +6365,20 @@ let MhBrowserService = (() => {
6142
6365
  this.windows.endDispatch(page);
6143
6366
  }
6144
6367
  }
6145
- async discover(tab, address, response) {
6146
- 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);
6147
6370
  const relocated = main !== void 0 && !samePage(main.finalUrl, address);
6148
- const domLinks = relocated ? [] : await readDomLinks(tab.page);
6371
+ const domLinks = relocated ? [] : await readDomLinks(tab.page, signal);
6149
6372
  const previous = this.shown.get(tab.id);
6150
6373
  return loadShown({
6151
6374
  pageUrl: canonical(address),
6152
6375
  ...optionalFragment(address),
6153
- snapshot: await this.aria(tab),
6376
+ snapshot: await this.aria(tab, signal),
6154
6377
  ...main !== void 0 && !relocated ? { main } : {},
6155
6378
  ...domLinks.length > 0 ? { domLinks } : {},
6156
6379
  ...previous !== void 0 ? { previous } : {},
6157
6380
  relocate: relocated,
6158
- fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
6381
+ fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
6159
6382
  });
6160
6383
  }
6161
6384
  fetchForRead(url, accept, ifNoneMatch, signal) {
@@ -6165,13 +6388,13 @@ let MhBrowserService = (() => {
6165
6388
  ...signal !== void 0 ? { signal } : {}
6166
6389
  });
6167
6390
  }
6168
- async aria(tab) {
6169
- return tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
6391
+ async aria(tab, signal) {
6392
+ return untilAborted(tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
6170
6393
  this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
6171
6394
  return "";
6172
- });
6395
+ }), capped(signal, READ_MS));
6173
6396
  }
6174
- async pageFromShown(tab, shown, extra) {
6397
+ async pageFromShown(tab, shown, extra, signal) {
6175
6398
  const current = shown ?? {
6176
6399
  pageUrl: canonical(tab.url),
6177
6400
  digest: "",
@@ -6179,10 +6402,10 @@ let MhBrowserService = (() => {
6179
6402
  text: "",
6180
6403
  snapshot: ""
6181
6404
  };
6182
- const title = tab.dialogHeld ? "" : await tab.page.title().catch((error) => {
6405
+ const title = tab.dialogHeld ? "" : await untilAborted(tab.page.title().catch((error) => {
6183
6406
  this.ctx.logger.debug("page title failed: %s", errorText(error));
6184
6407
  return "";
6185
- });
6408
+ }), capped(signal, READ_MS));
6186
6409
  this.windows.noteTitle(tab, title);
6187
6410
  const view = {
6188
6411
  tab: tab.id,
@@ -6227,7 +6450,7 @@ let MhBrowserService = (() => {
6227
6450
  if (via !== void 0) return observationOf(url, via);
6228
6451
  }
6229
6452
  const existing = this.tabOn(owner, url);
6230
- if (existing !== void 0) return observationOf(url, await this.refreshView(existing));
6453
+ if (existing !== void 0) return observationOf(url, await this.refreshView(existing, signal));
6231
6454
  const opened = await this.openOne(owner, url, void 0, signal, false);
6232
6455
  this.keepAgentView(opened);
6233
6456
  const view = opened.view;
@@ -6251,22 +6474,22 @@ let MhBrowserService = (() => {
6251
6474
  tabs.sort((left, right) => right.touchedAt - left.touchedAt);
6252
6475
  return tabs[0];
6253
6476
  }
6254
- async refreshView(live) {
6477
+ async refreshView(live, signal) {
6255
6478
  this.windows.touch(live);
6256
- 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);
6257
6480
  const address = live.page.url();
6258
6481
  const previous = this.shown.get(live.id);
6259
6482
  const shown = await loadShown({
6260
6483
  pageUrl: canonical(address),
6261
6484
  ...optionalFragment(address),
6262
- snapshot: await this.aria(live),
6485
+ snapshot: await this.aria(live, signal),
6263
6486
  ...previous !== void 0 ? { previous } : {},
6264
6487
  relocate: previous !== void 0 && !samePage(previous.pageUrl, address),
6265
- fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch)
6488
+ fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch, signal)
6266
6489
  });
6267
6490
  this.shown.set(live.id, shown);
6268
6491
  this.windows.noteUrl(live, address);
6269
- return this.pageFromShown(live, shown);
6492
+ return this.pageFromShown(live, shown, void 0, signal);
6270
6493
  }
6271
6494
  /** The most recently touched idle tab on this address. A tab the person holds is left alone. */
6272
6495
  idleTabOn(owner, url) {
@@ -6299,7 +6522,7 @@ let MhBrowserService = (() => {
6299
6522
  const load = await waitForLoad(live.page, signal);
6300
6523
  if (load.dialog !== void 0) this.windows.holdDialog(live);
6301
6524
  this.windows.touch(live);
6302
- const shown = await this.discover(live, live.page.url(), response);
6525
+ const shown = await this.discover(live, live.page.url(), response, signal);
6303
6526
  this.shown.set(live.id, shown);
6304
6527
  const view = await this.pageFromShown(live, shown, {
6305
6528
  ...load.load ? {} : { incomplete: {
@@ -6307,11 +6530,11 @@ let MhBrowserService = (() => {
6307
6530
  pendingRequests: load.pendingRequests
6308
6531
  } },
6309
6532
  ...load.dialog !== void 0 ? { dialog: load.dialog } : {}
6310
- });
6533
+ }, signal);
6311
6534
  return this.attachDownload(live, view, false);
6312
6535
  } catch (error) {
6313
6536
  if (isDownloadStart(error) || live.pendingDownload !== void 0) {
6314
- 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);
6315
6538
  if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
6316
6539
  this.applyDocumentType(live, view);
6317
6540
  return this.attachDownload(live, view, true);
@@ -6343,7 +6566,7 @@ let MhBrowserService = (() => {
6343
6566
  return {
6344
6567
  kind: "response",
6345
6568
  status: response.status(),
6346
- body: new Uint8Array(await response.body()),
6569
+ body: new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))),
6347
6570
  ...location === void 0 || location === "" ? {} : { location },
6348
6571
  ...mediaType === void 0 ? {} : { mediaType }
6349
6572
  };
@@ -6537,22 +6760,21 @@ let MhBrowserService = (() => {
6537
6760
  }
6538
6761
  return urls;
6539
6762
  }
6540
- async snapshotNamed(owner, tab) {
6763
+ async snapshotNamed(owner, tab, signal) {
6541
6764
  if (tab === void 0) return void 0;
6542
6765
  const live = this.windows.get(owner, tab);
6543
6766
  if (live === void 0) return void 0;
6544
- const shown = await this.refreshSnapshot(live);
6767
+ const shown = await this.refreshSnapshot(live, signal);
6545
6768
  this.shown.set(live.id, shown);
6546
- return this.pageFromShown(live, shown, { surface: "snapshot" });
6547
- }
6548
- exclusive(owner, op) {
6549
- const run = (this.queues.get(owner) ?? Promise.resolve()).then(op, op);
6550
- const tail = run.then(() => void 0, () => void 0);
6551
- this.queues.set(owner, tail);
6552
- tail.then(() => {
6553
- 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);
6554
6776
  });
6555
- return run;
6777
+ return queued.run;
6556
6778
  }
6557
6779
  };
6558
6780
  })();
@@ -6600,24 +6822,24 @@ function fragmentOf(url) {
6600
6822
  return;
6601
6823
  }
6602
6824
  }
6603
- async function mainDocument(response) {
6825
+ async function mainDocument(response, signal) {
6604
6826
  const headers = response.headers();
6605
6827
  const mediaType = headers["content-type"];
6606
6828
  const link = headers.link;
6607
6829
  return {
6608
6830
  status: response.status(),
6609
6831
  finalUrl: response.url(),
6610
- body: await response.text(),
6832
+ body: await untilAborted(response.text(), capped(signal, READ_MS)),
6611
6833
  ...mediaType !== void 0 ? { mediaType } : {},
6612
6834
  ...link !== void 0 ? { link } : {}
6613
6835
  };
6614
6836
  }
6615
- async function readDomLinks(page) {
6616
- 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) => ({
6617
6839
  rel: element.getAttribute("rel") ?? "",
6618
6840
  href: element.getAttribute("href") ?? "",
6619
6841
  title: element.getAttribute("title") ?? ""
6620
- }))).catch(() => [])).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
6842
+ }))).catch(() => []), capped(signal, READ_MS))).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
6621
6843
  rel: row.rel,
6622
6844
  href: row.href,
6623
6845
  ...row.title !== "" ? { title: row.title } : {}
@@ -6890,9 +7112,10 @@ function browserHandoverTool(ctx, reader, headed = true) {
6890
7112
  if (exec.signal.aborted) return quiet();
6891
7113
  const session = sessionFor(ctx, exec);
6892
7114
  try {
7115
+ const tab = givenString(args.tab);
6893
7116
  await ctx.mhBrowser.handover(session.id, {
6894
7117
  reason: args.reason,
6895
- ...args.tab === void 0 ? {} : { tab: args.tab }
7118
+ ...tab === void 0 ? {} : { tab }
6896
7119
  }, exec.signal);
6897
7120
  const answer = await askUntilAnswered(ctx, args.reason, exec, headed);
6898
7121
  if (answer === ABORT || exec.signal.aborted) return quiet();
@@ -7036,6 +7259,7 @@ function browserInvokeTool(ctx, reader) {
7036
7259
  if (resolved === void 0) throw new BrowserToolError("NOT_DELIVERED", `${NOT_DELIVERED_REASON}: ${args.page}`);
7037
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(", ")}`);
7038
7261
  const input = args.input === void 0 ? void 0 : asObject(args.input);
7262
+ const item = givenString(args.item);
7039
7263
  try {
7040
7264
  const sent = await ctx.mhBrowser.invoke(session.id, resolved.url, {
7041
7265
  name: args.name,
@@ -7043,7 +7267,7 @@ function browserInvokeTool(ctx, reader) {
7043
7267
  url: resolved.url,
7044
7268
  text: resolved.text
7045
7269
  },
7046
- ...args.item === void 0 ? {} : { item: args.item },
7270
+ ...item === void 0 ? {} : { item },
7047
7271
  ...input === void 0 ? {} : { input }
7048
7272
  }, {
7049
7273
  signal: exec.signal,
@@ -7168,18 +7392,19 @@ function browserOpenTool(ctx, reader) {
7168
7392
  }),
7169
7393
  async execute(args, exec) {
7170
7394
  if (args.pages.length < 1 || args.pages.length > 8) throw new BrowserToolError("INVALID_ARGS", "pages takes 1 to 8 entries");
7171
- for (const page of args.pages) {
7172
- const waitFor = page.waitFor;
7395
+ const items = args.pages.map((page) => {
7396
+ const waitFor = page.waitFor === void 0 ? void 0 : compactWait(page.waitFor);
7173
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");
7174
- }
7398
+ return {
7399
+ url: page.url,
7400
+ ...page.snapshot === true ? { snapshot: true } : {},
7401
+ ...waitFor === void 0 ? {} : { waitFor }
7402
+ };
7403
+ });
7175
7404
  const session = sessionFor(ctx, exec);
7176
7405
  ctx.mhBrowser.noteWorkspace(session.id, writeTarget(ctx, session));
7177
7406
  try {
7178
- const pages = await ctx.mhBrowser.open(session.id, args.pages.map((page) => ({
7179
- url: page.url,
7180
- ...page.snapshot === true ? { snapshot: true } : {},
7181
- ...page.waitFor === void 0 ? {} : { waitFor: compactWait(page.waitFor) }
7182
- })), exec.signal);
7407
+ const pages = await ctx.mhBrowser.open(session.id, items, exec.signal);
7183
7408
  const block = tabBlock(ctx, reader, session);
7184
7409
  return {
7185
7410
  text: `${renderOutcome(pages, void 0, true)}${block.suffix}`,
@@ -7196,10 +7421,13 @@ function browserOpenTool(ctx, reader) {
7196
7421
  });
7197
7422
  }
7198
7423
  function compactWait(waitFor) {
7424
+ const text = givenString(waitFor.text);
7425
+ const textGone = givenString(waitFor.textGone);
7426
+ const time = givenTime(waitFor.time);
7199
7427
  return {
7200
- ...waitFor.text !== void 0 ? { text: waitFor.text } : {},
7201
- ...waitFor.textGone !== void 0 ? { textGone: waitFor.textGone } : {},
7202
- ...waitFor.time !== void 0 ? { time: waitFor.time } : {}
7428
+ ...text === void 0 ? {} : { text },
7429
+ ...textGone === void 0 ? {} : { textGone },
7430
+ ...time === void 0 ? {} : { time }
7203
7431
  };
7204
7432
  }
7205
7433
  function titleOf(name, url) {