@reticlehq/server 3.8.0 → 3.8.2

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.
Files changed (115) hide show
  1. package/SKILL.md +2 -2
  2. package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
  3. package/dist/command/cli/answers/gate-hook-message.js +5 -1
  4. package/dist/command/cli/auth/cloud-session.d.ts +1 -19
  5. package/dist/command/cli/auth/device-flow.d.ts +3 -37
  6. package/dist/command/cli/cli-doctor.js +30 -23
  7. package/dist/command/cli/cli-flow-commands.js +7 -1
  8. package/dist/command/cli/cli-parse-serve.d.ts +24 -0
  9. package/dist/command/cli/cli-parse-serve.js +124 -0
  10. package/dist/command/cli/cli-parse.d.ts +1 -10
  11. package/dist/command/cli/cli-parse.js +62 -121
  12. package/dist/command/cli/cli-usage.js +5 -2
  13. package/dist/command/cli/cloud-link.d.ts +1 -20
  14. package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
  15. package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
  16. package/dist/command/cli/ports/port-holder.js +6 -1
  17. package/dist/command/cli/project-name.d.ts +1 -7
  18. package/dist/command/cli/project-name.js +1 -0
  19. package/dist/command/daemon/daemon.d.ts +6 -0
  20. package/dist/command/daemon/daemon.js +52 -2
  21. package/dist/command/daemon/win32-escape.d.ts +32 -0
  22. package/dist/command/daemon/win32-escape.js +119 -0
  23. package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
  24. package/dist/command/setup/bringup/dev-server-wait.js +5 -1
  25. package/dist/command/version/version-skew.d.ts +2 -0
  26. package/dist/command/version/version-skew.js +7 -3
  27. package/dist/features/crawl/crawl-tools.js +2 -2
  28. package/dist/features/exhaust/ledger.d.ts +15 -96
  29. package/dist/features/exhaust/ledger.js +3 -3
  30. package/dist/features/license/license.d.ts +2 -14
  31. package/dist/hooks/hook-commands.js +19 -9
  32. package/dist/judgement/capsule/capsule-store.d.ts +2 -39
  33. package/dist/language/flows/annotate-tools.js +1 -1
  34. package/dist/language/flows/fields/flow-secret-field.js +1 -1
  35. package/dist/language/flows/flow-expect-grammar.js +14 -6
  36. package/dist/language/flows/flow-lease.d.ts +27 -0
  37. package/dist/language/flows/flow-lease.js +59 -0
  38. package/dist/language/flows/flow-replay-run.d.ts +1 -87
  39. package/dist/language/flows/flow-replay-run.js +88 -283
  40. package/dist/language/flows/flow-replayable.d.ts +1 -1
  41. package/dist/language/flows/flow-start-path.d.ts +108 -0
  42. package/dist/language/flows/flow-start-path.js +313 -0
  43. package/dist/language/flows/flow-tools.d.ts +2 -23
  44. package/dist/language/flows/flow-tools.js +34 -66
  45. package/dist/language/flows/recording/flake.d.ts +3 -27
  46. package/dist/language/flows/recording/flake.js +1 -1
  47. package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
  48. package/dist/machine/git-tracks.js +1 -0
  49. package/dist/memory/cloud/cloud-sync.d.ts +2 -24
  50. package/dist/memory/intent/intent-shard.d.ts +41 -266
  51. package/dist/memory/journal/envelope-store.js +1 -1
  52. package/dist/memory/journal/envelope.d.ts +6 -154
  53. package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
  54. package/dist/memory/journal/on-disk/ambient-file.js +1 -1
  55. package/dist/memory/journal/self-observation.js +3 -1
  56. package/dist/memory/journal/session-journal.js +39 -20
  57. package/dist/portal/input/drive-error.d.ts +6 -0
  58. package/dist/portal/input/drive-error.js +9 -0
  59. package/dist/portal/input/real-input-press.d.ts +34 -0
  60. package/dist/portal/input/real-input-press.js +150 -0
  61. package/dist/portal/input/real-input.d.ts +6 -23
  62. package/dist/portal/input/real-input.js +95 -192
  63. package/dist/portal/input/viewport-tools.d.ts +19 -15
  64. package/dist/portal/pool/browser-pool.d.ts +5 -55
  65. package/dist/portal/pool/browser-pool.js +121 -100
  66. package/dist/portal/pool/pool-lease.d.ts +128 -0
  67. package/dist/portal/pool/pool-lease.js +49 -0
  68. package/dist/portal/production/production-activation.d.ts +5 -27
  69. package/dist/portal/session/control-envelope.d.ts +1 -1
  70. package/dist/portal/session/zero-install-first-call.d.ts +3 -0
  71. package/dist/portal/session/zero-install-first-call.js +33 -12
  72. package/dist/surface/mcp/mcp.d.ts +3 -3
  73. package/dist/surface/mcp/mcp.js +4 -6
  74. package/dist/surface/tools/act/act-danger.js +32 -17
  75. package/dist/surface/tools/act/durable.js +1 -1
  76. package/dist/surface/tools/act-sequence-tool.js +3 -3
  77. package/dist/surface/tools/act-tools.js +15 -16
  78. package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
  79. package/dist/surface/tools/coverage-tools.js +1 -1
  80. package/dist/surface/tools/dynamic-tools.d.ts +8 -0
  81. package/dist/surface/tools/dynamic-tools.js +36 -16
  82. package/dist/surface/tools/empty-session-list.d.ts +5 -29
  83. package/dist/surface/tools/feedback-tools.js +1 -1
  84. package/dist/surface/tools/gap-novelty.d.ts +1 -1
  85. package/dist/surface/tools/gaps/capability-absences.js +4 -3
  86. package/dist/surface/tools/harness-gate.d.ts +1 -1
  87. package/dist/surface/tools/harness-toolset.js +3 -4
  88. package/dist/surface/tools/lease-permissions.d.ts +1 -1
  89. package/dist/surface/tools/lease-readiness.d.ts +23 -1
  90. package/dist/surface/tools/lease-readiness.js +75 -9
  91. package/dist/surface/tools/lease-tools.d.ts +1 -52
  92. package/dist/surface/tools/lease-tools.js +79 -142
  93. package/dist/surface/tools/observe-tools.js +3 -1
  94. package/dist/surface/tools/read-tools.js +5 -5
  95. package/dist/surface/tools/real-input-attempt.js +70 -31
  96. package/dist/surface/tools/session-ports.d.ts +57 -0
  97. package/dist/surface/tools/session-ports.js +102 -0
  98. package/dist/surface/tools/tool-kit.d.ts +5 -3
  99. package/dist/surface/tools/tools.js +1 -1
  100. package/dist/telemetry/daemon-telemetry.d.ts +3 -1
  101. package/dist/telemetry/daemon-telemetry.js +9 -2
  102. package/dist/telemetry/telemetry.d.ts +3 -1
  103. package/dist/telemetry/telemetry.js +2 -0
  104. package/docs/agent-cheatsheet.md +12 -0
  105. package/docs/cli/gate.mdx +1 -1
  106. package/docs/cli/verify.mdx +8 -1
  107. package/docs/first-drive.md +7 -0
  108. package/docs/multi-agent-testing.md +2 -0
  109. package/docs/packages/react.mdx +14 -2
  110. package/docs/packages/vite-plugin.mdx +1 -0
  111. package/docs/predicates.mdx +6 -0
  112. package/docs/skill.md +3 -3
  113. package/docs/troubleshooting.mdx +10 -0
  114. package/docs/usage.md +1 -1
  115. package/package.json +8 -9
@@ -10,25 +10,20 @@
10
10
  * pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
11
11
  */
12
12
  import { takeJsCoverage } from './js-coverage.js';
13
+ import { DriveError } from './drive-error.js';
14
+ import { pressViaKeyboard } from './real-input-press.js';
13
15
  import { stampedDriveUrl } from './drive-url-stamp.js';
14
16
  import { launchChromium } from '../../launch-chromium.js';
15
17
  import { chromiumLaunchHint, gotoOptions, SCREENSHOT_DETERMINISM, captureWithChromeHidden, } from '../pool/playwright-launcher.js';
16
18
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
17
19
  import { getSessionMetrics } from '../../telemetry/session-metrics.js';
18
20
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
19
- import { ActionType, clampHoldMs, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, explicitCodeFromArgs, InputMode, InputModeReason, pressKeyFromArgs, pressKeysFromArgs, pressModifiersFromArgs, } from '@reticlehq/core';
21
+ import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, ClickButton, clickButtonFromArgs, InputMode, pressModifiersFromArgs, } from '@reticlehq/core';
20
22
  import { installNetworkMocks } from './network-mock.js';
21
23
  import { attachNetworkDetail } from './network-detail.js';
22
24
  import { injectedConnectArgs } from '../pool/zero-install.js';
23
- /** Structured, code-tagged failure so callers branch on cause, not message text. */
24
- export class DriveError extends Error {
25
- code;
26
- constructor(code, message) {
27
- super(message);
28
- this.code = code;
29
- this.name = 'DriveError';
30
- }
31
- }
25
+ export { DriveError };
26
+ export { unspellablePressReason } from './real-input-press.js';
32
27
  /** Center of a viewport box in CSS px. Pure — unit-tested directly. */
33
28
  export function boxCenter(box) {
34
29
  return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
@@ -63,149 +58,12 @@ function undriven(action, box) {
63
58
  const REAL_INPUT_SETTLE_MS = 16;
64
59
  /** Default number of interpolation steps for a native drag. */
65
60
  const DEFAULT_DRAG_STEPS = 8;
66
- /**
67
- * Drive a `press` with a real keyboard — coordinate-free; the key goes to whatever holds focus.
68
- * Key and modifiers come from the same core helpers the synthetic dispatcher uses, so the two paths
69
- * cannot read the same args differently.
70
- *
71
- * A `holdMs` splits the chord into down / wait / up, the same shape the page uses for a held key.
72
- * `keyboard.press` would send it instantly instead, reporting a hold-to-confirm that never held.
73
- * The one thing this cannot reproduce is the auto-repeat a held key emits: the driver sends a single
74
- * keydown, where the in-page path sends the repeats a browser would. Named rather than papered over
75
- * — an app that counts repeats needs the synthetic path, and a caller that sees a held key do
76
- * nothing now has a reason to try it.
77
- */
78
- async function pressViaKeyboard(page, args, sleep, now) {
79
- const key = pressKeyFromArgs(args);
80
- const modifiers = pressModifiersFromArgs(args);
81
- const chord = 0 < modifiers.length ? `${modifiers.join('+')}+${key}` : key;
82
- const holdMs = clampHoldMs(args.holdMs);
83
- if (0 === holdMs) {
84
- // `press` DOES take a chord string ("Control+k"), so the tap path needs no splitting.
85
- await page.keyboard.press(chord);
86
- return 0;
87
- }
88
- /*
89
- * `down`/`up` take ONE key each, unlike `press`: `down('Control+k')` throws
90
- * `Unknown key: "Control+k"` — measured on a real Chromium, not assumed. The chord string the
91
- * tap path uses therefore cannot express a hold, so the chord is split here: modifiers down in
92
- * chord order, the key last, and released in reverse (key first, modifiers reversed), which is
93
- * what a hand does and what an app's keyup bookkeeping expects.
94
- *
95
- * Without the split a held shortcut threw before a single key moved, the caller fell back to
96
- * the synthetic path, and a real hold was impossible for any press with modifiers — the exact
97
- * gesture `holdMs` exists for.
98
- *
99
- * ## Why every key that went down is tracked, and every exit path cleans up
100
- *
101
- * The down calls are the ones that MOVE the keyboard, and they can fail partway: `Control` goes
102
- * down, the key after it rejects, and the driver's copy of `Control` is still held. That path
103
- * used to fall straight to the caller's synthetic replay — which presses the same chord again on
104
- * a keyboard that is already holding a modifier. Tracking what actually went down is what makes
105
- * the cleanup below able to release the right keys, and only those: releasing a key that never
106
- * went down is its own lie, and it would turn a clean failure into an unnecessary refusal.
107
- */
108
- const pressed = [];
109
- const pressDown = async (k) => {
110
- await page.keyboard.down(k);
111
- pressed.push(k);
112
- };
113
- try {
114
- for (const modifier of modifiers)
115
- await pressDown(modifier);
116
- await pressDown(key);
117
- }
118
- catch (error) {
119
- // The chord is PARTIALLY down. Release what is held before reporting the failure; if that
120
- // cannot be proved, `releaseKeys` throws RELEASE_FAILED and the caller refuses the replay —
121
- // the same rule as a failed release on the ordinary path, for the same reason. Only when the
122
- // cleanup DOES succeed is the original error safe to hand back.
123
- await releaseKeys(page, pressed);
124
- throw error;
125
- }
126
- const startedAt = now();
127
- try {
128
- await sleep(holdMs);
129
- }
130
- catch (error) {
131
- // The wait failed, and the caller is about to run the synthetic path — which presses this SAME
132
- // key a second time. If the driver's copy stays down, a held modifier then colours every later
133
- // action for the rest of the run. Best-effort on purpose: whatever went wrong with the wait is
134
- // the error worth reporting. A cleanup that cannot be proved is the exception — that one is a
135
- // stuck key, and it must reach the caller as a refusal rather than be swallowed.
136
- try {
137
- await releaseKeys(page, pressed);
138
- }
139
- catch {
140
- throw new DriveError(DriveErrorCode.RELEASE_FAILED, 'the wait failed and the keys it pressed could not be released, so they may still be held');
141
- }
142
- throw error;
143
- }
144
- // NOT best-effort: on the ordinary path a failed release is a real failure the caller must hear
145
- // about, and swallowing it would report a press whose key is still down.
146
- await releaseKeys(page, pressed);
147
- return now() - startedAt;
148
- }
149
- /**
150
- * Release the given keys, in reverse press order, and report a failure as one that leaves the
151
- * keyboard UNKNOWN rather than as an ordinary provider error.
152
- *
153
- * Every release is attempted even after one fails: a stuck `Control` is worse than a stuck letter,
154
- * so a failure on one key must not skip the rest. The first failure is what is thrown, since it is
155
- * the one that happened first, and it is wrapped in `DriveError` so the caller can tell "this
156
- * gesture failed" from "this gesture may have left a key down" — the difference between a safe
157
- * synthetic replay and an unsafe one.
158
- */
159
- async function releaseKeys(page, pressed) {
160
- let failure;
161
- for (const k of [...pressed].reverse()) {
162
- try {
163
- await page.keyboard.up(k);
164
- }
165
- catch (error) {
166
- failure ??= error;
167
- }
168
- }
169
- if (failure !== undefined) {
170
- throw new DriveError(DriveErrorCode.RELEASE_FAILED, `the key was pressed but could not be released, so it may still be held: ${describeFailure(failure)}`);
171
- }
172
- }
173
- /**
174
- * A thrown value as a line a reader can act on.
175
- *
176
- * Not `String(error)`: a non-Error thrown value (a rejected plain object, an SDK that throws a
177
- * record) stringifies to `[object Object]`, which names nothing and hides the one fact the message
178
- * exists to carry. Anything that is neither an Error nor a string says so rather than pretending.
179
- */
180
- function describeFailure(error) {
181
- if (error instanceof Error)
182
- return error.message;
183
- return 'string' === typeof error ? error : 'an unknown error';
184
- }
185
- /**
186
- * The `press`es this module will NOT drive: a chord that only the in-page dispatcher can express.
187
- *
188
- * `args.keys` asks for several keys held down TOGETHER, pressed in order and released in reverse.
189
- * That is not a `page.keyboard.press` chord — a non-modifier key cannot appear before the last `+`
190
- * — and a real keyboard cannot aim a key at a named element. `args.code` is the third: the driver
191
- * presses by KEY name and offers no way to send a physical `code` that disagrees with it, so
192
- * `{ text: 'z', code: 'KeyY' }` would strike a different key than the one asked for. All three are
193
- * routed to the synthetic path by the caller, which reports the reason rather than pretending a
194
- * different gesture happened.
195
- *
196
- * Ordered by how specifically the caller asked: `keys` then `code` name the KEY, `ref` names only
197
- * the target. Two can apply at once, and the earlier one is the more unusual request — the reason
198
- * should name that, not the generic "you passed a ref".
199
- */
200
- export function unspellablePressReason(ref, args) {
201
- if (0 < pressKeysFromArgs(args).length) {
202
- return InputModeReason.SYNTHETIC_MULTI_KEY_PRESS_PREFERRED;
61
+ /** The mouse options a click needs: the button, only when it is not the default left one. */
62
+ function clickOptionsOf(args) {
63
+ const button = clickButtonFromArgs({ button: args.button });
64
+ if (button === ClickButton.RIGHT || button === ClickButton.MIDDLE) {
65
+ return { button };
203
66
  }
204
- if (explicitCodeFromArgs(args) !== undefined) {
205
- return InputModeReason.SYNTHETIC_KEY_CODE_PRESS_PREFERRED;
206
- }
207
- if (0 < ref.length)
208
- return InputModeReason.SYNTHETIC_ELEMENT_PRESS_PREFERRED;
209
67
  return undefined;
210
68
  }
211
69
  /**
@@ -241,19 +99,32 @@ now = Date.now) {
241
99
  await sleep(REAL_INPUT_SETTLE_MS);
242
100
  return real(true);
243
101
  }
244
- if (action === ActionType.CLICK) {
245
- await page.mouse.move(cx, cy);
246
- await page.mouse.click(cx, cy);
247
- return real(true);
248
- }
249
- if (action === ActionType.DBLCLICK) {
102
+ if (action === ActionType.CLICK || action === ActionType.DBLCLICK) {
250
103
  await page.mouse.move(cx, cy);
251
- await page.mouse.dblclick(cx, cy);
104
+ const options = clickOptionsOf(args);
105
+ const modifiers = pressModifiersFromArgs(args);
106
+ for (const modifier of modifiers) {
107
+ await page.keyboard.down(modifier);
108
+ }
109
+ try {
110
+ if (action === ActionType.CLICK) {
111
+ await page.mouse.click(cx, cy, options);
112
+ }
113
+ else {
114
+ await page.mouse.dblclick(cx, cy, options);
115
+ }
116
+ }
117
+ finally {
118
+ for (const modifier of [...modifiers].reverse()) {
119
+ await page.keyboard.up(modifier);
120
+ }
121
+ }
252
122
  return real(true);
253
123
  }
254
124
  if (action === ActionType.DRAG) {
255
- if (args.toBox === undefined)
125
+ if (args.toBox === undefined) {
256
126
  return real(false);
127
+ }
257
128
  const dst = boxCenter(args.toBox);
258
129
  const steps = args.steps ?? DEFAULT_DRAG_STEPS;
259
130
  await page.mouse.move(cx, cy);
@@ -341,8 +212,9 @@ export class CdpRealInputProvider {
341
212
  /** Attach the response listener the first time we see a page; a no-op afterwards. */
342
213
  #listen(page) {
343
214
  const sink = this.#onNetworkDetail;
344
- if (sink === undefined || this.#listening.has(page))
215
+ if (sink === undefined || this.#listening.has(page)) {
345
216
  return;
217
+ }
346
218
  this.#listening.add(page);
347
219
  attachNetworkDetail(page, sink);
348
220
  }
@@ -354,8 +226,9 @@ export class CdpRealInputProvider {
354
226
  // Playwright's Browser exposes isConnected(); a test fake may not, so only an EXPLICIT false
355
227
  // drops the cache — a fake without the method is assumed live.
356
228
  const alive = this.#browser.isConnected?.() ?? true;
357
- if (alive)
229
+ if (alive) {
358
230
  return this.#browser;
231
+ }
359
232
  this.#browser = undefined; // fall through and reconnect
360
233
  }
361
234
  try {
@@ -369,11 +242,13 @@ export class CdpRealInputProvider {
369
242
  }
370
243
  async #pageFor(sessionUrl) {
371
244
  const browser = await this.#ensureBrowser();
372
- if (browser === undefined)
245
+ if (browser === undefined) {
373
246
  return undefined;
247
+ }
374
248
  const page = selectPage(browser.contexts().flatMap((c) => c.pages()), sessionUrl);
375
- if (page !== undefined)
249
+ if (page !== undefined) {
376
250
  this.#listen(page);
251
+ }
377
252
  return page;
378
253
  }
379
254
  async takeCodeCoverage(sessionUrl) {
@@ -390,22 +265,25 @@ export class CdpRealInputProvider {
390
265
  }
391
266
  async perform(sessionUrl, action, box, args) {
392
267
  const page = await this.#pageFor(sessionUrl);
393
- if (page === undefined)
268
+ if (page === undefined) {
394
269
  return undriven(action, box);
270
+ }
395
271
  return performGesture(page, action, box, args, this.#sleep, this.#now);
396
272
  }
397
273
  /** PNG of the correlated page, or undefined if none matches. */
398
274
  async screenshot(sessionUrl, opts) {
399
275
  const page = await this.#pageFor(sessionUrl);
400
- if (page === undefined)
276
+ if (page === undefined) {
401
277
  return undefined;
278
+ }
402
279
  return capturePage(page, opts);
403
280
  }
404
281
  /** The context's cookies and per-origin storage; undefined when no driven page matches. */
405
282
  async storageState(sessionUrl) {
406
283
  const page = await this.#pageFor(sessionUrl);
407
- if (page === undefined)
284
+ if (page === undefined) {
408
285
  return undefined;
286
+ }
409
287
  return page.context().storageState();
410
288
  }
411
289
  /**
@@ -416,27 +294,32 @@ export class CdpRealInputProvider {
416
294
  */
417
295
  async applyStorageState(sessionUrl, state) {
418
296
  const page = await this.#pageFor(sessionUrl);
419
- if (page === undefined)
297
+ if (page === undefined) {
420
298
  return false;
299
+ }
421
300
  const saved = state;
422
- if (saved.cookies !== undefined)
301
+ if (saved.cookies !== undefined) {
423
302
  await page.context().addCookies(saved.cookies);
303
+ }
424
304
  for (const origin of saved.origins ?? []) {
425
305
  // Only the document for an origin can write that origin's localStorage, so this is applied in
426
306
  // the page rather than through the context. A page sitting on a different origin cannot be
427
307
  // given another origin's storage, and silently succeeding there would be the quiet half-restore
428
308
  // this whole path exists to avoid.
429
- if (!page.url().startsWith(origin.origin))
309
+ if (!page.url().startsWith(origin.origin)) {
430
310
  continue;
311
+ }
431
312
  await page.evaluate((entries) => {
432
313
  // Reached through `globalThis` rather than `window`: this body is serialised and runs in the
433
314
  // PAGE, but it is typechecked here, and this package never pulls in the DOM lib — a server
434
315
  // that can name `window` is one step from a server that uses it.
435
316
  const store = globalThis.localStorage;
436
- if (store === undefined)
317
+ if (store === undefined) {
437
318
  return;
438
- for (const entry of entries)
319
+ }
320
+ for (const entry of entries) {
439
321
  store.setItem(entry.name, entry.value);
322
+ }
440
323
  }, origin.localStorage ?? []);
441
324
  }
442
325
  return true;
@@ -444,16 +327,18 @@ export class CdpRealInputProvider {
444
327
  /** Apply network-mock rules to the correlated page; false when no driven page matches. */
445
328
  async setMocks(sessionUrl, rules) {
446
329
  const page = await this.#pageFor(sessionUrl);
447
- if (page === undefined)
330
+ if (page === undefined) {
448
331
  return false;
332
+ }
449
333
  await installNetworkMocks(page, rules);
450
334
  return true;
451
335
  }
452
336
  /** Pin the correlated page's viewport to fixed dimensions; false when no driven page matches. */
453
337
  async setViewport(sessionUrl, size) {
454
338
  const page = await this.#pageFor(sessionUrl);
455
- if (page === undefined)
339
+ if (page === undefined) {
456
340
  return false;
341
+ }
457
342
  await page.setViewportSize({ width: size.width, height: size.height });
458
343
  return true;
459
344
  }
@@ -461,8 +346,9 @@ export class CdpRealInputProvider {
461
346
  async dispose() {
462
347
  const browser = this.#browser;
463
348
  this.#browser = undefined;
464
- if (browser !== undefined)
349
+ if (browser !== undefined) {
465
350
  await browser.close();
351
+ }
466
352
  }
467
353
  }
468
354
  const INJECT_CONNECT_WAIT_MS = 8_000;
@@ -519,8 +405,9 @@ export class LaunchedRealInputProvider {
519
405
  const page = await this.#browser.newPage(this.#storageState !== undefined ? { storageState: this.#storageState } : undefined);
520
406
  this.#page = page;
521
407
  // Capture CDP-authoritative response detail into the driven session's journal (best-effort).
522
- if (this.#onNetworkDetail !== undefined)
408
+ if (this.#onNetworkDetail !== undefined) {
523
409
  attachNetworkDetail(page, this.#onNetworkDetail);
410
+ }
524
411
  try {
525
412
  // Same navigation rule as the pool, and for the same measured reason: Playwright's default
526
413
  // waits for `load`, which an app with one never-finishing subresource never fires — 30s of
@@ -542,8 +429,9 @@ export class LaunchedRealInputProvider {
542
429
  */
543
430
  async #tryInjectConnect(page) {
544
431
  const opts = this.#injectConnect;
545
- if (opts === undefined)
432
+ if (opts === undefined) {
546
433
  return;
434
+ }
547
435
  try {
548
436
  await page.waitForFunction('!!globalThis.__reticleInstance', {
549
437
  timeout: INJECT_CONNECT_WAIT_MS,
@@ -567,8 +455,9 @@ export class LaunchedRealInputProvider {
567
455
  */
568
456
  #livePage() {
569
457
  const page = this.#page;
570
- if (page === undefined)
458
+ if (page === undefined) {
571
459
  return undefined;
460
+ }
572
461
  if (true === page.isClosed?.()) {
573
462
  this.#page = undefined; // never ask a corpse twice
574
463
  return undefined;
@@ -577,10 +466,12 @@ export class LaunchedRealInputProvider {
577
466
  }
578
467
  isAvailableFor(sessionUrl) {
579
468
  const page = this.#livePage();
580
- if (page === undefined)
469
+ if (page === undefined) {
581
470
  return Promise.resolve(false);
582
- if (page.url() === sessionUrl)
471
+ }
472
+ if (page.url() === sessionUrl) {
583
473
  return Promise.resolve(true);
474
+ }
584
475
  return Promise.resolve(stripVolatile(page.url()) === stripVolatile(sessionUrl));
585
476
  }
586
477
  /**
@@ -593,8 +484,9 @@ export class LaunchedRealInputProvider {
593
484
  */
594
485
  async storageState(_sessionUrl) {
595
486
  const page = this.#livePage();
596
- if (page === undefined)
487
+ if (page === undefined) {
597
488
  return undefined;
489
+ }
598
490
  return page.context().storageState();
599
491
  }
600
492
  /**
@@ -603,37 +495,44 @@ export class LaunchedRealInputProvider {
603
495
  */
604
496
  async applyStorageState(_sessionUrl, state) {
605
497
  const page = this.#livePage();
606
- if (page === undefined)
498
+ if (page === undefined) {
607
499
  return false;
500
+ }
608
501
  const saved = state;
609
- if (saved.cookies !== undefined)
502
+ if (saved.cookies !== undefined) {
610
503
  await page.context().addCookies(saved.cookies);
504
+ }
611
505
  for (const origin of saved.origins ?? []) {
612
506
  // A page sitting on a different origin cannot be given another origin's storage, and silently
613
507
  // succeeding there is the quiet half-restore this whole path exists to avoid.
614
- if (!page.url().startsWith(origin.origin))
508
+ if (!page.url().startsWith(origin.origin)) {
615
509
  continue;
510
+ }
616
511
  await page.evaluate((entries) => {
617
512
  const store = globalThis.localStorage;
618
- if (store === undefined)
513
+ if (store === undefined) {
619
514
  return;
620
- for (const entry of entries)
515
+ }
516
+ for (const entry of entries) {
621
517
  store.setItem(entry.name, entry.value);
518
+ }
622
519
  }, origin.localStorage ?? []);
623
520
  }
624
521
  return true;
625
522
  }
626
523
  perform(_sessionUrl, action, box, args) {
627
524
  const page = this.#livePage();
628
- if (page === undefined)
525
+ if (page === undefined) {
629
526
  return Promise.resolve(undriven(action, box));
527
+ }
630
528
  return performGesture(page, action, box, args, this.#sleep, this.#now);
631
529
  }
632
530
  /** PNG of the owned page, or undefined before navigate / after dispose. */
633
531
  screenshot(_sessionUrl, opts) {
634
532
  const page = this.#livePage();
635
- if (page === undefined)
533
+ if (page === undefined) {
636
534
  return Promise.resolve(undefined);
535
+ }
637
536
  return capturePage(page, opts);
638
537
  }
639
538
  /**
@@ -646,8 +545,9 @@ export class LaunchedRealInputProvider {
646
545
  */
647
546
  async setViewport(_sessionUrl, size) {
648
547
  const page = this.#livePage();
649
- if (page === undefined)
548
+ if (page === undefined) {
650
549
  return false;
550
+ }
651
551
  await page.setViewportSize({ width: size.width, height: size.height });
652
552
  return true;
653
553
  }
@@ -658,8 +558,9 @@ export class LaunchedRealInputProvider {
658
558
  /** Apply network-mock rules to the owned page; false before navigate / after dispose. */
659
559
  async setMocks(_sessionUrl, rules) {
660
560
  const page = this.#livePage();
661
- if (page === undefined)
561
+ if (page === undefined) {
662
562
  return false;
563
+ }
663
564
  await installNetworkMocks(page, rules);
664
565
  return true;
665
566
  }
@@ -668,8 +569,9 @@ export class LaunchedRealInputProvider {
668
569
  const browser = this.#browser;
669
570
  this.#browser = undefined;
670
571
  this.#page = undefined;
671
- if (browser !== undefined)
572
+ if (browser !== undefined) {
672
573
  await browser.close();
574
+ }
673
575
  }
674
576
  }
675
577
  /** Drop hash/query so a page whose URL drifted by fragment still correlates to the session. */
@@ -688,8 +590,9 @@ export class LaunchedRealInputProvider {
688
590
  */
689
591
  export function selectPage(pages, sessionUrl) {
690
592
  const exact = pages.find((p) => p.url() === sessionUrl);
691
- if (exact !== undefined)
593
+ if (exact !== undefined) {
692
594
  return exact;
595
+ }
693
596
  const target = stripVolatile(sessionUrl);
694
597
  const loose = pages.filter((p) => stripVolatile(p.url()) === target);
695
598
  return 1 === loose.length ? loose[0] : undefined;
@@ -1,24 +1,28 @@
1
1
  import { z } from 'zod';
2
2
  import type { ToolDef } from '../../surface/tools/tool-kit.js';
3
3
  import { type LeaseEmulation, type LeaseMedia } from '../pool/lease-emulation.js';
4
- export declare const COLOR_SCHEME_ARG: z.ZodOptional<z.ZodEnum<["light", "dark", "no-preference"]>>;
5
- export declare const REDUCED_MOTION_ARG: z.ZodOptional<z.ZodEnum<["reduce", "no-preference"]>>;
4
+ export declare const COLOR_SCHEME_ARG: z.ZodOptional<z.ZodEnum<{
5
+ light: "light";
6
+ dark: "dark";
7
+ "no-preference": "no-preference";
8
+ }>>;
9
+ export declare const REDUCED_MOTION_ARG: z.ZodOptional<z.ZodEnum<{
10
+ reduce: "reduce";
11
+ "no-preference": "no-preference";
12
+ }>>;
6
13
  export declare const LEASE_EMULATE_ARG: z.ZodOptional<z.ZodObject<{
7
- colorScheme: z.ZodOptional<z.ZodEnum<["light", "dark", "no-preference"]>>;
8
- reducedMotion: z.ZodOptional<z.ZodEnum<["reduce", "no-preference"]>>;
14
+ colorScheme: z.ZodOptional<z.ZodEnum<{
15
+ light: "light";
16
+ dark: "dark";
17
+ "no-preference": "no-preference";
18
+ }>>;
19
+ reducedMotion: z.ZodOptional<z.ZodEnum<{
20
+ reduce: "reduce";
21
+ "no-preference": "no-preference";
22
+ }>>;
9
23
  width: z.ZodOptional<z.ZodNumber>;
10
24
  height: z.ZodOptional<z.ZodNumber>;
11
- }, "strict", z.ZodTypeAny, {
12
- width?: number | undefined;
13
- height?: number | undefined;
14
- colorScheme?: "light" | "dark" | "no-preference" | undefined;
15
- reducedMotion?: "reduce" | "no-preference" | undefined;
16
- }, {
17
- width?: number | undefined;
18
- height?: number | undefined;
19
- colorScheme?: "light" | "dark" | "no-preference" | undefined;
20
- reducedMotion?: "reduce" | "no-preference" | undefined;
21
- }>>;
25
+ }, z.core.$strict>>;
22
26
  /** The media part of what a caller asked for, or undefined when it asked for none. */
23
27
  export declare function mediaOf(raw: {
24
28
  colorScheme?: unknown;
@@ -14,61 +14,9 @@ import type { Launcher, PooledMockRule, ScreenshotOptions } from './pool-contrac
14
14
  import type { RealInputArgs, RealInputResult } from '../input/real-input.js';
15
15
  import { type LeaseEmulation, type LeaseMedia } from './lease-emulation.js';
16
16
  export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, } from './pool-contract.js';
17
- /** A leased context+page for one flow. `release` frees the slot and closes the context. */
18
- export interface Lease {
19
- readonly sessionId: string;
20
- readonly url: string;
21
- readonly navStatus?: number;
22
- release(): Promise<void>;
23
- /**
24
- * Hand the page the zero-install reader: evaluated in the live document, and added ahead of every
25
- * document this lease loads next. False when there is no reader, or the page cannot run one.
26
- * Rejects with `NotHtmlDocumentError` when the page is not an HTML document.
27
- */
28
- injectReader?(): Promise<boolean>;
29
- /** Whether the page's document has not reached `complete` yet. False when it cannot be read. */
30
- isLoading?(): Promise<boolean>;
31
- }
32
- /** Default lease time-to-live: a lease untouched for this long is presumed orphaned and reclaimed. */
33
- export declare const DEFAULT_LEASE_TTL_MS: number;
34
- /**
35
- * How long a lease outlives the MCP connection that took it. Long enough for an agent whose
36
- * connection only blipped to touch it again; short enough that a gone agent's window closes soon.
37
- */
38
- export declare const ORPHANED_LEASE_GRACE_MS = 30000;
39
- interface BrowserPoolOptions {
40
- /** Max simultaneous leased contexts. Over-cap acquires queue. */
41
- maxContexts: number;
42
- /** Stable id generator for lease sessionIds (injected to stay clock-/random-free in logic). */
43
- genSessionId: () => string;
44
- /** Injected clock (ms). Defaults to Date.now; tests pass a controllable one. */
45
- now?: () => number;
46
- /** A lease untouched for longer than this is reclaimed by sweepExpired. */
47
- leaseTtlMs?: number;
48
- /** Per-lease navigation timeout — a page that won't load fails its own lease, never blocks a slot. */
49
- navTimeoutMs?: number;
50
- /**
51
- * The zero-install reader, built where the daemon knows its own port and pairing token. A thunk so
52
- * the ~450KB build is only read if some page ever needs it; undefined when there is none to give.
53
- */
54
- zeroInstallScript?: () => string | undefined;
55
- /**
56
- * Launches a browser with a window, for a lease somebody wants to watch. Launched only when the
57
- * first headed lease is asked for. Absent: a headed lease is refused.
58
- */
59
- launchHeaded?: Launcher;
60
- }
61
- /** What a headed lease is refused with where no window can be opened. */
62
- export declare const NO_HEADED_BROWSER = "no headed browser can be opened here";
63
- /**
64
- * An acquire that asked to be refused rather than queued found every slot taken (#1535). Queued,
65
- * it waited behind the cap with no deadline and no status, and `reticle drive` reported the 60 s
66
- * timeout as a daemon shutting down. The message names the cap and the one override for it.
67
- */
68
- export declare class PoolAtCapacityError extends Error {
69
- readonly max: number;
70
- constructor(max: number);
71
- }
17
+ import { type BrowserPoolOptions, type Lease } from './pool-lease.js';
18
+ export { DEFAULT_LEASE_TTL_MS, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, PoolAtCapacityError, } from './pool-lease.js';
19
+ export type { Lease } from './pool-lease.js';
72
20
  /**
73
21
  * Owns a headless browser (and, when asked, a headed one) and leases isolated contexts out of them,
74
22
  * capped at `maxContexts` across both.
@@ -171,6 +119,8 @@ export declare class BrowserPool {
171
119
  hoverLease(sessionId: string, x: number, y: number): Promise<boolean>;
172
120
  /** Set a live lease's media features, or false when this is not a lease or its page cannot. */
173
121
  emulateLease(sessionId: string, media: LeaseMedia): Promise<boolean>;
122
+ /** Whether this lease's page is still loading. False for a non-lease, or a page that cannot say. */
123
+ leaseLoading(sessionId: string): Promise<boolean>;
174
124
  /** Whether this session is a lease whose page has a real keyboard. */
175
125
  hasKeyboard(sessionId: string): boolean;
176
126
  /**