@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,11 +10,9 @@
10
10
  * Attach-only: the pool drives a browser against an already-running dev server — it never starts one.
11
11
  */
12
12
  import * as production from '../../portal/production/production-activation.js';
13
- import { fixturePortFor, seedFromStorageState, } from '../../portal/input/storage-fixture.js';
14
- import { mutationPortFor } from '../../portal/input/network-mutation.js';
15
13
  import { z } from 'zod';
16
14
  import { leaseNotConnectedHint, leaseReaderFailedHint, leaseStillLoadingHint, } from './lease-hint.js';
17
- import { connectOrInject, probeLeaseAlive, tabHidden } from './lease-readiness.js';
15
+ import { connectOrInject, moveReusedLease, notReadyReason, probeLeaseAlive, } from './lease-readiness.js';
18
16
  import { probeSdkMarker } from './gaps/sdk-marker-probe.js';
19
17
  import { observeWebDocument } from '../../portal/session/dev-server/served-document.js';
20
18
  import { diagnoseObservedWebCsp } from '@reticlehq/init';
@@ -31,6 +29,7 @@ import { asString } from '@reticlehq/core';
31
29
  import { chromiumPreflightRefusal } from '../../command/cli/doctor/browser/chromium-hint.js';
32
30
  import { LEASE_PERMISSIONS_ARG, hintOf, notificationReadBack, parseLeasePermissions, permissionRefusal, refuseRegrant, } from './lease-permissions.js';
33
31
  import { LEASE_EMULATE_ARG, leaseEmulationOf } from '../../portal/input/viewport-tools.js';
32
+ export { sessionMutationPort, sessionPerturbationPort, suiteFixturePort, suiteFixtureSeed, } from './session-ports.js';
34
33
  /**
35
34
  * Everything the daemon already knows about why a leased tab might not have dialled in.
36
35
  *
@@ -54,11 +53,13 @@ import { LEASE_EMULATE_ARG, leaseEmulationOf } from '../../portal/input/viewport
54
53
  */
55
54
  async function observedCspBlock(url, port) {
56
55
  const document = await observeWebDocument(url);
57
- if (document === undefined)
56
+ if (document === undefined) {
58
57
  return undefined;
58
+ }
59
59
  const refusesSocket = diagnoseObservedWebCsp(document, port).some((finding) => `${finding.problem} ${finding.fix}`.includes(CONNECT_SRC));
60
- if (!refusesSocket)
60
+ if (!refusesSocket) {
61
61
  return undefined;
62
+ }
62
63
  const allow = `ws://localhost:${String(port)} ws://127.0.0.1:${String(port)}`;
63
64
  return {
64
65
  problem: `its ${CONNECT_SRC} does not allow ws://localhost:${String(port)}`,
@@ -102,8 +103,9 @@ export function appendReticleParams(url, session, projectId, hud) {
102
103
  if (projectId !== undefined && projectId.length > 0) {
103
104
  u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
104
105
  }
105
- if (hud !== undefined)
106
+ if (hud !== undefined) {
106
107
  u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
108
+ }
107
109
  return u.toString();
108
110
  }
109
111
  catch {
@@ -137,8 +139,9 @@ function newLeaseId() {
137
139
  * so `lease-abc` cannot claim `lease-abcdef`.
138
140
  */
139
141
  export function resolveLeasedSessionId(sessions, leaseId) {
140
- if (sessions.get(leaseId) !== undefined)
142
+ if (sessions.get(leaseId) !== undefined) {
141
143
  return leaseId;
144
+ }
142
145
  return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
143
146
  }
144
147
  /**
@@ -156,17 +159,20 @@ export function resolveLeasedSessionId(sessions, leaseId) {
156
159
  */
157
160
  export function carryReticleIdentity(fromUrl, toUrl) {
158
161
  const session = sessionParamOf(fromUrl);
159
- if (session === undefined)
162
+ if (session === undefined) {
160
163
  return toUrl;
164
+ }
161
165
  // Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
162
- if (sessionParamOf(toUrl) !== undefined)
166
+ if (sessionParamOf(toUrl) !== undefined) {
163
167
  return toUrl;
168
+ }
164
169
  const projectId = projectParamOf(fromUrl);
165
170
  return appendReticleParams(toUrl, session, projectId);
166
171
  }
167
172
  function projectParamOf(url) {
168
- if (url === undefined)
173
+ if (url === undefined) {
169
174
  return undefined;
175
+ }
170
176
  try {
171
177
  return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
172
178
  }
@@ -175,8 +181,9 @@ function projectParamOf(url) {
175
181
  }
176
182
  }
177
183
  function sessionParamOf(url) {
178
- if (url === undefined)
184
+ if (url === undefined) {
179
185
  return undefined;
186
+ }
180
187
  try {
181
188
  return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
182
189
  }
@@ -209,8 +216,9 @@ export async function acquireLeasedSession(pool, sessions, url, projectId, seedS
209
216
  registeredId = resolveLeasedSessionId(sessions, lease.sessionId);
210
217
  return registeredId !== undefined;
211
218
  });
212
- if (registeredId !== undefined)
219
+ if (registeredId !== undefined) {
213
220
  pool.alias?.(registeredId, lease.sessionId);
221
+ }
214
222
  const effectiveId = registeredId ?? lease.sessionId;
215
223
  if (seedStorage !== undefined) {
216
224
  const session = sessions.get(effectiveId);
@@ -244,7 +252,8 @@ export const LEASE_ACQUIRE_TOOL = {
244
252
  inputSchema: {
245
253
  url: z
246
254
  .string()
247
- .describe('URL of the already-running app to drive (e.g. http://localhost:3000/dashboard).'),
255
+ .optional()
256
+ .describe('URL of the already-running app to drive (e.g. http://localhost:3000/dashboard). Omit it to lease the one dev server that runs in this project folder.'),
248
257
  projectId: z
249
258
  .string()
250
259
  .optional()
@@ -311,6 +320,10 @@ export const LEASE_ACQUIRE_TOOL = {
311
320
  .boolean()
312
321
  .optional()
313
322
  .describe('True when this acquire returned an existing live lease on the same origin rather than minting a second context.'),
323
+ navigated: z
324
+ .boolean()
325
+ .optional()
326
+ .describe('Present when a reused lease was on another page. True: its tab was moved to url. False: it could not be moved, and url is the page it is on.'),
314
327
  hint: z.string().optional(),
315
328
  production: production.PRODUCTION_LEASE_OUTPUT,
316
329
  versionSkew: z
@@ -322,11 +335,14 @@ export const LEASE_ACQUIRE_TOOL = {
322
335
  },
323
336
  handler: async (deps, args) => {
324
337
  const pool = deps.pool;
325
- if (pool === undefined)
338
+ if (pool === undefined) {
326
339
  throw new Error(POOL_UNAVAILABLE);
327
- const url = asString(args['url']);
328
- if (url === undefined || 0 === url.length)
329
- throw new Error('reticle_lease{action:"acquire"} requires a url');
340
+ }
341
+ const given = asString(args['url']);
342
+ const url = given === undefined || 0 === given.length ? await deps.projectAppUrl?.() : given;
343
+ if (url === undefined) {
344
+ throw new Error('reticle_lease{action:"acquire"} requires a url: no single dev server runs in this project folder. Start the app, or pass the url it serves.');
345
+ }
330
346
  // Preflight the browser before spending the round trip. Without it a missing Playwright Chromium
331
347
  // only surfaces inside pool.acquire, where the launch failure is caught and reported as
332
348
  // "could not open <url> — is the app running?" — sending the caller to debug an app that is
@@ -334,8 +350,9 @@ export const LEASE_ACQUIRE_TOOL = {
334
350
  // installed" so error-recovery routes it to the NO_POOL fix (install + drive a human tab).
335
351
  if (deps.browserProbe !== undefined) {
336
352
  const refusal = chromiumPreflightRefusal(await deps.browserProbe());
337
- if (refusal !== undefined)
353
+ if (refusal !== undefined) {
338
354
  throw new Error(refusal);
355
+ }
339
356
  }
340
357
  const projectId = asString(args['projectId']);
341
358
  const root = asString(args['root']);
@@ -438,16 +455,16 @@ export const LEASE_ACQUIRE_TOOL = {
438
455
  // old, or there may be none at all — `unresponsive` is set by past commands failing, and
439
456
  // its own contract says absence means "answering, OR NOT ASKED YET". So the happy path of
440
457
  // a first acquire pays nothing for this.
458
+ const tabUrl = () => deps.sessions.get(resolved)?.url;
459
+ const moved = await moveReusedLease(tabUrl, url, () => pool.gotoLease(existing, carryReticleIdentity(tabUrl(), url)), deps.now);
441
460
  const reusedSession = deps.sessions.get(resolved);
442
- const alive = await probeLeaseAlive(reusedSession);
443
- const reuseReason = !alive
444
- ? LeaseNotReadyReason.SDK_STOPPED_ANSWERING
445
- : tabHidden(reusedSession)
446
- ? LeaseNotReadyReason.TAB_HIDDEN
447
- : undefined;
461
+ const reuseReason = false === moved?.arrived
462
+ ? LeaseNotReadyReason.SDK_NEVER_DIALLED
463
+ : notReadyReason(await probeLeaseAlive(reusedSession), reusedSession);
448
464
  return {
449
465
  sessionId: resolved,
450
- url,
466
+ url: moved?.url ?? url,
467
+ ...(moved === undefined ? {} : { navigated: moved.navigated }),
451
468
  ready: reuseReason === undefined,
452
469
  ...(reuseReason === undefined ? {} : { notReadyReason: reuseReason }),
453
470
  reused: true,
@@ -470,8 +487,9 @@ export const LEASE_ACQUIRE_TOOL = {
470
487
  }
471
488
  const sessionId = newLeaseId();
472
489
  // Before the page can dial in: the session is stamped with its root the moment it registers.
473
- if (root !== undefined)
490
+ if (root !== undefined) {
474
491
  claimArtifactRoot(sessionId, root);
492
+ }
475
493
  const navUrl = appendReticleParams(activated?.url ?? url, sessionId, projectId, hud);
476
494
  let lease;
477
495
  try {
@@ -490,11 +508,13 @@ export const LEASE_ACQUIRE_TOOL = {
490
508
  });
491
509
  }
492
510
  catch (err) {
493
- if (err instanceof PoolAtCapacityError)
511
+ if (err instanceof PoolAtCapacityError) {
494
512
  throw err;
513
+ }
495
514
  const refusal = permissionRefusal(err);
496
- if (refusal !== undefined)
515
+ if (refusal !== undefined) {
497
516
  throw refusal;
517
+ }
498
518
  if (err instanceof Error && err.message.startsWith('Storage seeding failed')) {
499
519
  throw new Error(scrubSeedFromError(err.message, validatedSeed));
500
520
  }
@@ -516,8 +536,9 @@ export const LEASE_ACQUIRE_TOOL = {
516
536
  // under the id we are about to hand back, and the pool is keyed by the id it navigated with;
517
537
  // without this the touches miss, the lease ages out despite continuous activity, and the
518
538
  // reaper closes the context mid-flow. See BrowserPool.alias and #157.
519
- if (registeredId !== undefined)
539
+ if (registeredId !== undefined) {
520
540
  pool.alias(registeredId, lease.sessionId);
541
+ }
521
542
  const effectiveSessionId = registeredId ?? lease.sessionId;
522
543
  const session = deps.sessions.get(effectiveSessionId);
523
544
  if (validatedSeed !== undefined && session !== undefined) {
@@ -528,17 +549,29 @@ export const LEASE_ACQUIRE_TOOL = {
528
549
  }
529
550
  // The lease now exists, so any HUD a human is watching has just gone dark. Say so. A lease whose
530
551
  // reader failed is already released, so there is nothing to say.
531
- if (readerFailure === undefined)
552
+ if (readerFailure === undefined) {
532
553
  tellWatchers(deps, projectId, AGENT_DRIVING_ELSEWHERE);
554
+ }
533
555
  // ready means the SDK dialled in — not that contracts match. Carry the skew warning on acquire
534
556
  // so the agent does not learn it only after a CDP tool invents a closed page (#688).
535
557
  const mintReason = readerFailure?.reason ??
536
- (!ready
537
- ? LeaseNotReadyReason.SDK_NEVER_DIALLED
538
- : tabHidden(session)
539
- ? LeaseNotReadyReason.TAB_HIDDEN
540
- : undefined);
558
+ notReadyReason(ready, session, LeaseNotReadyReason.SDK_NEVER_DIALLED);
541
559
  const versionSkew = registeredId === undefined ? undefined : deps.sessions.get(registeredId)?.versionSkew;
560
+ let notReadyHint;
561
+ if (!ready) {
562
+ if (readerFailure !== undefined) {
563
+ notReadyHint = leaseReaderFailedHint(url, readerFailure);
564
+ }
565
+ else if (true === stillLoading) {
566
+ notReadyHint = leaseStillLoadingHint(url, Date.now() - waitStartedAt);
567
+ }
568
+ else if (activated !== undefined) {
569
+ notReadyHint = production.notActivatedHint(url);
570
+ }
571
+ else {
572
+ notReadyHint = await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId));
573
+ }
574
+ }
542
575
  return {
543
576
  sessionId: registeredId ?? lease.sessionId,
544
577
  url,
@@ -563,15 +596,7 @@ export const LEASE_ACQUIRE_TOOL = {
563
596
  ...(versionSkew === undefined ? {} : { versionSkew }),
564
597
  ...production.summary(activated),
565
598
  ...(initScript === undefined ? {} : { hostStub: HOST_STUB_NOTE }),
566
- ...hintOf(ready
567
- ? undefined
568
- : readerFailure !== undefined
569
- ? leaseReaderFailedHint(url, readerFailure)
570
- : true === stillLoading
571
- ? leaseStillLoadingHint(url, Date.now() - waitStartedAt)
572
- : activated !== undefined
573
- ? production.notActivatedHint(url)
574
- : await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId)), ready ? await notificationReadBack(pool, lease.sessionId, permissions) : undefined),
599
+ ...hintOf(notReadyHint, ready ? await notificationReadBack(pool, lease.sessionId, permissions) : undefined),
575
600
  };
576
601
  }
577
602
  finally {
@@ -605,8 +630,9 @@ const LEASE_RELEASE_TOOL = {
605
630
  },
606
631
  handler: async (deps, args) => {
607
632
  const pool = deps.pool;
608
- if (pool === undefined)
633
+ if (pool === undefined) {
609
634
  throw new Error(POOL_UNAVAILABLE);
635
+ }
610
636
  const sessionId = asString(args['sessionId']);
611
637
  if (sessionId === undefined || 0 === sessionId.length) {
612
638
  throw new Error('reticle_lease{action:"release"} requires a sessionId');
@@ -635,8 +661,9 @@ export async function releaseLease(deps, pool, sessionId) {
635
661
  dropArtifactRootClaim(sessionId);
636
662
  // Only once the LAST lease is gone. Announcing "live again" while another lease still drives
637
663
  // would be a lie, and a HUD that says the wrong thing is worse than one that says nothing.
638
- if (0 === pool.activeCount())
664
+ if (0 === pool.activeCount()) {
639
665
  tellWatchers(deps, projectId, AGENT_DRIVING_HERE_AGAIN);
666
+ }
640
667
  return pool.activeCount();
641
668
  }
642
669
  /**
@@ -664,11 +691,13 @@ const PREFER_EXISTING_NOTE = 'a non-leased tab for this app was already connecte
664
691
  */
665
692
  function liveTabFor(deps, projectId, url) {
666
693
  const pool = deps.pool;
667
- if (pool === undefined)
694
+ if (pool === undefined) {
668
695
  return undefined;
696
+ }
669
697
  const wanted = originOf(url);
670
- if (wanted === undefined)
698
+ if (wanted === undefined) {
671
699
  return undefined;
700
+ }
672
701
  try {
673
702
  const candidates = deps.sessions
674
703
  .all()
@@ -692,8 +721,9 @@ function liveTabFor(deps, projectId, url) {
692
721
  */
693
722
  function tellWatchers(deps, projectId, text) {
694
723
  const pool = deps.pool;
695
- if (pool === undefined)
724
+ if (pool === undefined) {
696
725
  return;
726
+ }
697
727
  try {
698
728
  const candidates = deps.sessions
699
729
  .all()
@@ -707,96 +737,3 @@ function tellWatchers(deps, projectId, text) {
707
737
  }
708
738
  }
709
739
  export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL];
710
- /**
711
- * The state a suite should boot its flows from: whatever the agent is already sitting in.
712
- *
713
- * This is the join the fixture work was building toward. A suite's flows each start from cold, so
714
- * fifty of them prove the login works fifty times — and the flow that LOGS OUT leaves every flow
715
- * after it signed out, which no navigation repairs, because the problem is not where the subject is
716
- * but what it holds. A seed is applied to an isolated context BEFORE the first navigation, which is
717
- * exactly and only when a fixture can work.
718
- *
719
- * Best-effort, deliberately. A fixture is an OPTIMISATION over running from cold, and running from
720
- * cold is correct: a suite that refused to start because a cookie jar could not be read would have
721
- * traded a slow answer for no answer. Every "no" here — no provider, no storage support, nothing
722
- * driven, a browser that threw, nothing worth seeding — produces the same honest `undefined`, and
723
- * the suite runs exactly as it did before any of this existed.
724
- */
725
- export async function suiteFixtureSeed(realInput, appUrl) {
726
- if (realInput === undefined)
727
- return undefined;
728
- try {
729
- const port = await fixturePortFor(realInput, appUrl);
730
- if (port === undefined)
731
- return undefined;
732
- return seedFromStorageState(await port.capture(), appUrl);
733
- }
734
- catch {
735
- return undefined;
736
- }
737
- }
738
- /**
739
- * A way to make this session's page SLOW on purpose, or nothing.
740
- *
741
- * The third of these seams, for the same reason as the other two: a feature reaching into the input
742
- * layer directly is a feature depending on a driver, and the boundary guard asks about it.
743
- *
744
- * Installs delays and NOTHING else. A rule that carried a status or a body would substitute a
745
- * response the server never sent, and an app raced against invented data produces invented races —
746
- * a fabricated defect handed to somebody at 3am, which is worse than finding nothing.
747
- */
748
- export async function sessionPerturbationPort(realInput, appUrl) {
749
- if (realInput === undefined || appUrl === undefined)
750
- return undefined;
751
- const setMocks = realInput.setMocks?.bind(realInput);
752
- if (setMocks === undefined)
753
- return undefined;
754
- if (!(await realInput.isAvailableFor(appUrl)))
755
- return undefined;
756
- return {
757
- slow: async (rules) => {
758
- await setMocks(appUrl, [...rules]);
759
- },
760
- /** Clearing is the undo. A page left slowed is damage the next run inherits. */
761
- clear: async () => {
762
- await setMocks(appUrl, []);
763
- },
764
- };
765
- }
766
- /**
767
- * Save/restore for this session's page, or nothing.
768
- *
769
- * The same job as `suiteFixtureSeed` next door, stopping one step earlier: that one captures and
770
- * converts, this hands back the PORT so a caller can re-apply what it captured later. It lives here
771
- * for the reason written above — a feature reaching into the input layer directly is a feature
772
- * depending on a driver, and the boundary guard asks about it. It asked.
773
- */
774
- export async function suiteFixturePort(realInput, appUrl) {
775
- if (realInput === undefined || appUrl === undefined)
776
- return undefined;
777
- try {
778
- return await fixturePortFor(realInput, appUrl);
779
- }
780
- catch {
781
- return undefined;
782
- }
783
- }
784
- /**
785
- * A way to break this session's page, or nothing.
786
- *
787
- * Sits beside `suiteFixtureSeed` because it is the same shape of job: turning a provider capability
788
- * into something a feature can use, on the one side of the graph that is allowed to know about both.
789
- * A flow tool reaching into the input layer directly would be a feature depending on a driver, which
790
- * is the reach the boundary guard exists to ask about.
791
- */
792
- export async function sessionMutationPort(realInput, appUrl) {
793
- if (realInput === undefined || appUrl === undefined)
794
- return undefined;
795
- try {
796
- return await mutationPortFor(realInput, appUrl);
797
- }
798
- catch {
799
- // A provider that cannot answer is the same answer as a provider that cannot break anything.
800
- return undefined;
801
- }
802
- }
@@ -705,7 +705,9 @@ export const OBSERVE_TOOLS = [
705
705
  .optional()
706
706
  .describe('Total matches before `limit` — present only when capped.'),
707
707
  droppedOldest: z.number().optional().describe('How many older matches `limit` dropped.'),
708
- hint: z.object({ totalInWindow: z.number(), byLevel: z.record(z.number()) }).optional(),
708
+ hint: z
709
+ .object({ totalInWindow: z.number(), byLevel: z.record(z.string(), z.number()) })
710
+ .optional(),
709
711
  cost: z.object({ bytes: z.number(), tokens: z.number() }).optional(),
710
712
  ...bufferOutputShape,
711
713
  },
@@ -199,7 +199,7 @@ export const READ_TOOLS = [
199
199
  },
200
200
  outputSchema: {
201
201
  recordingName: z.string(),
202
- program: z.unknown(),
202
+ program: z.unknown().optional(),
203
203
  warning: z.string().optional(),
204
204
  proposedConsequences: z
205
205
  .array(z.unknown())
@@ -386,7 +386,7 @@ export const READ_TOOLS = [
386
386
  ...sessionIdShape,
387
387
  },
388
388
  outputSchema: {
389
- stores: z.record(z.unknown()).optional(),
389
+ stores: z.record(z.string(), z.unknown()).optional(),
390
390
  storeNames: z.array(z.string()).optional(),
391
391
  found: z.boolean().optional(),
392
392
  value: z.unknown().optional(),
@@ -598,9 +598,9 @@ export const READ_TOOLS = [
598
598
  ...sessionIdShape,
599
599
  },
600
600
  outputSchema: {
601
- local: z.record(z.string()).optional(),
602
- session: z.record(z.string()).optional(),
603
- cookies: z.record(z.string()).optional(),
601
+ local: z.record(z.string(), z.string()).optional(),
602
+ session: z.record(z.string(), z.string()).optional(),
603
+ cookies: z.record(z.string(), z.string()).optional(),
604
604
  area: z.string().optional(),
605
605
  key: z.string().optional(),
606
606
  value: z.string().optional(),