@reticlehq/server 3.8.2 → 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 (224) 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/network-mock.js +3 -2
  114. package/dist/portal/input/real-input.d.ts +15 -0
  115. package/dist/portal/input/real-input.js +59 -3
  116. package/dist/portal/input/viewport-tools.js +6 -3
  117. package/dist/portal/pool/browser-pool.d.ts +5 -1
  118. package/dist/portal/pool/browser-pool.js +32 -38
  119. package/dist/portal/pool/context-permissions.d.ts +20 -17
  120. package/dist/portal/pool/context-permissions.js +63 -1
  121. package/dist/portal/pool/csp-widen.d.ts +62 -0
  122. package/dist/portal/pool/csp-widen.js +132 -0
  123. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  124. package/dist/portal/pool/lease-page-error.js +18 -0
  125. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  126. package/dist/portal/pool/page-error-journal.js +181 -0
  127. package/dist/portal/pool/playwright-launcher.js +21 -8
  128. package/dist/portal/pool/pool-contract.d.ts +22 -0
  129. package/dist/portal/pool/pool-lease.d.ts +35 -2
  130. package/dist/portal/pool/pool-lease.js +23 -0
  131. package/dist/portal/pool/zero-install.js +69 -1
  132. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  133. package/dist/portal/session/dev-server/dev-command.js +56 -8
  134. package/dist/portal/session/no-session-diagnosis.js +10 -3
  135. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  136. package/dist/portal/session/no-session-next-action.js +44 -4
  137. package/dist/portal/session/no-session-watch.js +3 -1
  138. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  139. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  140. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  141. package/dist/portal/session/recorded-gaps.js +5 -2
  142. package/dist/portal/session/session-manager.js +40 -21
  143. package/dist/portal/session/session.js +4 -3
  144. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  145. package/dist/portal/session/zero-install-first-call.js +34 -2
  146. package/dist/start-wiring.d.ts +29 -2
  147. package/dist/start-wiring.js +73 -9
  148. package/dist/surface/mcp/mcp-proxy.js +10 -2
  149. package/dist/surface/mcp/mcp.js +5 -3
  150. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  151. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  152. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  153. package/dist/surface/tools/act/act-preflight.js +37 -17
  154. package/dist/surface/tools/act/act-target.js +3 -1
  155. package/dist/surface/tools/act/after-match.d.ts +7 -0
  156. package/dist/surface/tools/act/after-match.js +33 -0
  157. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  158. package/dist/surface/tools/act/resolve-target.js +35 -8
  159. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  160. package/dist/surface/tools/act/sequence-grade.js +8 -1
  161. package/dist/surface/tools/act-sequence-tool.js +3 -0
  162. package/dist/surface/tools/act-tools.js +10 -10
  163. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  164. package/dist/surface/tools/browser-tools.js +75 -7
  165. package/dist/surface/tools/dynamic-tools.js +2 -1
  166. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  167. package/dist/surface/tools/empty-session-list.js +4 -2
  168. package/dist/surface/tools/error-recovery.js +43 -22
  169. package/dist/surface/tools/invoke-tool.js +27 -20
  170. package/dist/surface/tools/lease-hint.d.ts +7 -0
  171. package/dist/surface/tools/lease-hint.js +10 -3
  172. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  173. package/dist/surface/tools/lease-readiness.js +47 -0
  174. package/dist/surface/tools/lease-seed.d.ts +2 -0
  175. package/dist/surface/tools/lease-seed.js +2 -1
  176. package/dist/surface/tools/lease-tools.js +43 -15
  177. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  178. package/dist/surface/tools/repeat-hint.js +171 -0
  179. package/dist/surface/tools/run-report.d.ts +2 -2
  180. package/dist/surface/tools/run-report.js +27 -15
  181. package/dist/surface/tools/tool-kit.d.ts +12 -0
  182. package/dist/surface/tools/tool-kit.js +7 -0
  183. package/dist/surface/tools/tools.js +26 -13
  184. package/dist/telemetry/argument-shape.js +16 -7
  185. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  186. package/dist/telemetry/cli-telemetry.js +15 -0
  187. package/dist/telemetry/init-telemetry.d.ts +2 -1
  188. package/dist/telemetry/init-telemetry.js +47 -2
  189. package/dist/telemetry/install-trace.js +23 -4
  190. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  191. package/dist/telemetry/onboarding-funnel.js +25 -3
  192. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  193. package/dist/telemetry/proxy-telemetry.js +2 -2
  194. package/dist/telemetry/session-metrics.d.ts +1 -1
  195. package/dist/telemetry/session-metrics.js +67 -27
  196. package/dist/telemetry/telemetry.d.ts +3 -1
  197. package/dist/telemetry/telemetry.js +60 -28
  198. package/dist/telemetry/tool-refused.d.ts +2 -2
  199. package/dist/telemetry/tool-refused.js +9 -2
  200. package/docs/architecture.md +3 -6
  201. package/docs/cli/doctor.mdx +41 -2
  202. package/docs/cli/help.mdx +4 -2
  203. package/docs/cli/license.mdx +4 -5
  204. package/docs/cli/setup.mdx +13 -2
  205. package/docs/cli/status.mdx +4 -0
  206. package/docs/cli.mdx +1 -1
  207. package/docs/deploy-checks.md +25 -0
  208. package/docs/desktop.mdx +1 -1
  209. package/docs/enterprise.md +15 -20
  210. package/docs/environment.mdx +1 -0
  211. package/docs/first-drive.md +1 -1
  212. package/docs/install-manual.mdx +16 -1
  213. package/docs/packages/engine.mdx +1 -1
  214. package/docs/packages/server.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages/test.mdx +1 -1
  217. package/docs/packages.mdx +1 -1
  218. package/docs/platform-integration.md +1 -1
  219. package/docs/skill.md +1 -1
  220. package/docs/telemetry.md +4 -2
  221. package/package.json +13 -9
  222. package/LICENSE-ENTERPRISE +0 -37
  223. package/dist/features/ee/audit-log.d.ts +0 -14
  224. package/dist/features/ee/audit-log.js +0 -11
@@ -8,6 +8,8 @@
8
8
  * rest of the budget the caller already granted is spent here, in three steps, each deciding
9
9
  * nothing on its own.
10
10
  */
11
+ import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
12
+ import { evaluatePredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
11
13
  import { waitForInFlight } from './settle-in-flight.js';
12
14
  import { waitForReaction } from './react-grace.js';
13
15
  import { revertedAfterMatch } from './durable.js';
@@ -28,3 +30,34 @@ export async function finishAfterMatch(session, until, since, baselines, remaini
28
30
  // 3. Then ask whether what held still holds — see revertedAfterMatch.
29
31
  return revertedAfterMatch(session, until, since, baselines);
30
32
  }
33
+ /**
34
+ * How long a match that holds only on hidden nodes gets to show. An entrance fade or a toast mounted
35
+ * at opacity 0 reads hidden for its first frame, and the wait matched on that frame. A node hidden
36
+ * by design pays this once, on a verdict that is `unknown` either way.
37
+ */
38
+ const SHOWN_GRACE_MS = 1000;
39
+ /** Poll cadence — the same as the reaction grace. */
40
+ const SHOWN_POLL_MS = 50;
41
+ /**
42
+ * A green that rests only on hidden matches, read again until a match shows, for at most
43
+ * SHOWN_GRACE_MS. Returns the reading that shows one, or `verdict` unchanged. Nothing is predicted:
44
+ * the verdict is judged on what the page painted, and a node that stays hidden stays `hidden_match`.
45
+ */
46
+ export async function awaitShownMatch(session, until, since, baselines, verdict, remaining) {
47
+ if (!verdict.pass || hiddenMatchNote(until, verdict.evidence) === undefined) {
48
+ return verdict;
49
+ }
50
+ // Counted in reads, not read off the session clock, so a clock that does not move still ends it.
51
+ const reads = Math.floor(Math.min(SHOWN_GRACE_MS, remaining()) / SHOWN_POLL_MS);
52
+ for (let read = 0; read < reads; read += 1) {
53
+ await new Promise((resolve) => setTimeout(resolve, SHOWN_POLL_MS));
54
+ const now = await evaluatePredicate(session, until, since, true, baselines);
55
+ if (!now.pass) {
56
+ return verdict;
57
+ }
58
+ if (hiddenMatchNote(until, now.evidence) === undefined) {
59
+ return now;
60
+ }
61
+ }
62
+ return verdict;
63
+ }
@@ -9,6 +9,8 @@ export type TargetResolution = {
9
9
  readonly kind: 'error';
10
10
  readonly message: string;
11
11
  };
12
+ /** The words before the nearest-target list in a no_match refusal. The repeat hint reads after it. */
13
+ export declare const NEAREST_TARGETS_LEAD = " The nearest targets on the page, most similar first: ";
12
14
  /**
13
15
  * Turn a target QUERY into the single ref an action can be performed on.
14
16
  *
@@ -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';
@@ -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,