@reticlehq/server 3.8.2 → 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 (477) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/SKILL.md +13 -12
  5. package/bin/launch.js +13 -0
  6. package/bin/package.json +3 -0
  7. package/bin/reticle.js +66 -8
  8. package/dist/chromium-launch-options.d.ts +25 -0
  9. package/dist/chromium-launch-options.js +21 -1
  10. package/dist/command/cli/adhoc-suite.js +2 -0
  11. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  12. package/dist/command/cli/adhoc-verdict.js +50 -160
  13. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  14. package/dist/command/cli/answers/gate-exit.js +12 -2
  15. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  16. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  17. package/dist/command/cli/ci-hint.d.ts +44 -0
  18. package/dist/command/cli/ci-hint.js +72 -0
  19. package/dist/command/cli/cli-doctor.d.ts +6 -1
  20. package/dist/command/cli/cli-doctor.js +46 -5
  21. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  22. package/dist/command/cli/cli-flow-commands.js +39 -24
  23. package/dist/command/cli/cli-kill.d.ts +21 -0
  24. package/dist/command/cli/cli-kill.js +47 -4
  25. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  26. package/dist/command/cli/cli-parse-compare.js +75 -0
  27. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  28. package/dist/command/cli/cli-parse-grade.js +75 -0
  29. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  30. package/dist/command/cli/cli-parse-grammar.js +18 -20
  31. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  32. package/dist/command/cli/cli-parse-verify.js +63 -14
  33. package/dist/command/cli/cli-parse.d.ts +22 -17
  34. package/dist/command/cli/cli-parse.js +54 -32
  35. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  36. package/dist/command/cli/cli-update-commands.js +29 -20
  37. package/dist/command/cli/cli-usage.js +61 -31
  38. package/dist/command/cli/cli-verify.d.ts +17 -3
  39. package/dist/command/cli/cli-verify.js +59 -21
  40. package/dist/command/cli/cloud-cli.js +2 -119
  41. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  42. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  43. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  44. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  45. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  46. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  47. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  48. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  49. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  50. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  51. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  52. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  53. package/dist/command/cli/doctor-repairs.js +214 -0
  54. package/dist/command/cli/drive/drive-command.js +3 -7
  55. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  56. package/dist/command/cli/ensure-daemon.js +57 -0
  57. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  58. package/dist/command/cli/finished-run/grade-command.js +180 -0
  59. package/dist/command/cli/first-use-checks.d.ts +41 -0
  60. package/dist/command/cli/first-use-checks.js +127 -0
  61. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  62. package/dist/command/cli/first-verdict-node.js +189 -0
  63. package/dist/command/cli/first-verdict.d.ts +126 -0
  64. package/dist/command/cli/first-verdict.js +267 -0
  65. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  66. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  67. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  68. package/dist/command/cli/lifecycle/daemon-lifecycle.js +7 -6
  69. package/dist/command/cli/mcp-command.js +13 -4
  70. package/dist/command/cli/ports/port-holder.d.ts +11 -0
  71. package/dist/command/cli/ports/port-holder.js +36 -4
  72. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  73. package/dist/command/cli/setup-mcp-cli.js +98 -23
  74. package/dist/command/cli/status/status-lines.d.ts +2 -0
  75. package/dist/command/cli/status/status-lines.js +72 -17
  76. package/dist/command/cli/status-fields.d.ts +5 -1
  77. package/dist/command/cli/status-fields.js +24 -6
  78. package/dist/command/cli/try-command.d.ts +7 -0
  79. package/dist/command/cli/try-command.js +11 -3
  80. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  81. package/dist/command/cli/two-builds/compare-command.js +314 -0
  82. package/dist/command/cli/verify-against.d.ts +61 -0
  83. package/dist/command/cli/verify-against.js +187 -0
  84. package/dist/command/cli/verify-no-session.d.ts +2 -0
  85. package/dist/command/cli/verify-no-session.js +13 -0
  86. package/dist/command/cli/verify-results.d.ts +35 -2
  87. package/dist/command/cli/verify-results.js +76 -10
  88. package/dist/command/cli.js +112 -70
  89. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  90. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  91. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  92. package/dist/command/daemon/daemon-argv.js +11 -0
  93. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  94. package/dist/command/daemon/daemon-resolve.js +27 -1
  95. package/dist/command/daemon/daemon.d.ts +22 -4
  96. package/dist/command/daemon/daemon.js +52 -10
  97. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  98. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  99. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  100. package/dist/command/daemon/lifetime/death-cause.js +149 -0
  101. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  102. package/dist/command/daemon/lifetime/heartbeat.js +11 -15
  103. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  104. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  105. package/dist/command/daemon/port-state.d.ts +14 -0
  106. package/dist/command/daemon/port-state.js +31 -0
  107. package/dist/command/setup/agent-configs.d.ts +2 -0
  108. package/dist/command/setup/agent-configs.js +42 -15
  109. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  110. package/dist/command/setup/agent-next-steps.js +284 -0
  111. package/dist/command/setup/agent-writer.js +11 -6
  112. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  113. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  114. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  115. package/dist/command/setup/bringup/static-page-server.js +43 -9
  116. package/dist/command/setup/init/init-host.js +2 -0
  117. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  118. package/dist/command/setup/init/init-runtime.js +63 -21
  119. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  120. package/dist/command/setup/mcp-launcher.js +11 -0
  121. package/dist/command/setup/node-effects.d.ts +3 -2
  122. package/dist/command/setup/node-effects.js +9 -3
  123. package/dist/command/setup/remaining-steps.d.ts +8 -0
  124. package/dist/command/setup/remaining-steps.js +10 -0
  125. package/dist/command/setup/run-setup.d.ts +2 -0
  126. package/dist/command/setup/run-setup.js +9 -1
  127. package/dist/command/setup/setup-command.js +36 -15
  128. package/dist/command/setup/setup-mcp.d.ts +8 -0
  129. package/dist/command/setup/setup-mcp.js +47 -20
  130. package/dist/command/setup/start-dev-server.d.ts +8 -0
  131. package/dist/command/setup/start-dev-server.js +43 -0
  132. package/dist/command/update/updater.js +6 -0
  133. package/dist/command/version/identity/server-version.d.ts +2 -0
  134. package/dist/command/version/identity/server-version.js +2 -0
  135. package/dist/command/version/version-skew.d.ts +7 -0
  136. package/dist/command/version/version-skew.js +14 -3
  137. package/dist/features/compare/action-plan.d.ts +260 -0
  138. package/dist/features/compare/action-plan.js +543 -0
  139. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  140. package/dist/features/compare/behaviour-compare.js +85 -0
  141. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  142. package/dist/features/compare/behaviour-diff.js +202 -0
  143. package/dist/features/compare/behaviour-report.d.ts +40 -0
  144. package/dist/features/compare/behaviour-report.js +128 -0
  145. package/dist/features/compare/build-readings.d.ts +106 -0
  146. package/dist/features/compare/build-readings.js +250 -0
  147. package/dist/features/compare/compare-diff.d.ts +62 -0
  148. package/dist/features/compare/compare-diff.js +177 -0
  149. package/dist/features/compare/compare-report-html.d.ts +6 -0
  150. package/dist/features/compare/compare-report-html.js +102 -0
  151. package/dist/features/compare/compare-report.d.ts +15 -0
  152. package/dist/features/compare/compare-report.js +73 -0
  153. package/dist/features/compare/compare-run.d.ts +60 -0
  154. package/dist/features/compare/compare-run.js +123 -0
  155. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  156. package/dist/features/compare/outside/effect-lines.js +103 -0
  157. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  158. package/dist/features/compare/outside/state-sources.js +164 -0
  159. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  160. package/dist/features/compare/outside/value-dictionary.js +227 -0
  161. package/dist/features/compare/visible-content.d.ts +20 -0
  162. package/dist/features/compare/visible-content.js +125 -0
  163. package/dist/features/crawl/crawl-empty.js +2 -1
  164. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  165. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  166. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  167. package/dist/features/crawl/crawl-tools.js +28 -12
  168. package/dist/features/crawl/crawl.d.ts +8 -9
  169. package/dist/features/crawl/crawl.js +61 -65
  170. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  171. package/dist/features/exhaust/app-coverage.js +3 -1
  172. package/dist/features/exhaust/code-coverage.js +8 -4
  173. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  174. package/dist/features/exhaust/edge-cases.js +310 -0
  175. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  176. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  177. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  178. package/dist/features/exhaust/edges/edge-run.js +169 -0
  179. package/dist/features/exhaust/explorer.d.ts +87 -14
  180. package/dist/features/exhaust/explorer.js +159 -25
  181. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  182. package/dist/features/exhaust/failure-branches.js +29 -11
  183. package/dist/features/exhaust/invariants.d.ts +112 -0
  184. package/dist/features/exhaust/invariants.js +188 -0
  185. package/dist/features/exhaust/ledger.d.ts +68 -0
  186. package/dist/features/exhaust/ledger.js +121 -13
  187. package/dist/features/exhaust/session-fold.d.ts +10 -0
  188. package/dist/features/exhaust/session-fold.js +46 -3
  189. package/dist/features/exhaust/session-port.d.ts +3 -1
  190. package/dist/features/exhaust/session-port.js +97 -28
  191. package/dist/features/grade/grade-answer.d.ts +99 -0
  192. package/dist/features/grade/grade-answer.js +112 -0
  193. package/dist/features/grade/grade-noise.d.ts +27 -0
  194. package/dist/features/grade/grade-noise.js +55 -0
  195. package/dist/features/grade/grade-renumber.d.ts +12 -0
  196. package/dist/features/grade/grade-renumber.js +86 -0
  197. package/dist/features/grade/grade-state.d.ts +50 -0
  198. package/dist/features/grade/grade-state.js +93 -0
  199. package/dist/features/grade/grade-suggest.d.ts +33 -0
  200. package/dist/features/grade/grade-suggest.js +98 -0
  201. package/dist/features/license/license.d.ts +11 -71
  202. package/dist/features/license/license.js +43 -136
  203. package/dist/features/visual/desktop-capture.d.ts +33 -0
  204. package/dist/features/visual/desktop-capture.js +94 -0
  205. package/dist/features/visual/step-shots.js +139 -35
  206. package/dist/features/visual/visual-tools.d.ts +0 -18
  207. package/dist/features/visual/visual-tools.js +67 -99
  208. package/dist/import-graph.js +5 -6
  209. package/dist/index.d.ts +3 -1
  210. package/dist/index.js +19 -7
  211. package/dist/judgement/capsule/causal-summary.js +9 -2
  212. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  213. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  214. package/dist/judgement/runs/reticle-runner.js +16 -2
  215. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  216. package/dist/judgement/runs/run-ledger.js +98 -69
  217. package/dist/judgement/runs/runner-port.d.ts +2 -0
  218. package/dist/judgement/runs/runner-port.js +23 -5
  219. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  220. package/dist/judgement/runs/verification-sync.js +21 -10
  221. package/dist/language/flows/affected-tools.d.ts +1 -1
  222. package/dist/language/flows/affected-tools.js +49 -30
  223. package/dist/language/flows/change/git-changed.d.ts +2 -1
  224. package/dist/language/flows/change/git-changed.js +19 -2
  225. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  226. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  227. package/dist/language/flows/flow-anchor.d.ts +60 -2
  228. package/dist/language/flows/flow-anchor.js +125 -20
  229. package/dist/language/flows/flow-classify.d.ts +20 -0
  230. package/dist/language/flows/flow-classify.js +90 -12
  231. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  232. package/dist/language/flows/flow-cross-step.js +25 -1
  233. package/dist/language/flows/flow-journey.d.ts +38 -4
  234. package/dist/language/flows/flow-journey.js +138 -8
  235. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  236. package/dist/language/flows/flow-replay-run.js +76 -14
  237. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  238. package/dist/language/flows/flow-replay.d.ts +19 -1
  239. package/dist/language/flows/flow-replay.js +169 -52
  240. package/dist/language/flows/flow-replayable.js +7 -1
  241. package/dist/language/flows/flow-start-path.d.ts +34 -5
  242. package/dist/language/flows/flow-start-path.js +202 -15
  243. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  244. package/dist/language/flows/flow-step-runners.js +184 -109
  245. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  246. package/dist/language/flows/flow-store-for-session.js +51 -5
  247. package/dist/language/flows/flow-tools.js +46 -33
  248. package/dist/language/flows/flows.js +176 -76
  249. package/dist/language/flows/recording/tape/recordings.d.ts +21 -16
  250. package/dist/language/flows/recording/tape/recordings.js +112 -49
  251. package/dist/language/flows/replay.d.ts +36 -12
  252. package/dist/language/flows/replay.js +193 -50
  253. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  254. package/dist/language/flows/steps/page-steps.js +198 -0
  255. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  256. package/dist/language/flows/suite/sequential-run.js +96 -0
  257. package/dist/language/flows/verify-change-tools.js +26 -15
  258. package/dist/launch-chromium.d.ts +1 -0
  259. package/dist/launch-chromium.js +49 -12
  260. package/dist/memory/cloud/background-sync.d.ts +13 -0
  261. package/dist/memory/cloud/background-sync.js +24 -3
  262. package/dist/memory/cloud/sync-daemon.js +26 -27
  263. package/dist/memory/cloud/sync-screenshots.js +6 -1
  264. package/dist/memory/journal/attach-journal.js +2 -1
  265. package/dist/memory/journal/drive-flow.d.ts +7 -0
  266. package/dist/memory/journal/drive-flow.js +44 -13
  267. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  268. package/dist/memory/journal/session-journal.d.ts +7 -0
  269. package/dist/memory/journal/session-journal.js +58 -10
  270. package/dist/memory/project/root-claims.d.ts +9 -0
  271. package/dist/memory/project/root-claims.js +9 -4
  272. package/dist/portal/bridge/bridge.d.ts +13 -4
  273. package/dist/portal/bridge/bridge.js +10 -9
  274. package/dist/portal/input/download-capture.d.ts +7 -0
  275. package/dist/portal/input/download-capture.js +92 -0
  276. package/dist/portal/input/network-detail.d.ts +6 -0
  277. package/dist/portal/input/network-detail.js +12 -1
  278. package/dist/portal/input/network-mock-tools.js +49 -13
  279. package/dist/portal/input/network-mock.d.ts +8 -0
  280. package/dist/portal/input/network-mock.js +30 -10
  281. package/dist/portal/input/page-capture.d.ts +24 -0
  282. package/dist/portal/input/page-capture.js +33 -0
  283. package/dist/portal/input/real-input.d.ts +35 -18
  284. package/dist/portal/input/real-input.js +99 -33
  285. package/dist/portal/input/viewport-tools.d.ts +8 -0
  286. package/dist/portal/input/viewport-tools.js +81 -21
  287. package/dist/portal/pool/browser-pool.d.ts +16 -30
  288. package/dist/portal/pool/browser-pool.js +64 -52
  289. package/dist/portal/pool/context-permissions.d.ts +34 -17
  290. package/dist/portal/pool/context-permissions.js +94 -1
  291. package/dist/portal/pool/csp-widen.d.ts +62 -0
  292. package/dist/portal/pool/csp-widen.js +132 -0
  293. package/dist/portal/pool/determinism.d.ts +52 -0
  294. package/dist/portal/pool/determinism.js +259 -0
  295. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  296. package/dist/portal/pool/lease-emulation.js +18 -0
  297. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  298. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  299. package/dist/portal/pool/lease-page-error.d.ts +56 -0
  300. package/dist/portal/pool/lease-page-error.js +160 -0
  301. package/dist/portal/pool/page-error-journal.d.ts +63 -0
  302. package/dist/portal/pool/page-error-journal.js +208 -0
  303. package/dist/portal/pool/playwright-launcher.js +71 -15
  304. package/dist/portal/pool/pool-contract.d.ts +49 -1
  305. package/dist/portal/pool/pool-lease.d.ts +70 -2
  306. package/dist/portal/pool/pool-lease.js +23 -0
  307. package/dist/portal/pool/zero-install.d.ts +8 -0
  308. package/dist/portal/pool/zero-install.js +91 -1
  309. package/dist/portal/session/dev-server/dev-command.d.ts +11 -2
  310. package/dist/portal/session/dev-server/dev-command.js +110 -16
  311. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  312. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  313. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  314. package/dist/portal/session/dev-server/project-ports.js +62 -0
  315. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  316. package/dist/portal/session/facts/departed-network.js +142 -0
  317. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  318. package/dist/portal/session/no-session-diagnosis.js +76 -27
  319. package/dist/portal/session/no-session-next-action.d.ts +14 -0
  320. package/dist/portal/session/no-session-next-action.js +56 -7
  321. package/dist/portal/session/no-session-watch.d.ts +6 -0
  322. package/dist/portal/session/no-session-watch.js +41 -4
  323. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  324. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  325. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  326. package/dist/portal/session/recorded-gaps.js +5 -2
  327. package/dist/portal/session/serving-directory.d.ts +10 -0
  328. package/dist/portal/session/serving-directory.js +39 -8
  329. package/dist/portal/session/session-manager.d.ts +10 -0
  330. package/dist/portal/session/session-manager.js +211 -81
  331. package/dist/portal/session/session.js +4 -3
  332. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  333. package/dist/portal/session/zero-install-first-call.js +34 -2
  334. package/dist/start-wiring.d.ts +30 -2
  335. package/dist/start-wiring.js +74 -9
  336. package/dist/surface/mcp/mcp-proxy.js +10 -2
  337. package/dist/surface/mcp/mcp.js +8 -5
  338. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  339. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  340. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  341. package/dist/surface/mcp/shared-params.d.ts +2 -0
  342. package/dist/surface/mcp/shared-params.js +31 -0
  343. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  344. package/dist/surface/tools/act/act-observation.js +118 -12
  345. package/dist/surface/tools/act/act-preflight.js +38 -18
  346. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  347. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  348. package/dist/surface/tools/act/act-target.js +3 -1
  349. package/dist/surface/tools/act/after-match.d.ts +10 -0
  350. package/dist/surface/tools/act/after-match.js +40 -0
  351. package/dist/surface/tools/act/already-true.js +1 -1
  352. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  353. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  354. package/dist/surface/tools/act/resolve-target.d.ts +17 -1
  355. package/dist/surface/tools/act/resolve-target.js +59 -9
  356. package/dist/surface/tools/act/sequence-grade.d.ts +15 -1
  357. package/dist/surface/tools/act/sequence-grade.js +45 -27
  358. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  359. package/dist/surface/tools/act-command.d.ts +23 -0
  360. package/dist/surface/tools/act-command.js +50 -0
  361. package/dist/surface/tools/act-sequence-tool.js +81 -50
  362. package/dist/surface/tools/act-tools.d.ts +1 -13
  363. package/dist/surface/tools/act-tools.js +83 -95
  364. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  365. package/dist/surface/tools/agent/build-staleness.js +1 -1
  366. package/dist/surface/tools/assert/assert-verdict.js +18 -12
  367. package/dist/surface/tools/browser-tools.js +87 -13
  368. package/dist/surface/tools/compare-tools.d.ts +11 -0
  369. package/dist/surface/tools/compare-tools.js +94 -0
  370. package/dist/surface/tools/coverage-tools.js +1 -1
  371. package/dist/surface/tools/dynamic-tools.js +11 -3
  372. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  373. package/dist/surface/tools/empty-session-list.js +4 -2
  374. package/dist/surface/tools/error-recovery.d.ts +12 -1
  375. package/dist/surface/tools/error-recovery.js +131 -40
  376. package/dist/surface/tools/explore-tools.js +1 -1
  377. package/dist/surface/tools/invoke-tool.js +29 -21
  378. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  379. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  380. package/dist/surface/tools/lease-hint.d.ts +42 -1
  381. package/dist/surface/tools/lease-hint.js +52 -3
  382. package/dist/surface/tools/lease-readiness.d.ts +47 -0
  383. package/dist/surface/tools/lease-readiness.js +126 -0
  384. package/dist/surface/tools/lease-seed.d.ts +16 -1
  385. package/dist/surface/tools/lease-seed.js +37 -4
  386. package/dist/surface/tools/lease-tools.d.ts +3 -41
  387. package/dist/surface/tools/lease-tools.js +104 -124
  388. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  389. package/dist/surface/tools/lease-url-identity.js +97 -0
  390. package/dist/surface/tools/merge-tools.js +99 -2
  391. package/dist/surface/tools/merged-name-redirect.js +5 -1
  392. package/dist/surface/tools/observe-tools.js +15 -9
  393. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  394. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  395. package/dist/surface/tools/read-tools.js +33 -30
  396. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  397. package/dist/surface/tools/real-input-attempt.js +143 -28
  398. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  399. package/dist/surface/tools/repeat-hint.js +171 -0
  400. package/dist/surface/tools/run-report.d.ts +2 -2
  401. package/dist/surface/tools/run-report.js +27 -15
  402. package/dist/surface/tools/session-ports.d.ts +14 -0
  403. package/dist/surface/tools/session-ports.js +53 -0
  404. package/dist/surface/tools/tool-kit.d.ts +18 -0
  405. package/dist/surface/tools/tool-kit.js +9 -3
  406. package/dist/surface/tools/tool-surface.js +0 -1
  407. package/dist/surface/tools/tools.js +54 -18
  408. package/dist/surface/tools/type-spelling.d.ts +13 -0
  409. package/dist/surface/tools/type-spelling.js +36 -0
  410. package/dist/telemetry/agent-client.d.ts +6 -0
  411. package/dist/telemetry/agent-client.js +10 -0
  412. package/dist/telemetry/argument-shape.js +17 -7
  413. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  414. package/dist/telemetry/cli-telemetry.js +15 -0
  415. package/dist/telemetry/init-telemetry.d.ts +2 -1
  416. package/dist/telemetry/init-telemetry.js +47 -2
  417. package/dist/telemetry/install-trace.js +23 -4
  418. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  419. package/dist/telemetry/onboarding-funnel.js +25 -3
  420. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  421. package/dist/telemetry/proxy-telemetry.js +2 -2
  422. package/dist/telemetry/session-metrics.d.ts +1 -1
  423. package/dist/telemetry/session-metrics.js +67 -27
  424. package/dist/telemetry/telemetry.d.ts +3 -1
  425. package/dist/telemetry/telemetry.js +60 -28
  426. package/dist/telemetry/tool-refused.d.ts +6 -3
  427. package/dist/telemetry/tool-refused.js +15 -3
  428. package/docs/actions.mdx +3 -3
  429. package/docs/agent-cheatsheet.md +1 -1
  430. package/docs/architecture.md +3 -6
  431. package/docs/capabilities.mdx +61 -1
  432. package/docs/cli/affected.mdx +9 -7
  433. package/docs/cli/cloud.mdx +3 -3
  434. package/docs/cli/compare.mdx +94 -0
  435. package/docs/cli/doctor.mdx +41 -2
  436. package/docs/cli/grade.mdx +97 -0
  437. package/docs/cli/help.mdx +4 -2
  438. package/docs/cli/license.mdx +13 -12
  439. package/docs/cli/restart.mdx +7 -5
  440. package/docs/cli/rollback.mdx +5 -5
  441. package/docs/cli/setup.mdx +13 -2
  442. package/docs/cli/status.mdx +4 -0
  443. package/docs/cli/stop.mdx +3 -3
  444. package/docs/cli/update.mdx +2 -2
  445. package/docs/cli/verify.mdx +23 -1
  446. package/docs/cli.mdx +34 -24
  447. package/docs/deploy-checks.md +25 -0
  448. package/docs/desktop.mdx +1 -1
  449. package/docs/enterprise.md +17 -22
  450. package/docs/environment.mdx +1 -0
  451. package/docs/first-drive.md +1 -1
  452. package/docs/flows.md +11 -1
  453. package/docs/frameworks.mdx +9 -9
  454. package/docs/getting-started.md +1 -1
  455. package/docs/install-manual.mdx +16 -1
  456. package/docs/license-activation.mdx +3 -3
  457. package/docs/packages/engine.mdx +1 -1
  458. package/docs/packages/server.mdx +1 -1
  459. package/docs/packages/tauri.mdx +1 -1
  460. package/docs/packages/test.mdx +1 -1
  461. package/docs/packages.mdx +1 -1
  462. package/docs/platform-integration.md +1 -1
  463. package/docs/predicates.mdx +1 -1
  464. package/docs/quickstart.mdx +68 -49
  465. package/docs/skill-setup.md +1 -1
  466. package/docs/skill.md +14 -13
  467. package/docs/telemetry.md +6 -4
  468. package/docs/tools/navigate.mdx +1 -1
  469. package/docs/tools/verify.mdx +4 -1
  470. package/docs/troubleshooting.mdx +1 -1
  471. package/docs/usage.md +17 -14
  472. package/package.json +18 -14
  473. package/LICENSE-ENTERPRISE +0 -37
  474. package/dist/features/ee/audit-log.d.ts +0 -14
  475. package/dist/features/ee/audit-log.js +0 -11
  476. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  477. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -37,6 +37,16 @@ export interface DevProxy {
37
37
  * only: a remote page's port says nothing about any process on this machine.
38
38
  */
39
39
  export declare function servingDirectoryOf(origin: string, io?: ServingDirectoryIo): string | undefined;
40
+ /** How many ports one scan asks about. Each lookup blocks the daemon for two `lsof` runs. */
41
+ export declare const MAX_FOLDER_LOOKUPS = 16;
42
+ /**
43
+ * The folder of each port in `ports`, or undefined when it cannot be read.
44
+ *
45
+ * `known` is the last scan's answer. A port that stays open keeps it, so a machine with many
46
+ * listeners pays once. A port past the bound waits for the next scan. A closed port leaves the map,
47
+ * so the next process on it is asked about.
48
+ */
49
+ export declare function listenerFolders(ports: readonly number[], known: ReadonlyMap<number, string | undefined>, directoryOf: (port: number) => string | undefined): Map<number, string | undefined>;
40
50
  /**
41
51
  * The live answers for a worktree-scoped resolve: the calling agent's checkout (from the directory
42
52
  * its MCP proxy runs in) and each tab's checkout (from the dev server that served it).
@@ -40,13 +40,15 @@ function readOrUndefined(path) {
40
40
  function readPortless() {
41
41
  const dir = process.env[PORTLESS_STATE_DIR_ENV] ?? join(homedir(), PORTLESS_STATE_DIR);
42
42
  const port = Number(readOrUndefined(join(dir, PORTLESS_PORT_FILE))?.trim());
43
- if (!Number.isInteger(port))
43
+ if (!Number.isInteger(port)) {
44
44
  return undefined;
45
+ }
45
46
  let routes = [];
46
47
  try {
47
48
  const parsed = PortlessRoutesSchema.safeParse(JSON.parse(readOrUndefined(join(dir, PORTLESS_ROUTES_FILE)) ?? '[]'));
48
- if (parsed.success)
49
+ if (parsed.success) {
49
50
  routes = parsed.data;
51
+ }
50
52
  }
51
53
  catch {
52
54
  // A table being rewritten reads as empty: the page stays unplaced, never placed wrong.
@@ -75,27 +77,55 @@ export function servingDirectoryOf(origin, io = liveIo) {
75
77
  catch {
76
78
  return undefined;
77
79
  }
78
- if (!isLoopbackHostname(url.hostname) && !isLocalhostSubdomain(url.hostname))
80
+ if (!isLoopbackHostname(url.hostname) && !isLocalhostSubdomain(url.hostname)) {
79
81
  return undefined;
82
+ }
80
83
  // Exact first: a dev proxy serves every worktree on one port, under the name it announced.
81
84
  const byUrl = io.devServers().find((entry) => originOf(entry.url) === url.origin);
82
- if (byUrl !== undefined)
85
+ if (byUrl !== undefined) {
83
86
  return byUrl.root;
87
+ }
84
88
  const pagePort = '' === url.port ? DEFAULT_PORT[url.protocol] : Number(url.port);
85
- if (pagePort === undefined)
89
+ if (pagePort === undefined) {
86
90
  return undefined;
91
+ }
87
92
  // Behind a dev proxy the page's port is the proxy's, and `lsof` on it names the proxy's folder.
88
93
  // The proxy's route table names the dev server's own port.
89
94
  const proxy = isLocalhostSubdomain(url.hostname) ? io.devProxy?.() : undefined;
90
95
  const route = proxy?.routes.find((r) => r.hostname === url.hostname.toLowerCase());
91
- if (route === undefined && true === proxy?.ports.includes(pagePort))
96
+ if (route === undefined && true === proxy?.ports.includes(pagePort)) {
92
97
  return undefined;
98
+ }
93
99
  const port = route?.port ?? pagePort;
94
100
  const announced = io.devServers().find((entry) => entry.port === port);
95
- if (announced !== undefined)
101
+ if (announced !== undefined) {
96
102
  return announced.root;
103
+ }
97
104
  return io.listenerCwd(port) ?? undefined;
98
105
  }
106
+ /** How many ports one scan asks about. Each lookup blocks the daemon for two `lsof` runs. */
107
+ export const MAX_FOLDER_LOOKUPS = 16;
108
+ /**
109
+ * The folder of each port in `ports`, or undefined when it cannot be read.
110
+ *
111
+ * `known` is the last scan's answer. A port that stays open keeps it, so a machine with many
112
+ * listeners pays once. A port past the bound waits for the next scan. A closed port leaves the map,
113
+ * so the next process on it is asked about.
114
+ */
115
+ export function listenerFolders(ports, known, directoryOf) {
116
+ const folders = new Map();
117
+ let lookups = 0;
118
+ for (const port of ports) {
119
+ if (known.has(port)) {
120
+ folders.set(port, known.get(port));
121
+ }
122
+ else if (lookups < MAX_FOLDER_LOOKUPS) {
123
+ lookups += 1;
124
+ folders.set(port, directoryOf(port));
125
+ }
126
+ }
127
+ return folders;
128
+ }
99
129
  /** The checkout whose dev server serves `origin` right now, or undefined. */
100
130
  function checkoutServing(origin) {
101
131
  const serving = servingDirectoryOf(origin);
@@ -122,8 +152,9 @@ export function liveCheckoutSource(placeOrigin = checkoutServing) {
122
152
  : undefined;
123
153
  },
124
154
  ofTab: (tab) => {
125
- if (placed.has(tab))
155
+ if (placed.has(tab)) {
126
156
  return placed.get(tab);
157
+ }
127
158
  const origin = originOf(tab.url);
128
159
  const checkout = origin === undefined ? undefined : placeOrigin(origin);
129
160
  placed.set(tab, checkout);
@@ -64,6 +64,8 @@ export declare class SessionManager {
64
64
  isParked(sessionId: string): boolean;
65
65
  remove(session: Session): boolean;
66
66
  get(sessionId: string): Session | undefined;
67
+ /** The id the same tab connected as after this session's page went away, if one has. */
68
+ successorId(sessionId: string): string | undefined;
67
69
  /**
68
70
  * Did this id belong to a session that has since disconnected?
69
71
  *
@@ -149,6 +151,14 @@ export declare class SessionManager {
149
151
  /** Every departed session's URL, oldest first. The port scan has to see all of them, not only the newest. */
150
152
  tombstoneUrls(): readonly string[];
151
153
  resolve(sessionId?: string, scope?: ResolveScope): Session;
154
+ /**
155
+ * Resolve the session for a call that only some sessions can serve.
156
+ *
157
+ * With no sessionId, resolve() chooses by health and recency alone. A lease that could resize the
158
+ * page lost to a focused tab that could not, and the call was refused (#1959). Here, when exactly
159
+ * one candidate can serve the call, that candidate is chosen.
160
+ */
161
+ resolveServing(sessionId: string | undefined, canServe: (session: Session) => boolean | Promise<boolean>): Promise<Session>;
152
162
  }
153
163
  export interface Tab {
154
164
  id: string;
@@ -1,4 +1,4 @@
1
- import { NO_SESSION_CONNECTED_ERROR, originOf, samePath } from '@reticlehq/core';
1
+ import { NO_SESSION_CONNECTED_ERROR, NO_SESSION_NAVIGATE_FIX, originOf, samePath, } from '@reticlehq/core';
2
2
  import { notePendingNoSessionReason } from '../../telemetry/tool-refused.js';
3
3
  import { declareDrivenRedactionKeys, forgetDrivenRedactionKeys, } from '../input/driven-redaction.js';
4
4
  import { Session } from './session.js';
@@ -6,6 +6,7 @@ import { AttachmentHistory } from './presence/attachment-history.js';
6
6
  import { claimedSessionOf, openedBeforeDeparture, pickDocumentSuccessor, } from './session-successor.js';
7
7
  import { SESSION_DISCONNECTED_REASON, SESSION_PARKED_REASON } from './facts/session-replaced.js';
8
8
  import { isErrorDocument } from './session-info.js';
9
+ import { carryOnNewDocument, departedNetworkOf, } from './facts/departed-network.js';
9
10
  /** Core's `originOf`, re-exported for this module's existing importers. */
10
11
  export { originOf };
11
12
  /**
@@ -14,8 +15,9 @@ export { originOf };
14
15
  * no scope, return the list unchanged (legacy single-/multi-session behavior).
15
16
  */
16
17
  function scopeSessions(sessions, scope) {
17
- if (scope === undefined)
18
+ if (scope === undefined) {
18
19
  return sessions;
20
+ }
19
21
  if (scope.projectId !== undefined) {
20
22
  return sessions.filter((s) => s.projectId === scope.projectId);
21
23
  }
@@ -36,18 +38,10 @@ function scopeSessions(sessions, scope) {
36
38
  * target it: both facts are already in hand here.
37
39
  */
38
40
  export function scopeMissError(connected, scope) {
39
- const who = scope?.projectId !== undefined
40
- ? `project '${scope.projectId}'`
41
- : scope?.url !== undefined
42
- ? `your app at ${scope.url}`
43
- : 'the active project';
41
+ const who = scopeName(scope);
44
42
  const anyUntagged = connected.some((s) => s.projectId === undefined);
45
43
  const anyTagged = connected.some((s) => s.projectId !== undefined);
46
- const how = anyUntagged && !anyTagged
47
- ? 'with no projectId'
48
- : anyUntagged && anyTagged
49
- ? 'under a different project or with no projectId'
50
- : 'under a different project';
44
+ const how = whereConnected(anyUntagged, anyTagged);
51
45
  const listed = connected
52
46
  .map((s) => s.projectId !== undefined
53
47
  ? `'${s.projectId}' (${s.url}, sessionId '${s.id}')`
@@ -61,6 +55,48 @@ export function scopeMissError(connected, scope) {
61
55
  `${listed}. The daemon scopes to the .reticle.json of the directory it was started in, so ` +
62
56
  `this is a scope mismatch, not a dead app. ${advice}`);
63
57
  }
58
+ /** What the scope names, in the words of the refusal. */
59
+ function scopeName(scope) {
60
+ if (scope?.projectId !== undefined) {
61
+ return `project '${scope.projectId}'`;
62
+ }
63
+ if (scope?.url !== undefined) {
64
+ return `your app at ${scope.url}`;
65
+ }
66
+ return 'the active project';
67
+ }
68
+ /** Where the connected sessions are, given which of them carry a projectId. */
69
+ function whereConnected(anyUntagged, anyTagged) {
70
+ if (!anyUntagged) {
71
+ return 'under a different project';
72
+ }
73
+ if (!anyTagged) {
74
+ return 'with no projectId';
75
+ }
76
+ return 'under a different project or with no projectId';
77
+ }
78
+ /**
79
+ * A session's rank for auto-selection: lower is better.
80
+ *
81
+ * 0 = non-throttled (visible + recently-heard), 1 = throttled (hidden or stale heartbeat),
82
+ * 2 = an error document, 3 = attached but not answering commands. A wedged tab keeps streaming
83
+ * events, so recency RATES IT HIGHEST — it was beating a healthy sibling on the one signal scoring
84
+ * looked at, and every call auto-targeted the one tab guaranteed to time out. An error document (a
85
+ * dev server's 404 that still mounted the SDK) ranks below any working page, hidden or not: it is
86
+ * never the app the agent was asked about.
87
+ */
88
+ function rankOf(session) {
89
+ if (session.unresponsive()) {
90
+ return 3;
91
+ }
92
+ if (isErrorDocument(session.page)) {
93
+ return 2;
94
+ }
95
+ if (session.throttled()) {
96
+ return 1;
97
+ }
98
+ return 0;
99
+ }
64
100
  /**
65
101
  * Owns the set of connected browser sessions and the smart auto-selection that resolves which one a
66
102
  * tool targets when the agent omits an explicit sessionId. Extracted from session.ts so each file
@@ -95,6 +131,11 @@ export class SessionManager {
95
131
  #tombstones = new Map();
96
132
  /** The tabs already open when each tombstoned session departed: siblings, never its successor. */
97
133
  #siblingsAtDeparture = new Map();
134
+ /**
135
+ * The network record each departed document made after the agent's last act, kept until the same
136
+ * tab's next document connects (#1944). Bounded with the tombstones. See departed-network.ts.
137
+ */
138
+ #departedNetwork = new Map();
98
139
  /**
99
140
  * The active project's scope, set once from the daemon's .reticle.json. When a tool resolves a session
100
141
  * without passing its own scope, this is applied — so auto-selection is project-scoped by default
@@ -124,8 +165,9 @@ export class SessionManager {
124
165
  }
125
166
  add(session) {
126
167
  this.#everConnected = true;
127
- if (session.projectId !== undefined)
168
+ if (session.projectId !== undefined) {
128
169
  this.#connectedProjects.add(session.projectId);
170
+ }
129
171
  // Ordering, not lifetime: this is what makes a remembered refusal distinguishable from a live
130
172
  // one. Here for the same reason `#everConnected` is — `add` is the one method every path that
131
173
  // registers a session goes through.
@@ -143,10 +185,24 @@ export class SessionManager {
143
185
  // that silently fails to register is a leak nothing would report.
144
186
  declareDrivenRedactionKeys(session.id, session.redactKeys);
145
187
  this.#attachment.attached(session.id);
188
+ this.#carryDepartedNetwork(session, previous);
146
189
  this.#tombstones.delete(session.id);
147
190
  this.#siblingsAtDeparture.delete(session.id);
148
191
  return previous;
149
192
  }
193
+ /**
194
+ * Give a new document the requests its tab's previous document made after the last act (#1944).
195
+ * `previous` is that document when its socket has not closed yet; else the record kept at removal.
196
+ */
197
+ #carryDepartedNetwork(session, previous) {
198
+ const departed = previous !== undefined && previous !== session
199
+ ? departedNetworkOf(previous)
200
+ : this.#departedNetwork.get(session.id);
201
+ this.#departedNetwork.delete(session.id);
202
+ if (departed !== undefined) {
203
+ carryOnNewDocument(session, departed);
204
+ }
205
+ }
150
206
  /**
151
207
  * The other tabs of the same app as `driven`.
152
208
  *
@@ -171,28 +227,39 @@ export class SessionManager {
171
227
  session.disconnect(SESSION_PARKED_REASON);
172
228
  this.remove(session);
173
229
  this.#tombstones.delete(session.id);
230
+ this.#departedNetwork.delete(session.id);
174
231
  }
175
232
  isParked(sessionId) {
176
233
  return this.#parked.has(sessionId);
177
234
  }
178
235
  #parked = new Set();
179
236
  remove(session) {
180
- if (this.#sessions.get(session.id) !== session)
237
+ if (this.#sessions.get(session.id) !== session) {
181
238
  return false;
239
+ }
182
240
  session.rejectAll(SESSION_DISCONNECTED_REASON);
183
241
  forgetDrivenRedactionKeys(session.id);
184
242
  // Recorded, not forgotten: a session that comes back needs its gap measured, and a listing after
185
243
  // the reconnect is exactly where that matters.
186
244
  this.#attachment.detached(session.id);
187
245
  this.#rememberTombstone(session);
246
+ // Kept only beside a tombstone, so the tombstone cap bounds it too.
247
+ const departedNetwork = this.#tombstones.has(session.id)
248
+ ? departedNetworkOf(session)
249
+ : undefined;
250
+ if (departedNetwork !== undefined) {
251
+ this.#departedNetwork.set(session.id, departedNetwork);
252
+ }
188
253
  return this.#sessions.delete(session.id);
189
254
  }
190
255
  #rememberTombstone(session) {
191
256
  const origin = originOf(session.url);
192
- if (origin === undefined)
257
+ if (origin === undefined) {
193
258
  return;
194
- if (this.#tombstones.has(session.id))
259
+ }
260
+ if (this.#tombstones.has(session.id)) {
195
261
  this.#tombstones.delete(session.id);
262
+ }
196
263
  this.#siblingsAtDeparture.set(session.id, new Set([...this.#sessions.values()]
197
264
  .filter((s) => s !== session && openedBeforeDeparture(s, session))
198
265
  .map((s) => s.id)));
@@ -206,16 +273,19 @@ export class SessionManager {
206
273
  });
207
274
  while (this.#tombstones.size > MAX_TOMBSTONES) {
208
275
  const oldest = this.#tombstones.keys().next().value;
209
- if (oldest === undefined)
276
+ if (oldest === undefined) {
210
277
  break;
278
+ }
211
279
  this.#tombstones.delete(oldest);
212
280
  this.#siblingsAtDeparture.delete(oldest);
281
+ this.#departedNetwork.delete(oldest);
213
282
  }
214
283
  }
215
284
  #successorOf(sessionId) {
216
285
  const departed = this.#tombstones.get(sessionId);
217
- if (departed === undefined)
286
+ if (departed === undefined) {
218
287
  return undefined;
288
+ }
219
289
  const siblings = this.#siblingsAtDeparture.get(sessionId);
220
290
  const picked = pickDocumentSuccessor(this.all()
221
291
  .filter((s) => true !== siblings?.has(s.id))
@@ -224,13 +294,18 @@ export class SessionManager {
224
294
  url: s.url,
225
295
  ...(s.projectId === undefined ? {} : { projectId: s.projectId }),
226
296
  })), departed);
227
- if (picked === undefined)
297
+ if (picked === undefined) {
228
298
  return undefined;
299
+ }
229
300
  return this.#sessions.get(picked.id);
230
301
  }
231
302
  get(sessionId) {
232
303
  return this.#sessions.get(sessionId);
233
304
  }
305
+ /** The id the same tab connected as after this session's page went away, if one has. */
306
+ successorId(sessionId) {
307
+ return this.#successorOf(sessionId)?.id;
308
+ }
234
309
  /**
235
310
  * Did this id belong to a session that has since disconnected?
236
311
  *
@@ -396,8 +471,9 @@ export class SessionManager {
396
471
  */
397
472
  noteClosure(reason, at, page) {
398
473
  this.#recentClosures.push({ at, reason, ...(page === undefined ? {} : { page }) });
399
- if (this.#recentClosures.length > MAX_REMEMBERED_CLOSURES)
474
+ if (this.#recentClosures.length > MAX_REMEMBERED_CLOSURES) {
400
475
  this.#recentClosures.shift();
476
+ }
401
477
  this.#connectedSinceLastClosure = false;
402
478
  }
403
479
  /** The most recent bridge-initiated close, if any. */
@@ -443,8 +519,9 @@ export class SessionManager {
443
519
  */
444
520
  lastKnown() {
445
521
  let last;
446
- for (const record of this.#tombstones.values())
522
+ for (const record of this.#tombstones.values()) {
447
523
  last = record;
524
+ }
448
525
  return last;
449
526
  }
450
527
  /**
@@ -456,8 +533,9 @@ export class SessionManager {
456
533
  lastKnownFor(projectId) {
457
534
  let last;
458
535
  for (const record of this.#tombstones.values()) {
459
- if (record.projectId === projectId)
536
+ if (record.projectId === projectId) {
460
537
  last = record;
538
+ }
461
539
  }
462
540
  return last;
463
541
  }
@@ -496,40 +574,19 @@ export class SessionManager {
496
574
  const ended = sessionId.startsWith(LEASE_ID_PREFIX)
497
575
  ? ` Lease '${sessionId}' has ended (it expired or was released); lease a new tab to go on.`
498
576
  : '';
499
- if (0 === own.length)
577
+ if (0 === own.length) {
500
578
  return (`no connected session with id '${sessionId}'.${ended} ` +
501
579
  'Omit sessionId to let Reticle scope to your project, or lease a tab of your own.');
580
+ }
502
581
  const named = own.map((s) => `'${s.id}' (${s.url})`).join(', ');
503
582
  return (`no connected session with id '${sessionId}'.${ended} Your app's tabs: ${named}. ` +
504
583
  'Retry with one of those, or omit sessionId entirely and let Reticle scope to your project.');
505
584
  }
506
- resolve(sessionId, scope) {
507
- if (sessionId !== undefined) {
508
- const found = this.#sessions.get(sessionId) ?? this.#successorOf(sessionId);
509
- if (found === undefined) {
510
- throw new Error(this.#unknownSessionError(sessionId));
511
- }
512
- found.markAgentActivity(); // liveness — a targeted tool keeps the session alive / revives it
513
- return found;
514
- }
515
- if (0 === this.#sessions.size) {
516
- const closure = this.lastClosure();
517
- // A diagnosis beats a checklist: it names which of the three causes this actually is.
518
- const hint = this.#noSessionHint?.();
519
- // BOTH, when there are both — the two answer different questions and the pair is strictly
520
- // more than either. This used to be an either/or, and a recorded refusal DISCARDED the whole
521
- // diagnosis: the one case where the daemon has hard evidence about why nothing is connected
522
- // was also the one case where it threw away everything else it had learned, including the
523
- // port scan and the next action. The refusal leads, because it is the fact.
524
- const refusal = closure === undefined
525
- ? ''
526
- : ` NOTE: the bridge REFUSED or closed a connection recently — "${closure.reason}". The app is probably still running and trying to connect: it was turned away, and the SDK does not retry after a policy close. The reason above names the fix — do not go looking for a stopped dev server.`;
527
- // Hand the branch code to the refusal that is about to be reported for this throw. The
528
- // diagnosis is computed HERE and the refusal is classified from the message downstream, so
529
- // without this every no-session refusal lands in one undifferentiated bucket (#615).
530
- notePendingNoSessionReason(this.#noSessionReason?.());
531
- throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}`);
532
- }
585
+ /**
586
+ * The sessions an id-less call may choose from: in the project scope, then in the caller's own
587
+ * checkout. Throws when those scopes leave nothing, or leave tabs of more than one project.
588
+ */
589
+ #candidates(scope) {
533
590
  // Scope to the agent's active project FIRST, so a stray tab from another app/origin (e.g. a
534
591
  // leftover dashboard on a different port) is structurally unselectable — it never enters the
535
592
  // candidate set, no matter how recently it was heard from. This is the anti-cross-talk guard.
@@ -541,8 +598,9 @@ export class SessionManager {
541
598
  const byCheckout = this.#checkoutScope === undefined
542
599
  ? { sessions: inProject }
543
600
  : scopeToCallerCheckout(inProject, this.#checkoutScope.caller(), this.#checkoutScope.ofTab);
544
- if (byCheckout.refusal !== undefined)
601
+ if (byCheckout.refusal !== undefined) {
545
602
  throw new Error(byCheckout.refusal);
603
+ }
546
604
  const all = byCheckout.sessions;
547
605
  if (0 === all.length) {
548
606
  // Sessions exist, but none belong to the scoped project — never fall back to a foreign tab.
@@ -551,27 +609,20 @@ export class SessionManager {
551
609
  // exists to be. Naming the tab and its sessionId costs the agent one extra argument.
552
610
  throw new Error(scopeMissError(connected, effectiveScope));
553
611
  }
554
- if (1 === all.length) {
555
- const [only] = all;
556
- if (only === undefined)
557
- throw new Error('session lookup failed');
558
- only.markAgentActivity();
559
- return only;
560
- }
561
612
  // Two projects and nothing to choose between them: refuse, BEFORE any scoring.
562
613
  //
563
614
  // Reported from the field: a call from `apps/next-app-router` was answered by a `rowy-*`
564
615
  // session, and `act_and_wait` spent 16.9s returning `verified:'no'` about an app never under
565
616
  // test — a confident false verdict, with a source pointer, about code the agent never touched.
566
617
  //
567
- // The recency rule below cannot prevent it. It asks "is one of these clearly fresher", which is
568
- // the right question for two tabs of ONE app and the wrong question for two apps: the fresher
569
- // tab of the wrong project is still the wrong project. The reported case had a clear winner,
570
- // so the ambiguity check passed and the wrong app was selected.
618
+ // The recency rule in resolve() cannot prevent it. It asks "is one of these clearly fresher",
619
+ // which is the right question for two tabs of ONE app and the wrong question for two apps: the
620
+ // fresher tab of the wrong project is still the wrong project. The reported case had a clear
621
+ // winner, so the ambiguity check passed and the wrong app was selected.
571
622
  //
572
623
  // This sits ahead of scoring so freshness cannot rescue it. It only fires when nothing scoped
573
- // the call — an explicit `sessionId` returns far above, and an explicit or default scope has
574
- // already narrowed `all` to one project by here.
624
+ // the call — an explicit `sessionId` never reaches here, and an explicit or default scope has
625
+ // already narrowed `all` to one project by here. One session is one project, so it never fires.
575
626
  const projects = new Set(all.map((s) => s.projectId).filter((p) => p !== undefined));
576
627
  if (projects.size > 1) {
577
628
  const listed = all
@@ -584,25 +635,54 @@ export class SessionManager {
584
635
  'never touched — the daemon scopes to the directory it was started in, so running it above ' +
585
636
  'several apps leaves it unable to tell them apart.');
586
637
  }
587
- // Multiple sessions: score each (lower = better candidate for auto-selection).
588
- // 0 = non-throttled (visible + recently-heard), 1 = throttled (hidden or stale heartbeat),
589
- // 2 = attached but not answering commands. A wedged tab keeps streaming events, so recency
590
- // RATES IT HIGHEST — it was beating a healthy sibling on the one signal scoring looked at, and
591
- // every call auto-targeted the one tab guaranteed to time out.
592
- const scored = all.map((s) => ({
593
- s,
594
- // An error document (a dev server's 404 that still mounted the SDK) ranks below any working
595
- // page, hidden or not: it is never the app the agent was asked about.
596
- score: s.unresponsive() ? 3 : isErrorDocument(s.page) ? 2 : s.throttled() ? 1 : 0,
597
- ms: s.lastSeenMs(),
598
- }));
638
+ return all;
639
+ }
640
+ resolve(sessionId, scope) {
641
+ if (sessionId !== undefined) {
642
+ const found = this.#sessions.get(sessionId) ?? this.#successorOf(sessionId);
643
+ if (found === undefined) {
644
+ throw new Error(this.#unknownSessionError(sessionId));
645
+ }
646
+ found.markAgentActivity(); // liveness — a targeted tool keeps the session alive / revives it
647
+ return found;
648
+ }
649
+ if (0 === this.#sessions.size) {
650
+ const closure = this.lastClosure();
651
+ // A diagnosis beats a checklist: it names which of the three causes this actually is.
652
+ const hint = this.#noSessionHint?.();
653
+ // BOTH, when there are both — the two answer different questions and the pair is strictly
654
+ // more than either. This used to be an either/or, and a recorded refusal DISCARDED the whole
655
+ // diagnosis: the one case where the daemon has hard evidence about why nothing is connected
656
+ // was also the one case where it threw away everything else it had learned, including the
657
+ // port scan and the next action. The refusal leads, because it is the fact.
658
+ const refusal = closure === undefined
659
+ ? ''
660
+ : ` NOTE: the bridge REFUSED or closed a connection recently — "${closure.reason}". The app is probably still running and trying to connect: it was turned away, and the SDK does not retry after a policy close. The reason above names the fix — do not go looking for a stopped dev server.`;
661
+ // Hand the branch code to the refusal that is about to be reported for this throw. The
662
+ // diagnosis is computed HERE and the refusal is classified from the message downstream, so
663
+ // without this every no-session refusal lands in one undifferentiated bucket (#615).
664
+ notePendingNoSessionReason(this.#noSessionReason?.());
665
+ throw new Error(`${hint ?? NO_SESSION_CONNECTED_ERROR}${refusal}${NO_SESSION_NAVIGATE_FIX}`);
666
+ }
667
+ const all = this.#candidates(scope);
668
+ if (1 === all.length) {
669
+ const [only] = all;
670
+ if (only === undefined) {
671
+ throw new Error('session lookup failed');
672
+ }
673
+ only.markAgentActivity();
674
+ return only;
675
+ }
676
+ // Multiple sessions: score each (lower = better candidate for auto-selection). See rankOf().
677
+ const scored = all.map((s) => ({ s, score: rankOf(s), ms: s.lastSeenMs() }));
599
678
  const bestScore = Math.min(...scored.map((x) => x.score));
600
679
  const candidates = scored.filter((x) => x.score === bestScore);
601
680
  // Sort candidates by recency (ascending lastSeenMs = most recently active first).
602
681
  candidates.sort((a, b) => a.ms - b.ms);
603
682
  const [best, runnerUp] = candidates;
604
- if (best === undefined)
683
+ if (best === undefined) {
605
684
  throw new Error('session lookup failed');
685
+ }
606
686
  // Only auto-select if there is a clear winner.
607
687
  //
608
688
  // When at least one non-throttled (focused/visible) session exists, require a >1 s recency
@@ -636,6 +716,44 @@ export class SessionManager {
636
716
  best.s.markAgentActivity();
637
717
  return best.s;
638
718
  }
719
+ /**
720
+ * Resolve the session for a call that only some sessions can serve.
721
+ *
722
+ * With no sessionId, resolve() chooses by health and recency alone. A lease that could resize the
723
+ * page lost to a focused tab that could not, and the call was refused (#1959). Here, when exactly
724
+ * one candidate can serve the call, that candidate is chosen.
725
+ */
726
+ async resolveServing(sessionId, canServe) {
727
+ // A named session, or none connected: resolve() already gives the answer and its refusals.
728
+ if (sessionId !== undefined || 0 === this.#sessions.size) {
729
+ return this.resolve(sessionId);
730
+ }
731
+ // The same candidates resolve() chooses from, so the project and checkout scopes still apply.
732
+ // canServe can ask a browser, so each answer is awaited in turn.
733
+ const serving = [];
734
+ for (const candidate of this.#candidates()) {
735
+ if (await canServe(candidate)) {
736
+ serving.push(candidate);
737
+ }
738
+ }
739
+ // Exactly one can serve the call: there is nothing to guess.
740
+ const [onlyServing, secondServing] = serving;
741
+ if (onlyServing !== undefined && secondServing === undefined) {
742
+ onlyServing.markAgentActivity();
743
+ return onlyServing;
744
+ }
745
+ // None can serve: return the usual choice, so the tool gives its own refusal and its advice.
746
+ // Several can serve, and the usual choice cannot: choosing among them is a guess, so refuse
747
+ // and name them. canServe admits only the caller's own sessions, so no other agent's lease is
748
+ // named here.
749
+ const chosen = this.resolve();
750
+ if (0 < serving.length && !serving.includes(chosen)) {
751
+ const ids = serving.map((s) => `'${s.id}'`).join(', ');
752
+ throw new Error(`no sessionId was given, and ${String(serving.length)} sessions can serve this call: ` +
753
+ `${ids}. Pass sessionId to choose one.`);
754
+ }
755
+ return chosen;
756
+ }
639
757
  }
640
758
  /**
641
759
  * Which of a project's tabs belong to the agent's own git checkout.
@@ -649,18 +767,29 @@ export class SessionManager {
649
767
  * dropped. A tab whose checkout cannot be read stays, because it can be the agent's own.
650
768
  */
651
769
  export function scopeToCallerCheckout(sessions, caller, checkoutOfTab) {
652
- if (caller === undefined)
770
+ if (caller === undefined) {
653
771
  return { sessions: [...sessions] };
772
+ }
654
773
  const placed = sessions.map((session) => ({ session, checkout: checkoutOfTab(session) }));
655
774
  const mine = placed.filter((p) => isSame(p.checkout?.root, caller.root)).map((p) => p.session);
656
- if (mine.length > 0)
775
+ if (mine.length > 0) {
657
776
  return { sessions: mine };
777
+ }
658
778
  const siblings = placed.filter((p) => isSame(p.checkout?.commonDir, caller.commonDir));
659
779
  if (siblings.length !== placed.length) {
660
780
  return { sessions: placed.filter((p) => !siblings.includes(p)).map((p) => p.session) };
661
781
  }
662
- if (0 === siblings.length)
782
+ if (0 === siblings.length) {
663
783
  return { sessions: [] };
784
+ }
785
+ // The caller is the folder the MCP proxy started in, not always where the agent works. Claude Code
786
+ // starts the proxy in the main checkout and then moves the agent into a linked worktree (#1916).
787
+ // So from the main checkout, one linked worktree that serves every tab is the agent's own: drive it.
788
+ // The tab names its `worktree`. Tabs of two worktrees, or a proxy in a linked worktree, stay refused.
789
+ const roots = new Set(siblings.map((p) => p.checkout?.root));
790
+ if (!caller.linked && 1 === roots.size && true === siblings[0]?.checkout?.linked) {
791
+ return { sessions: siblings.map((p) => p.session) };
792
+ }
664
793
  const listed = siblings
665
794
  .map((p) => `${describeCheckout(p.checkout)} (sessionId '${p.session.id}', ${p.session.url})`)
666
795
  .join(', ');
@@ -675,8 +804,9 @@ export function scopeToCallerCheckout(sessions, caller, checkoutOfTab) {
675
804
  const isSame = (a, b) => a !== undefined && samePath(a, b);
676
805
  /** `/wt/a (branch feat/a)`, or the folder alone on a detached HEAD. */
677
806
  export function describeCheckout(checkout) {
678
- if (checkout === undefined)
807
+ if (checkout === undefined) {
679
808
  return 'an unknown folder';
809
+ }
680
810
  return checkout.branch === undefined
681
811
  ? checkout.root
682
812
  : `${checkout.root} (branch ${checkout.branch})`;
@@ -1,6 +1,7 @@
1
1
  import { noteOwnReload, notePageLoaded } from '../../features/harness/drive-runs.js';
2
2
  import { asProjectId } from '@reticlehq/core';
3
3
  import { narrationArgs } from './narration-args.js';
4
+ import { sdkBehindNote } from '../../command/version/version-skew.js';
4
5
  import { refusedResult } from './page-commands/undeclared-command.js';
5
6
  import { impactSnapshot, recordImpact } from '../../memory/impact/impact-recorder.js';
6
7
  import { callingAgent } from '../../hooks/coding-agents.js';
@@ -580,9 +581,9 @@ export class Session {
580
581
  this.recordActedRef(ref);
581
582
  if (name === ReticleCommand.REFRESH)
582
583
  noteOwnReload(this.id, this.#clock());
583
- // Refused before it is sent, when the page said it does not serve this. The rule, and why
584
- // an absent declaration is not an empty list, live in `undeclared-command.ts`.
585
- const refused = refusedResult(name, this.commands, `${COMMAND_ID_PREFIX}refused`);
584
+ // Refused before it is sent when the page did not declare it. See `undeclared-command.ts`.
585
+ const behind = sdkBehindNote(this.sdkVersion);
586
+ const refused = refusedResult(name, this.commands, `${COMMAND_ID_PREFIX}refused`, behind);
586
587
  if (refused !== undefined)
587
588
  return Promise.resolve(refused);
588
589
  // The page already re-dialled under this same id. Nothing has been sent yet, so handing the