@reticlehq/server 3.9.0 → 3.10.1

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 (351) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +43 -2
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/init.mdx +2 -2
  322. package/docs/cli/license.mdx +9 -7
  323. package/docs/cli/restart.mdx +7 -5
  324. package/docs/cli/rollback.mdx +5 -5
  325. package/docs/cli/stop.mdx +3 -3
  326. package/docs/cli/update.mdx +2 -2
  327. package/docs/cli/verify.mdx +23 -1
  328. package/docs/cli.mdx +33 -23
  329. package/docs/deploy-checks.md +1 -1
  330. package/docs/desktop.mdx +1 -1
  331. package/docs/enterprise.md +2 -2
  332. package/docs/environment.mdx +1 -1
  333. package/docs/flows.md +11 -1
  334. package/docs/frameworks.mdx +9 -9
  335. package/docs/getting-started.md +1 -1
  336. package/docs/install-manual.mdx +1 -1
  337. package/docs/license-activation.mdx +3 -3
  338. package/docs/packages/tauri.mdx +1 -1
  339. package/docs/packages.mdx +1 -1
  340. package/docs/predicates.mdx +1 -1
  341. package/docs/quickstart.mdx +68 -49
  342. package/docs/skill-setup.md +1 -1
  343. package/docs/skill.md +14 -13
  344. package/docs/telemetry.md +2 -2
  345. package/docs/tools/navigate.mdx +1 -1
  346. package/docs/tools/verify.mdx +4 -1
  347. package/docs/troubleshooting.mdx +30 -1
  348. package/docs/usage.md +17 -14
  349. package/package.json +11 -14
  350. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  351. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -55,8 +55,9 @@ const URL_THEN_LEASE = 'Once you have that URL, open it with reticle_lease {acti
55
55
  * diagnosed the problem.
56
56
  */
57
57
  function leaseAdvice(base, facts) {
58
- if (true === facts.desktop)
58
+ if (true === facts.desktop) {
59
59
  return DESKTOP_NO_BROWSER;
60
+ }
60
61
  const caveat = leaseCaveat(facts.leaseBrowser);
61
62
  return caveat === undefined ? base : `${base} ${caveat}`;
62
63
  }
@@ -87,12 +88,14 @@ const TAB_GONE_WHAT = 'The tab was closed, navigated away, or hard-reloaded.';
87
88
  * page down is no longer the same dead end as "the human closed the tab".
88
89
  */
89
90
  function tabGoneWhat(url) {
90
- if (undefined === url || '' === url)
91
+ if (undefined === url || '' === url) {
91
92
  return TAB_GONE_WHAT;
93
+ }
92
94
  const torn = pageTornDownWhileOn(url);
93
95
  const first = torn.charAt(0);
94
- if ('' === first)
96
+ if ('' === first) {
95
97
  return TAB_GONE_WHAT;
98
+ }
96
99
  return `${first.toUpperCase()}${torn.slice(1)}. ${TAB_GONE_WHAT}`;
97
100
  }
98
101
  /**
@@ -158,6 +161,12 @@ const DO_NOT_REINSTALL = 'Re-running the install is the wrong move here: it cann
158
161
  * evidence that was never gathered.
159
162
  */
160
163
  const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
164
+ /**
165
+ * The project's own sources the scan adds to the well-known list (reticle#1367, reticle#1543).
166
+ *
167
+ * Named in the "nothing else" sentences so that "the scan is partial" says what it did check.
168
+ */
169
+ const PROJECT_PORT_SOURCES = "the port in this project's dev script and `.env` files, and the ports of earlier sessions";
161
170
  /**
162
171
  * How to say "open the app" to an agent that may have no `reticle` binary.
163
172
  *
@@ -180,8 +189,9 @@ const INIT_CMD = '`npx @reticlehq/server init`';
180
189
  */
181
190
  function slowListenerClause(facts, after = '') {
182
191
  const slow = facts.slowListeners ?? [];
183
- if (0 === slow.length)
192
+ if (0 === slow.length) {
184
193
  return '';
194
+ }
185
195
  const ports = slow.join(', ');
186
196
  const subject = 1 === slow.length ? `Port ${ports} ACCEPTED` : `Ports ${ports} ACCEPTED`;
187
197
  return (`${subject} a connection but did not answer in time, which is what a server-rendered dev ` +
@@ -216,6 +226,22 @@ function openTheApp(facts) {
216
226
  function orOpenCommand(facts) {
217
227
  return true === facts.desktop ? '' : ` (or run ${OPEN_CMD_BARE})`;
218
228
  }
229
+ /**
230
+ * The start advice's lead when processes in this folder listen. Empty when none do.
231
+ *
232
+ * Names ports and no URL. It ends where the caller's "start it" begins, so an agent whose listeners
233
+ * are only its own tools still gets the app started.
234
+ */
235
+ function folderListenerClause(facts) {
236
+ const ports = facts.folderListeners ?? [];
237
+ if (0 === ports.length) {
238
+ return '';
239
+ }
240
+ const named = ports.map((port) => `:${String(port)}`).join(', ');
241
+ return (`No port Reticle scans serves a page, but processes in this folder listen on ${named}. ` +
242
+ 'Reticle cannot tell if one is this app, because agent tools listen here too. ' +
243
+ 'If one of them IS this app, open its URL. If none is, ');
244
+ }
219
245
  /**
220
246
  * What a machine-wide port scan can and cannot say.
221
247
  *
@@ -224,7 +250,14 @@ function orOpenCommand(facts) {
224
250
  * is up and never dialled" is unsound: those ports can belong to anything, and the app's own port
225
251
  * may not be in the scanned set at all.
226
252
  */
227
- function unattributedListeners(listening) {
253
+ function unattributedListeners(facts) {
254
+ const { listening } = facts;
255
+ const folder = facts.folderListeners ?? [];
256
+ if (0 < folder.length && 0 === listening.length) {
257
+ const named = folder.map((port) => `:${String(port)}`).join(', ');
258
+ return (`No port the scan covers serves a page, but processes in this folder listen on ${named}. ` +
259
+ 'Reticle cannot tell if one is this app, because agent tools listen here too.');
260
+ }
228
261
  if (0 === listening.length) {
229
262
  return (`No listener found that I can attribute to this project: the scan covers ${SCANNED_PORTS} ` +
230
263
  'across the whole machine, so an app on any other port is invisible to it.');
@@ -242,8 +275,9 @@ function unattributedListeners(listening) {
242
275
  */
243
276
  function portMismatchClause(facts) {
244
277
  const configured = facts.projectPort;
245
- if (configured === undefined || configured === facts.port)
278
+ if (configured === undefined || configured === facts.port) {
246
279
  return '';
280
+ }
247
281
  return (` They already disagree: \`.reticle.json\` says port ${String(configured)} and this daemon is` +
248
282
  ` on ${String(facts.port)}, so the SDK is dialling ${String(configured)} and nothing is there.` +
249
283
  ` Either restart the daemon on ${String(configured)}, or update \`.reticle.json\` to` +
@@ -258,8 +292,9 @@ function portMismatchClause(facts) {
258
292
  */
259
293
  function siblingListenerClause(facts) {
260
294
  const occupied = facts.siblingListeners ?? [];
261
- if (0 === occupied.length)
295
+ if (0 === occupied.length) {
262
296
  return '';
297
+ }
263
298
  const mismatch = facts.projectPort;
264
299
  const extra = mismatch !== undefined && mismatch !== facts.port
265
300
  ? occupied.filter((port) => port !== mismatch)
@@ -291,8 +326,9 @@ function rankedCauses(facts) {
291
326
  */
292
327
  function configsElsewhereClause(facts) {
293
328
  const configs = facts.configsElsewhere ?? [];
294
- if (0 === configs.length)
329
+ if (0 === configs.length) {
295
330
  return '';
331
+ }
296
332
  const named = configs
297
333
  .map((c) => (c.projectId === undefined ? c.directory : `${c.directory} ('${c.projectId}')`))
298
334
  .join(', ');
@@ -303,10 +339,12 @@ function configsElsewhereClause(facts) {
303
339
  /** Where we looked, when we found nothing. An absence nobody can check is not evidence. */
304
340
  function searchedClause(facts) {
305
341
  const searched = facts.searchedDirectories ?? [];
306
- if (0 === searched.length)
342
+ if (0 === searched.length) {
307
343
  return '';
308
- if ((facts.configsElsewhere ?? []).length > 0)
344
+ }
345
+ if ((facts.configsElsewhere ?? []).length > 0) {
309
346
  return '';
347
+ }
310
348
  return ` I looked in: ${searched.join(', ')}. If your app is not in that list, that is the answer — this daemon is standing somewhere else.`;
311
349
  }
312
350
  /**
@@ -318,20 +356,24 @@ function searchedClause(facts) {
318
356
  */
319
357
  function stallClause(facts) {
320
358
  const { daemonUpMs, initialized, everConnected } = facts;
321
- if (everConnected)
359
+ if (everConnected) {
322
360
  return '';
323
- if (!initialized)
361
+ }
362
+ if (!initialized) {
324
363
  return '';
325
- if (daemonUpMs === undefined || daemonUpMs < STALL_AFTER_MS)
364
+ }
365
+ if (daemonUpMs === undefined || daemonUpMs < STALL_AFTER_MS) {
326
366
  return '';
367
+ }
327
368
  const minutes = Math.round(daemonUpMs / 60_000);
328
369
  return (`This daemon has been running for ${String(minutes)} minutes and no app has connected. ` +
329
370
  'The most likely cause is that the dev server was not restarted after adding the Reticle ' +
330
371
  'plugin, so the running bundle carries no SDK. Restart the dev server first. ');
331
372
  }
332
373
  function alreadyListeningClause(listening) {
333
- if (0 === listening.length)
374
+ if (0 === listening.length) {
334
375
  return '';
376
+ }
335
377
  if (1 === listening.length) {
336
378
  const port = listening[0];
337
379
  return (` Something is serving a page on :${String(port)}; it may not be this app — just open ` +
@@ -450,9 +492,9 @@ export function explainNoSession(facts) {
450
492
  // wiring is unproven, printed by a daemon holding proof that it is.
451
493
  if (true === facts.previouslyConnected) {
452
494
  const listeners = 0 === listening.length
453
- ? `${unattributedListeners(listening)} If the dev server is not running, start it first ` +
495
+ ? `${unattributedListeners(facts)} If the dev server is not running, start it first ` +
454
496
  '(the command is in `next_action`).'
455
- : unattributedListeners(listening);
497
+ : unattributedListeners(facts);
456
498
  return reason(NoSessionReason.APP_NOT_REOPENED, `${RESTARTED_LEAD} ${openTheApp(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${DO_NOT_REINSTALL} ${listeners} ${rankedCauses(facts)}`);
457
499
  }
458
500
  // A config found elsewhere outranks every "you may have no SDK" branch below: those reason from
@@ -461,7 +503,7 @@ export function explainNoSession(facts) {
461
503
  if (!initialized && (facts.configsElsewhere ?? []).length > 0) {
462
504
  return reason(NoSessionReason.CONFIG_ELSEWHERE, 'no browser session connected, and this daemon has never seen one. ' +
463
505
  `${configsElsewhereClause(facts)} ${openTheApp(facts)} ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, 'This daemon was started somewhere that is not the project, which is the normal outcome when ' +
464
- `an editor launches it from your home directory. ${unattributedListeners(listening)} ` +
506
+ `an editor launches it from your home directory. ${unattributedListeners(facts)} ` +
465
507
  `${rankedCauses(facts)}`);
466
508
  }
467
509
  if (0 === listening.length) {
@@ -481,12 +523,12 @@ export function explainNoSession(facts) {
481
523
  // is expected rather than diagnostic.
482
524
  if (!initialized) {
483
525
  return reason(NoSessionReason.NO_LISTENER_NO_CONFIG, 'no browser session connected. Two things to weigh, and neither of them is proof. ' +
484
- `(1) ${slowListenerClause(facts, ' Setting that aside: ')}Nothing is listening on the ports Reticle scans, so the ` +
485
- 'dev server may not be running: START IT YOURSELF, in the background, using the command ' +
526
+ `(1) ${slowListenerClause(facts, ' Setting that aside: ')}${folderListenerClause(facts) ||
527
+ 'Nothing is listening on the ports Reticle scans, so the dev server may not be running: '}START IT YOURSELF, in the background, using the command ` +
486
528
  'in `next_action`, and tell the human in one line that it is running. ' +
487
529
  `(2) There is no \`.reticle.json\` in ${where}, which is the file ${INIT_CMD} writes, so ` +
488
530
  `the app may carry no Reticle SDK. Check the app's OWN directory before re-running it. ` +
489
- `${ZERO_INSTALL} ${RETRY}`, `The scan covers ${SCANNED_PORTS} and nothing else, so a server on any other port is ` +
531
+ `${ZERO_INSTALL} ${RETRY}`, `The scan covers ${SCANNED_PORTS}, plus ${PROJECT_PORT_SOURCES}, and nothing else, so a server on any other port is ` +
490
532
  'invisible to it: if the app IS running, ask for its URL rather than assuming it is down, ' +
491
533
  `and open it with ${OPEN_CMD}. The missing config is not proof either — in a monorepo the ` +
492
534
  'daemon often runs at the root while the app lives in a subdirectory, and an app wired by ' +
@@ -501,15 +543,15 @@ export function explainNoSession(facts) {
501
543
  `Wait for the first compile to finish, then open the app (the command is in \`next_action\`). ${RETRY}`, `${leaseAdvice(URL_THEN_LEASE, facts)}${siblingListenerClause(facts)}`);
502
544
  }
503
545
  return reason(NoSessionReason.NO_LISTENER, `${stallClause(facts)}no browser session connected, and this daemon has never seen one. ${openTheApp(facts)} ` +
504
- 'Nothing is listening on the ports Reticle scans either, and the usual reason is a dev ' +
505
- 'server that is not running: start it yourself in the background with the command in ' +
546
+ `${folderListenerClause(facts) ||
547
+ 'Nothing is listening on the ports Reticle scans either, and the usual reason is a dev server that is not running: '}start it yourself in the background with the command in ` +
506
548
  '`next_action`, tell the human in one line that it is running, then open the app in a ' +
507
549
  `browser. ${RETRY}`,
508
550
  // The caveat is in the detail rather than omitted because the scan is NARROW: an empty result
509
551
  // is not proof of absence. Reported twice, once against a server answering 200 on :7699 and
510
552
  // once against one on :5000 under a custom hostname. The common defaults are in the scanned
511
553
  // set, which narrows the gap and cannot close it — anything passed to `--port` is invisible.
512
- `The scan covers ${SCANNED_PORTS} and nothing else, so it is not proof: a server on any other ` +
554
+ `The scan covers ${SCANNED_PORTS}, plus ${PROJECT_PORT_SOURCES}, and nothing else, so it is not proof: a server on any other ` +
513
555
  'port is invisible to it. If the app IS running, ask the human for its URL rather than ' +
514
556
  // Deliberately NOT offering reticle_lease here, and a test pins that: a lease opens a URL, and
515
557
  // if nothing is listening there is nothing at any URL to open. Asking for the real one is the
@@ -526,11 +568,11 @@ export function explainNoSession(facts) {
526
568
  'there and restart the dev server; if it has one, the app is wired and simply has no page ' +
527
569
  `open — ${OPEN_CMD}. ${ZERO_INSTALL} ${RETRY}`, 'That absence is not proof: the same one is expected in a monorepo whose daemon runs at the ' +
528
570
  'root while the app lives in a subdirectory, or in an app wired by the Vite or Babel ' +
529
- `plugin. ${unattributedListeners(listening)}${searchedClause(facts)}${siblingListenerClause(facts)}`);
571
+ `plugin. ${unattributedListeners(facts)}${searchedClause(facts)}${siblingListenerClause(facts)}`);
530
572
  }
531
573
  return reason(NoSessionReason.SDK_NOT_REACHING_DAEMON, `${stallClause(facts)}no browser session connected, and this daemon has never seen one for this project, which is ` +
532
574
  `wired for Reticle. ${openTheApp(facts)} If the page IS open and still does not appear, the SDK ` +
533
- `is not reaching this daemon (on ${String(port)}). ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${unattributedListeners(listening)} ${rankedCauses(facts)}`);
575
+ `is not reaching this daemon (on ${String(port)}). ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, `${unattributedListeners(facts)} ${rankedCauses(facts)}`);
534
576
  }
535
577
  /**
536
578
  * Pair a branch's verdict with its prose, so neither can be produced without the other.
@@ -30,6 +30,8 @@ interface NextActionFacts {
30
30
  listening: readonly number[];
31
31
  /** Scanned ports that serve a page from a process that cannot be tied to this project. */
32
32
  otherListeners?: readonly number[];
33
+ /** Ports that processes in this folder listen on. See `NoSessionFacts.folderListeners`. */
34
+ folderListeners?: readonly number[];
33
35
  dev: DevCommand | undefined;
34
36
  /**
35
37
  * File-existence predicate for the project root, pure for testing.
@@ -114,7 +114,15 @@ function neverWired(facts) {
114
114
  * The start advice's first claim when no port is this project's. A scan that heard a page it could
115
115
  * not tie to this project says so, because "nothing is listening" is then false.
116
116
  */
117
- function notThisProject(others) {
117
+ function notThisProject(others, folder = []) {
118
+ // First: the folder is nearer to this app than a machine-wide scan. It is still no URL to hand
119
+ // over, because agent tools listen in this folder too (reticle#1543).
120
+ if (0 < folder.length) {
121
+ const ports = folder.map((port) => `:${String(port)}`).join(', ');
122
+ return (`processes in this folder listen on ${ports}, and Reticle cannot tell if one is this app, ` +
123
+ 'because agent tools listen here too — if one of them IS this app, open it instead of ' +
124
+ 'starting a second one, ');
125
+ }
118
126
  if (others === undefined || 0 === others.length) {
119
127
  return 'nothing is listening on the ports Reticle scans, so the app is probably not running — ';
120
128
  }
@@ -279,9 +287,10 @@ export function nextActionFor(facts) {
279
287
  const stoppedNote = stopped === undefined
280
288
  ? ''
281
289
  : ` The session that was on ${redactUrl(stopped.url)} is gone and nothing listens there now.`;
282
- const notRunning = departedPortIsGone
290
+ const folder = facts.folderListeners ?? [];
291
+ const notRunning = departedPortIsGone && 0 === folder.length
283
292
  ? "nothing answers on the port this project's app was last on, so the app is probably not running — "
284
- : notThisProject(facts.otherListeners);
293
+ : notThisProject(facts.otherListeners, folder);
285
294
  if (dev === undefined) {
286
295
  const childApps = facts.childApps ?? [];
287
296
  if (1 < childApps.length) {
@@ -74,6 +74,12 @@ interface NoSessionWatchOptions {
74
74
  * to it. Injected for tests; a test that injects `probe` and omits this observes no folder.
75
75
  */
76
76
  listenerDirectory?: (port: number) => string | undefined;
77
+ /**
78
+ * Every TCP port on this machine with a listener (reticle#1543).
79
+ *
80
+ * Injected for tests; a test that injects `probe` and omits this observes no listener.
81
+ */
82
+ listeningPorts?: () => readonly number[];
77
83
  /**
78
84
  * What the route the last session was on answers right now, as an HTTP status, or undefined
79
85
  * when there is no answer to report.
@@ -13,11 +13,12 @@ import { probeRouteStatus } from './dev-server/route-status-probe.js';
13
13
  import { probeDevServerStates } from './dev-server/dev-server-probe.js';
14
14
  import { existsSync } from 'node:fs';
15
15
  import { homedir } from 'node:os';
16
- import { join, resolve } from 'node:path';
16
+ import { join, parse, resolve } from 'node:path';
17
17
  import { callingClientDirectory } from '../../hooks/client-directory.js';
18
18
  import { registeredElsewhere } from '../../memory/recall/registered-projects.js';
19
19
  import { explainNoSession } from './no-session-diagnosis.js';
20
20
  import { childDevApps, detectDevCommandInProject, initCommandIn, } from './dev-server/dev-command.js';
21
+ import { envFilePorts } from './dev-server/project-ports.js';
21
22
  import { nextActionFor, portOfUrl, renderNextAction } from './no-session-next-action.js';
22
23
  import { DEV_SERVER_PORTS, readProjectFramework, readProjectId, readProjectIsDesktop, readProjectPort, } from '../../command/cli/ports/resolve/cli-port.js';
23
24
  import { discoverProjectConfigs } from '../../command/cli/config/config-discovery.js';
@@ -27,10 +28,11 @@ import { daemonsServingProjectElsewhere, projectDaemonsElsewhere, projectlessNot
27
28
  import { stallUptime } from './stall-clock.js';
28
29
  import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
29
30
  import { findOccupiedSiblings } from '../../command/cli/ports/sibling-ports.js';
31
+ import { captureLookup, findListeningPorts } from '../../command/cli/ports/port-holder.js';
30
32
  import { isAuthRefusalReason } from '../bridge/auth-failure-reason.js';
31
33
  import { devServersForProject } from '@reticlehq/core';
32
34
  import { readDevServers } from '../../command/daemon/dev-servers.js';
33
- import { servingDirectoryOf } from './serving-directory.js';
35
+ import { listenerFolders, servingDirectoryOf } from './serving-directory.js';
34
36
  import { within } from './zero-install-first-call.js';
35
37
  /** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
36
38
  const REFRESH_MS = 15_000;
@@ -77,6 +79,16 @@ export function startNoSessionWatch(options) {
77
79
  (options.probe === undefined
78
80
  ? (port) => servingDirectoryOf(`${LOCALHOST}:${String(port)}`)
79
81
  : () => undefined);
82
+ const listeningPorts = options.listeningPorts ??
83
+ (options.probe === undefined
84
+ ? () => findListeningPorts(captureLookup)
85
+ : () => []);
86
+ /**
87
+ * The folder of each listener outside `listening`, `slowListeners` and the Reticle daemons.
88
+ *
89
+ * Beside `listenerDirs` and never in `listening`: see `NoSessionFacts.folderListeners`.
90
+ */
91
+ let unscannedFolders = new Map();
80
92
  let siblingListeners = [];
81
93
  /**
82
94
  * What the last-known route answered, cached beside the port scan for the same reason the scan
@@ -163,7 +175,7 @@ export function startNoSessionWatch(options) {
163
175
  if (projectId === undefined) {
164
176
  return projectlessNote(options.port, projectDaemonsElsewhere(options.port, stateDir, isAlive));
165
177
  }
166
- return splitBrainNote(options.port, daemonsServingProjectElsewhere(projectId, options.port, stateDir, isAlive, (port) => hasProjectConnectedBefore(stateDir, port, projectId)));
178
+ return splitBrainNote(options.port, daemonsServingProjectElsewhere(projectId, options.port, stateDir, isAlive, (port) => hasProjectConnectedBefore(stateDir, port, projectId)), { projectId, home: stateDir });
167
179
  };
168
180
  // Every path that registers a session goes through SessionManager.add, so this is the one hook
169
181
  // that makes the bit durable. Recorded per port + projectId so a shared 4400 cannot make one
@@ -208,6 +220,10 @@ export function startNoSessionWatch(options) {
208
220
  if (pinned !== undefined) {
209
221
  ports.add(pinned);
210
222
  }
223
+ // A script that reads its port from `.env` pins nothing in `package.json` (reticle#1543).
224
+ for (const port of envFilePorts(directory)) {
225
+ ports.add(port);
226
+ }
211
227
  const known = options.sessions.lastKnown?.();
212
228
  const knownPort = known === undefined ? undefined : portOfUrl(known.url);
213
229
  if (knownPort !== undefined) {
@@ -223,6 +239,19 @@ export function startNoSessionWatch(options) {
223
239
  }
224
240
  return [...ports];
225
241
  };
242
+ /**
243
+ * The ports that processes in `dir` listen on, without the ones in `shown`.
244
+ *
245
+ * Nothing for the home folder or a filesystem root. A daemon an editor starts there holds every
246
+ * process on the machine inside its "folder", so the answer would prove nothing.
247
+ */
248
+ const folderListeners = (dir, shown) => {
249
+ const folder = resolve(dir);
250
+ if (folder === homedir() || folder === parse(folder).root) {
251
+ return [];
252
+ }
253
+ return [...unscannedFolders.entries()].flatMap(([port, cwd]) => within(cwd, folder) && !shown.includes(port) ? [port] : []);
254
+ };
226
255
  /**
227
256
  * Is `port` demonstrably this project's app? Either its dev server announced it, or this
228
257
  * project's own last session was on it. Anything else is a guess, and a guess that opens a
@@ -300,6 +329,8 @@ export function startNoSessionWatch(options) {
300
329
  : options.probe === undefined
301
330
  ? await findOccupiedSiblings(options.port, probeDaemon)
302
331
  : [];
332
+ const reported = new Set([options.port, ...siblingListeners, ...ports, ...slowListeners]);
333
+ unscannedFolders = listenerFolders(listeningPorts().filter((port) => !reported.has(port)), unscannedFolders, listenerDirectory);
303
334
  // Asked once per departed URL, on the same background cadence as the port scan, and never
304
335
  // on the hint's own path. A url already answered is not asked again; a new departure is.
305
336
  const known = options.sessions.lastKnown?.();
@@ -447,6 +478,7 @@ export function startNoSessionWatch(options) {
447
478
  : {}),
448
479
  listening: heard,
449
480
  slowListeners: slow,
481
+ folderListeners: folderListeners(dir, heard),
450
482
  port: options.port,
451
483
  directory: dir,
452
484
  previouslyConnected,
@@ -473,6 +505,7 @@ export function startNoSessionWatch(options) {
473
505
  listening: heard,
474
506
  ...(0 === others.length ? {} : { otherListeners: others }),
475
507
  slowListeners: slow,
508
+ folderListeners: folderListeners(dir, heard),
476
509
  ...(known === undefined
477
510
  ? {}
478
511
  : {
@@ -526,6 +559,7 @@ export function startNoSessionWatch(options) {
526
559
  ...(split === undefined ? {} : { splitBrain: split }),
527
560
  listening: observedListening(),
528
561
  slowListeners,
562
+ folderListeners: folderListeners(directory, observedListening()),
529
563
  // The url the departed tab was on — the same tombstone the prose diagnosis quotes, so the
530
564
  // command and the sentence beside it name the same page.
531
565
  ...(() => {
@@ -570,6 +604,7 @@ export function startNoSessionWatch(options) {
570
604
  ...scope,
571
605
  listening: observedListening(),
572
606
  slowListeners,
607
+ folderListeners: folderListeners(directory, observedListening()),
573
608
  port: options.port,
574
609
  // The directory `initialized` was decided in. Named in the message because "there is no
575
610
  // `.reticle.json`" is a claim about ONE directory, and a reader standing somewhere else
@@ -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;