@reticlehq/server 2.12.0 → 2.13.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 (386) hide show
  1. package/SKILL.md +67 -109
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +60 -0
  253. package/dist/setup/dev-server-wait.js +65 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +187 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { runTool } from './invoke-tool.js';
3
3
  import { buildErrorPayload } from './error-recovery.js';
4
- import { mergedNameRedirect, mergedNameMessage } from './merged-name-redirect.js';
4
+ import { mergedNameRedirect, mergedNameMessage, retiredToolNames } from './merged-name-redirect.js';
5
+ import { takeVersionSkewOnto } from '../version/version-nudge.js';
5
6
  import { ReticleTool } from './tool-names.js';
6
7
  import { ADVERTISE_ALL_ENV } from './tool-surface.js';
7
8
  import { getSessionMetrics } from '../telemetry/session-metrics.js';
@@ -110,11 +111,18 @@ export function buildDynamicTools(allTools, profile) {
110
111
  name: t.name,
111
112
  summary: firstSentence(t.description),
112
113
  }));
114
+ // Names that USED to be tools, against the call that replaces each. Instructions written
115
+ // against an earlier release are a permanent fact of the product — `reticle_crawl` is still
116
+ // named by guidance in the wild — and without this the catalogue is silent about them: the
117
+ // old name is simply absent, which reads as "no such capability" rather than "renamed".
118
+ // Reaching the new name then costs an error string, or never happens.
119
+ const retired = retiredToolNames();
113
120
  return Promise.resolve({
114
121
  total: catalog.length,
115
122
  tools: catalog,
123
+ retired,
116
124
  ...profileBlock,
117
- next: `All ${catalog.length} tools above are callable, advertised or not. Load full params with reticle_tools { names:[…] }, then call reticle_run { tool, args }.`,
125
+ next: `All ${catalog.length} tools above are callable, advertised or not. Load full params with reticle_tools { names:[…] }, then call reticle_run { tool, args }. \`retired\` maps names that are no longer tools to the call that replaced each.`,
118
126
  });
119
127
  }
120
128
  // The grammar rides ONLY on a `names:[…]` reply that asked for a tool taking a predicate, so
@@ -124,9 +132,20 @@ export function buildDynamicTools(allTools, profile) {
124
132
  return Promise.resolve({
125
133
  tools: names.map((n) => {
126
134
  const t = byName.get(n);
127
- return t === undefined
135
+ if (t !== undefined)
136
+ return { name: n, description: t.description, params: paramInfo(t.inputSchema) };
137
+ // `reticle_run` has answered a merged name with its replacement for a while; the tool
138
+ // asked to DISCOVER tools still said "unknown tool", which is the wrong answer in the one
139
+ // place an agent goes to resolve a name it is unsure of. Same redirect, same derivation.
140
+ const moved = mergedNameRedirect(n);
141
+ return moved === undefined
128
142
  ? { name: n, error: 'unknown tool' }
129
- : { name: n, description: t.description, params: paramInfo(t.inputSchema) };
143
+ : {
144
+ name: n,
145
+ error: mergedNameMessage(n, moved),
146
+ tool: moved.tool,
147
+ ...(moved.action === undefined ? {} : { action: moved.action }),
148
+ };
130
149
  }),
131
150
  ...(carriesPredicate ? { predicateGrammar: predicateGrammar() } : {}),
132
151
  });
@@ -222,7 +241,7 @@ export function buildDynamicTools(allTools, profile) {
222
241
  // is reached through here: a stale ref, a paused session, a missing pairing token all came
223
242
  // back as the agent's arguments being wrong, which is advice that spends the retry.
224
243
  const message = error instanceof Error ? error.message : String(error);
225
- return { ...buildErrorPayload(message), tool: name };
244
+ return { ...takeVersionSkewOnto(buildErrorPayload(message)), tool: name };
226
245
  }
227
246
  },
228
247
  };
@@ -49,6 +49,7 @@ export declare const RECOVERY: {
49
49
  */
50
50
  readonly CONFIRM_DANGEROUS: string;
51
51
  readonly UNSUPPORTED_SURFACE: string;
52
+ readonly HOVER_NEEDS_POINTER: string;
52
53
  readonly TOKEN_REQUIRED: string;
53
54
  readonly SCOPE_MISMATCH: string;
54
55
  readonly SESSION_GONE: string;
@@ -143,6 +143,12 @@ export const RECOVERY = {
143
143
  'editor keeps its own document model, so writing to the DOM would look right and submit the old ' +
144
144
  'content. Drive the same outcome another way (a plain input, a command, a keyboard action) or ' +
145
145
  'assert on the result instead of typing it. Known gap, already reported: no need to file it.',
146
+ HOVER_NEEDS_POINTER: 'Hover needs a real pointer: a synthetic mouseover does not apply CSS :hover, and Reticle ' +
147
+ 'refuses rather than reporting the styles as applied. Acquire a tab with ' +
148
+ 'reticle_run { tool: "reticle_lease", action: "acquire", url } — reticle_lease is not ' +
149
+ 'advertised under the default profile, so it is reached through reticle_run, not called ' +
150
+ 'directly — or ask the human to drive with `reticle drive` / RETICLE_CDP_URL. This is a ' +
151
+ 'deliberate refusal, not a defect: there is nothing to report.',
146
152
  TOKEN_REQUIRED: 'The bridge binds beyond localhost and requires a pairing token. Set the same token in the SDK ' +
147
153
  'init (@reticlehq/core) and the Reticle server config, then reconnect.',
148
154
  SCOPE_MISMATCH: 'A tab IS connected — it just belongs to a different project than the daemon is scoped to, so ' +
@@ -202,6 +208,7 @@ const REASON_OF = {
202
208
  TARGET_MISSED: RefusalReason.NO_MATCH,
203
209
  FLOW_STEP_MISSING: RefusalReason.NO_MATCH,
204
210
  UNSUPPORTED_SURFACE: RefusalReason.UNSUPPORTED,
211
+ HOVER_NEEDS_POINTER: RefusalReason.UNSUPPORTED,
205
212
  NOT_EDITABLE: RefusalReason.UNSUPPORTED,
206
213
  CONFIRM_DANGEROUS: RefusalReason.UNSUPPORTED,
207
214
  WRONG_TARGET: RefusalReason.UNSUPPORTED,
@@ -247,6 +254,7 @@ const RULES = [
247
254
  // broken. Order matters: contenteditable and disabled/readonly are `cannot <verb> …` messages too,
248
255
  // so they must be tested before the general wrong-target rule.
249
256
  { match: /contenteditable/i, hint: RECOVERY.UNSUPPORTED_SURFACE },
257
+ { match: /cannot hover without a real pointer/i, hint: RECOVERY.HOVER_NEEDS_POINTER },
250
258
  { match: /cannot \w+ a (disabled|readonly) </i, hint: RECOVERY.NOT_EDITABLE },
251
259
  // Three spellings ship — "action", "native action", "WebMCP tool" — and the rule matched one, so
252
260
  // two thirds of the same deliberate refusal still read as a possible defect.
@@ -5,13 +5,14 @@ import { getSessionMetrics } from '../telemetry/session-metrics.js';
5
5
  import { getTelemetry } from '../telemetry/telemetry.js';
6
6
  import { takeUpdateNudge } from '../update/update-nudge.js';
7
7
  import { takeVersionSkew } from '../version/version-nudge.js';
8
+ import { rewriteClosedAsSkew } from '../version/version-skew.js';
8
9
  import { noteToolCall } from '../daemon/daemon-usefulness.js';
9
10
  import { bugsInResult, routeOf } from '../telemetry/bug-found.js';
10
11
  import { noteToolServed, reportToolRefused } from '../telemetry/tool-refused.js';
11
12
  import { buildErrorPayload, refusalReasonFor } from './error-recovery.js';
12
13
  import { resultIsError } from '../mcp/mcp-is-error.js';
13
14
  import { verificationOf } from '../telemetry/verification-of.js';
14
- import { asString } from './tools-helpers.js';
15
+ import { asString, sessionIdFromArgs, spentRefFromArgs } from './tools-helpers.js';
15
16
  import { EnvelopeKey } from './tool-kit.js';
16
17
  import { ReticleTool } from './tool-names.js';
17
18
  import { takeFeedbackPrompt } from './feedback-tools.js';
@@ -19,7 +20,7 @@ import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
19
20
  import { noteRefsMinted, wrongTabRefusal } from '../session/ref-provenance.js';
20
21
  import { CAPTURED_TOOLS, noteCapturedCall, noteToolDispatched, } from '../honesty/feature-capture.js';
21
22
  import { span } from '../trace.js';
22
- import { deltaForToolResult, impactSnapshot, initImpact, recordImpact, } from '../impact/impact-recorder.js';
23
+ import { defectForToolResult, deltaForToolResult, impactSnapshot, initImpact, recordImpact, } from '../impact/impact-recorder.js';
23
24
  import { frictionOf, inviteFor } from './feedback-invite.js';
24
25
  /**
25
26
  * The live-session tools whose result MUST carry the
@@ -37,6 +38,20 @@ const ACTION_TOOLS = new Set([
37
38
  ReticleTool.ACT_SEQUENCE,
38
39
  ReticleTool.ACT_AND_WAIT,
39
40
  ]);
41
+ /**
42
+ * Tools that go through Playwright CDP rather than the page SDK.
43
+ *
44
+ * Under version skew these fail with "Target page, context or browser has been closed" while every
45
+ * DOM tool against the same tab still works (#688). They are session-EXEMPT (own result contracts),
46
+ * so the bound-tool resolve above never runs for them — resolve again here so we can refuse with
47
+ * the skew sentence before Playwright lies about a closed page.
48
+ */
49
+ const CDP_TOOLS = new Set([
50
+ ReticleTool.SCREENSHOT,
51
+ ReticleTool.VISUAL_DIFF,
52
+ ReticleTool.NETWORK_MOCK,
53
+ ReticleTool.VIEWPORT,
54
+ ]);
40
55
  /**
41
56
  * Tools whose result hands the agent refs. The tab they were minted in is remembered, so the call
42
57
  * that spends one cannot be routed to a different tab without saying so — see ref-provenance.ts.
@@ -103,6 +118,10 @@ export const SESSION_EXEMPT_TOOLS = new Set([
103
118
  ReticleTool.INTENT, // reads/writes .reticle/intent.json; sessionId only picks the project
104
119
  ReticleTool.CONTEXT, // folds the journal + intent ledger; must still answer when nothing is connected
105
120
  ReticleTool.PROJECT, // reads .reticle/project.json
121
+ // Reads the team's shared memory over HTTP; sessionId only resolves WHICH project's link file to
122
+ // use. It must answer with no tab connected — "what does this project know?" is a question an
123
+ // agent asks before it has opened anything.
124
+ ReticleTool.MEMORY,
106
125
  ReticleTool.RUN_EXPORT, // reads .reticle/runs/<id>.json (verification-run artifact)
107
126
  ReticleTool.SESSION, // merged lifecycle/human-channel family (tune/yield/end/resume/messages/review/narrate)
108
127
  ReticleTool.SCREENSHOT, // own contract; provider-driven, not a live-DOM-health read
@@ -287,7 +306,7 @@ export async function runTool(tool, deps, args) {
287
306
  getSessionMetrics().recordAction();
288
307
  const settleTiming = getSessionMetrics().startToolCall(tool.name, args);
289
308
  const startedAt = Date.now();
290
- const rawSessionId = asString(args['sessionId']);
309
+ const rawSessionId = sessionIdFromArgs(args);
291
310
  const bound = SESSION_BOUND_TOOLS.has(tool.name);
292
311
  // Resolve the session identity ONCE, up front, for a live-session tool. The lease heartbeat must
293
312
  // target the session the handler will ACTUALLY drive — which, when the agent omits sessionId, is the
@@ -296,7 +315,7 @@ export async function runTool(tool, deps, args) {
296
315
  // before the handler so a long ACT_AND_WAIT is protected for its whole duration; on failure leave it
297
316
  // to the handler to throw the canonical no-session error.
298
317
  let session;
299
- if (bound) {
318
+ if (bound || CDP_TOOLS.has(tool.name)) {
300
319
  try {
301
320
  session = deps.sessions.resolve(rawSessionId);
302
321
  }
@@ -304,6 +323,13 @@ export async function runTool(tool, deps, args) {
304
323
  session = undefined;
305
324
  }
306
325
  }
326
+ // CDP tools against a skewed session: refuse with the sentence reticle_sessions already knows,
327
+ // before Playwright invents a closed page. ready:true leases still hit this path.
328
+ if (session?.versionSkew !== undefined && CDP_TOOLS.has(tool.name)) {
329
+ const message = session.versionSkew;
330
+ reportRefusal(tool.name, message);
331
+ throw new Error(message);
332
+ }
307
333
  // The read-only calls the journal never keeps, recorded HERE because this is the one dispatch
308
334
  // point every call routes through — a second recording site is a second thing to forget, and this
309
335
  // instrument exists precisely because nothing was recorded before. Recorded before the handler
@@ -348,7 +374,7 @@ export async function runTool(tool, deps, args) {
348
374
  // the guessed tab and return a green about a page nobody asked about.
349
375
  if (session !== undefined) {
350
376
  const wrongTab = wrongTabRefusal({
351
- ref: asString(args['ref']),
377
+ ref: spentRefFromArgs(args),
352
378
  explicitSessionId: rawSessionId,
353
379
  chosenSessionId: session.id,
354
380
  connected: () => deps.sessions.list(),
@@ -375,7 +401,22 @@ export async function runTool(tool, deps, args) {
375
401
  // to the agent by the MCP boundary, and discarded. Reported here rather than at that boundary
376
402
  // because there are two of them (mcp.ts and the reticle_run hatch) and this is the one place both
377
403
  // go through — a third would otherwise be invisible from the day it was added.
378
- const message = error instanceof Error ? error.message : String(error);
404
+ //
405
+ // Under version skew, Playwright's "Target page has been closed" is the wrong cause (#688): the
406
+ // page is still dialled in. Prefer the session's skew sentence when we already know the pair is
407
+ // mismatched — including for tools that are not in CDP_TOOLS (real-input fallbacks that throw).
408
+ let skewText = session?.versionSkew;
409
+ if (skewText === undefined) {
410
+ try {
411
+ skewText = deps.sessions.resolve(rawSessionId).versionSkew;
412
+ }
413
+ catch {
414
+ skewText = undefined;
415
+ }
416
+ }
417
+ const rewritten = rewriteClosedAsSkew(error, skewText);
418
+ const toThrow = rewritten ?? error;
419
+ const message = toThrow instanceof Error ? toThrow.message : String(toThrow);
379
420
  reportRefusal(tool.name, message);
380
421
  // The invitation on THIS path is the one `buildErrorPayload` attaches when it does not recognise
381
422
  // the error, and it was never counted — so the denominator excluded the commonest friction there
@@ -384,7 +425,7 @@ export async function runTool(tool, deps, args) {
384
425
  if (tool.name !== ReticleTool.RUN && buildErrorPayload(message).feedback !== undefined) {
385
426
  getSessionMetrics().recordFeedbackPrompt();
386
427
  }
387
- throw error;
428
+ throw toThrow;
388
429
  }
389
430
  finally {
390
431
  // In a `finally` so a THROWN call still settles: otherwise every failing tool would leak a
@@ -410,11 +451,20 @@ export async function runTool(tool, deps, args) {
410
451
  // The user's own record of what Reticle did for them. Same chokepoint as everything else that
411
452
  // counts, for the same reason: a second recording site is a second thing to forget. Separate
412
453
  // store from telemetry - this one never leaves the machine.
413
- recordImpact(deltaForToolResult(raw, Date.now() - startedAt, resultIsError(raw)));
454
+ const finishedAt = Date.now();
455
+ const defect = defectForToolResult(raw, finishedAt);
456
+ // Which project's ledger this call belongs to. Resolved from the SESSION being driven, not from
457
+ // wherever the daemon happens to stand: one daemon serves many projects, and recording every
458
+ // call against its own cwd is how one app's verdicts ended up on another account's dashboard.
459
+ // Falls back to the daemon root when no session can be named, which is the old behaviour.
460
+ const artifactRoot = session?.artifactRoot ?? deps.reticleRoot;
461
+ recordImpact(deltaForToolResult(raw, finishedAt - startedAt, resultIsError(raw)), { ...(defect === undefined ? {} : { defect }) }, artifactRoot);
414
462
  // ...and the tab being driven is told, so the report is live rather than a thing you reload.
415
463
  // Optional-call, not optional-chain-on-the-object: a test double is a partial Session, and a
416
464
  // courtesy push must never be the reason a tool call throws.
417
- session?.pushImpact?.(impactSnapshot);
465
+ // Bound to THIS session's root: a tab must be shown its own project's numbers, not whichever
466
+ // project the daemon was started in.
467
+ session?.pushImpact?.(() => impactSnapshot(artifactRoot));
418
468
  if (resultIsError(raw))
419
469
  reportRefusal(tool.name, raw.error);
420
470
  else {
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Lease tools — the agent-facing surface of the BrowserPool.
3
3
  *
4
- * `reticle_lease_acquire` opens a fresh isolated headless context navigated to the app URL and returns
5
- * the sessionId the app's SDK will register (stamped via __reticle_session so the lease and the session
6
- * correlate 1:1). This is the "one of 10 flows" entry point: 10 agents acquire 10 leases, the pool
7
- * keeps them in ONE browser, capped and queued. `reticle_lease_release` frees the slot.
4
+ * `reticle_lease_acquire` opens an isolated headless context navigated to the app URL and returns
5
+ * the sessionId the app's SDK will register. A second acquire on the same origin reuses the live
6
+ * lease rather than minting another tab — that second tab used to poison default session resolution
7
+ * for the rest of the run. The parallel suite still mints via `acquireLeasedSession`, because ten
8
+ * flows on one origin need isolation on purpose.
8
9
  *
9
10
  * Attach-only: the pool drives a browser against an already-running dev server — it never starts one.
10
11
  */
11
12
  import type { ToolDef } from './tool-kit.js';
13
+ /** The origin of a URL, or undefined when it does not parse. */
14
+ export declare function originOf(url: string): string | undefined;
12
15
  /**
13
16
  * Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
14
17
  * adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
@@ -40,6 +43,20 @@ export declare function resolveLeasedSessionId(sessions: {
40
43
  url?: string;
41
44
  }[];
42
45
  }, leaseId: string): string | undefined;
46
+ /**
47
+ * Carry a claimed identity across a navigation.
48
+ *
49
+ * A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
50
+ * context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
51
+ * reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
52
+ * first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
53
+ * the lease was orphaned: "no sessions are connected at all", with a browser window still open on
54
+ * screen. A leased browser that cannot survive being navigated is not a usable one.
55
+ *
56
+ * A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
57
+ * a human's own URL.
58
+ */
59
+ export declare function carryReticleIdentity(fromUrl: string | undefined, toUrl: string): string;
43
60
  /**
44
61
  * Poll until the leased tab's SDK has actually registered on the bridge, so the sessionId we return
45
62
  * is immediately usable (the page navigates first, then connects a moment later — without this wait
@@ -86,4 +103,13 @@ export declare function waitForLeasedSession(isConnected: () => boolean, sleeper
86
103
  * any other call instead of being a second, invisible dispatch path.
87
104
  */
88
105
  export declare const LEASE_ACQUIRE_TOOL: ToolDef;
106
+ /**
107
+ * What to say when a lease was taken while a real tab was already open.
108
+ *
109
+ * Not a refusal. Leases are the highest-value path for autonomous work and an agent that genuinely
110
+ * needs isolation must still get one — but a human watching their own tab cannot see a lease, so
111
+ * when both exist the visible one is the better default and the agent should hear that here, where
112
+ * it is choosing, rather than discover it when somebody asks why nothing is happening.
113
+ */
114
+ export declare const PREFER_EXISTING_NOTE = "a non-leased tab for this app was already connected \u2014 that is the one a human can see, and this lease is not. Unless you need an isolated context (a second identity, a clean profile, parallel flows), release this lease and drive that sessionId instead.";
89
115
  export declare const LEASE_TOOLS: ToolDef[];
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Lease tools — the agent-facing surface of the BrowserPool.
3
3
  *
4
- * `reticle_lease_acquire` opens a fresh isolated headless context navigated to the app URL and returns
5
- * the sessionId the app's SDK will register (stamped via __reticle_session so the lease and the session
6
- * correlate 1:1). This is the "one of 10 flows" entry point: 10 agents acquire 10 leases, the pool
7
- * keeps them in ONE browser, capped and queued. `reticle_lease_release` frees the slot.
4
+ * `reticle_lease_acquire` opens an isolated headless context navigated to the app URL and returns
5
+ * the sessionId the app's SDK will register. A second acquire on the same origin reuses the live
6
+ * lease rather than minting another tab — that second tab used to poison default session resolution
7
+ * for the rest of the run. The parallel suite still mints via `acquireLeasedSession`, because ten
8
+ * flows on one origin need isolation on purpose.
8
9
  *
9
10
  * Attach-only: the pool drives a browser against an already-running dev server — it never starts one.
10
11
  */
@@ -13,6 +14,7 @@ import { leaseNotConnectedHint } from './lease-hint.js';
13
14
  import { probeSdkMarker } from './sdk-marker-probe.js';
14
15
  import { readProjectFramework, readProjectId } from '../cli/cli-port.js';
15
16
  import { hasConnectedBefore } from '../session/connection-memory.js';
17
+ import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../session/lease-visibility.js';
16
18
  import { reticleStateHome } from '../daemon/daemon.js';
17
19
  import { RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
18
20
  import { ReticleTool } from './tool-names.js';
@@ -47,6 +49,20 @@ async function leaseEvidence(deps, port, url) {
47
49
  };
48
50
  }
49
51
  const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
52
+ /** The origin of a URL, or undefined when it does not parse. */
53
+ export function originOf(url) {
54
+ try {
55
+ return new URL(url).origin;
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ const ALREADY_HELD_LEASE = 'you already hold a lease on this origin';
62
+ function alreadyHeldHint(sessionId, origin) {
63
+ return (`${ALREADY_HELD_LEASE} (${origin}): ${sessionId} — reused it rather than opening a second ` +
64
+ 'tab, which would make every later call require sessionId. Release first if you want a fresh one.');
65
+ }
50
66
  /**
51
67
  * Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
52
68
  * adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
@@ -115,6 +131,39 @@ export function resolveLeasedSessionId(sessions, leaseId) {
115
131
  return leaseId;
116
132
  return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
117
133
  }
134
+ /**
135
+ * Carry a claimed identity across a navigation.
136
+ *
137
+ * A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
138
+ * context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
139
+ * reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
140
+ * first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
141
+ * the lease was orphaned: "no sessions are connected at all", with a browser window still open on
142
+ * screen. A leased browser that cannot survive being navigated is not a usable one.
143
+ *
144
+ * A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
145
+ * a human's own URL.
146
+ */
147
+ export function carryReticleIdentity(fromUrl, toUrl) {
148
+ const session = sessionParamOf(fromUrl);
149
+ if (session === undefined)
150
+ return toUrl;
151
+ // Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
152
+ if (sessionParamOf(toUrl) !== undefined)
153
+ return toUrl;
154
+ const projectId = projectParamOf(fromUrl);
155
+ return appendReticleParams(toUrl, session, projectId);
156
+ }
157
+ function projectParamOf(url) {
158
+ if (url === undefined)
159
+ return undefined;
160
+ try {
161
+ return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
162
+ }
163
+ catch {
164
+ return undefined;
165
+ }
166
+ }
118
167
  function sessionParamOf(url) {
119
168
  if (url === undefined)
120
169
  return undefined;
@@ -173,7 +222,7 @@ export async function waitForLeasedSession(isConnected, sleeper = sleep, attempt
173
222
  */
174
223
  export const LEASE_ACQUIRE_TOOL = {
175
224
  name: ReticleTool.LEASE_ACQUIRE,
176
- description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running and embed @reticlehq/core). Returns the sessionId the leased tab registers — pass it to other tools. The pool keeps all leases in ONE browser and caps concurrency; if at capacity this waits for a free slot. Release with reticle_lease{action:"release"} when the flow is done.',
225
+ description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running and embed @reticlehq/core). 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 at capacity this waits for a free slot. 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.',
177
226
  inputSchema: {
178
227
  url: z
179
228
  .string()
@@ -194,7 +243,21 @@ export const LEASE_ACQUIRE_TOOL = {
194
243
  .describe('Milliseconds until this lease expires if untouched. Each tool call that targets this session resets the clock. Plan your work to finish or re-acquire before this runs out.'),
195
244
  leased: z.number().describe('How many contexts are currently leased from the pool.'),
196
245
  queued: z.number().describe('How many acquires are waiting for a free slot.'),
246
+ preferExisting: z
247
+ .object({ sessionId: z.string(), note: z.string() })
248
+ .optional()
249
+ .describe('Present when a live NON-leased tab for this app was already connected. That tab is the one a human can see; this lease is not. Release this lease and drive that sessionId unless you specifically need an isolated context.'),
250
+ reused: z
251
+ .boolean()
252
+ .optional()
253
+ .describe('True when this acquire returned an existing live lease on the same origin rather than minting a second context.'),
197
254
  hint: z.string().optional(),
255
+ versionSkew: z
256
+ .string()
257
+ .optional()
258
+ .describe("Present when the leased tab's SDK disagrees with this daemon. ready is still true — the " +
259
+ 'page dialled in — but CDP-backed tools (screenshot, viewport, real input, network_mock) ' +
260
+ 'will fail until the versions match. Same sentence as reticle_sessions.versionSkew.'),
198
261
  },
199
262
  handler: async (deps, args) => {
200
263
  const pool = deps.pool;
@@ -215,6 +278,33 @@ export const LEASE_ACQUIRE_TOOL = {
215
278
  }
216
279
  }
217
280
  const projectId = asString(args['projectId']);
281
+ // Sampled BEFORE acquiring: afterwards this lease is itself a session, and the point is to name
282
+ // a tab that already existed. A human's open tab is the one they can watch, so if one is here
283
+ // the agent should be told at the moment it is choosing — not after it has gone dark on them.
284
+ const alreadyOpen = liveTabFor(deps, projectId);
285
+ const origin = originOf(url);
286
+ const existing = origin === undefined ? undefined : pool.leaseIdOnOrigin?.(origin);
287
+ if (existing !== undefined && origin !== undefined) {
288
+ if (deps.sessions.get(existing) !== undefined) {
289
+ pool.touch(existing);
290
+ return {
291
+ sessionId: existing,
292
+ url,
293
+ ready: true,
294
+ reused: true,
295
+ expiresInMs: pool.leaseTtlMs(),
296
+ leased: pool.activeCount(),
297
+ queued: pool.queuedCount(),
298
+ hint: alreadyHeldHint(existing, origin),
299
+ ...(alreadyOpen === undefined
300
+ ? {}
301
+ : { preferExisting: { sessionId: alreadyOpen, note: PREFER_EXISTING_NOTE } }),
302
+ };
303
+ }
304
+ // The lease is still held but the tab has gone — a reload dropped the session. Free the
305
+ // slot and mint, rather than handing back a dead id or leaving both contexts occupied.
306
+ await pool.release(existing);
307
+ }
218
308
  const sessionId = newLeaseId();
219
309
  const navUrl = appendReticleParams(url, sessionId, projectId);
220
310
  let lease;
@@ -240,6 +330,11 @@ export const LEASE_ACQUIRE_TOOL = {
240
330
  // reaper closes the context mid-flow. See BrowserPool.alias and #157.
241
331
  if (registeredId !== undefined)
242
332
  pool.alias(registeredId, lease.sessionId);
333
+ // The lease now exists, so any HUD a human is watching has just gone dark. Say so.
334
+ tellWatchers(deps, projectId, AGENT_DRIVING_ELSEWHERE);
335
+ // ready means the SDK dialled in — not that contracts match. Carry the skew warning on acquire
336
+ // so the agent does not learn it only after a CDP tool invents a closed page (#688).
337
+ const versionSkew = registeredId === undefined ? undefined : deps.sessions.get(registeredId)?.versionSkew;
243
338
  return {
244
339
  sessionId: registeredId ?? lease.sessionId,
245
340
  url,
@@ -247,6 +342,15 @@ export const LEASE_ACQUIRE_TOOL = {
247
342
  expiresInMs: pool.leaseTtlMs(),
248
343
  leased: pool.activeCount(),
249
344
  queued: pool.queuedCount(),
345
+ ...(alreadyOpen === undefined
346
+ ? {}
347
+ : {
348
+ preferExisting: {
349
+ sessionId: alreadyOpen,
350
+ note: PREFER_EXISTING_NOTE,
351
+ },
352
+ }),
353
+ ...(versionSkew === undefined ? {} : { versionSkew }),
250
354
  ...(ready
251
355
  ? {}
252
356
  : { hint: await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId)) }),
@@ -282,8 +386,68 @@ const LEASE_RELEASE_TOOL = {
282
386
  if (sessionId === undefined || 0 === sessionId.length) {
283
387
  throw new Error('reticle_lease{action:"release"} requires a sessionId');
284
388
  }
389
+ // Read the project BEFORE releasing: afterwards the session is gone and there is nothing to
390
+ // ask which project it belonged to.
391
+ const projectId = deps.sessions.get(sessionId)?.projectId;
285
392
  await pool.release(sessionId);
393
+ // Only once the LAST lease is gone. Announcing "live again" while another lease still drives
394
+ // would be a lie, and a HUD that says the wrong thing is worse than one that says nothing.
395
+ if (0 === pool.activeCount())
396
+ tellWatchers(deps, projectId, AGENT_DRIVING_HERE_AGAIN);
286
397
  return { released: true, leased: pool.activeCount() };
287
398
  },
288
399
  };
400
+ /**
401
+ * What to say when a lease was taken while a real tab was already open.
402
+ *
403
+ * Not a refusal. Leases are the highest-value path for autonomous work and an agent that genuinely
404
+ * needs isolation must still get one — but a human watching their own tab cannot see a lease, so
405
+ * when both exist the visible one is the better default and the agent should hear that here, where
406
+ * it is choosing, rather than discover it when somebody asks why nothing is happening.
407
+ */
408
+ export const PREFER_EXISTING_NOTE = 'a non-leased tab for this app was already connected — that is the one a human can see, and this lease is not. Unless you need an isolated context (a second identity, a clean profile, parallel flows), release this lease and drive that sessionId instead.';
409
+ /**
410
+ * The first live non-leased session for this project, if any.
411
+ *
412
+ * Reuses the watcher selector rather than re-deriving "which sessions belong to a human": one rule
413
+ * for one question, so the tab we announce to and the tab we recommend can never disagree.
414
+ */
415
+ function liveTabFor(deps, projectId) {
416
+ const pool = deps.pool;
417
+ if (pool === undefined)
418
+ return undefined;
419
+ try {
420
+ const candidates = deps.sessions
421
+ .all()
422
+ .map((session) => ({ id: session.id, projectId: session.projectId }));
423
+ return watchersToNotify(candidates, pool.leasedSessionIds(), projectId)[0];
424
+ }
425
+ catch {
426
+ return undefined;
427
+ }
428
+ }
429
+ /**
430
+ * Tell the tabs a human is watching that the agent has gone somewhere they cannot see.
431
+ *
432
+ * Fire-and-forget, and deliberately never able to fail the tool: this is a courtesy to a person, and
433
+ * a lease that succeeded must not be reported as failed because a narration could not be posted to
434
+ * some unrelated tab. `pushNarration` is already fire-and-forget; the try/catch covers a session
435
+ * that disconnects between listing it and posting to it.
436
+ */
437
+ function tellWatchers(deps, projectId, text) {
438
+ const pool = deps.pool;
439
+ if (pool === undefined)
440
+ return;
441
+ try {
442
+ const candidates = deps.sessions
443
+ .all()
444
+ .map((session) => ({ id: session.id, projectId: session.projectId }));
445
+ for (const id of watchersToNotify(candidates, pool.leasedSessionIds(), projectId)) {
446
+ deps.sessions.get(id)?.pushNarration(text);
447
+ }
448
+ }
449
+ catch {
450
+ // A HUD that missed one line is a smaller problem than a lease reported as broken.
451
+ }
452
+ }
289
453
  export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL];
@@ -8,6 +8,19 @@ interface MergedNameRedirect {
8
8
  }
9
9
  /** Where `name` went, or undefined when it is a live tool or not ours. */
10
10
  export declare function mergedNameRedirect(name: string): MergedNameRedirect | undefined;
11
+ /**
12
+ * Every retired name against the call that replaces it — the tombstones, as one block.
13
+ *
14
+ * The redirect above answers an agent that already guessed the old name. This answers the one that
15
+ * is reading the surface to find out what exists, and is the case a redirect cannot reach: nothing
16
+ * in the catalogue says `reticle_crawl` became `reticle_verify { action: "crawl" }`, so instructions
17
+ * written against an earlier release lead either to an error string or to nothing at all.
18
+ *
19
+ * Derived from the same two declarations as `BY_OLD_NAME`, for the same reason: a hand-written
20
+ * migration list is one merge away from being wrong, and wrong is worse than absent here — the agent
21
+ * retries into it.
22
+ */
23
+ export declare function retiredToolNames(): Readonly<Record<string, string>>;
11
24
  /** The sentence handed to an agent that called `name`. */
12
25
  export declare function mergedNameMessage(name: string, redirect: MergedNameRedirect): string;
13
26
  export {};
@@ -46,6 +46,28 @@ const BY_OLD_NAME = (() => {
46
46
  export function mergedNameRedirect(name) {
47
47
  return BY_OLD_NAME.get(name);
48
48
  }
49
+ /**
50
+ * Every retired name against the call that replaces it — the tombstones, as one block.
51
+ *
52
+ * The redirect above answers an agent that already guessed the old name. This answers the one that
53
+ * is reading the surface to find out what exists, and is the case a redirect cannot reach: nothing
54
+ * in the catalogue says `reticle_crawl` became `reticle_verify { action: "crawl" }`, so instructions
55
+ * written against an earlier release lead either to an error string or to nothing at all.
56
+ *
57
+ * Derived from the same two declarations as `BY_OLD_NAME`, for the same reason: a hand-written
58
+ * migration list is one merge away from being wrong, and wrong is worse than absent here — the agent
59
+ * retries into it.
60
+ */
61
+ export function retiredToolNames() {
62
+ const tombstones = {};
63
+ for (const [old, redirect] of BY_OLD_NAME) {
64
+ tombstones[old] =
65
+ redirect.action === undefined
66
+ ? `retired; ${redirect.note ?? `use ${redirect.tool}`}`
67
+ : `retired; use ${redirect.tool} { action: "${redirect.action}" }`;
68
+ }
69
+ return tombstones;
70
+ }
49
71
  /** The sentence handed to an agent that called `name`. */
50
72
  export function mergedNameMessage(name, redirect) {
51
73
  return redirect.action === undefined