@reticlehq/server 3.8.1 → 3.9.0

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 (231) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/bin/launch.js +13 -0
  5. package/bin/package.json +3 -0
  6. package/bin/reticle.js +66 -8
  7. package/dist/chromium-launch-options.d.ts +25 -0
  8. package/dist/chromium-launch-options.js +21 -1
  9. package/dist/command/cli/adhoc-verdict.js +34 -7
  10. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  11. package/dist/command/cli/answers/gate-exit.js +12 -2
  12. package/dist/command/cli/ci-hint.d.ts +44 -0
  13. package/dist/command/cli/ci-hint.js +72 -0
  14. package/dist/command/cli/cli-doctor.d.ts +6 -1
  15. package/dist/command/cli/cli-doctor.js +45 -4
  16. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  17. package/dist/command/cli/cli-flow-commands.js +39 -17
  18. package/dist/command/cli/cli-kill.d.ts +21 -0
  19. package/dist/command/cli/cli-kill.js +46 -3
  20. package/dist/command/cli/cli-parse.d.ts +5 -2
  21. package/dist/command/cli/cli-parse.js +10 -5
  22. package/dist/command/cli/cli-usage.js +3 -2
  23. package/dist/command/cli/cli-verify.d.ts +6 -1
  24. package/dist/command/cli/cli-verify.js +32 -13
  25. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  26. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  27. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  28. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  29. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  30. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  31. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  32. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  33. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  34. package/dist/command/cli/doctor-repairs.js +214 -0
  35. package/dist/command/cli/drive/drive-command.js +3 -7
  36. package/dist/command/cli/first-use-checks.d.ts +41 -0
  37. package/dist/command/cli/first-use-checks.js +127 -0
  38. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  39. package/dist/command/cli/first-verdict-node.js +189 -0
  40. package/dist/command/cli/first-verdict.d.ts +126 -0
  41. package/dist/command/cli/first-verdict.js +267 -0
  42. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  43. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  44. package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
  45. package/dist/command/cli/mcp-command.js +13 -4
  46. package/dist/command/cli/ports/port-holder.d.ts +2 -0
  47. package/dist/command/cli/ports/port-holder.js +11 -3
  48. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  49. package/dist/command/cli/setup-mcp-cli.js +98 -23
  50. package/dist/command/cli/status/status-lines.d.ts +1 -0
  51. package/dist/command/cli/status/status-lines.js +50 -17
  52. package/dist/command/cli/status-fields.js +17 -3
  53. package/dist/command/cli/try-command.d.ts +7 -0
  54. package/dist/command/cli/try-command.js +11 -3
  55. package/dist/command/cli.js +70 -33
  56. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  57. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  58. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  59. package/dist/command/daemon/daemon-argv.js +11 -0
  60. package/dist/command/daemon/daemon.d.ts +10 -3
  61. package/dist/command/daemon/daemon.js +17 -6
  62. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  63. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  64. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  65. package/dist/command/daemon/lifetime/death-cause.js +130 -0
  66. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  67. package/dist/command/daemon/lifetime/heartbeat.js +4 -13
  68. package/dist/command/daemon/port-state.d.ts +14 -0
  69. package/dist/command/daemon/port-state.js +31 -0
  70. package/dist/command/setup/agent-configs.d.ts +2 -0
  71. package/dist/command/setup/agent-configs.js +42 -15
  72. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  73. package/dist/command/setup/agent-next-steps.js +284 -0
  74. package/dist/command/setup/agent-writer.js +11 -6
  75. package/dist/command/setup/init/init-host.js +2 -0
  76. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  77. package/dist/command/setup/init/init-runtime.js +57 -21
  78. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  79. package/dist/command/setup/mcp-launcher.js +11 -0
  80. package/dist/command/setup/setup-command.js +30 -12
  81. package/dist/command/setup/setup-mcp.d.ts +8 -0
  82. package/dist/command/setup/setup-mcp.js +47 -20
  83. package/dist/command/setup/start-dev-server.d.ts +8 -0
  84. package/dist/command/setup/start-dev-server.js +43 -0
  85. package/dist/command/version/identity/server-version.d.ts +2 -0
  86. package/dist/command/version/identity/server-version.js +2 -0
  87. package/dist/command/version/version-skew.d.ts +7 -0
  88. package/dist/command/version/version-skew.js +13 -2
  89. package/dist/features/license/license.d.ts +11 -71
  90. package/dist/features/license/license.js +43 -136
  91. package/dist/features/visual/desktop-capture.d.ts +33 -0
  92. package/dist/features/visual/desktop-capture.js +94 -0
  93. package/dist/features/visual/step-shots.js +139 -35
  94. package/dist/features/visual/visual-tools.d.ts +0 -18
  95. package/dist/features/visual/visual-tools.js +2 -81
  96. package/dist/import-graph.js +5 -6
  97. package/dist/index.d.ts +3 -1
  98. package/dist/index.js +14 -5
  99. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  100. package/dist/judgement/runs/run-ledger.js +98 -69
  101. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  102. package/dist/judgement/runs/verification-sync.js +21 -10
  103. package/dist/language/flows/flow-store-for-session.js +7 -5
  104. package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
  105. package/dist/language/flows/recording/tape/recordings.js +41 -44
  106. package/dist/language/flows/replay.d.ts +17 -7
  107. package/dist/language/flows/replay.js +95 -44
  108. package/dist/launch-chromium.d.ts +1 -0
  109. package/dist/launch-chromium.js +49 -12
  110. package/dist/memory/cloud/sync-screenshots.js +6 -1
  111. package/dist/portal/bridge/bridge.d.ts +13 -4
  112. package/dist/portal/bridge/bridge.js +10 -9
  113. package/dist/portal/input/drive-error.d.ts +6 -0
  114. package/dist/portal/input/drive-error.js +9 -0
  115. package/dist/portal/input/network-mock.js +3 -2
  116. package/dist/portal/input/real-input-press.d.ts +34 -0
  117. package/dist/portal/input/real-input-press.js +150 -0
  118. package/dist/portal/input/real-input.d.ts +21 -23
  119. package/dist/portal/input/real-input.js +153 -194
  120. package/dist/portal/input/viewport-tools.js +6 -3
  121. package/dist/portal/pool/browser-pool.d.ts +5 -1
  122. package/dist/portal/pool/browser-pool.js +32 -38
  123. package/dist/portal/pool/context-permissions.d.ts +20 -17
  124. package/dist/portal/pool/context-permissions.js +63 -1
  125. package/dist/portal/pool/csp-widen.d.ts +62 -0
  126. package/dist/portal/pool/csp-widen.js +132 -0
  127. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  128. package/dist/portal/pool/lease-page-error.js +18 -0
  129. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  130. package/dist/portal/pool/page-error-journal.js +181 -0
  131. package/dist/portal/pool/playwright-launcher.js +21 -8
  132. package/dist/portal/pool/pool-contract.d.ts +22 -0
  133. package/dist/portal/pool/pool-lease.d.ts +35 -2
  134. package/dist/portal/pool/pool-lease.js +23 -0
  135. package/dist/portal/pool/zero-install.js +69 -1
  136. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  137. package/dist/portal/session/dev-server/dev-command.js +56 -8
  138. package/dist/portal/session/no-session-diagnosis.js +10 -3
  139. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  140. package/dist/portal/session/no-session-next-action.js +44 -4
  141. package/dist/portal/session/no-session-watch.js +3 -1
  142. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  143. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  144. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  145. package/dist/portal/session/recorded-gaps.js +5 -2
  146. package/dist/portal/session/session-manager.js +40 -21
  147. package/dist/portal/session/session.js +4 -3
  148. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  149. package/dist/portal/session/zero-install-first-call.js +34 -2
  150. package/dist/start-wiring.d.ts +29 -2
  151. package/dist/start-wiring.js +73 -9
  152. package/dist/surface/mcp/mcp-proxy.js +10 -2
  153. package/dist/surface/mcp/mcp.js +5 -3
  154. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  155. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  156. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  157. package/dist/surface/tools/act/act-preflight.js +37 -17
  158. package/dist/surface/tools/act/act-target.js +3 -1
  159. package/dist/surface/tools/act/after-match.d.ts +7 -0
  160. package/dist/surface/tools/act/after-match.js +33 -0
  161. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  162. package/dist/surface/tools/act/resolve-target.js +35 -8
  163. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  164. package/dist/surface/tools/act/sequence-grade.js +8 -1
  165. package/dist/surface/tools/act-sequence-tool.js +3 -0
  166. package/dist/surface/tools/act-tools.js +12 -12
  167. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  168. package/dist/surface/tools/browser-tools.js +75 -7
  169. package/dist/surface/tools/dynamic-tools.js +2 -1
  170. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  171. package/dist/surface/tools/empty-session-list.js +4 -2
  172. package/dist/surface/tools/error-recovery.js +43 -22
  173. package/dist/surface/tools/invoke-tool.js +27 -20
  174. package/dist/surface/tools/lease-hint.d.ts +7 -0
  175. package/dist/surface/tools/lease-hint.js +10 -3
  176. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  177. package/dist/surface/tools/lease-readiness.js +47 -0
  178. package/dist/surface/tools/lease-seed.d.ts +2 -0
  179. package/dist/surface/tools/lease-seed.js +2 -1
  180. package/dist/surface/tools/lease-tools.js +43 -15
  181. package/dist/surface/tools/real-input-attempt.js +70 -31
  182. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  183. package/dist/surface/tools/repeat-hint.js +171 -0
  184. package/dist/surface/tools/run-report.d.ts +2 -2
  185. package/dist/surface/tools/run-report.js +27 -15
  186. package/dist/surface/tools/tool-kit.d.ts +12 -0
  187. package/dist/surface/tools/tool-kit.js +7 -0
  188. package/dist/surface/tools/tools.js +26 -13
  189. package/dist/telemetry/argument-shape.js +16 -7
  190. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  191. package/dist/telemetry/cli-telemetry.js +15 -0
  192. package/dist/telemetry/init-telemetry.d.ts +2 -1
  193. package/dist/telemetry/init-telemetry.js +47 -2
  194. package/dist/telemetry/install-trace.js +23 -4
  195. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  196. package/dist/telemetry/onboarding-funnel.js +25 -3
  197. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  198. package/dist/telemetry/proxy-telemetry.js +2 -2
  199. package/dist/telemetry/session-metrics.d.ts +1 -1
  200. package/dist/telemetry/session-metrics.js +67 -27
  201. package/dist/telemetry/telemetry.d.ts +3 -1
  202. package/dist/telemetry/telemetry.js +60 -28
  203. package/dist/telemetry/tool-refused.d.ts +2 -2
  204. package/dist/telemetry/tool-refused.js +9 -2
  205. package/docs/architecture.md +3 -6
  206. package/docs/cli/doctor.mdx +41 -2
  207. package/docs/cli/help.mdx +4 -2
  208. package/docs/cli/license.mdx +4 -5
  209. package/docs/cli/setup.mdx +13 -2
  210. package/docs/cli/status.mdx +4 -0
  211. package/docs/cli.mdx +1 -1
  212. package/docs/deploy-checks.md +25 -0
  213. package/docs/desktop.mdx +1 -1
  214. package/docs/enterprise.md +15 -20
  215. package/docs/environment.mdx +1 -0
  216. package/docs/first-drive.md +1 -1
  217. package/docs/install-manual.mdx +16 -1
  218. package/docs/packages/engine.mdx +1 -1
  219. package/docs/packages/react.mdx +14 -2
  220. package/docs/packages/server.mdx +1 -1
  221. package/docs/packages/tauri.mdx +1 -1
  222. package/docs/packages/test.mdx +1 -1
  223. package/docs/packages/vite-plugin.mdx +1 -0
  224. package/docs/packages.mdx +1 -1
  225. package/docs/platform-integration.md +1 -1
  226. package/docs/skill.md +1 -1
  227. package/docs/telemetry.md +4 -2
  228. package/package.json +14 -10
  229. package/LICENSE-ENTERPRISE +0 -37
  230. package/dist/features/ee/audit-log.d.ts +0 -14
  231. package/dist/features/ee/audit-log.js +0 -11
@@ -32,8 +32,9 @@ const ELISION = '… [elided] …';
32
32
  */
33
33
  function capMessage(message) {
34
34
  const max = TRANSPORT_LIMITS.MAX_ERROR_LENGTH;
35
- if (max >= message.length)
35
+ if (max >= message.length) {
36
36
  return message;
37
+ }
37
38
  const keep = Math.floor((max - ELISION.length) / 2);
38
39
  return `${message.slice(0, keep)}${ELISION}${message.slice(message.length - keep)}`;
39
40
  }
@@ -83,8 +84,9 @@ function isUnissuedRefRefusal(message) {
83
84
  }
84
85
  /** A message that already carries its own concrete next action, so nothing should be appended. */
85
86
  function isSelfRecovering(message) {
86
- if (message.includes(SELF_RECOVERING_MARKER))
87
+ if (message.includes(SELF_RECOVERING_MARKER)) {
87
88
  return true;
89
+ }
88
90
  return SELF_RECOVERING.some((rule) => rule.match.test(message));
89
91
  }
90
92
  /** The recovery hints, named so they are not free strings and can be asserted in tests. */
@@ -142,8 +144,9 @@ export const RECOVERY = {
142
144
  * carry: which of them to reach for, and that nothing was acted on.
143
145
  */
144
146
  TARGET_MISSED: 'The call was valid — the selector matched nothing on the page RIGHT NOW, and nothing was ' +
145
- 'acted on. Take a reticle_snapshot to see what is actually there (a view may still be ' +
146
- 'rendering, or the control may be named differently), then retry with what it shows. This is ' +
147
+ 'acted on. If the message lists the nearest targets and one is the element you meant, retry ' +
148
+ 'with its ref. If not, take a reticle_snapshot to see what is actually there (a view may still ' +
149
+ 'be rendering, or the control may be named differently), then retry with what it shows. This is ' +
147
150
  'a miss, not a Reticle defect: there is nothing to report.',
148
151
  /**
149
152
  * The selector was underspecified, which is the caller's to fix and the same class as a miss.
@@ -464,13 +467,16 @@ const RULES = [
464
467
  * saying a server is running and the recovery saying to go start one.
465
468
  */
466
469
  export function recoveryFor(message) {
467
- if (isUnissuedRefRefusal(message))
470
+ if (isUnissuedRefRefusal(message)) {
468
471
  return RECOVERY.UNISSUED_REF;
469
- if (isSelfRecovering(message))
472
+ }
473
+ if (isSelfRecovering(message)) {
470
474
  return undefined;
475
+ }
471
476
  for (const rule of RULES) {
472
- if (rule.match.test(message))
477
+ if (rule.match.test(message)) {
473
478
  return rule.hint;
479
+ }
474
480
  }
475
481
  return undefined;
476
482
  }
@@ -483,25 +489,31 @@ export function recoveryFor(message) {
483
489
  * this table rather than about anybody's app.
484
490
  */
485
491
  export function refusalReasonFor(rawMessage, coded) {
486
- if (coded !== undefined)
492
+ if (coded !== undefined) {
487
493
  return REASON_OF_CODE[coded.code];
494
+ }
488
495
  const message = zodArrayAsSentence(rawMessage);
489
- if (isUnissuedRefRefusal(message))
496
+ if (isUnissuedRefRefusal(message)) {
490
497
  return RefusalReason.BAD_ARGS;
498
+ }
491
499
  // The no-session diagnosis inspected the machine and named the cause itself, so it never reaches
492
500
  // the recovery table. It is the single largest refusal there is; missing it would gut the metric.
493
- if (message.includes(SELF_RECOVERING_MARKER))
501
+ if (message.includes(SELF_RECOVERING_MARKER)) {
494
502
  return RefusalReason.NO_SESSION;
503
+ }
495
504
  for (const rule of SELF_RECOVERING) {
496
- if (rule.match.test(message))
505
+ if (rule.match.test(message)) {
497
506
  return rule.reason;
507
+ }
498
508
  }
499
509
  // Same order as buildErrorPayload: a schema rejection is the agent's own call, whatever else in
500
510
  // the message might also match.
501
- if (ARGUMENT_REJECTION.test(message))
511
+ if (ARGUMENT_REJECTION.test(message)) {
502
512
  return RefusalReason.BAD_ARGS;
503
- if (INVALID_NAME_REJECTION.test(message))
513
+ }
514
+ if (INVALID_NAME_REJECTION.test(message)) {
504
515
  return RefusalReason.BAD_ARGS;
516
+ }
505
517
  const hint = recoveryFor(message);
506
518
  return (hint === undefined ? undefined : REASON_BY_HINT.get(hint)) ?? RefusalReason.OTHER;
507
519
  }
@@ -520,8 +532,9 @@ export function refusalReasonFor(rawMessage, coded) {
520
532
  /** Max issues rendered — a union rejection can emit one per member, which is how the array became unreadable. */
521
533
  const MAX_RENDERED_ISSUES = 3;
522
534
  function isZodIssue(value) {
523
- if (typeof value !== 'object' || null === value)
535
+ if (typeof value !== 'object' || null === value) {
524
536
  return false;
537
+ }
525
538
  const v = value;
526
539
  return 'string' === typeof v['code'] && 'string' === typeof v['message'];
527
540
  }
@@ -548,8 +561,9 @@ function issueClause(issue) {
548
561
  */
549
562
  function zodArrayAsSentence(message) {
550
563
  const text = message.trim();
551
- if (!text.startsWith('['))
564
+ if (!text.startsWith('[')) {
552
565
  return message;
566
+ }
553
567
  let parsed;
554
568
  try {
555
569
  parsed = JSON.parse(text);
@@ -557,8 +571,9 @@ function zodArrayAsSentence(message) {
557
571
  catch {
558
572
  return message; // not JSON at all — e.g. prose that happens to open with a bracket
559
573
  }
560
- if (!Array.isArray(parsed) || 0 === parsed.length || !parsed.every(isZodIssue))
574
+ if (!Array.isArray(parsed) || 0 === parsed.length || !parsed.every(isZodIssue)) {
561
575
  return message;
576
+ }
562
577
  const shown = parsed.slice(0, MAX_RENDERED_ISSUES).map(issueClause).join('; ');
563
578
  const rest = parsed.length - MAX_RENDERED_ISSUES;
564
579
  return rest > 0 ? `${shown} (and ${String(rest)} more)` : shown;
@@ -613,8 +628,9 @@ const REASON_OF_CODE = {
613
628
  * its sentence: the not-linked refusal was matched by its prose, and the prose was rewritten.
614
629
  */
615
630
  export function codedRefusalOf(error) {
616
- if (!(error instanceof Error))
631
+ if (!(error instanceof Error)) {
617
632
  return undefined;
633
+ }
618
634
  const parsed = CodedRefusalSchema.safeParse({
619
635
  code: 'code' in error ? error.code : undefined,
620
636
  next: 'next' in error ? error.next : undefined,
@@ -631,24 +647,29 @@ export function buildErrorPayload(rawMessage, coded) {
631
647
  // leave an unparseable fragment as the agent's error text — the worst of both shapes.
632
648
  const message = capMessage(zodArrayAsSentence(rawMessage));
633
649
  // A coded refusal says who does what next itself: no hint beside it, and no bug-report ask.
634
- if (coded !== undefined)
650
+ if (coded !== undefined) {
635
651
  return { error: message, code: coded.code, next: coded.next };
636
- if (isUnissuedRefRefusal(message))
652
+ }
653
+ if (isUnissuedRefRefusal(message)) {
637
654
  return { error: message, recovery: RECOVERY.UNISSUED_REF };
655
+ }
638
656
  // A self-diagnosing message gets NEITHER a generic recovery nor the feedback ask. It already
639
657
  // inspected the machine and named the cause; a second, contradictory hint is noise, and inviting a
640
658
  // bug report about a condition Reticle diagnosed itself is exactly backwards.
641
- if (isSelfRecovering(message))
659
+ if (isSelfRecovering(message)) {
642
660
  return { error: message };
661
+ }
643
662
  // An argument rejection is the agent's to fix, not a Reticle defect. Reported from the field: a
644
663
  // clean `unrecognized_keys: ["value"]` came back wrapped in "may be a defect in Reticle", which
645
664
  // spends the agent's turn and fills the feedback channel with reports about malformed calls.
646
- if (ARGUMENT_REJECTION.test(message))
665
+ if (ARGUMENT_REJECTION.test(message)) {
647
666
  return { error: message, recovery: ARGUMENT_RECOVERY };
667
+ }
648
668
  // Reticle's own name validators rejecting a caller's value: an argument rejection that the schema
649
669
  // cannot express (the shape is a runtime pattern, not a type), so it needs its own arm.
650
- if (INVALID_NAME_REJECTION.test(message))
670
+ if (INVALID_NAME_REJECTION.test(message)) {
651
671
  return { error: message, recovery: RECOVERY.INVALID_NAME };
672
+ }
652
673
  const recovery = recoveryFor(message);
653
674
  return recovery !== undefined
654
675
  ? { error: message, recovery }
@@ -17,6 +17,7 @@ import { endsRun, noteToolResult, runReportField } from './run-report.js';
17
17
  import { runLedger } from '../../judgement/runs/run-ledger.js';
18
18
  import { readAppNotes } from './agent/app-notes.js';
19
19
  import { forTheAgent } from './agent/for-the-agent.js';
20
+ import { inRepeatStreak, withHintOn } from './repeat-hint.js';
20
21
  import { feedbackAlreadyAsked, noteFeedbackAsked } from './agent/notices.js';
21
22
  import { SKEW_SHORT_WARNING, SKEW_VERDICT_WARNING } from '../../portal/session/session-health.js';
22
23
  import { verifyNextBaton, SUPPRESS_VERIFY_NEXT_ENV } from './verify-next-baton.js';
@@ -46,6 +47,7 @@ import { takeStepShot } from '../../features/visual/step-shots.js';
46
47
  import { defectForToolResult, deltaForToolResult, impactSnapshot, initImpact, recordImpact, } from '../../memory/impact/impact-recorder.js';
47
48
  import { FrictionKind, frictionOf, inviteFor } from './feedback-invite.js';
48
49
  import { VACUOUS_BECAUSE } from '@reticlehq/engine/evidence/verified.js';
50
+ import { NEXT_STEP_FIELD } from './empty-session-list.js';
49
51
  /**
50
52
  * Emit `verification_completed` when a verification tool produced a verdict.
51
53
  *
@@ -134,11 +136,11 @@ function reportBugsFound(toolName, result, sessionId) {
134
136
  * tool's name, and then hands the same failure back out to be reported a second time as
135
137
  * `reticle_run`. A count that doubles because of HOW a tool was reached is not a measurement.
136
138
  */
137
- function reportRefusal(toolName, message, error) {
139
+ function reportRefusal(toolName, args, message, error) {
138
140
  if (toolName === ReticleTool.RUN) {
139
141
  return;
140
142
  }
141
- reportToolRefused(toolName, refusalReasonFor(message, codedRefusalOf(error)));
143
+ reportToolRefused(toolName, refusalReasonFor(message, codedRefusalOf(error)), args);
142
144
  }
143
145
  /**
144
146
  * Which friction this result represents, counting the invitation if there is one.
@@ -256,25 +258,20 @@ export async function runTool(tool, deps, given, call) {
256
258
  // The project of the session a call ends, read while the session still exists: releasing a
257
259
  // tab removes it, and an unreadable project lost an unlinked user's cloud line.
258
260
  const endedIn = agentCall && ends ? safeRoot(() => sessionRoot(deps, sessionIdFromArgs(args))) : undefined;
259
- const raw = await loggedTool(tool, deps, args, call);
261
+ // `reticle_run` is counted once, as the tool it reached; never another agent's call (rule 8).
262
+ const owner = currentDrivenBy() === undefined && ReticleTool.RUN !== tool.name;
263
+ const { raw, hint } = await inRepeatStreak(owner ? deps.attachId : undefined, tool.name, args, () => loggedTool(tool, deps, args, call));
260
264
  const attach = currentDrivenBy() === undefined ? deps.attachId : undefined;
261
265
  const root = attach === undefined
262
266
  ? undefined
263
267
  : safeRoot(() => sessionRoot(deps, sessionIdFromArgs(args)));
264
268
  // The app notes before this call, so a note it learns counts as memory written.
265
269
  const notesBefore = agentCall && root !== undefined ? readAppNotes(root) : [];
266
- const answer = forTheAgent({
267
- name: tool.name,
268
- args,
269
- result: raw,
270
- attach,
271
- root,
272
- fold: NOTICE_FOLD,
273
- });
270
+ const answer = withHintOn(forTheAgent({ name: tool.name, args, result: raw, attach, root, fold: NOTICE_FOLD }), hint);
274
271
  if (!agentCall) {
275
272
  return answer;
276
273
  }
277
- runLedger.after(deps.now());
274
+ runLedger.after(deps.now(), deps.attachId);
278
275
  noteToolResult(runLedger, tool.name, args, raw);
279
276
  const notesAfter = root === undefined ? [] : readAppNotes(root);
280
277
  runLedger.memoryWritten(notesAfter.filter((note) => !notesBefore.includes(note)).length);
@@ -282,7 +279,10 @@ export async function runTool(tool, deps, given, call) {
282
279
  if (!ends || null === answer || 'object' !== typeof answer) {
283
280
  return answer;
284
281
  }
285
- return { ...answer, ...runReportField(runLedger, endedIn ?? root, process.env, deps.now()) };
282
+ return {
283
+ ...answer,
284
+ ...runReportField(runLedger, endedIn ?? root, process.env, deps.now(), deps.attachId),
285
+ };
286
286
  }));
287
287
  }
288
288
  /** Advisories that become once-per-session notice lines. See agent/notices.ts. */
@@ -391,7 +391,7 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
391
391
  'value — a selector or an id rather than a document, or a slice of the text you need.';
392
392
  // Refused before the handler was ever entered, and still a refusal the agent has to recover
393
393
  // from. Reported here rather than at the return below, which this branch never reaches.
394
- reportRefusal(tool.name, error);
394
+ reportRefusal(tool.name, args, error);
395
395
  return { error };
396
396
  }
397
397
  // One driver per tab. The Harness clicking through a page while the agent clicks too is two hands
@@ -399,7 +399,7 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
399
399
  const busy = ACTION_TOOLS.has(tool.name) ? harnessDriving(deps, args) : undefined;
400
400
  if (busy !== undefined) {
401
401
  const error = msgHarnessHasTheTab(busy);
402
- reportRefusal(tool.name, error);
402
+ reportRefusal(tool.name, args, error);
403
403
  return { error };
404
404
  }
405
405
  // An ACTION is what gets abandoned. Counted here, against verifications, so "the agent drove the
@@ -448,7 +448,7 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
448
448
  // before Playwright invents a closed page. ready:true leases still hit this path.
449
449
  if (session?.versionSkew !== undefined && CDP_TOOLS.has(tool.name)) {
450
450
  const message = session.versionSkew;
451
- reportRefusal(tool.name, message);
451
+ reportRefusal(tool.name, args, message);
452
452
  throw new Error(message);
453
453
  }
454
454
  // A ref that was handed out by a DIFFERENT tab, on a call that named no session. Refused here, at
@@ -466,7 +466,7 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
466
466
  if (wrongTab !== undefined) {
467
467
  // Refused before the handler was entered, so it is reported here — the same shape as the
468
468
  // oversized-argument refusal above.
469
- reportRefusal(tool.name, wrongTab);
469
+ reportRefusal(tool.name, args, wrongTab);
470
470
  return { error: wrongTab };
471
471
  }
472
472
  }
@@ -516,7 +516,7 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
516
516
  const rewritten = rewriteClosedAsSkew(error, skewText);
517
517
  const toThrow = rewritten ?? error;
518
518
  const message = toThrow instanceof Error ? toThrow.message : String(toThrow);
519
- reportRefusal(tool.name, message, toThrow);
519
+ reportRefusal(tool.name, args, message, toThrow);
520
520
  // The invitation on THIS path is the one `buildErrorPayload` attaches when it does not recognise
521
521
  // the error, and it was never counted — so the denominator excluded the commonest friction there
522
522
  // is. Gated on the payload actually carrying the ask, because a recognised error gets a recovery
@@ -580,7 +580,7 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
580
580
  // project the daemon was started in.
581
581
  session?.pushImpact?.(() => impactSnapshot(artifactRoot));
582
582
  if (resultIsError(raw)) {
583
- reportRefusal(tool.name, raw.error);
583
+ reportRefusal(tool.name, args, raw.error);
584
584
  }
585
585
  else {
586
586
  noteToolServed();
@@ -660,6 +660,11 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
660
660
  })
661
661
  : undefined;
662
662
  const platformLine = platform === undefined ? nudge : nudge === undefined ? platform : `${platform}\n${nudge}`;
663
+ // The first call of this connection, in an unwired app with nothing attached: the offer the
664
+ // plugin's SessionStart hook makes, which `/reload-plugins` never fires. Always taken, so a call
665
+ // that already said it (the opened tab's notice, an empty list's `next_step`) still uses the once.
666
+ const firstOffer = agentCall && deps.attachId !== undefined ? deps.firstSessionOffer?.(deps.attachId) : undefined;
667
+ const offer = opened === undefined && !(NEXT_STEP_FIELD in (answer ?? {})) ? firstOffer : undefined;
663
668
  const drives = agentCall
664
669
  ? takeDriveNotes('string' === typeof answer?.['runId'] ? answer['runId'] : undefined)
665
670
  : [];
@@ -675,7 +680,8 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
675
680
  undelivered === undefined &&
676
681
  friction === undefined &&
677
682
  platformLine === undefined &&
678
- next === undefined
683
+ next === undefined &&
684
+ offer === undefined
679
685
  ? raw
680
686
  : {
681
687
  // First, so a model that truncates a long result still reads it.
@@ -696,6 +702,7 @@ async function dispatchTool(tool, deps, args, call, seen = {}) {
696
702
  ...(update !== undefined ? { [EnvelopeKey.UPDATE_AVAILABLE]: update } : {}),
697
703
  ...(skew !== undefined ? { [EnvelopeKey.VERSION_SKEW]: skew } : {}),
698
704
  ...(platformLine !== undefined ? { [EnvelopeKey.PLATFORM]: platformLine } : {}),
705
+ ...(offer === undefined ? {} : { [EnvelopeKey.FIRST_SESSION_OFFER]: offer }),
699
706
  ...(0 === drives.length ? {} : { [EnvelopeKey.HARNESS]: drives.join('\n') }),
700
707
  ...(notes === undefined ? {} : { [EnvelopeKey.CONTROL]: notes }),
701
708
  ...(undelivered !== undefined
@@ -56,6 +56,13 @@ export interface LeaseEvidence {
56
56
  */
57
57
  dialledUrl?: string;
58
58
  }
59
+ /**
60
+ * The last sentence of every not-connected hint. It is true for an MCP caller, which keeps the
61
+ * lease. The CLI releases the lease itself, and puts `LEASE_RELEASED_SENTENCE` in its place.
62
+ */
63
+ export declare const LEASE_KEPT_SENTENCE = "The tab stays leased. Release it with reticle_lease{action:\"release\"}.";
64
+ /** The sentence for a lease that is already released. */
65
+ export declare const LEASE_RELEASED_SENTENCE = "The lease is released.";
59
66
  export declare function leaseNotConnectedHint(url: string, port: number, evidence?: LeaseEvidence): string;
60
67
  /**
61
68
  * The leased page was still loading when the wait ran out, so nothing has failed yet: a slow first
@@ -37,7 +37,14 @@ const REAL_CAUSES = 'Causes that produce exactly this and are NOT a port problem
37
37
  const NUXT_FIRST = 'This project is Nuxt: the most likely cause by a distance is a dev server that predates the ' +
38
38
  'Reticle plugin, because Nuxt does NOT register a newly added plugin on HMR. Restart `nuxt dev` ' +
39
39
  'and re-acquire before investigating anything else.';
40
- const RELEASE = 'The tab stays leased either way — release it with reticle_lease{action:"release"}.';
40
+ /**
41
+ * The last sentence of every not-connected hint. It is true for an MCP caller, which keeps the
42
+ * lease. The CLI releases the lease itself, and puts `LEASE_RELEASED_SENTENCE` in its place.
43
+ */
44
+ export const LEASE_KEPT_SENTENCE = 'The tab stays leased. Release it with reticle_lease{action:"release"}.';
45
+ const RELEASE = LEASE_KEPT_SENTENCE;
46
+ /** The sentence for a lease that is already released. */
47
+ export const LEASE_RELEASED_SENTENCE = 'The lease is released.';
41
48
  const PRODUCTION_STUB = ' If this is a production build (`vite build`, `next build`, `next start`, or a deployed site), it strips ' +
42
49
  'Reticle or replaces it with an inert stub by design; point the lease at the dev server instead.';
43
50
  function isRemoteUrl(url) {
@@ -168,8 +175,8 @@ export function leaseStillLoadingHint(url, waitedMs) {
168
175
  /** The hint for a lease whose zero-install reader did not run (#1701). The lease is already released. */
169
176
  export function leaseReaderFailedHint(url, failure) {
170
177
  return failure.reason === LeaseNotReadyReason.NOT_HTML_DOCUMENT
171
- ? `${url} is not an HTML page, so no SDK can connect to it. The lease is released. ` +
178
+ ? `${url} is not an HTML page, so no SDK can connect to it. ${LEASE_RELEASED_SENTENCE} ` +
172
179
  'Acquire an HTML page of the app instead.'
173
- : `The zero-install reader failed on ${url}: ${failure.cause}. The lease is released. ` +
180
+ : `The zero-install reader failed on ${url}: ${failure.cause}. ${LEASE_RELEASED_SENTENCE} ` +
174
181
  'If the tab closed during setup, acquire again.';
175
182
  }
@@ -43,6 +43,27 @@ export declare function notReadyReason(answered: boolean, session: unknown, fail
43
43
  * failure than the one being fixed.
44
44
  */
45
45
  export declare function probeLeaseAlive(session: ProbeableSession | undefined, timeoutMs?: number): Promise<boolean>;
46
+ /**
47
+ * The longest `reticle_lease { action: "acquire" }` waits for its tab to be ready, when the caller
48
+ * gives no `timeout_ms`. It is the longest wait before this bound existed: the connect wait, the
49
+ * wait after the reader, and the loading grace rounds. So no acquire that was ready before is now
50
+ * refused.
51
+ */
52
+ export declare const LEASE_READY_TIMEOUT_MS: number;
53
+ /**
54
+ * A connect wait for `connectOrInject` that ends at the deadline. Each wait is one connect wait at
55
+ * most, so the reader still goes in at the same time. A wait that starts after the deadline looks
56
+ * once and does not sleep.
57
+ */
58
+ export declare function connectWaitUntil(deadline: number, now: () => number, sleeper?: (ms: number) => Promise<void>): (isConnected: () => boolean) => Promise<boolean>;
59
+ /**
60
+ * Wait until the leased tab is registered, answers a probe and is visible, or the deadline passes.
61
+ *
62
+ * A row in the sessions map is not proof that the next call works (#1650): the tab can be attached
63
+ * and answer nothing. So `ready` waits for a reply. Returns the reason at the deadline, or
64
+ * `undefined` when the tab is ready. It always looks once, also when the deadline has passed.
65
+ */
66
+ export declare function awaitLeaseAnswering(registered: () => ProbeableSession | undefined, deadline: number, now: () => number, sleeper?: (ms: number) => Promise<void>): Promise<LeaseNotReadyReason | undefined>;
46
67
  /**
47
68
  * Poll until the leased tab's SDK has actually registered on the bridge, so the sessionId we return
48
69
  * is immediately usable (the page navigates first, then connects a moment later — without this wait
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { LeaseNotReadyReason, ReticleCommand, RETICLE_URL_PARAM } from '@reticlehq/core';
8
8
  import { NotHtmlDocumentError } from '../../portal/pool/zero-install.js';
9
+ import { waitForReconnect } from '../../portal/session/session-reconnect.js';
9
10
  /**
10
11
  * How long a liveness probe waits for the tab to say anything at all.
11
12
  *
@@ -74,6 +75,52 @@ const LEASE_READY_POLL_MS = 100;
74
75
  /** Extra readiness waits allowed while the leased page is still loading (each one is ~10 s). */
75
76
  const LEASE_LOADING_GRACE_ROUNDS = 3;
76
77
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
78
+ /** One connect wait: how long a tab gets to dial in before the zero-install reader goes in. */
79
+ const LEASE_CONNECT_WAIT_MS = LEASE_READY_ATTEMPTS * LEASE_READY_POLL_MS;
80
+ /**
81
+ * The longest `reticle_lease { action: "acquire" }` waits for its tab to be ready, when the caller
82
+ * gives no `timeout_ms`. It is the longest wait before this bound existed: the connect wait, the
83
+ * wait after the reader, and the loading grace rounds. So no acquire that was ready before is now
84
+ * refused.
85
+ */
86
+ export const LEASE_READY_TIMEOUT_MS = LEASE_CONNECT_WAIT_MS * (2 + LEASE_LOADING_GRACE_ROUNDS);
87
+ /** `waitForReconnect` waits for `current` to differ from `previous`: these two always differ. */
88
+ const NOT_REGISTERED = {};
89
+ const REGISTERED = {};
90
+ /**
91
+ * A connect wait for `connectOrInject` that ends at the deadline. Each wait is one connect wait at
92
+ * most, so the reader still goes in at the same time. A wait that starts after the deadline looks
93
+ * once and does not sleep.
94
+ */
95
+ export function connectWaitUntil(deadline, now, sleeper = sleep) {
96
+ return (isConnected) => waitForReconnect({
97
+ current: () => (isConnected() ? REGISTERED : undefined),
98
+ previous: NOT_REGISTERED,
99
+ timeoutMs: Math.min(LEASE_CONNECT_WAIT_MS, Math.max(0, deadline - now())),
100
+ now,
101
+ sleep: sleeper,
102
+ pollMs: LEASE_READY_POLL_MS,
103
+ });
104
+ }
105
+ /**
106
+ * Wait until the leased tab is registered, answers a probe and is visible, or the deadline passes.
107
+ *
108
+ * A row in the sessions map is not proof that the next call works (#1650): the tab can be attached
109
+ * and answer nothing. So `ready` waits for a reply. Returns the reason at the deadline, or
110
+ * `undefined` when the tab is ready. It always looks once, also when the deadline has passed.
111
+ */
112
+ export async function awaitLeaseAnswering(registered, deadline, now, sleeper = sleep) {
113
+ for (;;) {
114
+ const session = registered();
115
+ const reason = session === undefined
116
+ ? LeaseNotReadyReason.SDK_STOPPED_ANSWERING
117
+ : notReadyReason(await probeLeaseAlive(session), session);
118
+ if (reason === undefined || now() >= deadline) {
119
+ return reason;
120
+ }
121
+ await sleeper(LEASE_READY_POLL_MS);
122
+ }
123
+ }
77
124
  /**
78
125
  * Poll until the leased tab's SDK has actually registered on the bridge, so the sessionId we return
79
126
  * is immediately usable (the page navigates first, then connects a moment later — without this wait
@@ -36,6 +36,8 @@ export declare const NAV_TIMEOUT_ADVICE: string;
36
36
  * cases and leads with the retry.
37
37
  */
38
38
  export declare function navFailureMessage(url: string, err: unknown, seed?: SeedStorage): string;
39
+ /** Whether a path is a sign-in page, by its name: `/login`, `/signin`, `/auth/login` and similar. */
40
+ export declare const isAuthPath: (pathname: string) => boolean;
39
41
  /**
40
42
  * Determine if a required seedStorage application precondition failed.
41
43
  *
@@ -127,6 +127,8 @@ export function navFailureMessage(url, err, seed) {
127
127
  : `within ${String(Number(budgetMs) / 1000)} s`;
128
128
  return `could not open ${url} — ${NAV_TIMEOUT_LEAD} ${within} (${reason}). ${NAV_TIMEOUT_ADVICE}`;
129
129
  }
130
+ /** Whether a path is a sign-in page, by its name: `/login`, `/signin`, `/auth/login` and similar. */
131
+ export const isAuthPath = (pathname) => /(?:^|\/)(?:login|signin|sign-in|auth\/login|auth\/signin|session\/new)(?:$|\/)/i.test(pathname);
130
132
  /**
131
133
  * Determine if a required seedStorage application precondition failed.
132
134
  *
@@ -159,7 +161,6 @@ export function evaluateSeedPrecondition(requestedUrl, actualUrl, seed, navStatu
159
161
  return `seeded storage precondition not established: cross-origin redirect from ${reqParsed.origin} to ${actParsed.origin} skipped origin-scoped storage injection`;
160
162
  }
161
163
  // Clear redirect to an authentication/login endpoint when the requested URL was not a login page
162
- const isAuthPath = (pathname) => /(?:^|\/)(?:login|signin|sign-in|auth\/login|auth\/signin|session\/new)(?:$|\/)/i.test(pathname);
163
164
  if (!isAuthPath(reqParsed.pathname) && isAuthPath(actParsed.pathname)) {
164
165
  return `seeded authentication precondition not established: redirected from ${reqParsed.pathname} to login page (${actParsed.pathname})`;
165
166
  }