@reticlehq/server 3.9.0 → 3.10.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 (350) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +6 -0
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/license.mdx +9 -7
  322. package/docs/cli/restart.mdx +7 -5
  323. package/docs/cli/rollback.mdx +5 -5
  324. package/docs/cli/stop.mdx +3 -3
  325. package/docs/cli/update.mdx +2 -2
  326. package/docs/cli/verify.mdx +23 -1
  327. package/docs/cli.mdx +33 -23
  328. package/docs/deploy-checks.md +1 -1
  329. package/docs/desktop.mdx +1 -1
  330. package/docs/enterprise.md +2 -2
  331. package/docs/environment.mdx +1 -1
  332. package/docs/flows.md +11 -1
  333. package/docs/frameworks.mdx +9 -9
  334. package/docs/getting-started.md +1 -1
  335. package/docs/install-manual.mdx +1 -1
  336. package/docs/license-activation.mdx +3 -3
  337. package/docs/packages/tauri.mdx +1 -1
  338. package/docs/packages.mdx +1 -1
  339. package/docs/predicates.mdx +1 -1
  340. package/docs/quickstart.mdx +68 -49
  341. package/docs/skill-setup.md +1 -1
  342. package/docs/skill.md +14 -13
  343. package/docs/telemetry.md +2 -2
  344. package/docs/tools/navigate.mdx +1 -1
  345. package/docs/tools/verify.mdx +4 -1
  346. package/docs/troubleshooting.mdx +1 -1
  347. package/docs/usage.md +17 -14
  348. package/package.json +13 -13
  349. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  350. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -11,8 +11,8 @@
11
11
  */
12
12
  import * as production from '../../portal/production/production-activation.js';
13
13
  import { z } from 'zod';
14
- import { leaseNotConnectedHint, leaseReaderFailedHint, leaseStillLoadingHint, } from './lease-hint.js';
15
- import { LEASE_READY_TIMEOUT_MS, awaitLeaseAnswering, connectOrInject, connectWaitUntil, moveReusedLease, notReadyReason, probeLeaseAlive, } from './lease-readiness.js';
14
+ import { keptLeaseAnswer, notLoadedCause, leaseNotConnectedHint, leaseReaderFailedHint, leaseStillLoadingHint, } from './lease-hint.js';
15
+ import { LEASE_READY_TIMEOUT_MS, awaitLeaseAnswering, connectOrInject, connectWaitUntil, moveReusedLease, notReadyReason, probeLeaseAlive, reconnectHeldLease, HeldLeaseReload, } from './lease-readiness.js';
16
16
  import { probeSdkMarker } from './gaps/sdk-marker-probe.js';
17
17
  import { observeWebDocument } from '../../portal/session/dev-server/served-document.js';
18
18
  import { diagnoseObservedWebCsp } from '@reticlehq/init';
@@ -21,7 +21,7 @@ import { hasAnyAppConnectedBefore } from '../../memory/recall/prior/connection-m
21
21
  import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../../portal/session/lease-visibility.js';
22
22
  import { reticleStateHome } from '../../command/daemon/daemon.js';
23
23
  import { claimArtifactRoot, dropArtifactRootClaim } from '../../memory/project/root-claims.js';
24
- import { LeaseNotReadyReason, RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT, SeedStorageSchema, originOf, } from '@reticlehq/core';
24
+ import { LeaseNotReadyReason, RETICLE_DEFAULT_PORT, SeedStorageSchema, originOf, } from '@reticlehq/core';
25
25
  import { HudVisibility, ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
26
26
  import { HOST_STUB_NOTE } from './tool-kit.js';
27
27
  import { PoolAtCapacityError } from '../../portal/pool/browser-pool.js';
@@ -30,6 +30,8 @@ import { timeoutMsSchema } from './args/numeric-bounds.js';
30
30
  import { chromiumPreflightRefusal } from '../../command/cli/doctor/browser/chromium-hint.js';
31
31
  import { LEASE_PERMISSIONS_ARG, hintOf, notificationReadBack, parseLeasePermissions, permissionRefusal, refuseRegrant, } from './lease-permissions.js';
32
32
  import { LEASE_EMULATE_ARG, leaseEmulationOf } from '../../portal/input/viewport-tools.js';
33
+ import { LEASE_DETERMINISTIC_ARG, leaseDeterminismOf } from '../../portal/pool/determinism.js';
34
+ import { LEASE_COVERAGE_TOOL, LEASE_JS_COVERAGE_ARG } from './lease-coverage-tools.js';
33
35
  export { sessionMutationPort, sessionPerturbationPort, suiteFixturePort, suiteFixtureSeed, } from './session-ports.js';
34
36
  /**
35
37
  * Everything the daemon already knows about why a leased tab might not have dialled in.
@@ -87,38 +89,17 @@ async function leaseEvidence(deps, port, url) {
87
89
  }
88
90
  const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
89
91
  const ALREADY_HELD_LEASE = 'you already hold a lease on this origin';
92
+ const DETERMINISTIC_NOTE = 'Time stands still on this tab. Step it with reticle_clock { stepFrames: N }, then read or capture.';
90
93
  function alreadyHeldHint(sessionId, origin) {
91
94
  return (`${ALREADY_HELD_LEASE} (${origin}): ${sessionId} — reused it rather than opening a second ` +
92
95
  'tab, which would make every later call require sessionId. It is still signed in as whoever ' +
93
96
  'used it last (cookies and storage come with it): release it first for a fresh context or ' +
94
97
  'another identity, or pass fresh: true to open a separate tab and keep this one.');
95
98
  }
96
- /**
97
- * Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
98
- * adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
99
- */
100
- export function appendReticleParams(url, session, projectId, hud) {
101
- try {
102
- const u = new URL(url);
103
- u.searchParams.set(RETICLE_URL_PARAM.SESSION, session);
104
- if (projectId !== undefined && projectId.length > 0) {
105
- u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
106
- }
107
- if (hud !== undefined) {
108
- u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
109
- }
110
- return u.toString();
111
- }
112
- catch {
113
- const sep = url.includes('?') ? '&' : '?';
114
- const proj = projectId !== undefined && projectId.length > 0
115
- ? `&${RETICLE_URL_PARAM.PROJECT}=${encodeURIComponent(projectId)}`
116
- : '';
117
- return `${url}${sep}${RETICLE_URL_PARAM.SESSION}=${encodeURIComponent(session)}${proj}`;
118
- }
119
- }
99
+ export { appendReticleParams, carryReticleIdentity, resolveLeasedSessionId, } from './lease-url-identity.js';
100
+ import { appendReticleParams, carryReticleIdentity, resolveLeasedSessionId, } from './lease-url-identity.js';
120
101
  export { cleanNavError, evaluateSeedPrecondition, scrubSeedFromError } from './lease-seed.js';
121
- import { evaluateSeedPrecondition, looksLikeStorageStateExport, navFailureMessage, scrubSeedFromError, } from './lease-seed.js';
102
+ import { evaluateSeedPrecondition, looksLikeStorageStateExport, navFailureMessage, scrubSeedFromError, serverAcceptedAfterTimeout, } from './lease-seed.js';
122
103
  /** A fresh, collision-resistant lease id. Uses crypto at the I/O boundary (not pure logic). */
123
104
  function newLeaseId() {
124
105
  const uuid = 'function' === typeof globalThis.crypto?.randomUUID
@@ -126,84 +107,19 @@ function newLeaseId() {
126
107
  : `${String(Date.now())}-${String(performance.now())}`;
127
108
  return `lease-${uuid}`;
128
109
  }
129
- /**
130
- * The session a lease's tab actually registered as, or undefined while nothing has.
131
- *
132
- * Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
133
- * SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
134
- * which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
135
- * known id. Matching on the lease id alone could never see those tabs, so the lease reported
136
- * `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
137
- *
138
- * The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
139
- * carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
140
- * so `lease-abc` cannot claim `lease-abcdef`.
141
- */
142
- export function resolveLeasedSessionId(sessions, leaseId) {
143
- if (sessions.get(leaseId) !== undefined) {
144
- return leaseId;
145
- }
146
- return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
147
- }
148
- /**
149
- * Carry a claimed identity across a navigation.
150
- *
151
- * A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
152
- * context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
153
- * reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
154
- * first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
155
- * the lease was orphaned: "no sessions are connected at all", with a browser window still open on
156
- * screen. A leased browser that cannot survive being navigated is not a usable one.
157
- *
158
- * A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
159
- * a human's own URL.
160
- */
161
- export function carryReticleIdentity(fromUrl, toUrl) {
162
- const session = sessionParamOf(fromUrl);
163
- if (session === undefined) {
164
- return toUrl;
165
- }
166
- // Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
167
- if (sessionParamOf(toUrl) !== undefined) {
168
- return toUrl;
169
- }
170
- const projectId = projectParamOf(fromUrl);
171
- return appendReticleParams(toUrl, session, projectId);
172
- }
173
- function projectParamOf(url) {
174
- if (url === undefined) {
175
- return undefined;
176
- }
177
- try {
178
- return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
179
- }
180
- catch {
181
- return undefined;
182
- }
183
- }
184
- function sessionParamOf(url) {
185
- if (url === undefined) {
186
- return undefined;
187
- }
188
- try {
189
- return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
190
- }
191
- catch {
192
- return undefined;
193
- }
194
- }
195
110
  /**
196
111
  * Acquire a leased, SDK-connected context for a caller that is not the lease TOOL (the parallel suite).
197
112
  * Returns the session id plus its release; the caller must release in a finally so a crashed flow never
198
113
  * leaks a pool slot and starves the rest of the suite.
199
114
  */
200
- export async function acquireLeasedSession(pool, sessions, url, projectId, seedStorage) {
115
+ export async function acquireLeasedSession(pool, sessions, url, projectId, seedStorage, ignoreHTTPSErrors) {
201
116
  const sessionId = newLeaseId();
202
117
  let lease;
203
118
  try {
204
119
  lease = await pool.acquire(appendReticleParams(url, sessionId, projectId), {
205
120
  sessionId,
206
121
  ...(seedStorage !== undefined ? { seedStorage } : {}),
122
+ ...(true === ignoreHTTPSErrors ? { ignoreHTTPSErrors: true } : {}),
207
123
  });
208
124
  }
209
125
  catch (err) {
@@ -249,7 +165,7 @@ export function hasOriginLock(origin) {
249
165
  }
250
166
  export const LEASE_ACQUIRE_TOOL = {
251
167
  name: ReticleTool.LEASE_ACQUIRE,
252
- description: 'Lease a fresh isolated browser context from the shared pool (shown or hidden as the daemon was started; `headed: true` forces a window) and navigate it to the app URL (the app must already be running; one with no Reticle SDK gets one supplied, reported as zeroInstall). If this origin is already leased and still connected, this returns THAT session rather than minting a second tab — a second acquire on the same origin poisons default session resolution. Returns the sessionId the leased tab registers — pass it to other tools. The pool keeps all leases in ONE browser and caps concurrency; if every slot is taken this is refused at once, naming the cap, rather than queued. Release with reticle_lease{action:"release"} when the flow is done. PREFER AN ALREADY-OPEN TAB: if reticle_sessions lists a non-leased session for this app, drive THAT instead — a lease is invisible to the person watching the app, whose HUD lives in their own tab, and a tab flagged hidden/throttled is often still driveable. Lease for isolation you actually need (a second identity, a clean context, parallel flows) or when driving the open tab has failed — this call answers with `preferExisting` when a live tab was available.',
168
+ description: 'Lease a fresh isolated browser context from the shared pool (shown or hidden as the daemon was started; `headed: true` forces a window) and navigate it to the app URL (the app must already be running; one with no Reticle SDK gets one supplied, reported as zeroInstall). If this origin is already leased, this returns THAT session rather than minting a second tab (a tab that has not connected is loaded again in its own context, so its cookies and sign-in are kept) — a second acquire on the same origin poisons default session resolution. Returns the sessionId the leased tab registers — pass it to other tools. The pool keeps all leases in ONE browser and caps concurrency; if every slot is taken this is refused at once, naming the cap, rather than queued. Release with reticle_lease{action:"release"} when the flow is done. PREFER AN ALREADY-OPEN TAB: if reticle_sessions lists a non-leased session for this app, drive THAT instead — a lease is invisible to the person watching the app, whose HUD lives in their own tab, and a tab flagged hidden/throttled is often still driveable. Lease for isolation you actually need (a second identity, a clean context, parallel flows) or when driving the open tab has failed — this call answers with `preferExisting` when a live tab was available.',
253
169
  inputSchema: {
254
170
  url: z
255
171
  .string()
@@ -282,6 +198,8 @@ export const LEASE_ACQUIRE_TOOL = {
282
198
  .describe('How the HUD starts on the page: shown (default), hidden, or removed.'),
283
199
  permissions: LEASE_PERMISSIONS_ARG,
284
200
  emulate: LEASE_EMULATE_ARG,
201
+ deterministic: LEASE_DETERMINISTIC_ARG,
202
+ jsCoverage: LEASE_JS_COVERAGE_ARG,
285
203
  production: production.PRODUCTION_LEASE_ARG,
286
204
  timeout_ms: timeoutMsSchema
287
205
  .optional()
@@ -289,7 +207,7 @@ export const LEASE_ACQUIRE_TOOL = {
289
207
  root: z
290
208
  .string()
291
209
  .optional()
292
- .describe("Absolute path of the .reticle folder this lease's runs belong in — the caller's project. The project folder itself is accepted when it contains a .reticle. Without it, the page's project id decides, and a page with none lands in ~/.reticle/unmatched, which nothing syncs."),
210
+ .describe("Absolute path of the .reticle folder this lease's runs belong in — the caller's project. An existing project folder is accepted even when it has no .reticle yet (the first run writes it). Omitted, the page's project id decides, and a page with none lands in ~/.reticle/unmatched, which nothing syncs."),
293
211
  },
294
212
  outputSchema: {
295
213
  sessionId: z.string(),
@@ -301,6 +219,10 @@ export const LEASE_ACQUIRE_TOOL = {
301
219
  .boolean()
302
220
  .optional()
303
221
  .describe('Present when this app ships no Reticle SDK and the lease supplied one. Verdicts work on the DOM, network, console and routes; there is no framework adapter, so no component state and no source file:line. Install with `npx @reticlehq/server init` for those.'),
222
+ deterministic: z
223
+ .string()
224
+ .optional()
225
+ .describe('Present when this tab runs a seeded random source and a stepped clock. Its time moves only on reticle_clock { stepFrames }, so a wait for a timer or an animation never ends until you step.'),
304
226
  hostStub: z
305
227
  .string()
306
228
  .optional()
@@ -371,7 +293,11 @@ export const LEASE_ACQUIRE_TOOL = {
371
293
  const headed = true === args['headed'];
372
294
  const hud = Object.values(HudVisibility).find((v) => v === args['hud']);
373
295
  const permissions = production.leasePermissions(parseLeasePermissions(args['permissions']), args['production']);
374
- const emulate = leaseEmulationOf(args['emulate']);
296
+ // Determinism is set on the context, like the emulation, so it travels with it.
297
+ const deterministic = leaseDeterminismOf(args['deterministic']);
298
+ const emulated = leaseEmulationOf(args['emulate']);
299
+ const emulate = deterministic === undefined ? emulated : { ...(emulated ?? {}), deterministic };
300
+ const jsCoverage = true === args['jsCoverage'];
375
301
  // The stub of the shell bindings, when the caller supplied one. Installed by the pool before
376
302
  // the first navigation, so it is already defined when the app's first module runs.
377
303
  const initScript = asString(args['initScript']);
@@ -443,6 +369,7 @@ export const LEASE_ACQUIRE_TOOL = {
443
369
  if ((validatedSeed !== undefined ||
444
370
  certMismatch ||
445
371
  emulate !== undefined ||
372
+ jsCoverage ||
446
373
  activated !== undefined) &&
447
374
  existing !== undefined) {
448
375
  // Seeding or a different certificate setting asked for: release its own context so the new
@@ -457,6 +384,9 @@ export const LEASE_ACQUIRE_TOOL = {
457
384
  // context — the tab-poisoning this branch exists to prevent, reached through the branch
458
385
  // itself. It bites when the first acquire returned `ready: false`, since the mint path only
459
386
  // aliases once its wait has resolved, so nothing recorded the app's own name (#692).
387
+ // A tab that never registered is loaded again in its OWN context, and kept while it is open:
388
+ // releasing it and minting threw away the cookies, so a signed-in tab came back signed out (#1924).
389
+ const reload = await reconnectHeldLease((ms) => pool.gotoLease(existing, carryReticleIdentity(pool.leaseUrl(existing), url), ms), async () => (await pool.viewportOfLease(existing)) !== undefined, () => pool.leaseLoading(existing), () => resolveLeasedSessionId(deps.sessions, existing) !== undefined, deps.now() + (asNumber(args['timeout_ms']) ?? LEASE_READY_TIMEOUT_MS), deps.now);
460
390
  const resolved = resolveLeasedSessionId(deps.sessions, existing);
461
391
  if (resolved !== undefined) {
462
392
  // The other name this lease answers to, told to the pool exactly as the mint path tells it.
@@ -496,9 +426,14 @@ export const LEASE_ACQUIRE_TOOL = {
496
426
  : { preferExisting: { sessionId: alreadyOpen, note: PREFER_EXISTING_NOTE } }),
497
427
  };
498
428
  }
499
- // The lease is still held but the tab has gone — a reload dropped the session, and no session
500
- // anywhere is driving it. Free the slot and mint, rather than handing back a dead id or
501
- // leaving both contexts occupied.
429
+ if (reload === HeldLeaseReload.RELOADED || reload === HeldLeaseReload.NOT_LOADED) {
430
+ const cause = reload === HeldLeaseReload.RELOADED
431
+ ? await notConnectedHint(deps, url, pool.dialFailureUrl?.(existing))
432
+ : await notLoadedCause(url, deps.portAccepts);
433
+ return keptLeaseAnswer({ sessionId: existing, origin, url, reload }, pool, cause);
434
+ }
435
+ // The tab is gone: its page or its context was closed. Free the slot and mint, rather than
436
+ // handing back a dead id or leaving both contexts occupied.
502
437
  await pool.release(existing);
503
438
  }
504
439
  const sessionId = newLeaseId();
@@ -521,6 +456,7 @@ export const LEASE_ACQUIRE_TOOL = {
521
456
  ...(permissions !== undefined ? { permissions } : {}),
522
457
  ...(true === args['ignoreHTTPSErrors'] ? { ignoreHTTPSErrors: true } : {}),
523
458
  ...(emulate === undefined ? {} : { emulate }),
459
+ ...(jsCoverage ? { jsCoverage } : {}),
524
460
  });
525
461
  }
526
462
  catch (err) {
@@ -535,8 +471,10 @@ export const LEASE_ACQUIRE_TOOL = {
535
471
  throw new Error(scrubSeedFromError(err.message, validatedSeed));
536
472
  }
537
473
  // A raw page.goto failure is noisy and leaks the internal URL params — surface a clean,
538
- // actionable message instead, one that tells a slow first compile from a missing app.
539
- throw new Error(navFailureMessage(url, err, validatedSeed));
474
+ // actionable message instead, one that tells a slow first compile from a missing app. A
475
+ // timeout on a port that accepted the connection is a server still compiling (#2005).
476
+ const accepted = await serverAcceptedAfterTimeout(url, err, validatedSeed, deps.portAccepts);
477
+ throw new Error(navFailureMessage(url, err, validatedSeed, accepted));
540
478
  }
541
479
  // Wait for the leased tab's SDK to connect so the returned sessionId is usable right away.
542
480
  // Resolved rather than assumed: an app that names its own session registers under that name,
@@ -618,6 +556,7 @@ export const LEASE_ACQUIRE_TOOL = {
618
556
  ...(versionSkew === undefined ? {} : { versionSkew }),
619
557
  ...production.summary(activated),
620
558
  ...(initScript === undefined ? {} : { hostStub: HOST_STUB_NOTE }),
559
+ ...(deterministic === undefined ? {} : { deterministic: DETERMINISTIC_NOTE }),
621
560
  ...cspWidenedOf(pool, lease.sessionId),
622
561
  ...hintOf(notReadyHint, ready ? await notificationReadBack(pool, lease.sessionId, permissions) : undefined),
623
562
  };
@@ -646,11 +585,12 @@ async function notConnectedHint(deps, url, dialledUrl) {
646
585
  }
647
586
  const LEASE_RELEASE_TOOL = {
648
587
  name: ReticleTool.LEASE_RELEASE,
649
- description: 'Release a leased browser context by sessionId, closing it and freeing the pool slot for a queued acquire. Call this when a flow finishes so the pool stays within its concurrency cap.',
588
+ description: 'Release a leased browser context by sessionId, closing it and freeing the pool slot for a queued acquire. Call this when a flow finishes so the pool stays within its concurrency cap. With no sessionId, it releases the one lease this agent holds.',
650
589
  inputSchema: {
651
590
  sessionId: z
652
591
  .string()
653
- .describe('The leased sessionId returned by reticle_lease{action:"acquire"}.'),
592
+ .optional()
593
+ .describe('The leased sessionId returned by reticle_lease{action:"acquire"}. Omit it when this agent holds one lease.'),
654
594
  },
655
595
  outputSchema: {
656
596
  released: z.boolean(),
@@ -661,10 +601,7 @@ const LEASE_RELEASE_TOOL = {
661
601
  if (pool === undefined) {
662
602
  throw new Error(POOL_UNAVAILABLE);
663
603
  }
664
- const sessionId = asString(args['sessionId']);
665
- if (sessionId === undefined || 0 === sessionId.length) {
666
- throw new Error('reticle_lease{action:"release"} requires a sessionId');
667
- }
604
+ const sessionId = asString(args['sessionId']) ?? ownOnlyLease(deps, pool);
668
605
  // An id that holds no lease (a typo, or one that expired) answered `released: true`.
669
606
  if (pool.leaseUrl(sessionId) === undefined) {
670
607
  const held = pool.leasedSessionIds();
@@ -674,6 +611,21 @@ const LEASE_RELEASE_TOOL = {
674
611
  return { released: true, leased: await releaseLease(deps, pool, sessionId) };
675
612
  },
676
613
  };
614
+ /**
615
+ * The one lease this caller holds, for a release with no sessionId. Another agent's lease is never
616
+ * this caller's to release (#1226), so none or several is a refusal that names what is held.
617
+ */
618
+ function ownOnlyLease(deps, pool) {
619
+ const own = pool.leasedSessionIds().filter((id) => pool.heldBy(id, deps.attachId));
620
+ const [only] = own;
621
+ if (only !== undefined && 1 === own.length) {
622
+ return only;
623
+ }
624
+ const held = 0 === own.length
625
+ ? 'this agent holds no lease'
626
+ : `this agent holds ${String(own.length)} leases: ${own.join(', ')}`;
627
+ throw new Error(`reticle_lease{action:"release"} needs a sessionId: ${held}.`);
628
+ }
677
629
  /**
678
630
  * Release one lease and everything that hangs off it. Returns how many leases are still held.
679
631
  *
@@ -764,4 +716,4 @@ function tellWatchers(deps, projectId, text) {
764
716
  // A HUD that missed one line is a smaller problem than a lease reported as broken.
765
717
  }
766
718
  }
767
- export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL];
719
+ export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL, LEASE_COVERAGE_TOOL];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Reticle's identity params on a lease URL: stamp them, read them back, carry them across a
3
+ * navigation, and match a registered session to the lease that stamped it.
4
+ *
5
+ * Split out of lease-tools.ts at the 1000-line cap. Pure: no I/O.
6
+ */
7
+ import { type HudVisibility } from '@reticlehq/core';
8
+ /**
9
+ * Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
10
+ * adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
11
+ */
12
+ export declare function appendReticleParams(url: string, session: string, projectId?: string, hud?: HudVisibility): string;
13
+ /**
14
+ * The session a lease's tab actually registered as, or undefined while nothing has.
15
+ *
16
+ * Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
17
+ * SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
18
+ * which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
19
+ * known id. Matching on the lease id alone could never see those tabs, so the lease reported
20
+ * `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
21
+ *
22
+ * The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
23
+ * carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
24
+ * so `lease-abc` cannot claim `lease-abcdef`.
25
+ */
26
+ export declare function resolveLeasedSessionId(sessions: {
27
+ get: (id: string) => unknown;
28
+ all: () => {
29
+ id: string;
30
+ url?: string;
31
+ }[];
32
+ }, leaseId: string): string | undefined;
33
+ /**
34
+ * Carry a claimed identity across a navigation.
35
+ *
36
+ * A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
37
+ * context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
38
+ * reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
39
+ * first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
40
+ * the lease was orphaned: "no sessions are connected at all", with a browser window still open on
41
+ * screen. A leased browser that cannot survive being navigated is not a usable one.
42
+ *
43
+ * A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
44
+ * a human's own URL.
45
+ */
46
+ export declare function carryReticleIdentity(fromUrl: string | undefined, toUrl: string): string;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Reticle's identity params on a lease URL: stamp them, read them back, carry them across a
3
+ * navigation, and match a registered session to the lease that stamped it.
4
+ *
5
+ * Split out of lease-tools.ts at the 1000-line cap. Pure: no I/O.
6
+ */
7
+ import { RETICLE_URL_PARAM } from '@reticlehq/core';
8
+ /**
9
+ * Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
10
+ * adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
11
+ */
12
+ export function appendReticleParams(url, session, projectId, hud) {
13
+ try {
14
+ const u = new URL(url);
15
+ u.searchParams.set(RETICLE_URL_PARAM.SESSION, session);
16
+ if (projectId !== undefined && projectId.length > 0) {
17
+ u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
18
+ }
19
+ if (hud !== undefined) {
20
+ u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
21
+ }
22
+ return u.toString();
23
+ }
24
+ catch {
25
+ const sep = url.includes('?') ? '&' : '?';
26
+ const proj = projectId !== undefined && projectId.length > 0
27
+ ? `&${RETICLE_URL_PARAM.PROJECT}=${encodeURIComponent(projectId)}`
28
+ : '';
29
+ return `${url}${sep}${RETICLE_URL_PARAM.SESSION}=${encodeURIComponent(session)}${proj}`;
30
+ }
31
+ }
32
+ /**
33
+ * The session a lease's tab actually registered as, or undefined while nothing has.
34
+ *
35
+ * Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
36
+ * SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
37
+ * which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
38
+ * known id. Matching on the lease id alone could never see those tabs, so the lease reported
39
+ * `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
40
+ *
41
+ * The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
42
+ * carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
43
+ * so `lease-abc` cannot claim `lease-abcdef`.
44
+ */
45
+ export function resolveLeasedSessionId(sessions, leaseId) {
46
+ if (sessions.get(leaseId) !== undefined) {
47
+ return leaseId;
48
+ }
49
+ return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
50
+ }
51
+ /**
52
+ * Carry a claimed identity across a navigation.
53
+ *
54
+ * A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
55
+ * context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
56
+ * reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
57
+ * first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
58
+ * the lease was orphaned: "no sessions are connected at all", with a browser window still open on
59
+ * screen. A leased browser that cannot survive being navigated is not a usable one.
60
+ *
61
+ * A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
62
+ * a human's own URL.
63
+ */
64
+ export function carryReticleIdentity(fromUrl, toUrl) {
65
+ const session = sessionParamOf(fromUrl);
66
+ if (session === undefined) {
67
+ return toUrl;
68
+ }
69
+ // Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
70
+ if (sessionParamOf(toUrl) !== undefined) {
71
+ return toUrl;
72
+ }
73
+ const projectId = projectParamOf(fromUrl);
74
+ return appendReticleParams(toUrl, session, projectId);
75
+ }
76
+ function projectParamOf(url) {
77
+ if (url === undefined) {
78
+ return undefined;
79
+ }
80
+ try {
81
+ return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function sessionParamOf(url) {
88
+ if (url === undefined) {
89
+ return undefined;
90
+ }
91
+ try {
92
+ return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ }
@@ -36,14 +36,104 @@ function strayKeysRefusal(tool, action, stray, actions) {
36
36
  /** The union of member input shapes, every field optional so one schema serves every action. */
37
37
  function unionShape(actions) {
38
38
  const shape = {};
39
+ const contested = contestedKeys(actions);
39
40
  for (const tool of Object.values(actions)) {
40
41
  for (const [key, schema] of Object.entries(tool.inputSchema)) {
41
- // First definition wins; siblings share field names/meanings by construction.
42
- shape[key] ??= schema.isOptional() ? schema : schema.optional();
42
+ if (key in shape) {
43
+ continue;
44
+ }
45
+ // First definition wins, unless members accept different values for it: see contestedKeys.
46
+ if (contested.has(key)) {
47
+ shape[key] = eitherBound(key, actions);
48
+ }
49
+ else {
50
+ shape[key] = schema.isOptional() ? schema : schema.optional();
51
+ }
43
52
  }
44
53
  }
45
54
  return shape;
46
55
  }
56
+ /**
57
+ * What a field accepts, without its description: two members that word one field differently
58
+ * still accept the same values.
59
+ */
60
+ function boundOf(schema) {
61
+ try {
62
+ const json = { ...z.toJSONSchema(schema, { unrepresentable: 'any' }) };
63
+ delete json['description'];
64
+ return JSON.stringify(json);
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ /**
71
+ * The fields two members accept different values for (#2144).
72
+ *
73
+ * `reticle_assert { action: "now" }` caps `timeout_ms` at one request, and `action: "wait"` takes up
74
+ * to 600000 and answers `resume_ms` for the rest. With the first definition winning, the merged tool
75
+ * kept the cap of "now", and refused every wait longer than one request: `reticle verify --expect
76
+ * --timeout 120000` printed `verified: unknown`.
77
+ */
78
+ function contestedKeys(actions) {
79
+ const seen = new Map();
80
+ const contested = new Set();
81
+ for (const tool of Object.values(actions)) {
82
+ for (const [key, schema] of Object.entries(tool.inputSchema)) {
83
+ const bound = boundOf(schema);
84
+ if (!seen.has(key)) {
85
+ seen.set(key, bound);
86
+ }
87
+ else if (seen.get(key) !== bound) {
88
+ contested.add(key);
89
+ }
90
+ }
91
+ }
92
+ return contested;
93
+ }
94
+ /**
95
+ * A contested field accepts what any member accepts. The dispatch then checks the value against the
96
+ * chosen action's own schema (`contestedRefusal`), so no action gets a value it would refuse.
97
+ */
98
+ function eitherBound(key, actions) {
99
+ const owners = Object.entries(actions).filter(([, tool]) => key in tool.inputSchema);
100
+ const schemas = owners.flatMap(([, tool]) => {
101
+ const schema = tool.inputSchema[key];
102
+ return schema === undefined ? [] : [schema];
103
+ });
104
+ const described = owners
105
+ .map(([action, tool]) => {
106
+ const said = tool.inputSchema[key]?.description;
107
+ return said === undefined ? undefined : `For action "${action}": ${said}`;
108
+ })
109
+ .filter((said) => said !== undefined)
110
+ .join(' ');
111
+ const [first, second, ...rest] = schemas;
112
+ if (first === undefined || second === undefined) {
113
+ throw new Error(`mergeTools: contested key '${key}' has fewer than two schemas`);
114
+ }
115
+ const either = z.union([first, second, ...rest]).optional();
116
+ return 0 === described.length ? either : either.describe(described);
117
+ }
118
+ /** The refusal for a contested field whose value the chosen action does not accept. */
119
+ function contestedRefusal(tool, action, args, chosen, contested) {
120
+ const named = [];
121
+ for (const key of contested) {
122
+ const schema = chosen.inputSchema[key];
123
+ if (schema === undefined || args[key] === undefined) {
124
+ continue;
125
+ }
126
+ const checked = schema.safeParse(args[key]);
127
+ if (!checked.success) {
128
+ named.push(`${key} (${checked.error.issues.map((issue) => issue.message).join('; ')})`);
129
+ }
130
+ }
131
+ if (0 === named.length) {
132
+ return undefined;
133
+ }
134
+ return (`invalid ${1 === named.length ? 'parameter' : 'parameters'} for ${tool} { action: "${action}" }: ` +
135
+ `${named.join('; ')} — NOT applied, so any result would be an answer to a different question`);
136
+ }
47
137
  /**
48
138
  * A member that declares `action` itself cannot be merged, and the failure was SILENT.
49
139
  *
@@ -120,6 +210,7 @@ export function mergeTools(spec) {
120
210
  if (spec.defaultAction !== undefined && !(spec.defaultAction in spec.actions)) {
121
211
  throw new Error(`mergeTools(${spec.name}): defaultAction '${spec.defaultAction}' is not one of its actions`);
122
212
  }
213
+ const contested = contestedKeys(spec.actions);
123
214
  return {
124
215
  name: spec.name,
125
216
  description: spec.description,
@@ -148,6 +239,12 @@ export function mergeTools(spec) {
148
239
  if (0 < stray.length && action !== undefined) {
149
240
  return Promise.reject(new Error(strayKeysRefusal(spec.name, action, stray, spec.actions)));
150
241
  }
242
+ const outOfBounds = action === undefined
243
+ ? undefined
244
+ : contestedRefusal(spec.name, action, args, chosen, contested);
245
+ if (outOfBounds !== undefined) {
246
+ return Promise.reject(new Error(outOfBounds));
247
+ }
151
248
  return chosen.handler(deps, args, call);
152
249
  },
153
250
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Where a merged or retired tool's old name went.
3
3
  *
4
- * Twenty-two names exported from `ReticleTool` are no longer tools: `reticle_record_start` became
4
+ * Many names exported from `ReticleTool` are no longer tools: `reticle_record_start` became
5
5
  * `reticle_record { action: "start" }`, `reticle_diff` became `reticle_baseline { action: "diff" }`,
6
6
  * and three were retired outright. Confirmed live in a field sweep, they all answered `unknown tool`
7
7
  * — which is the wrong answer, because the capability exists and only moved. An agent trained on an
@@ -23,6 +23,10 @@ const RETIRED_NOTE = {
23
23
  tool: ReticleTool.PROJECT,
24
24
  note: 'run outcomes are persisted automatically by reticle_flow_replay; read them with reticle_project',
25
25
  },
26
+ [ReticleTool.EXPLORE]: {
27
+ tool: ReticleTool.LOOK,
28
+ note: 'list the controls with reticle_look { action: "page", mode: "interactive" } (reticle_snapshot { mode: "interactive" }), and read console errors with reticle_observe { action: "console" } (reticle_console)',
29
+ },
26
30
  [ReticleTool.WAIT_READY]: {
27
31
  tool: ReticleTool.SESSIONS,
28
32
  note: 'waiting is implicit — the first live call already blocks until the session is ready',