@reticlehq/server 3.3.0 → 3.5.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 (326) hide show
  1. package/LICENSE-ENTERPRISE +1 -1
  2. package/README.md +3 -3
  3. package/SKILL.md +12 -14
  4. package/dist/chromium-launch-options.d.ts +4 -3
  5. package/dist/chromium-launch-options.js +6 -2
  6. package/dist/command/cli/adhoc-suite.d.ts +22 -0
  7. package/dist/command/cli/adhoc-suite.js +90 -19
  8. package/dist/command/cli/adhoc-verdict.d.ts +35 -0
  9. package/dist/command/cli/adhoc-verdict.js +90 -3
  10. package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
  11. package/dist/command/cli/answers/gate-hook-message.js +6 -0
  12. package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
  13. package/dist/command/cli/auth/cloud-keystore.js +0 -3
  14. package/dist/command/cli/auth/cloud-session.d.ts +6 -2
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  17. package/dist/command/cli/cli-doctor.js +7 -5
  18. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  19. package/dist/command/cli/cli-flow-commands.js +67 -4
  20. package/dist/command/cli/cli-parse.d.ts +8 -1
  21. package/dist/command/cli/cli-parse.js +24 -4
  22. package/dist/command/cli/cli-update-commands.js +8 -1
  23. package/dist/command/cli/cli-verify.d.ts +24 -3
  24. package/dist/command/cli/cli-verify.js +54 -11
  25. package/dist/command/cli/cloud-cli.js +64 -2
  26. package/dist/command/cli/cloud-kit.js +3 -5
  27. package/dist/command/cli/cloud-login.d.ts +1 -1
  28. package/dist/command/cli/cloud-login.js +27 -8
  29. package/dist/command/cli/demo/demo-page.js +6 -3
  30. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  31. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  32. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  33. package/dist/command/cli/launch/cli-launch.js +21 -9
  34. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  35. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  36. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  37. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  38. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  39. package/dist/command/cli/setup-mcp-cli.js +21 -13
  40. package/dist/command/cli/tutorial.d.ts +2 -2
  41. package/dist/command/cli/tutorial.js +21 -7
  42. package/dist/command/cli.js +79 -11
  43. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  44. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  45. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  46. package/dist/command/daemon/daemon-resolve.js +1 -1
  47. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  48. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  49. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  50. package/dist/command/dev/stale-issue-guard.js +8 -8
  51. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  52. package/dist/command/setup/bringup/connect-lease.js +38 -0
  53. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  54. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  55. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  56. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  57. package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
  58. package/dist/command/setup/bringup/dev-server-wait.js +1 -1
  59. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  60. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  61. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  62. package/dist/command/setup/bringup/static-page-server.js +151 -0
  63. package/dist/command/setup/desktop-shape.d.ts +4 -0
  64. package/dist/command/setup/desktop-shape.js +18 -2
  65. package/dist/command/setup/init/init-port.d.ts +15 -0
  66. package/dist/command/setup/init/init-port.js +44 -0
  67. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  68. package/dist/command/setup/init/init-runtime.js +91 -9
  69. package/dist/command/setup/node-effects.d.ts +27 -14
  70. package/dist/command/setup/node-effects.js +86 -45
  71. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  72. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  73. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  74. package/dist/command/setup/probe/page-probe.js +18 -3
  75. package/dist/command/setup/run-setup.d.ts +28 -1
  76. package/dist/command/setup/run-setup.js +103 -15
  77. package/dist/command/setup/session-pick.d.ts +13 -0
  78. package/dist/command/setup/session-pick.js +20 -0
  79. package/dist/command/setup/setup-command.d.ts +2 -14
  80. package/dist/command/setup/setup-command.js +105 -31
  81. package/dist/command/setup/setup-mcp.d.ts +2 -0
  82. package/dist/command/setup/setup-mcp.js +17 -8
  83. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  84. package/dist/command/setup/terminal/wait-progress.js +17 -0
  85. package/dist/command/version/peer-announce.d.ts +6 -2
  86. package/dist/command/version/peer-announce.js +8 -6
  87. package/dist/command/version/version-nudge.d.ts +17 -3
  88. package/dist/command/version/version-nudge.js +25 -4
  89. package/dist/features/crawl/crawl-empty.js +1 -1
  90. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  91. package/dist/features/crawl/crawl-tools.js +57 -0
  92. package/dist/features/crawl/crawl.js +171 -157
  93. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  94. package/dist/features/exhaust/app-coverage.js +31 -0
  95. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  96. package/dist/features/exhaust/code-coverage.js +77 -0
  97. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  98. package/dist/features/exhaust/exhaust-run.js +82 -0
  99. package/dist/features/exhaust/explorer.d.ts +103 -0
  100. package/dist/features/exhaust/explorer.js +195 -0
  101. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  102. package/dist/features/exhaust/failure-branches.js +92 -0
  103. package/dist/features/exhaust/ledger.d.ts +202 -0
  104. package/dist/features/exhaust/ledger.js +252 -0
  105. package/dist/features/exhaust/session-fold.d.ts +31 -0
  106. package/dist/features/exhaust/session-fold.js +79 -0
  107. package/dist/features/exhaust/session-port.d.ts +36 -0
  108. package/dist/features/exhaust/session-port.js +102 -0
  109. package/dist/features/harness/drive-report.d.ts +0 -9
  110. package/dist/features/harness/drive-report.js +18 -0
  111. package/dist/features/harness/driver.js +4 -7
  112. package/dist/features/harness/fill-values.d.ts +10 -0
  113. package/dist/features/harness/fill-values.js +50 -11
  114. package/dist/features/harness/jev-driver.js +30 -13
  115. package/dist/features/harness/openai-driver.d.ts +12 -0
  116. package/dist/features/harness/openai-driver.js +21 -12
  117. package/dist/features/harness/platform-config.js +5 -6
  118. package/dist/features/license/license.d.ts +2 -2
  119. package/dist/index.d.ts +9 -0
  120. package/dist/index.js +56 -19
  121. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  122. package/dist/judgement/domain/domain-model.js +3 -1
  123. package/dist/judgement/oracles/self-instrument.js +4 -2
  124. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  125. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  126. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  127. package/dist/judgement/runs/verification-sync.js +1 -1
  128. package/dist/language/flows/change/affected.d.ts +9 -0
  129. package/dist/language/flows/change/affected.js +35 -0
  130. package/dist/language/flows/change/gate.d.ts +20 -0
  131. package/dist/language/flows/change/gate.js +12 -1
  132. package/dist/language/flows/flow-author.d.ts +7 -0
  133. package/dist/language/flows/flow-author.js +19 -0
  134. package/dist/language/flows/flow-expect-grammar.js +1 -1
  135. package/dist/language/flows/flow-journey.d.ts +54 -0
  136. package/dist/language/flows/flow-journey.js +119 -0
  137. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  138. package/dist/language/flows/flow-replay-run.js +13 -47
  139. package/dist/language/flows/flow-replay.d.ts +22 -8
  140. package/dist/language/flows/flow-replay.js +55 -9
  141. package/dist/language/flows/flow-result.d.ts +2 -0
  142. package/dist/language/flows/flow-result.js +25 -0
  143. package/dist/language/flows/flow-tools.js +17 -5
  144. package/dist/language/flows/flows.d.ts +10 -1
  145. package/dist/language/flows/flows.js +90 -14
  146. package/dist/language/flows/recording/replay-halt.js +3 -1
  147. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  148. package/dist/language/flows/recording/tape/recordings.js +55 -1
  149. package/dist/language/flows/replay.d.ts +19 -0
  150. package/dist/language/flows/replay.js +42 -6
  151. package/dist/language/flows/verify-change-tools.js +5 -2
  152. package/dist/launch-chromium.d.ts +80 -0
  153. package/dist/launch-chromium.js +218 -0
  154. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  155. package/dist/memory/cloud/cloud-config.js +47 -7
  156. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  157. package/dist/memory/cloud/cloud-sync.js +6 -9
  158. package/dist/memory/cloud/harness-offer.js +5 -6
  159. package/dist/memory/cloud/harness-switch.js +5 -6
  160. package/dist/memory/cloud/sync-cycle.d.ts +77 -3
  161. package/dist/memory/cloud/sync-cycle.js +325 -55
  162. package/dist/memory/cloud/sync-daemon.js +24 -20
  163. package/dist/memory/cloud/sync-disk.js +16 -2
  164. package/dist/memory/intent/intent-shard.d.ts +10 -32
  165. package/dist/memory/intent/intent-shard.js +0 -10
  166. package/dist/memory/intent/intent-tools.js +8 -3
  167. package/dist/memory/journal/drive-flow.d.ts +18 -13
  168. package/dist/memory/journal/drive-flow.js +45 -37
  169. package/dist/memory/journal/envelope.d.ts +8 -8
  170. package/dist/memory/journal/journal-query.d.ts +14 -0
  171. package/dist/memory/journal/journal-query.js +15 -0
  172. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  173. package/dist/memory/journal/session-end.d.ts +9 -1
  174. package/dist/memory/journal/session-end.js +24 -12
  175. package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
  176. package/dist/memory/project/artifact-root-resolver.js +46 -19
  177. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  178. package/dist/memory/project/dir/reticle-dir.js +1 -0
  179. package/dist/memory/project/project-tools.js +3 -1
  180. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  181. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  182. package/dist/portal/bridge/bridge.d.ts +12 -3
  183. package/dist/portal/bridge/bridge.js +49 -3
  184. package/dist/portal/input/js-coverage.d.ts +25 -0
  185. package/dist/portal/input/js-coverage.js +30 -0
  186. package/dist/portal/input/real-input.d.ts +11 -0
  187. package/dist/portal/input/real-input.js +29 -4
  188. package/dist/portal/input/viewport-tools.js +16 -6
  189. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  190. package/dist/portal/pool/playwright-launcher.js +20 -13
  191. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  192. package/dist/portal/session/dev-server/dev-command.js +30 -12
  193. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  194. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  195. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  196. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  197. package/dist/portal/session/facts/observed-state.js +34 -0
  198. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  199. package/dist/portal/session/no-session-diagnosis.js +62 -5
  200. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  201. package/dist/portal/session/no-session-next-action.js +48 -9
  202. package/dist/portal/session/no-session-watch.d.ts +18 -0
  203. package/dist/portal/session/no-session-watch.js +88 -2
  204. package/dist/portal/session/session-health.js +3 -2
  205. package/dist/portal/session/session-manager.d.ts +20 -6
  206. package/dist/portal/session/session-manager.js +19 -3
  207. package/dist/portal/session/session-reaper.d.ts +28 -0
  208. package/dist/portal/session/session-reaper.js +49 -1
  209. package/dist/portal/session/session.d.ts +10 -8
  210. package/dist/portal/session/session.js +24 -32
  211. package/dist/surface/http-server.d.ts +3 -2
  212. package/dist/surface/http-server.js +8 -5
  213. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  214. package/dist/surface/mcp/mcp-proxy.js +2 -2
  215. package/dist/surface/mcp/mcp.js +5 -3
  216. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  217. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  218. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  219. package/dist/surface/mcp/server-instructions.js +9 -7
  220. package/dist/surface/tools/act/after-match.d.ts +17 -0
  221. package/dist/surface/tools/act/after-match.js +30 -0
  222. package/dist/surface/tools/act/durable.d.ts +69 -0
  223. package/dist/surface/tools/act/durable.js +139 -0
  224. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  225. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  226. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  227. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  228. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  229. package/dist/surface/tools/act/proved-controls.js +11 -0
  230. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  231. package/dist/surface/tools/act/sequence-grade.js +32 -1
  232. package/dist/surface/tools/act-capsule.d.ts +7 -0
  233. package/dist/surface/tools/act-capsule.js +46 -1
  234. package/dist/surface/tools/act-sequence-tool.js +25 -8
  235. package/dist/surface/tools/act-tools.js +28 -52
  236. package/dist/surface/tools/assert/assert-grade.js +12 -1
  237. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  238. package/dist/surface/tools/browser-tools.js +14 -2
  239. package/dist/surface/tools/coverage-tools.js +27 -1
  240. package/dist/surface/tools/dynamic-tools.js +29 -2
  241. package/dist/surface/tools/error-recovery.js +5 -6
  242. package/dist/surface/tools/feedback-invite.js +6 -4
  243. package/dist/surface/tools/harness-explore.d.ts +3 -2
  244. package/dist/surface/tools/harness-explore.js +12 -7
  245. package/dist/surface/tools/invoke-tool.js +12 -8
  246. package/dist/surface/tools/lease-tools.js +15 -6
  247. package/dist/surface/tools/live-call-text.js +4 -1
  248. package/dist/surface/tools/observe-tools.js +9 -1
  249. package/dist/surface/tools/tool-kit.d.ts +10 -0
  250. package/dist/surface/tools/tools.js +4 -0
  251. package/dist/telemetry/argument-shape.d.ts +0 -2
  252. package/dist/telemetry/argument-shape.js +1 -5
  253. package/dist/telemetry/error-class.d.ts +2 -3
  254. package/dist/telemetry/error-class.js +2 -3
  255. package/dist/telemetry/harness-drive.d.ts +0 -2
  256. package/dist/telemetry/harness-drive.js +0 -4
  257. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  258. package/dist/telemetry/onboarding-firsts.js +2 -2
  259. package/dist/wire-journal.d.ts +3 -0
  260. package/dist/wire-journal.js +4 -0
  261. package/dist/workspace-packages.d.ts +0 -2
  262. package/dist/workspace-packages.js +1 -9
  263. package/docs/README.md +4 -2
  264. package/docs/actions.mdx +3 -4
  265. package/docs/agent-cheatsheet.md +4 -4
  266. package/docs/architecture.md +6 -6
  267. package/docs/best-practices.mdx +1 -1
  268. package/docs/capabilities.mdx +32 -1
  269. package/docs/cli/cloud.mdx +20 -8
  270. package/docs/cli/doctor.mdx +4 -0
  271. package/docs/cli/feedback.mdx +2 -2
  272. package/docs/cli/gate.mdx +9 -4
  273. package/docs/cli/update.mdx +10 -0
  274. package/docs/cli.mdx +22 -7
  275. package/docs/desktop.mdx +14 -10
  276. package/docs/environment.mdx +87 -0
  277. package/docs/faq.mdx +18 -16
  278. package/docs/first-drive.md +5 -13
  279. package/docs/flows.md +21 -2
  280. package/docs/frameworks.mdx +29 -10
  281. package/docs/getting-started.md +15 -5
  282. package/docs/http-transport.md +6 -3
  283. package/docs/human-control.md +2 -2
  284. package/docs/index.mdx +16 -6
  285. package/docs/install-agentic.mdx +2 -2
  286. package/docs/install-manual.mdx +1 -1
  287. package/docs/integration-patterns.md +3 -3
  288. package/docs/multi-agent-testing.md +1 -1
  289. package/docs/packages/babel-plugin.mdx +1 -1
  290. package/docs/packages/browser.mdx +2 -2
  291. package/docs/packages/next.mdx +3 -0
  292. package/docs/packages/react.mdx +2 -2
  293. package/docs/packages/tauri.mdx +1 -1
  294. package/docs/packages/vite-plugin.mdx +4 -2
  295. package/docs/packages.mdx +9 -9
  296. package/docs/predicates.mdx +50 -5
  297. package/docs/quickstart.mdx +12 -10
  298. package/docs/recipes.mdx +2 -2
  299. package/docs/state-management.mdx +2 -2
  300. package/docs/system-map.md +1 -1
  301. package/docs/telemetry.md +1 -1
  302. package/docs/token-efficiency.md +3 -3
  303. package/docs/tools/act-and-wait.mdx +1 -0
  304. package/docs/tools/act-sequence.mdx +1 -1
  305. package/docs/tools/act.mdx +1 -1
  306. package/docs/tools/console.mdx +1 -1
  307. package/docs/tools/inspect.mdx +1 -1
  308. package/docs/tools/network.mdx +1 -1
  309. package/docs/tools/query.mdx +1 -1
  310. package/docs/tools/sessions.mdx +1 -1
  311. package/docs/tools/snapshot.mdx +1 -1
  312. package/docs/tools/state.mdx +1 -1
  313. package/docs/tools/wait-for.mdx +1 -1
  314. package/docs/usage.md +7 -7
  315. package/docs/use-cases.mdx +157 -0
  316. package/docs/vs-screenshots.mdx +3 -3
  317. package/docs/what-is-recorded.md +2 -2
  318. package/docs/what-is-reticle.mdx +17 -20
  319. package/docs/why-reticle.mdx +14 -21
  320. package/package.json +10 -9
  321. package/dist/judgement/capsule/minimize.d.ts +0 -12
  322. package/dist/judgement/capsule/minimize.js +0 -22
  323. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  324. package/dist/judgement/phenomena/phenomena.js +0 -101
  325. package/dist/language/flows/flow-report.d.ts +0 -22
  326. package/dist/language/flows/flow-report.js +0 -106
@@ -3,8 +3,12 @@ export declare function daemonPollDelayMs(attempt: number): number;
3
3
  * Returns true if something is already listening on the reticle port.
4
4
  * Uses a plain TCP probe so we don't create a side-effectful SSE session
5
5
  * inside the daemon just to check reachability.
6
+ *
7
+ * `host` defaults to loopback, which is every caller's actual target — except `bootSession`
8
+ * (reticlehq/reticle#1165 review), whose Bridge can bind a non-default host via `RETICLE_HOST`.
9
+ * Hardcoding loopback here made that preflight check the wrong address.
6
10
  */
7
- export declare function probeDaemon(port: number): Promise<boolean>;
11
+ export declare function probeDaemon(port: number, host?: string): Promise<boolean>;
8
12
  /**
9
13
  * Poll until a Reticle daemon is SERVING on `port`, or the deadline passes.
10
14
  *
@@ -20,3 +24,11 @@ export declare function waitForDaemon(port: number, probes?: {
20
24
  tcpOpen: (port: number) => Promise<boolean>;
21
25
  status: (port: number) => Promise<unknown>;
22
26
  }): Promise<void>;
27
+ /**
28
+ * Poll until `/status` on `port` answers as a Reticle daemon; false once the bound passes.
29
+ *
30
+ * One rule for the three commands that start a daemon. `init` used to borrow `waitForDaemon`'s
31
+ * shorter reconnect budget, and on a cold fresh HOME it printed "could not start the Reticle
32
+ * daemon" over a daemon that bound seconds later. Bounded polling, never a fixed sleep.
33
+ */
34
+ export declare function waitForDaemonBind(port: number): Promise<boolean>;
@@ -24,8 +24,12 @@ export function daemonPollDelayMs(attempt) {
24
24
  * Returns true if something is already listening on the reticle port.
25
25
  * Uses a plain TCP probe so we don't create a side-effectful SSE session
26
26
  * inside the daemon just to check reachability.
27
+ *
28
+ * `host` defaults to loopback, which is every caller's actual target — except `bootSession`
29
+ * (reticlehq/reticle#1165 review), whose Bridge can bind a non-default host via `RETICLE_HOST`.
30
+ * Hardcoding loopback here made that preflight check the wrong address.
27
31
  */
28
- export function probeDaemon(port) {
32
+ export function probeDaemon(port, host = LOOPBACK_HOST) {
29
33
  return new Promise((resolve) => {
30
34
  const socket = new net.Socket();
31
35
  socket.setTimeout(500);
@@ -38,7 +42,7 @@ export function probeDaemon(port) {
38
42
  socket.destroy();
39
43
  resolve(false);
40
44
  });
41
- socket.connect(port, LOOPBACK_HOST);
45
+ socket.connect(port, host);
42
46
  });
43
47
  }
44
48
  /**
@@ -69,3 +73,24 @@ export async function waitForDaemon(port, probes = { tcpOpen: probeDaemon, statu
69
73
  throw new Error(`reticle daemon did not become ready on port ${String(port)} within ` +
70
74
  `${String(DAEMON_READY_TIMEOUT_MS)}ms — ${describePresence(presence, port)}`);
71
75
  }
76
+ /** How long a spawned daemon gets to bind before `serve`, `restart` or `init` gives up on it. */
77
+ const BIND_TIMEOUT_MS = 15_000;
78
+ const BIND_POLL_MS = 150;
79
+ /**
80
+ * Poll until `/status` on `port` answers as a Reticle daemon; false once the bound passes.
81
+ *
82
+ * One rule for the three commands that start a daemon. `init` used to borrow `waitForDaemon`'s
83
+ * shorter reconnect budget, and on a cold fresh HOME it printed "could not start the Reticle
84
+ * daemon" over a daemon that bound seconds later. Bounded polling, never a fixed sleep.
85
+ */
86
+ export async function waitForDaemonBind(port) {
87
+ const deadline = Date.now() + BIND_TIMEOUT_MS;
88
+ for (;;) {
89
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
90
+ if (presence === PortPresence.DAEMON)
91
+ return true;
92
+ if (Date.now() >= deadline)
93
+ return false;
94
+ await delay(BIND_POLL_MS);
95
+ }
96
+ }
@@ -6,11 +6,9 @@
6
6
  * keep that file from becoming the largest thing on the user's disk.
7
7
  */
8
8
  import { appendFileSync, mkdirSync, renameSync, statSync, truncateSync } from 'node:fs';
9
- import { homedir } from 'node:os';
10
9
  import { join } from 'node:path';
11
- import { ReticleDir } from '@reticlehq/core';
12
10
  import { log } from '../../../log.js';
13
- import { MAX_DAEMON_LOG_BYTES, recoverOversizedLog, rotateDaemonLog, } from '../../../command/daemon/daemon.js';
11
+ import { MAX_DAEMON_LOG_BYTES, recoverOversizedLog, rotateDaemonLog, reticleStateHome, } from '../../../command/daemon/daemon.js';
14
12
  /**
15
13
  * Which port this proxy serves, for the log file name. Set once at startup.
16
14
  *
@@ -79,7 +77,7 @@ export function setProxyLogPort(port) {
79
77
  /** The proxy's own log file, so a silent drop leaves a readable trace the agent can go read. */
80
78
  export function proxyLogPath(port = logPort) {
81
79
  const name = port === undefined ? 'mcp-proxy.log' : `proxy-${String(port)}.log`;
82
- return join(homedir(), ReticleDir.ROOT, name);
80
+ return join(reticleStateHome(), name);
83
81
  }
84
82
  /**
85
83
  * Log to stderr (which the agent host usually swallows) AND to ~/.reticle/proxy-<port>.log, which it
@@ -96,7 +94,7 @@ export function proxyLogPath(port = logPort) {
96
94
  export function proxyLog(event, fields = {}) {
97
95
  log(event, fields);
98
96
  try {
99
- const dir = join(homedir(), ReticleDir.ROOT);
97
+ const dir = reticleStateHome();
100
98
  mkdirSync(dir, { recursive: true });
101
99
  // WITH A TIMESTAMP. This file is the only record of an outage that survives the session, and
102
100
  // without one it cannot answer the two questions anyone brings to it: when did this happen, and
@@ -44,9 +44,9 @@ import { surfaceVocabulary, listOf } from './surface-vocabulary.js';
44
44
  */
45
45
  const verdictDiscipline = (v) => `Reticle verifies a running web app from the inside: go (${v.navigate}), look (${listOf(v.look, v.find)}), act and prove in one hop (${v.actAndWait}), observe (${listOf(v.observe, v.state, v.network, v.console)}), assert (${v.assert}). Verify a user-facing change against the real app before you call it done — \`npx @reticlehq/server gate --since <ref>\` fails unless a passing artifact covers every flow your edits affect — and never weaken a check to make it pass.
46
46
 
47
- Only ${v.actAndWait} and ${v.assert} produce a verdict. ${v.act.length > 0 ? `${v.act} and e` : 'E'}verything else moves or reads the app and proves nothing, so a drive that ends without one of those two has no result however many tools it used. Prefer ${v.actAndWait}({ ref, action, until }) — it names the expected consequence BEFORE the action, which is the difference between a check and a rationalisation. Only verified:"yes" is a pass — "unknown" means Reticle could not tell what happened, "no-fault" means nothing was declared to prove. Report either as not proved.
47
+ Only ${v.act.length > 0 ? `${v.actAndWait}, ${v.assert} and ${v.act}{steps}` : `${v.actAndWait} and ${v.assert}`} produce a verdict. Everything else moves or reads the app and proves nothing, so a drive that ends without one of those two has no result however many tools it used. Prefer ${v.actAndWait}({ ref, action, until }) — it names the expected consequence BEFORE the action, which is the difference between a check and a rationalisation. Only verified:"yes" is a pass — "unknown" means Reticle could not tell what happened, "no-fault" means nothing was declared to prove. Report either as not proved.
48
48
 
49
- A "yes" over a clean capture IS the answer — re-reading the page after one finds the same state. When every consequence you set out to check has one, stop.
49
+ Done is the journey's END proved, not its first step: drive each step your change touches and give the last an \`until\` naming the end state (\`durable: true\` if it must persist). A "yes" on it over a clean capture IS the answer — stop.
50
50
 
51
51
  DIAGNOSING a bug? Read the source first — Reticle proves what the app DOES, not why. Drive to CONFIRM a fix, not to find one.`;
52
52
  /**
@@ -84,13 +84,15 @@ const replayFirst = (v) => {
84
84
  *
85
85
  * Deliberately concrete: the command, what it does to the app, and the call that confirms it. A
86
86
  * general instruction to "set Reticle up" is the state these agents are already in — they have the
87
- * tools and no app, and nothing tells them that combination is incomplete. It also names the dev
88
- * server restart, because a plugin added to a config the running server already read is the single
89
- * most common way this step half-succeeds.
87
+ * tools and no app, and nothing tells them that combination is incomplete. `init` starts the dev
88
+ * server and opens the app itself, so the restart is named only for the case it cannot cover: a dev
89
+ * server that was already running read the config before `init` edited it, which is the single most
90
+ * common way this step half-succeeds. Telling every agent to restart sent them to kill the server
91
+ * `init` had just started.
90
92
  */
91
93
  const firstMoveFor = (v) => `FIRST: no app has ever connected to Reticle in this project, so nothing here can be verified yet. Having these tools is not the same as being set up — the tools reach a daemon, and the daemon has nothing to look at until the app itself is instrumented.
92
94
 
93
- Fix that before anything else: run \`npx @reticlehq/server init\` in the project, then RESTART the dev server (a plugin added to a config the running server already read is not in the bundle), then load the app in a browser. Confirm with ${v.sessions} — a session listed there is the proof, and until one appears no other tool can tell you anything about this app.`;
95
+ Fix that before anything else: run \`npx @reticlehq/server init\` in the project. It starts the dev server and opens the app itself; only if a dev server was ALREADY running without Reticle, restart it and reload the tab. Confirm with ${v.sessions} — a session listed there is the proof, and until one appears no other tool can tell you anything about this app.`;
94
96
  /**
95
97
  * When to reach for the tools that are advertised but never explained, and how to reach the two
96
98
  * that are explained but not advertised. Both halves of the same defect.
@@ -111,7 +113,7 @@ Fix that before anything else: run \`npx @reticlehq/server init\` in the project
111
113
  */
112
114
  const reachFor = (v) => {
113
115
  const lines = [
114
- `${v.observe} is the evidence channel: everything the page did since a cursor, in one read. Take it BEFORE you call a build broken — measured, stripping the observation tools TRIPLED false alarms, because an agent that stops observing reaches for the verdict without the evidence. ${v.settle} settles a page that is changing without you; ${v.inspect} maps an element to its source file:line, which turns a finding into an edit${v.yield.length > 0 ? `; ${v.yield} hands the tab back to the human` : ''}.`,
116
+ `${v.observe} is the evidence channel: everything the page did since a cursor, in one read. Take it BEFORE you call a build broken — measured, stripping the observation tools TRIPLED false alarms. ${v.settle} settles a page that is changing without you; ${v.inspect} maps an element to its source file:line, which turns a finding into an edit${v.yield.length > 0 ? `; ${v.yield} hands the tab back to the human` : ''}.`,
115
117
  ];
116
118
  // Said ONLY when both halves are advertised. A catalogue with no way to invoke what it lists sends
117
119
  // the agent at a tool it cannot call and then off the product entirely — see
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What act_and_wait does between the moment its predicate first holds and the moment it grades.
3
+ *
4
+ * The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is while
5
+ * the write is still in flight — so the verdict was taken over a window the app had not finished,
6
+ * and came back `unknown / unsettled` asking the CALLER to re-check. A login that genuinely
7
+ * succeeded returned at 492ms of an 8000ms budget with every corroborating channel agreeing. So the
8
+ * rest of the budget the caller already granted is spent here, in three steps, each deciding
9
+ * nothing on its own.
10
+ */
11
+ import type { Predicate } from '@reticlehq/core';
12
+ import { waitForInFlight } from './settle-in-flight.js';
13
+ import { revertedAfterMatch } from './durable.js';
14
+ type Waited = Parameters<typeof waitForInFlight>[0] & Parameters<typeof revertedAfterMatch>[0];
15
+ /** Returns the sentence to fail with when the consequence reverted, else undefined. */
16
+ export declare function finishAfterMatch(session: Waited, until: Predicate, since: number, baselines: Parameters<typeof revertedAfterMatch>[3], remaining: () => number): Promise<string | undefined>;
17
+ export {};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What act_and_wait does between the moment its predicate first holds and the moment it grades.
3
+ *
4
+ * The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is while
5
+ * the write is still in flight — so the verdict was taken over a window the app had not finished,
6
+ * and came back `unknown / unsettled` asking the CALLER to re-check. A login that genuinely
7
+ * succeeded returned at 492ms of an 8000ms budget with every corroborating channel agreeing. So the
8
+ * rest of the budget the caller already granted is spent here, in three steps, each deciding
9
+ * nothing on its own.
10
+ */
11
+ import { waitForInFlight } from './settle-in-flight.js';
12
+ import { waitForReaction } from './react-grace.js';
13
+ import { revertedAfterMatch } from './durable.js';
14
+ /** Returns the sentence to fail with when the consequence reverted, else undefined. */
15
+ export async function finishAfterMatch(session, until, since, baselines, remaining) {
16
+ const sleep = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
17
+ // 1. Follow any open request to its response. A response that arrives as a FAILURE still
18
+ // contradicts — and now arrives INSIDE the window, where the detector can see it at all — while a
19
+ // request still open when the budget runs out reports unsettled, an honest limit rather than an
20
+ // early exit. Costs nothing on the common path, where no request is in flight.
21
+ await waitForInFlight(session, since, remaining(), sleep);
22
+ // 2. Then the app's REACTION to that response. Close the window between the response and the
23
+ // re-render and every channel agrees the app took a successful write and did nothing —
24
+ // `response-ignored`, a `verified:"no"` on a correct app, produced entirely by where we stopped
25
+ // looking. Paid only in the shape that would otherwise be accused, and short enough that a
26
+ // genuinely dropped response is still reported.
27
+ await waitForReaction(session, since, remaining(), sleep);
28
+ // 3. Then ask whether what held still holds — see revertedAfterMatch.
29
+ return revertedAfterMatch(session, until, since, baselines);
30
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `act_and_wait { durable: true }` — after a yes, reload and ask again.
3
+ *
4
+ * Every verdict is scoped to one action's window, so "saved" was proved by the toast that said so,
5
+ * and nothing ever asked whether the thing was still there once the page came back. That is the
6
+ * difference between a UI that advanced and a change that persisted, and it was reachable only by
7
+ * hand: navigate, then a separate assert that nothing tied to the action that caused it.
8
+ *
9
+ * Only the part of `until` that describes the page as it IS can be asked of a fresh document. A
10
+ * request or a signal happened once, in a window that died with the old document; a route is read
11
+ * from route events a reload does not replay. So those clauses are dropped, never re-asserted.
12
+ */
13
+ import { z } from 'zod';
14
+ import { type Predicate } from '@reticlehq/core';
15
+ import type { decideVerified } from '@reticlehq/engine/evidence/verified.js';
16
+ type VerifiedVerdict = ReturnType<typeof decideVerified>;
17
+ import { evaluatePredicate } from '@reticlehq/engine/question/predicate/predicate.js';
18
+ import type { Session } from '../../../portal/session/session.js';
19
+ export declare const durableArg: z.ZodOptional<z.ZodBoolean>;
20
+ export declare const durableOutput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
21
+ /** The part of a predicate a fresh document can answer, or undefined when there is none. */
22
+ export declare function durablePart(p: Predicate): Predicate | undefined;
23
+ /**
24
+ * What the re-check found. `held` absent means no answer: the page never came back to be asked, or
25
+ * it came back and could not answer (`inconclusive`).
26
+ */
27
+ export interface Durability {
28
+ held?: boolean;
29
+ observed?: string;
30
+ skipped?: string;
31
+ inconclusive?: string;
32
+ }
33
+ /**
34
+ * A re-check's predicate result, as durability. Only a page that ANSWERED can say the consequence is
35
+ * gone: an unreadable one (its store not up yet) or one the tab left mid-read is no answer, and
36
+ * reporting either as "did not persist" is a confident no about something nobody saw.
37
+ */
38
+ export declare function durabilityOf(verdict: {
39
+ pass: boolean;
40
+ observed?: string | undefined;
41
+ inconclusive?: string | undefined;
42
+ observationLost?: boolean | undefined;
43
+ }): Durability;
44
+ /**
45
+ * Apply `durable` to a decided verdict. Pure over `recheck`, which the caller supplies — see
46
+ * reloadAndRecheck for the one that reloads a real page.
47
+ */
48
+ export declare function withDurability(asked: unknown, until: Predicate, decision: VerifiedVerdict, recheck: (part: Predicate) => Promise<Durability>): Promise<{
49
+ decision: VerifiedVerdict;
50
+ durable?: Durability;
51
+ }>;
52
+ /** Reload the page, wait for it to re-announce itself, and ask `part` of the fresh document. */
53
+ export declare function reloadAndRecheck(deps: {
54
+ sessions: {
55
+ get(id: string): Session | undefined;
56
+ };
57
+ now: () => number;
58
+ }, session: Session, part: Predicate, timeoutMs: number): Promise<Durability>;
59
+ /**
60
+ * Whether the page-state part of a consequence that held has since gone — an optimistic UI that
61
+ * rolled back. Returns the sentence to fail with, or undefined when it still holds.
62
+ *
63
+ * The wait resolves the instant a predicate holds, and the in-flight wait after it only follows a
64
+ * request to its response. A row that appeared and was removed a tick later, with no failing
65
+ * request to contradict it, was a yes. Only element/text/state clauses are re-read: a request or a
66
+ * signal does not un-happen, and re-reading `settled` on a page that polls would accuse it falsely.
67
+ */
68
+ export declare function revertedAfterMatch(session: Parameters<typeof evaluatePredicate>[0], until: Predicate, since: number, baselines: Parameters<typeof evaluatePredicate>[4]): Promise<string | undefined>;
69
+ export {};
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `act_and_wait { durable: true }` — after a yes, reload and ask again.
3
+ *
4
+ * Every verdict is scoped to one action's window, so "saved" was proved by the toast that said so,
5
+ * and nothing ever asked whether the thing was still there once the page came back. That is the
6
+ * difference between a UI that advanced and a change that persisted, and it was reachable only by
7
+ * hand: navigate, then a separate assert that nothing tied to the action that caused it.
8
+ *
9
+ * Only the part of `until` that describes the page as it IS can be asked of a fresh document. A
10
+ * request or a signal happened once, in a window that died with the old document; a route is read
11
+ * from route events a reload does not replay. So those clauses are dropped, never re-asserted.
12
+ */
13
+ import { z } from 'zod';
14
+ import { PredicateKind, ReticleCommand, Verified, VerifiedReason, } from '@reticlehq/core';
15
+ import { evaluatePredicate, waitForPredicate, } from '@reticlehq/engine/question/predicate/predicate.js';
16
+ import { RELOAD_RECONNECT_TIMEOUT_MS, waitForReconnect, } from '../../../portal/session/session-reconnect.js';
17
+ export const durableArg = z
18
+ .boolean()
19
+ .optional()
20
+ .describe('After a yes, reload and require the element/text/state part of `until` to hold again.');
21
+ // Loose and undescribed on purpose: the output surface is re-sent every turn. `because` carries
22
+ // the answer; this is the detail — { held?, observed?, skipped? }.
23
+ export const durableOutput = z.record(z.unknown()).optional();
24
+ const RE_READABLE = new Set([
25
+ PredicateKind.ELEMENT,
26
+ PredicateKind.TEXT,
27
+ PredicateKind.STATE,
28
+ ]);
29
+ /** The part of a predicate a fresh document can answer, or undefined when there is none. */
30
+ export function durablePart(p) {
31
+ if (PredicateKind.ALL_OF === p.kind) {
32
+ const kept = p.predicates.map(durablePart).filter((c) => c !== undefined);
33
+ if (0 === kept.length)
34
+ return undefined;
35
+ return 1 === kept.length ? kept[0] : { kind: PredicateKind.ALL_OF, predicates: kept };
36
+ }
37
+ // Narrowing an OR to the branches that survive would change the claim, not re-check it.
38
+ if (PredicateKind.ANY_OF === p.kind)
39
+ return p.predicates.every((c) => durablePart(c) === c) ? p : undefined;
40
+ if (PredicateKind.NOT === p.kind)
41
+ return durablePart(p.predicate) === p.predicate ? p : undefined;
42
+ return RE_READABLE.has(p.kind) ? p : undefined;
43
+ }
44
+ /**
45
+ * A re-check's predicate result, as durability. Only a page that ANSWERED can say the consequence is
46
+ * gone: an unreadable one (its store not up yet) or one the tab left mid-read is no answer, and
47
+ * reporting either as "did not persist" is a confident no about something nobody saw.
48
+ */
49
+ export function durabilityOf(verdict) {
50
+ const observed = verdict.observed === undefined ? {} : { observed: verdict.observed };
51
+ if (true === verdict.pass)
52
+ return { held: true, ...observed };
53
+ if (verdict.inconclusive !== undefined)
54
+ return { inconclusive: verdict.inconclusive, ...observed };
55
+ if (true === verdict.observationLost)
56
+ return observed;
57
+ return { held: false, ...observed };
58
+ }
59
+ /**
60
+ * Apply `durable` to a decided verdict. Pure over `recheck`, which the caller supplies — see
61
+ * reloadAndRecheck for the one that reloads a real page.
62
+ */
63
+ export async function withDurability(asked, until, decision, recheck) {
64
+ if (true !== asked || Verified.YES !== decision.verified)
65
+ return { decision };
66
+ const part = durablePart(until);
67
+ if (part === undefined) {
68
+ const skipped = 'nothing declared describes the page itself — a request or a signal happens once and cannot be seen again after a reload; add an element, text or state clause';
69
+ return { decision, durable: { skipped } };
70
+ }
71
+ const durable = await recheck(part);
72
+ if (true === durable.held)
73
+ return { decision, durable };
74
+ if (durable.inconclusive !== undefined) {
75
+ return {
76
+ durable,
77
+ decision: {
78
+ verified: Verified.UNKNOWN,
79
+ verifiedReason: VerifiedReason.INCONCLUSIVE,
80
+ because: `the consequence held, but the reloaded page could not answer whether it still does (${durable.inconclusive})`,
81
+ },
82
+ };
83
+ }
84
+ if (durable.held === undefined) {
85
+ return {
86
+ durable,
87
+ decision: {
88
+ verified: Verified.UNKNOWN,
89
+ verifiedReason: VerifiedReason.OBSERVATION_LOST,
90
+ because: `the consequence held, but the page did not come back after the reload to be asked again${durable.observed === undefined ? '' : ` (${durable.observed})`}`,
91
+ },
92
+ };
93
+ }
94
+ return {
95
+ durable,
96
+ decision: {
97
+ verified: Verified.NO,
98
+ verifiedReason: VerifiedReason.ASSERTION_FAILED,
99
+ because: `the consequence held, then was gone after a reload — it did not persist${durable.observed === undefined ? '' : ` (observed ${durable.observed})`}`,
100
+ },
101
+ };
102
+ }
103
+ /** Reload the page, wait for it to re-announce itself, and ask `part` of the fresh document. */
104
+ export async function reloadAndRecheck(deps, session, part, timeoutMs) {
105
+ // Same floor navigate { reload } sets: nothing from the old document may answer for the new one.
106
+ session.lastAct.markNavigated(session.elapsed());
107
+ const sent = await session.command(ReticleCommand.REFRESH, { hard: false });
108
+ if (!sent.ok)
109
+ return { observed: sent.error ?? 'the reload was refused' };
110
+ const back = await waitForReconnect({
111
+ current: () => deps.sessions.get(session.id),
112
+ previous: session,
113
+ timeoutMs: Math.max(timeoutMs, RELOAD_RECONNECT_TIMEOUT_MS),
114
+ now: deps.now,
115
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
116
+ });
117
+ const fresh = back ? deps.sessions.get(session.id) : undefined;
118
+ if (fresh === undefined)
119
+ return {};
120
+ return durabilityOf(await waitForPredicate(fresh, part, timeoutMs, 0));
121
+ }
122
+ /**
123
+ * Whether the page-state part of a consequence that held has since gone — an optimistic UI that
124
+ * rolled back. Returns the sentence to fail with, or undefined when it still holds.
125
+ *
126
+ * The wait resolves the instant a predicate holds, and the in-flight wait after it only follows a
127
+ * request to its response. A row that appeared and was removed a tick later, with no failing
128
+ * request to contradict it, was a yes. Only element/text/state clauses are re-read: a request or a
129
+ * signal does not un-happen, and re-reading `settled` on a page that polls would accuse it falsely.
130
+ */
131
+ export async function revertedAfterMatch(session, until, since, baselines) {
132
+ const part = durablePart(until);
133
+ if (part === undefined)
134
+ return undefined;
135
+ const now = await evaluatePredicate(session, part, since, true, baselines);
136
+ if (true === now.pass || now.inconclusive !== undefined)
137
+ return undefined;
138
+ return `held, then reverted before the action finished${now.observed === undefined ? '' : ` — now ${now.observed}`}`;
139
+ }
@@ -17,6 +17,7 @@
17
17
  * would make the daemon wait. `assert` / `wait_for` / `act_and_wait` all spend the caller's
18
18
  * `timeout_ms` (resolve-within.ts); this is the same budget applied to the same wait.
19
19
  */
20
+ import type { Session } from '../../../../portal/session/session.js';
20
21
  import type { SessionManager } from '../../../../portal/session/session-manager.js';
21
22
  import type { NavigateArrival } from './navigate-result.js';
22
23
  /**
@@ -45,14 +46,20 @@ export declare function absoluteTarget(target: string, base: string): string;
45
46
  */
46
47
  export interface ArrivalScope {
47
48
  /**
48
- * The session the navigation was dispatched to.
49
+ * The actual Session object that received the navigation command.
49
50
  *
50
- * Exempt from the exclusion below, because it is legitimately at the target both before and after
51
- * a navigation that keeps the document — a reload, or a same-page route change. Excluding it would
52
- * report `confirmed: false` for the navigations most likely to have worked.
51
+ * A full-document navigation can destroy this session and reconnect the new document
52
+ * with the same id or a different id, so successor detection needs the original object.
53
53
  */
54
- navigatedId: string;
55
- /** Sessions already on the target BEFORE dispatch. They cannot be evidence of this arrival. */
54
+ navigatedSession: Session;
55
+ /**
56
+ * The URL the session was on before navigation started.
57
+ *
58
+ * This lets us distinguish "the navigation is still in progress" from
59
+ * "the app redirected the newly arrived document somewhere else."
60
+ */
61
+ navigatedFrom: string;
62
+ /** Sessions already on the target before dispatch cannot prove this navigation arrived. */
56
63
  priorIds: ReadonlySet<string>;
57
64
  }
58
65
  /**
@@ -17,6 +17,7 @@
17
17
  * would make the daemon wait. `assert` / `wait_for` / `act_and_wait` all spend the caller's
18
18
  * `timeout_ms` (resolve-within.ts); this is the same budget applied to the same wait.
19
19
  */
20
+ import { awaitDocumentSuccessor } from '../../../../portal/session/session-successor.js';
20
21
  /**
21
22
  * The default when the caller gives no `timeout_ms`: long enough for a dev server to serve a page
22
23
  * and the SDK to dial back; short enough to not hang. A caller who knows the app is slower says so.
@@ -69,19 +70,27 @@ export function idsAtTarget(sessions, target) {
69
70
  }
70
71
  return ids;
71
72
  }
72
- function findArrival(sessions, target, scope) {
73
+ function findArrival(sessions, target, scope, currentSession) {
73
74
  let arrived = null;
74
75
  for (const s of sessions.all()) {
75
- if (!samePage(s.url, target))
76
+ // The currently tracked document gets priority because it is the strongest
77
+ // evidence of where this navigation went.
78
+ if (s.id === currentSession.id) {
79
+ if (samePage(s.url, target)) {
80
+ return { sessionId: s.id };
81
+ }
82
+ // Intermediate routes are not necessarily redirects. Keep polling so
83
+ // the document has a chance to reach the requested target.
76
84
  continue;
77
- // The tab we drove is the answer whenever it is there, not a tie broken by scan order.
78
- if (s.id === scope.navigatedId)
79
- return { sessionId: s.id };
85
+ }
86
+ // A different session that was already at the target cannot prove this
87
+ // navigation arrived.
80
88
  if (scope.priorIds.has(s.id))
81
89
  continue;
82
- // Keep looking rather than returning: the driven session may still be ahead of us.
83
- if (null === arrived)
84
- arrived = { sessionId: s.id };
90
+ if (samePage(s.url, target)) {
91
+ if (null === arrived)
92
+ arrived = { sessionId: s.id };
93
+ }
85
94
  }
86
95
  return arrived;
87
96
  }
@@ -96,11 +105,39 @@ const REAL_CLOCK = {
96
105
  export async function awaitArrival(sessions, target, scope, timeoutMs = ARRIVAL_TIMEOUT_MS, clock = REAL_CLOCK) {
97
106
  const deadline = clock.now() + timeoutMs;
98
107
  for (;;) {
99
- const found = findArrival(sessions, target, scope);
108
+ // Keep checking the live session registry for the entire caller-provided
109
+ // budget. This is important because a document can disconnect and reconnect
110
+ // under another id, and the new session may be at the requested target,
111
+ // including on a different origin.
112
+ const found = findArrival(sessions, target, scope, scope.navigatedSession);
100
113
  if (found !== null)
101
114
  return found;
102
115
  if (clock.now() >= deadline)
103
- return null;
116
+ break;
104
117
  await clock.sleep(POLL_MS);
105
118
  }
119
+ // Only after the navigation wait has expired do we use successor detection.
120
+ // At this point it is no longer being used to decide whether the requested
121
+ // navigation arrived. It only helps explain where the replaced document
122
+ // landed when the target was never observed.
123
+ const current = sessions.get(scope.navigatedSession.id);
124
+ if (current !== undefined) {
125
+ if (!samePage(current.url, scope.navigatedFrom) && !samePage(current.url, target)) {
126
+ return {
127
+ sessionId: current.id,
128
+ landedOn: current.url,
129
+ };
130
+ }
131
+ return null;
132
+ }
133
+ const successor = await awaitDocumentSuccessor(sessions, scope.navigatedSession, 0, clock);
134
+ if (successor !== null &&
135
+ !samePage(successor.url, target) &&
136
+ !samePage(successor.url, scope.navigatedFrom)) {
137
+ return {
138
+ sessionId: successor.id,
139
+ landedOn: successor.url,
140
+ };
141
+ }
142
+ return null;
106
143
  }
@@ -1,18 +1,7 @@
1
- /**
2
- * The honest envelope for a navigation nobody can confirm.
3
- *
4
- * `window.location.assign(url)` returns before the page moves, and the SDK that would report on the
5
- * new document is destroyed by the navigation itself. So a successful NAVIGATE means the browser
6
- * accepted the instruction — not that anything arrived. Driven against a dead URL it returned
7
- * `{"ok":true}` while the session died, which is the worst combination available: the agent believes
8
- * it navigated, is looking at nothing, and has lost Reticle.
9
- *
10
- * `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
11
- * This gives navigate the same honesty without changing what it does.
12
- */
13
1
  /** The session the SDK reconnected as after the page moved, when one was observed in the window. */
14
2
  export interface NavigateArrival {
15
3
  sessionId: string;
4
+ landedOn?: string;
16
5
  }
17
6
  export declare function navigateResult(result: {
18
7
  ok?: unknown;
@@ -1,3 +1,16 @@
1
+ /**
2
+ * The honest envelope for a navigation nobody can confirm.
3
+ *
4
+ * `window.location.assign(url)` returns before the page moves, and the SDK that would report on the
5
+ * new document is destroyed by the navigation itself. So a successful NAVIGATE means the browser
6
+ * accepted the instruction — not that anything arrived. Driven against a dead URL it returned
7
+ * `{"ok":true}` while the session died, which is the worst combination available: the agent believes
8
+ * it navigated, is looking at nothing, and has lost Reticle.
9
+ *
10
+ * `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
11
+ * This gives navigate the same honesty without changing what it does.
12
+ */
13
+ import { NAVIGATION_REDIRECT_NOTE } from '@reticlehq/core';
1
14
  export function navigateResult(result,
2
15
  /**
3
16
  * Arrival, if the daemon saw the SDK come back. `null` keeps the original honest envelope.
@@ -26,12 +39,17 @@ waitedMs) {
26
39
  if (!ok)
27
40
  return base;
28
41
  if (arrival !== null) {
42
+ const redirected = 'string' === typeof arrival.landedOn;
29
43
  return {
30
44
  ...base,
31
- confirmed: true,
32
- // The SDK that reconnects after a navigation is a NEW session with a new id. Returning it here
33
- // is the difference between a confirmation and a confirmation the agent can act on.
45
+ confirmed: !redirected,
34
46
  sessionId: arrival.sessionId,
47
+ ...(redirected
48
+ ? {
49
+ landedOn: arrival.landedOn,
50
+ note: `${NAVIGATION_REDIRECT_NOTE} Landed on: ${arrival.landedOn}`,
51
+ }
52
+ : {}),
35
53
  };
36
54
  }
37
55
  return {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Remember the control an act_and_wait PROVED, for the coverage ledger's `proved` level.
3
+ *
4
+ * Structural rather than `Session`, and optional, so a test stub that never proves anything need not
5
+ * implement it — the same reason `beginAction` is optional on the crawl session.
6
+ */
7
+ export declare function noteProved(session: {
8
+ recordProvedFrom?: (actPayload: unknown) => void;
9
+ }, verified: string, actPayload: unknown): void;
@@ -0,0 +1,11 @@
1
+ import { Verified } from '@reticlehq/core';
2
+ /**
3
+ * Remember the control an act_and_wait PROVED, for the coverage ledger's `proved` level.
4
+ *
5
+ * Structural rather than `Session`, and optional, so a test stub that never proves anything need not
6
+ * implement it — the same reason `beginAction` is optional on the crawl session.
7
+ */
8
+ export function noteProved(session, verified, actPayload) {
9
+ if (Verified.YES === verified)
10
+ session.recordProvedFrom?.(actPayload);
11
+ }
@@ -35,6 +35,11 @@ export interface StepExpectation {
35
35
  /** What was seen instead, on a miss — the half of the answer a repair actually needs. */
36
36
  observed?: string;
37
37
  expected?: string;
38
+ /**
39
+ * Contradiction kinds positively observed in this step's window — deciding ones only, the same
40
+ * filter act_and_wait applies. A held step with one is a UI that advanced over a failed write.
41
+ */
42
+ contradicted?: readonly string[];
38
43
  }
39
44
  export interface SequenceGrade {
40
45
  verified: Verified;