@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
@@ -1,4 +1,5 @@
1
- import { asRecord, ElementState } from '@reticlehq/core';
1
+ import * as z from 'zod';
2
+ import { asRecord, ElementState, NearestTargetSchema } from '@reticlehq/core';
2
3
  /**
3
4
  * Roles that usually name the control an agent meant, ahead of a wrapping `generic` that
4
5
  * happened to inherit the same accessible name (header CTA vs empty-state CTA).
@@ -17,8 +18,9 @@ const ROLE_RANK = {
17
18
  searchbox: 10,
18
19
  };
19
20
  function roleRank(role) {
20
- if ('string' !== typeof role)
21
+ if ('string' !== typeof role) {
21
22
  return 100;
23
+ }
22
24
  return ROLE_RANK[role] ?? 50;
23
25
  }
24
26
  function statesOf(c) {
@@ -35,8 +37,9 @@ function inViewport(c) {
35
37
  */
36
38
  function compareCandidates(a, b) {
37
39
  const viewport = Number(inViewport(b)) - Number(inViewport(a));
38
- if (0 !== viewport)
40
+ if (0 !== viewport) {
39
41
  return viewport;
42
+ }
40
43
  return roleRank(a.role) - roleRank(b.role);
41
44
  }
42
45
  /**
@@ -50,14 +53,18 @@ function formatCandidate(c, anyInViewport) {
50
53
  const ref = 'string' === typeof c.ref ? c.ref : '?';
51
54
  const visible = false !== c.visible;
52
55
  let where;
53
- if (!visible)
56
+ if (!visible) {
54
57
  where = 'hidden';
55
- else if (inViewport(c))
58
+ }
59
+ else if (inViewport(c)) {
56
60
  where = 'in-viewport';
57
- else if (anyInViewport)
61
+ }
62
+ else if (anyInViewport) {
58
63
  where = 'off-screen';
59
- else
64
+ }
65
+ else {
60
66
  where = 'visible';
67
+ }
61
68
  const head = name.length > 0 ? `${ref} (${role} "${name}"` : `${ref} (${role}`;
62
69
  return `${head}; ${where})`;
63
70
  }
@@ -76,6 +83,25 @@ function nearNamesClause(hint) {
76
83
  'Use that name, or — when the name carries a part that changes, like a time — target it by ' +
77
84
  '`testid` or by `text` (a contained match).');
78
85
  }
86
+ const NearestTargetsSchema = z.array(NearestTargetSchema).min(1);
87
+ /** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
88
+ export const NEAREST_TARGETS_LEAD = ' The nearest targets on the page, most similar first: ';
89
+ /**
90
+ * The browser's `nearest` hint as a sentence: the targets most like the one asked for, with refs,
91
+ * so the retry is one call and not a snapshot loop. Empty when there is no list. It is advice
92
+ * only: the action stays refused, and nothing here picks a target.
93
+ */
94
+ function nearestClause(hint) {
95
+ const parsed = NearestTargetsSchema.safeParse(asRecord(hint)['nearest']);
96
+ if (!parsed.success) {
97
+ return '';
98
+ }
99
+ const listed = parsed.data
100
+ .map((t) => `${t.ref} (${t.role} "${t.name}${true === t.nameTruncated ? '…' : ''}")`)
101
+ .join(', ');
102
+ return (`${NEAREST_TARGETS_LEAD}${listed}. If one of them is the ` +
103
+ 'element you meant, pass its ref as `ref`.');
104
+ }
79
105
  /**
80
106
  * Turn a target QUERY into the single ref an action can be performed on.
81
107
  *
@@ -104,7 +130,8 @@ export function resolveTargetRef(candidates, hint) {
104
130
  return {
105
131
  kind: 'error',
106
132
  message: 'target matched no element. Nothing was acted on and no verdict is possible — ' +
107
- nearNamesClause(hint),
133
+ nearNamesClause(hint) +
134
+ nearestClause(hint),
108
135
  };
109
136
  }
110
137
  if (usable.length > 1) {
@@ -14,7 +14,7 @@
14
14
  * is verified FOR THOSE THREE and silent about nine, and `because` says so — reporting it as "25%
15
15
  * verified" would be the arithmetic that hides the nine.
16
16
  */
17
- import { Verified } from '@reticlehq/core';
17
+ import { Verified, VerifiedReason } from '@reticlehq/core';
18
18
  /**
19
19
  * What to do when a step's declared consequence does not hold.
20
20
  *
@@ -49,6 +49,8 @@ export interface StepExpectation {
49
49
  }
50
50
  export interface SequenceGrade {
51
51
  verified: Verified;
52
+ /** The deciding clause, from core's list, so a sequence verdict is counted by reason like any other. */
53
+ reason: VerifiedReason;
52
54
  /** Steps that declared a consequence. */
53
55
  declared: number;
54
56
  total: number;
@@ -14,7 +14,7 @@
14
14
  * is verified FOR THOSE THREE and silent about nine, and `because` says so — reporting it as "25%
15
15
  * verified" would be the arithmetic that hides the nine.
16
16
  */
17
- import { Verified } from '@reticlehq/core';
17
+ import { Verified, VerifiedReason } from '@reticlehq/core';
18
18
  /**
19
19
  * What to do when a step's declared consequence does not hold.
20
20
  *
@@ -36,6 +36,7 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
36
36
  const observed = missed.observed ?? 'nothing that matched';
37
37
  return {
38
38
  verified: Verified.NO,
39
+ reason: VerifiedReason.ASSERTION_FAILED,
39
40
  declared,
40
41
  total,
41
42
  because: `a declared consequence did not hold — observed ${observed}`,
@@ -47,6 +48,7 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
47
48
  const kinds = (steps[contradictedAt]?.contradicted ?? []).join(', ');
48
49
  return {
49
50
  verified: Verified.NO,
51
+ reason: VerifiedReason.CONTRADICTED,
50
52
  declared,
51
53
  total,
52
54
  because: `step ${String(contradictedAt + 1)}: channels disagree about this action (${kinds}) even though it went on`,
@@ -57,6 +59,7 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
57
59
  if (unjudged !== undefined)
58
60
  return {
59
61
  verified: Verified.UNKNOWN,
62
+ reason: VerifiedReason.INCONCLUSIVE,
60
63
  declared,
61
64
  total,
62
65
  because: `a step could not be judged — ${unjudged.undecided ?? ''}`,
@@ -77,6 +80,7 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
77
80
  if (0 === declared) {
78
81
  return {
79
82
  verified: Verified.UNKNOWN,
83
+ reason: VerifiedReason.NOTHING_DECLARED,
80
84
  declared,
81
85
  total,
82
86
  because: 0 === total
@@ -95,6 +99,7 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
95
99
  if (steps.length < total) {
96
100
  return {
97
101
  verified: Verified.UNKNOWN,
102
+ reason: VerifiedReason.EVIDENCE_INCOMPLETE,
98
103
  declared,
99
104
  total,
100
105
  because: `only ${String(steps.length)} of ${String(total)} steps ran, so the journey never reached its end`,
@@ -103,6 +108,7 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
103
108
  if (true !== steps.at(-1)?.declared) {
104
109
  return {
105
110
  verified: Verified.UNKNOWN,
111
+ reason: VerifiedReason.EVIDENCE_INCOMPLETE,
106
112
  declared,
107
113
  total,
108
114
  because: `the last step, where the journey ends, declared nothing — give it an \`expect\` naming the end state (${String(declared)} of ${String(total)} steps declared one)`,
@@ -110,6 +116,7 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
110
116
  }
111
117
  return {
112
118
  verified: Verified.YES,
119
+ reason: VerifiedReason.PROVED,
113
120
  declared,
114
121
  total,
115
122
  because: declared === total
@@ -153,6 +153,8 @@ export const ACT_SEQUENCE_TOOL = {
153
153
  stopped_at: z.number().optional(),
154
154
  /** THE field to gate on: "yes" | "no" | "unknown" — see `because`. */
155
155
  verified: z.string().optional(),
156
+ /** The deciding clause of `verified`, from the same list as every other verdict. */
157
+ verifiedReason: z.string().optional(),
156
158
  because: z.string().optional(),
157
159
  /**
158
160
  * Present only when the plan PROVED something — see `offerToKeep`. A plan that declared nothing
@@ -464,6 +466,7 @@ export const ACT_SEQUENCE_TOOL = {
464
466
  ...(stalledAt !== undefined ? { stalled_at: stalledAt } : {}),
465
467
  ...(stoppedAt !== undefined ? { stopped_at: stoppedAt } : {}),
466
468
  verified: grade.verified,
469
+ verifiedReason: grade.reason,
467
470
  because: grade.because,
468
471
  // Offered at the one moment the compiled program is in hand and free to keep.
469
472
  ...(() => {
@@ -34,7 +34,7 @@ import { declaresState } from '@reticlehq/engine/question/predicate/predicate-as
34
34
  import { isStateUnwatched } from '@reticlehq/engine/evidence/blind-spots.js';
35
35
  import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
36
36
  import { inFlightRequestLabels, repeatedRequestLabels } from './act/settle-in-flight.js';
37
- import { finishAfterMatch } from './act/after-match.js';
37
+ import { awaitShownMatch, finishAfterMatch } from './act/after-match.js';
38
38
  import { noteProved } from './act/proved-controls.js';
39
39
  import { decideVerified } from '@reticlehq/engine/evidence/verified.js';
40
40
  import { buildHonestyBlock, honestyForVerdict } from '@reticlehq/engine/evidence/honesty.js';
@@ -123,7 +123,7 @@ export const ACT_TOOLS = [
123
123
  args: z
124
124
  .record(z.string(), z.unknown())
125
125
  .optional()
126
- .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer or the key DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For press: { keys: ["Control","k"] } holds several keys down TOGETHER (pressed in order, released in reverse) and { code } names the physical key — both stay on the in-page dispatcher, the only path that can express them. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
126
+ .describe('Action-specific arguments: { value } for fill/select (select also drives an ARIA combobox: it opens it and clicks the option by its label), { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K) or a click/dblclick (Shift+click range select), { button: "right" | "middle" } for a click (right sends contextmenu), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer or the key DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For press: { keys: ["Control","k"] } holds several keys down TOGETHER (pressed in order, released in reverse) and { code } names the physical key — both stay on the in-page dispatcher, the only path that can express them. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
127
127
  refuseWhenThrottled: z
128
128
  .boolean()
129
129
  .optional()
@@ -271,7 +271,7 @@ export const ACT_TOOLS = [
271
271
  args: z
272
272
  .record(z.string(), z.unknown())
273
273
  .optional()
274
- .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
274
+ .describe('Action-specific arguments: { value } for fill/select (select also drives an ARIA combobox: it opens it and clicks the option by its label), { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut or a click (Meta/Control/Shift/Alt), { button: "right" | "middle" } for a click, { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
275
275
  predicate: PredicateSchema.optional().describe('Alias for `until`.'),
276
276
  until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
277
277
  timeout_ms: timeoutMsSchema
@@ -432,9 +432,10 @@ export const ACT_TOOLS = [
432
432
  const { alreadyTrue, alreadyTrueHiddenMatch, alreadyTrueEvidence, baselines } = await readAlreadyTrue(session, until, since);
433
433
  // Same as the ACT handler: the route this step RAN on, before the action can move the app.
434
434
  const routeBeforeWait = pathOf(session.url);
435
- // Recorded in `finally`, once the verdict is known — see captureVerdictedAct.
435
+ // Recorded in `finally`, once the verdict is known — see captureVerdictedAct. `held`: the
436
+ // action made the declared claim hold.
436
437
  let dispatched;
437
- let recordedVerdict;
438
+ let held = false;
438
439
  try {
439
440
  // actCommand is the single interception point for upload+path rewrite.
440
441
  //
@@ -484,6 +485,7 @@ export const ACT_TOOLS = [
484
485
  const reverted = await finishAfterMatch(session, until, since, baselines, left);
485
486
  if (reverted !== undefined)
486
487
  verdict = { pass: false, failureReason: reverted, observed: reverted };
488
+ verdict = await awaitShownMatch(session, until, since, baselines, verdict, left);
487
489
  }
488
490
  const r = asRecord(actResult?.result);
489
491
  if ('boolean' === typeof r['settled'])
@@ -580,10 +582,9 @@ export const ACT_TOOLS = [
580
582
  ...(0 === impeachingNotes.length ? {} : { blindSpots: impeachingNotes }),
581
583
  losses,
582
584
  });
583
- // A refuted expectation must not ride into a regression flow cut from the ambient tape; it is
584
- // kept as the capsule below instead. See RecordingStore.unassertLast.
585
- if (!verdict.pass && actResult !== null)
586
- deps.recordings.unassertLast();
585
+ // A refuted or already-true claim stays off the ambient tape (the capsule below keeps a
586
+ // refuted one). See captureVerdictedAct.
587
+ held = verdict.pass && !alreadyTrue;
587
588
  const capsuleSaved = await saveFailedAssertCapsule({
588
589
  deps,
589
590
  verdict,
@@ -690,8 +691,7 @@ export const ACT_TOOLS = [
690
691
  },
691
692
  ...(namedNetIsInFlight(until, stillInFlight) ? { namedRequestInFlight: true } : {}),
692
693
  }), (part) => reloadAndRecheck(deps, session, part, timeout));
693
- recordedVerdict = String(decision.verified);
694
- noteProved(session, recordedVerdict, actResult?.result);
694
+ noteProved(session, String(decision.verified), actResult?.result);
695
695
  // Computed once: the verdict block reports it, and the instrumentation gaps are a second
696
696
  // reading of the same evidence rather than a new observation.
697
697
  const actionSummary = causalSummary(windowEvents, {
@@ -834,7 +834,7 @@ export const ACT_TOOLS = [
834
834
  }
835
835
  finally {
836
836
  if (dispatched !== undefined)
837
- captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, recordedVerdict, session.id, wroteSince(acted, actedSince));
837
+ captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, held, session.id, wroteSince(acted, actedSince));
838
838
  deps.recordings.markEnded(pathOf(currentOf(deps.sessions, session).url), session.id);
839
839
  acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
840
840
  }
@@ -20,6 +20,28 @@ import { gradeOfPredicate } from './assert-grade.js';
20
20
  import { assertSource, stampedSourceIn } from './assert-source.js';
21
21
  import { VerdictAttribution, verdictAttributionOf } from '@reticlehq/core';
22
22
  import { noteSourceSeen } from '../../../portal/session/recorded-gaps.js';
23
+ import { sdkBehindNote } from '../../../portal/session/session-info.js';
24
+ // Unanchored: the engine prefixes the reason with "the page did not answer:".
25
+ const COMMAND_TIMEOUT = /command '[^']*' timed out after (\d+)ms/;
26
+ /**
27
+ * The cause and the one fix for a read that came back `unknown`, in place of a bare fact (#1990).
28
+ * A page whose SDK is behind this daemon may not serve `match` at all, so the fix is the same
29
+ * SDK-behind sentence the listing shows. On a current page the read ran out of the assertion's own
30
+ * budget, and the fix is more of it.
31
+ */
32
+ function diagnoseTimeout(reason, session) {
33
+ const behind = sdkBehindNote(session);
34
+ if (behind !== undefined) {
35
+ return `${reason} ${behind}`;
36
+ }
37
+ const granted = COMMAND_TIMEOUT.exec(reason)?.[1];
38
+ if (granted === undefined) {
39
+ return reason;
40
+ }
41
+ return (`${reason} This read was bounded by what the assertion's budget had left ` +
42
+ `(${granted}ms): the budget ran out while the page was still working. ` +
43
+ 'Give the assertion more time with `timeout_ms`.');
44
+ }
23
45
  /**
24
46
  * The honesty verdict for a plain `reticle_assert`.
25
47
  *
@@ -138,7 +160,12 @@ changeUndeclared) {
138
160
  });
139
161
  const outcomeUnread = unreadWriteLabels(windowEvents);
140
162
  const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
141
- const effectiveInconclusive = inconclusive ?? (!pass ? session.preconditionFailure?.() : undefined);
163
+ const rawInconclusive = inconclusive ?? (!pass ? session.preconditionFailure?.() : undefined);
164
+ // A timed-out page read is the one `unknown` whose cause and fix are knowable here (#1990).
165
+ let effectiveInconclusive = rawInconclusive;
166
+ if (rawInconclusive !== undefined && COMMAND_TIMEOUT.test(rawInconclusive)) {
167
+ effectiveInconclusive = diagnoseTimeout(rawInconclusive, session);
168
+ }
142
169
  /**
143
170
  * Does a green here rest on the window having been COMPLETE?
144
171
  *
@@ -1,10 +1,10 @@
1
- import { carryReticleIdentity } from './lease-tools.js';
1
+ import { carryReticleIdentity, LEASE_ACQUIRE_TOOL } from './lease-tools.js';
2
2
  import { z } from 'zod';
3
3
  import { navigateResult } from './act/navigation/navigate-result.js';
4
4
  import { absoluteTarget, awaitArrival, idsOpenAtDispatch, ARRIVAL_TIMEOUT_MS, } from './act/navigation/navigate-arrival.js';
5
5
  import { reloadResult } from './act/navigation/reload-result.js';
6
6
  import { waitForReconnect, RELOAD_RECONNECT_TIMEOUT_MS, } from '../../portal/session/session-reconnect.js';
7
- import { ReticleCommand } from '@reticlehq/core';
7
+ import { NO_SESSION_CONNECTED_ERROR, ReticleCommand, SESSION_STARTED_NOTE, SessionStartedVia, relativeUrlNoSession, } from '@reticlehq/core';
8
8
  import { ReticleTool } from '@reticlehq/core';
9
9
  import { asNumber, asString } from '@reticlehq/core';
10
10
  import { sessionIdShape, commandOrThrow } from './tool-kit.js';
@@ -17,8 +17,9 @@ const MSG_LEASE_WITHOUT_SDK = 'This page has no Reticle SDK, so only browser-lev
17
17
  async function navigateLease(deps, sessionId, url, args) {
18
18
  const timeoutMs = asNumber(args['timeout_ms']) ?? ARRIVAL_TIMEOUT_MS;
19
19
  const went = (await deps.pool?.gotoLease(sessionId, url, timeoutMs)) ?? false;
20
- if (!went)
20
+ if (!went) {
21
21
  return { ok: false, url, reason: 'the leased page did not navigate' };
22
+ }
22
23
  const back = await waitForSession(deps, sessionId, timeoutMs);
23
24
  return back
24
25
  ? { ok: true, url, confirmed: true, sessionId }
@@ -28,19 +29,74 @@ async function navigateLease(deps, sessionId, url, args) {
28
29
  async function waitForSession(deps, sessionId, timeoutMs) {
29
30
  const start = deps.now();
30
31
  for (;;) {
31
- if (deps.sessions.get(sessionId) !== undefined)
32
+ if (deps.sessions.get(sessionId) !== undefined) {
32
33
  return true;
33
- if (deps.now() - start >= timeoutMs)
34
+ }
35
+ if (deps.now() - start >= timeoutMs) {
34
36
  return false;
37
+ }
35
38
  await new Promise((resolve) => setTimeout(resolve, LEASE_POLL_MS));
36
39
  }
37
40
  }
38
41
  const LEASE_POLL_MS = 100;
42
+ /** `sessionStarted` on the result. See `SessionStarted` in core. */
43
+ const SessionStartedSchema = z.object({
44
+ sessionId: z.string(),
45
+ via: z.nativeEnum(SessionStartedVia),
46
+ zeroInstall: z.boolean().optional(),
47
+ });
48
+ /** The origin a relative-URL refusal names when the project app cannot be found. */
49
+ const EXAMPLE_ORIGIN = 'http://localhost:3000';
50
+ function isHttpUrl(url) {
51
+ try {
52
+ const { protocol } = new URL(url);
53
+ return 'http:' === protocol || 'https:' === protocol;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /**
60
+ * No session, and an absolute URL: open a lease on it through the lease tool itself, so the same
61
+ * owner, browser preflight and production rules apply as on an explicit `reticle_lease` acquire.
62
+ */
63
+ async function startSessionOn(deps, url) {
64
+ const leased = (await LEASE_ACQUIRE_TOOL.handler(deps, { url }));
65
+ const sessionStarted = {
66
+ sessionId: leased.sessionId,
67
+ via: SessionStartedVia.LEASE,
68
+ ...(true === leased.zeroInstall ? { zeroInstall: true } : {}),
69
+ };
70
+ const note = leased.hint === undefined ? SESSION_STARTED_NOTE : `${SESSION_STARTED_NOTE} ${leased.hint}`;
71
+ return {
72
+ ok: true,
73
+ url,
74
+ confirmed: leased.ready,
75
+ sessionId: leased.sessionId,
76
+ sessionStarted,
77
+ note,
78
+ };
79
+ }
80
+ /**
81
+ * No session, and a relative URL: nothing names the app. Keep the no-session refusal (and its
82
+ * diagnosis), and add the absolute form of the same path.
83
+ */
84
+ async function refuseRelativeWithoutSession(deps, path) {
85
+ let message = NO_SESSION_CONNECTED_ERROR;
86
+ try {
87
+ deps.sessions.resolve();
88
+ }
89
+ catch (err) {
90
+ message = err instanceof Error ? err.message : String(err);
91
+ }
92
+ const origin = (await deps.projectAppUrl?.().catch(() => undefined)) ?? EXAMPLE_ORIGIN;
93
+ throw new Error(`${message}${relativeUrlNoSession(path, origin)}`);
94
+ }
39
95
  export const BROWSER_TOOLS = [
40
96
  {
41
97
  name: ReticleTool.NAVIGATE,
42
98
  example: { url: '/settings' },
43
- description: 'Navigate the connected browser tab to a URL, or reload it in place with { reload: true } (add { hard: true } to bypass the cache). `ok` means the navigation was DISPATCHED — the SDK is torn down by the navigation, so the old page cannot report on it. The daemon then waits up to `timeout_ms` (default 5000) for the SDK to reconnect. `confirmed:true` with a `sessionId` means the requested page arrived and you can act immediately. `confirmed:false` with `landedOn` means the navigation arrived but the app landed at another URL, such as an authentication or route-guard redirect; use `landedOn` to understand where the browser actually ended up. `confirmed:false` without `landedOn` means the target was not observed within the wait window; a slow app may still be on its way, so increase `timeout_ms` when appropriate.',
99
+ description: 'Navigate the connected browser tab to a URL, or reload it in place with { reload: true } (add { hard: true } to bypass the cache). `ok` means the navigation was DISPATCHED — the SDK is torn down by the navigation, so the old page cannot report on it. The daemon then waits up to `timeout_ms` (default 5000) for the SDK to reconnect. `confirmed:true` with a `sessionId` means the requested page arrived and you can act immediately. `confirmed:false` with `landedOn` means the navigation arrived but the app landed at another URL, such as an authentication or route-guard redirect; use `landedOn` to understand where the browser actually ended up. `confirmed:false` without `landedOn` means the target was not observed within the wait window; a slow app may still be on its way, so increase `timeout_ms` when appropriate. With no session connected, an absolute URL opens a leased tab on it and the result carries `sessionStarted`.',
44
100
  inputSchema: {
45
101
  url: z.string().optional().describe('The URL to navigate to. Omit when using reload.'),
46
102
  reload: z
@@ -74,6 +130,8 @@ export const BROWSER_TOOLS = [
74
130
  * of "Reticle stopped waiting" and "the page never came back" you are looking at.
75
131
  */
76
132
  waitedMs: z.number().optional(),
133
+ /** Present only when no session was connected and this call opened one on the URL. */
134
+ sessionStarted: SessionStartedSchema.optional(),
77
135
  note: z.string().optional(),
78
136
  },
79
137
  handler: async (deps, args) => {
@@ -110,8 +168,9 @@ export const BROWSER_TOOLS = [
110
168
  return reloadResult(back, timeoutMs);
111
169
  }
112
170
  const requested = asString(args['url']);
113
- if (requested === undefined || 0 === requested.length)
171
+ if (requested === undefined || 0 === requested.length) {
114
172
  return { ok: false, reason: 'url required' };
173
+ }
115
174
  assertNotPaymentNavigation(requested);
116
175
  const named = asString(args['sessionId']);
117
176
  // A lease whose page has no SDK right now (it was navigated to an image, a PDF, another site):
@@ -123,6 +182,15 @@ export const BROWSER_TOOLS = [
123
182
  return navigateLease(deps, named, carryReticleIdentity(leaseUrl, requested), args);
124
183
  }
125
184
  }
185
+ // Nothing is connected at all, and the call named no tab. An absolute URL names the app, so
186
+ // open a lease on it instead of refusing. Several sessions, or one in another project, keep
187
+ // the refusal that resolve gives below: a lease there would guess.
188
+ if (named === undefined && 0 === deps.sessions.count()) {
189
+ if (isHttpUrl(requested)) {
190
+ return startSessionOn(deps, requested);
191
+ }
192
+ return refuseRelativeWithoutSession(deps, requested);
193
+ }
126
194
  // Record navigate as an action. Its window is usually empty (the page unloads and the SDK
127
195
  // reconnects), but the action record itself — "navigated to X" — is the causal fact worth keeping.
128
196
  const session = deps.sessions.resolve(asString(args['sessionId']));
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { runTool } from './invoke-tool.js';
3
3
  import { buildErrorPayload, codedRefusalOf } from './error-recovery.js';
4
+ import { withRepeatHint } from './repeat-hint.js';
4
5
  import { mergedNameRedirect, mergedNameMessage, retiredToolNames } from './merged-name-redirect.js';
5
6
  import { CATALOGUE_SUMMARY_LENGTH, firstSentence } from './first-sentence.js';
6
7
  import { takeVersionSkewOnto } from '../../command/version/version-nudge.js';
@@ -381,7 +382,7 @@ callable) {
381
382
  // is reached through here: a stale ref, a paused session, a missing pairing token all came
382
383
  // back as the agent's arguments being wrong, which is advice that spends the retry.
383
384
  const message = error instanceof Error ? error.message : String(error);
384
- const payload = buildErrorPayload(message, codedRefusalOf(error));
385
+ const payload = withRepeatHint(buildErrorPayload(message, codedRefusalOf(error)), error);
385
386
  return { ...takeVersionSkewOnto(payload, deps.peerSkew), tool: name };
386
387
  }
387
388
  },
@@ -2,6 +2,8 @@ import { z } from 'zod';
2
2
  import { wiringConsent } from '../../portal/session/no-session-next-action.js';
3
3
  export { wiringConsent };
4
4
  import type { ToolDeps } from './tool-kit.js';
5
+ /** The field that carries the next step for an unwired project. */
6
+ export declare const NEXT_STEP_FIELD = "next_step";
5
7
  /** The fields only an empty list carries, declared so a schema-aware client does not strip them. */
6
8
  export declare const EMPTY_SESSION_LIST_OUTPUT: {
7
9
  why: z.ZodOptional<z.ZodString>;
@@ -32,6 +32,8 @@ const LEASABLE_ACTIONS = new Set([
32
32
  NoSessionAction.OPEN_APP,
33
33
  ]);
34
34
  const LOCALHOST = 'http://localhost';
35
+ /** The field that carries the next step for an unwired project. */
36
+ export const NEXT_STEP_FIELD = 'next_step';
35
37
  /** The fields only an empty list carries, declared so a schema-aware client does not strip them. */
36
38
  export const EMPTY_SESSION_LIST_OUTPUT = {
37
39
  why: z
@@ -51,7 +53,7 @@ export const EMPTY_SESSION_LIST_OUTPUT = {
51
53
  .string()
52
54
  .optional()
53
55
  .describe('Present ONLY when `sessions` is empty: a call with the founder for the person you are working with. Offer it; never act on it.'),
54
- next_step: z
56
+ [NEXT_STEP_FIELD]: z
55
57
  .object({
56
58
  offered: z.nativeEnum(NextStepOffered),
57
59
  lease: z.string().optional(),
@@ -141,7 +143,7 @@ export function emptySessionList(deps, sessions) {
141
143
  sessions,
142
144
  ...(why === undefined ? {} : { why }),
143
145
  ...(next === undefined ? {} : { next_action: next }),
144
- ...(nextStep === undefined ? {} : { next_step: nextStep }),
146
+ ...(nextStep === undefined ? {} : { [NEXT_STEP_FIELD]: nextStep }),
145
147
  ...(undefined === lastKnown ? {} : { lastKnown }),
146
148
  // Setup that never connects is where most people give up; it is the talk worth having.
147
149
  talk_to_us: DiscoveryInvite.AGENT,