@reticlehq/server 3.5.0 → 3.6.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 (328) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +16 -8
  3. package/dist/chromium-launch-options.d.ts +3 -1
  4. package/dist/chromium-launch-options.js +2 -1
  5. package/dist/command/cli/adhoc-verdict.d.ts +1 -0
  6. package/dist/command/cli/adhoc-verdict.js +6 -3
  7. package/dist/command/cli/answers/open-note.d.ts +2 -0
  8. package/dist/command/cli/answers/open-note.js +22 -0
  9. package/dist/command/cli/cli-flow-commands.js +4 -8
  10. package/dist/command/cli/cli-parse.d.ts +1 -1
  11. package/dist/command/cli/cli-parse.js +10 -10
  12. package/dist/command/cli/cli-verify.d.ts +1 -1
  13. package/dist/command/cli/cli-verify.js +35 -25
  14. package/dist/command/cli/cloud-cli.js +51 -12
  15. package/dist/command/cli/cloud-login.d.ts +8 -0
  16. package/dist/command/cli/cloud-login.js +36 -2
  17. package/dist/command/cli/demo/demo-page.js +11 -3
  18. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  19. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  20. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  21. package/dist/command/cli/drive/drive-attach.js +71 -4
  22. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  23. package/dist/command/cli/drive/drive-command.js +13 -7
  24. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  25. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  26. package/dist/command/cli/ports/port-holder.js +24 -0
  27. package/dist/command/cli/setup-mcp-cli.js +6 -4
  28. package/dist/command/cli/tutorial.d.ts +2 -2
  29. package/dist/command/cli/tutorial.js +6 -9
  30. package/dist/command/cli.d.ts +6 -0
  31. package/dist/command/cli.js +28 -22
  32. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  33. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  34. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  35. package/dist/command/daemon/daemon-resolve.js +33 -2
  36. package/dist/command/setup/agent-writer.d.ts +1 -1
  37. package/dist/command/setup/agent-writer.js +3 -2
  38. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  39. package/dist/command/setup/bringup/connect-lease.js +1 -0
  40. package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
  41. package/dist/command/setup/bringup/dev-server-wait.js +19 -0
  42. package/dist/command/setup/init/init-port.d.ts +4 -0
  43. package/dist/command/setup/init/init-port.js +10 -0
  44. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  45. package/dist/command/setup/init/init-runtime.js +18 -1
  46. package/dist/command/setup/node-effects.js +2 -1
  47. package/dist/command/setup/remaining-steps.js +2 -1
  48. package/dist/command/setup/run-setup.d.ts +5 -0
  49. package/dist/command/setup/run-setup.js +35 -27
  50. package/dist/command/setup/setup-command.d.ts +2 -1
  51. package/dist/command/setup/setup-command.js +2 -0
  52. package/dist/command/version/version-skew.js +11 -2
  53. package/dist/features/crawl/crawl-tools.js +4 -0
  54. package/dist/features/crawl/crawl.d.ts +6 -0
  55. package/dist/features/crawl/crawl.js +9 -1
  56. package/dist/features/exhaust/ledger.d.ts +7 -0
  57. package/dist/features/exhaust/ledger.js +35 -9
  58. package/dist/features/harness/app-findings.d.ts +2 -0
  59. package/dist/features/harness/app-findings.js +52 -0
  60. package/dist/features/harness/drive-report.d.ts +27 -1
  61. package/dist/features/harness/drive-report.js +162 -11
  62. package/dist/features/harness/drivers.d.ts +10 -14
  63. package/dist/features/harness/drivers.js +10 -14
  64. package/dist/features/harness/fill-values.d.ts +3 -73
  65. package/dist/features/harness/fill-values.js +3 -169
  66. package/dist/features/harness/goals.d.ts +21 -0
  67. package/dist/features/harness/goals.js +77 -0
  68. package/dist/features/harness/harness.d.ts +21 -0
  69. package/dist/features/harness/harness.js +69 -2
  70. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  71. package/dist/features/harness/platform/drive-target.js +81 -0
  72. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  73. package/dist/features/harness/platform/local-apps.js +204 -0
  74. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  75. package/dist/features/harness/platform/remote-drive.js +365 -0
  76. package/dist/features/harness/platform/script.d.ts +48 -0
  77. package/dist/features/harness/platform/script.js +128 -0
  78. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  79. package/dist/features/harness/platform/server-driver.js +179 -0
  80. package/dist/features/harness/platform-config.d.ts +7 -0
  81. package/dist/features/harness/platform-config.js +14 -2
  82. package/dist/features/harness/script/app-controls.d.ts +5 -0
  83. package/dist/features/harness/script/app-controls.js +40 -0
  84. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  85. package/dist/features/harness/script/lane-ids.js +16 -0
  86. package/dist/features/harness/script/script-run.d.ts +41 -0
  87. package/dist/features/harness/script/script-run.js +299 -0
  88. package/dist/features/license/license.d.ts +2 -2
  89. package/dist/features/visual/visual-tools.d.ts +10 -1
  90. package/dist/features/visual/visual-tools.js +40 -3
  91. package/dist/hooks/driven-by.d.ts +4 -0
  92. package/dist/hooks/driven-by.js +16 -0
  93. package/dist/hooks/hook-commands.js +15 -4
  94. package/dist/hooks/tool-log.d.ts +9 -0
  95. package/dist/hooks/tool-log.js +29 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +76 -82
  98. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  99. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  100. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  101. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  102. package/dist/judgement/runs/artifact/run-store.js +26 -1
  103. package/dist/judgement/runs/drive-run.d.ts +22 -0
  104. package/dist/judgement/runs/drive-run.js +99 -1
  105. package/dist/judgement/runs/repair-prompt.js +1 -1
  106. package/dist/judgement/runs/reticle-runner.js +8 -1
  107. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  108. package/dist/judgement/runs/verification-sync.js +4 -3
  109. package/dist/language/flows/change/gate.d.ts +10 -0
  110. package/dist/language/flows/change/gate.js +22 -0
  111. package/dist/language/flows/decision.js +34 -0
  112. package/dist/language/flows/flow-anchor.d.ts +54 -8
  113. package/dist/language/flows/flow-anchor.js +88 -3
  114. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  115. package/dist/language/flows/flow-cross-step.js +21 -0
  116. package/dist/language/flows/flow-learning.js +8 -11
  117. package/dist/language/flows/flow-mutate-tools.js +5 -3
  118. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  119. package/dist/language/flows/flow-replay-run.js +101 -94
  120. package/dist/language/flows/flow-replay.d.ts +23 -1
  121. package/dist/language/flows/flow-replay.js +92 -19
  122. package/dist/language/flows/flow-scope.js +5 -1
  123. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  124. package/dist/language/flows/flow-step-runners.js +236 -42
  125. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  126. package/dist/language/flows/flow-store-for-session.js +2 -2
  127. package/dist/language/flows/flow-tools.js +58 -34
  128. package/dist/language/flows/flows.js +5 -2
  129. package/dist/language/flows/heal-run.js +6 -4
  130. package/dist/language/flows/recording/flake.d.ts +2 -2
  131. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  132. package/dist/language/flows/recording/tape/recordings.js +40 -8
  133. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  134. package/dist/language/flows/replay-from-hud.js +51 -0
  135. package/dist/language/flows/replay.d.ts +9 -5
  136. package/dist/language/flows/replay.js +27 -9
  137. package/dist/language/flows/suite/suite-flakes.js +2 -0
  138. package/dist/language/flows/suite-selection.d.ts +2 -15
  139. package/dist/language/flows/suite-selection.js +4 -3
  140. package/dist/language/flows/verify-change-tools.js +8 -4
  141. package/dist/launch-chromium.d.ts +27 -5
  142. package/dist/launch-chromium.js +38 -8
  143. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  144. package/dist/memory/cloud/cloud-config.js +15 -0
  145. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  146. package/dist/memory/cloud/cloud-sync.js +12 -1
  147. package/dist/memory/cloud/harness-config.d.ts +26 -1
  148. package/dist/memory/cloud/harness-config.js +99 -4
  149. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  150. package/dist/memory/cloud/hud-notices-source.js +128 -0
  151. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  152. package/dist/memory/cloud/memory-scope.js +205 -0
  153. package/dist/memory/cloud/sync-cycle.d.ts +12 -3
  154. package/dist/memory/cloud/sync-cycle.js +16 -3
  155. package/dist/memory/cloud/sync-daemon.js +7 -5
  156. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  157. package/dist/memory/cloud/sync-disk.js +5 -8
  158. package/dist/memory/impact/account-watch.d.ts +19 -0
  159. package/dist/memory/impact/account-watch.js +34 -0
  160. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  161. package/dist/memory/impact/impact-recorder.js +12 -2
  162. package/dist/memory/impact/impact-store.d.ts +16 -0
  163. package/dist/memory/impact/impact-store.js +142 -26
  164. package/dist/memory/intent/intent-shard.d.ts +2 -2
  165. package/dist/memory/intent/intent-tools.js +80 -2
  166. package/dist/memory/journal/envelope.d.ts +8 -8
  167. package/dist/memory/journal/journal-recorder.js +7 -1
  168. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  169. package/dist/memory/journal/on-disk/retention.js +5 -0
  170. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  171. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  172. package/dist/memory/journal/session-end.js +6 -4
  173. package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
  174. package/dist/memory/project/artifact-root-resolver.js +94 -13
  175. package/dist/memory/project/artifact-root.d.ts +42 -5
  176. package/dist/memory/project/artifact-root.js +87 -8
  177. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  178. package/dist/memory/project/dir/reticle-dir.js +3 -1
  179. package/dist/memory/project/project-tools.js +22 -3
  180. package/dist/memory/project/session-root.d.ts +24 -1
  181. package/dist/memory/project/session-root.js +41 -5
  182. package/dist/memory/project/sync-status.d.ts +39 -0
  183. package/dist/memory/project/sync-status.js +90 -0
  184. package/dist/memory/recall/memory-tools.js +2 -0
  185. package/dist/memory/recall/project-memory.d.ts +19 -0
  186. package/dist/memory/recall/project-memory.js +45 -6
  187. package/dist/portal/bridge/bridge.d.ts +3 -1
  188. package/dist/portal/bridge/bridge.js +19 -2
  189. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  190. package/dist/portal/bridge/pairing-token.js +17 -0
  191. package/dist/portal/input/real-input.d.ts +81 -10
  192. package/dist/portal/input/real-input.js +207 -22
  193. package/dist/portal/input/viewport-tools.js +3 -3
  194. package/dist/portal/pool/browser-pool.d.ts +22 -9
  195. package/dist/portal/pool/browser-pool.js +70 -35
  196. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  197. package/dist/portal/pool/playwright-launcher.js +17 -1
  198. package/dist/portal/pool/pool-contract.d.ts +7 -4
  199. package/dist/portal/session/command-payload.d.ts +8 -0
  200. package/dist/portal/session/command-payload.js +19 -0
  201. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  202. package/dist/portal/session/dev-server/dev-command.js +61 -12
  203. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  204. package/dist/portal/session/facts/departure-note.js +47 -0
  205. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  206. package/dist/portal/session/facts/session-replaced.js +5 -0
  207. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  208. package/dist/portal/session/first-run-wiring.js +132 -0
  209. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  210. package/dist/portal/session/human/notes-ledger.js +55 -0
  211. package/dist/portal/session/human/review-store.d.ts +12 -0
  212. package/dist/portal/session/human/review-store.js +18 -10
  213. package/dist/portal/session/live-control-tools.d.ts +0 -13
  214. package/dist/portal/session/live-control-tools.js +28 -3
  215. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  216. package/dist/portal/session/no-session-diagnosis.js +15 -0
  217. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  218. package/dist/portal/session/no-session-next-action.js +31 -5
  219. package/dist/portal/session/no-session-watch.d.ts +14 -1
  220. package/dist/portal/session/no-session-watch.js +63 -10
  221. package/dist/portal/session/presence/session-mirror.js +3 -0
  222. package/dist/portal/session/serving-directory.d.ts +27 -0
  223. package/dist/portal/session/serving-directory.js +45 -0
  224. package/dist/portal/session/session-health.d.ts +7 -0
  225. package/dist/portal/session/session-health.js +6 -0
  226. package/dist/portal/session/session-info.d.ts +8 -1
  227. package/dist/portal/session/session-info.js +6 -0
  228. package/dist/portal/session/session-manager.d.ts +21 -0
  229. package/dist/portal/session/session-manager.js +44 -8
  230. package/dist/portal/session/session-reaper.js +4 -1
  231. package/dist/portal/session/session-successor.d.ts +5 -0
  232. package/dist/portal/session/session-tools.js +26 -3
  233. package/dist/portal/session/session.d.ts +7 -2
  234. package/dist/portal/session/session.js +21 -18
  235. package/dist/start-options.d.ts +6 -0
  236. package/dist/status-payload.d.ts +10 -0
  237. package/dist/status-payload.js +23 -0
  238. package/dist/surface/http-server.js +11 -1
  239. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  240. package/dist/surface/mcp/mcp-proxy.js +36 -5
  241. package/dist/surface/mcp/mcp.d.ts +11 -6
  242. package/dist/surface/mcp/mcp.js +27 -13
  243. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  244. package/dist/surface/mcp/server-instructions.js +14 -4
  245. package/dist/surface/mcp/shared-params.js +2 -2
  246. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  247. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  248. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  249. package/dist/surface/tools/act-sequence-tool.js +2 -2
  250. package/dist/surface/tools/act-tools.js +23 -7
  251. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  252. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  253. package/dist/surface/tools/browser-tools.js +8 -1
  254. package/dist/surface/tools/chat-drives.d.ts +26 -0
  255. package/dist/surface/tools/chat-drives.js +165 -0
  256. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  257. package/dist/surface/tools/empty-session-list.js +80 -0
  258. package/dist/surface/tools/error-recovery.d.ts +6 -0
  259. package/dist/surface/tools/error-recovery.js +37 -4
  260. package/dist/surface/tools/explore-tools.d.ts +10 -1
  261. package/dist/surface/tools/explore-tools.js +82 -13
  262. package/dist/surface/tools/feedback-tools.js +14 -0
  263. package/dist/surface/tools/harness-explore.d.ts +96 -14
  264. package/dist/surface/tools/harness-explore.js +227 -206
  265. package/dist/surface/tools/harness-plan.d.ts +12 -0
  266. package/dist/surface/tools/harness-plan.js +31 -0
  267. package/dist/surface/tools/harness-script.d.ts +8 -0
  268. package/dist/surface/tools/harness-script.js +342 -0
  269. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  270. package/dist/surface/tools/harness-toolset.js +77 -6
  271. package/dist/surface/tools/invoke-tool.js +52 -1
  272. package/dist/surface/tools/lease-seed.d.ts +29 -0
  273. package/dist/surface/tools/lease-seed.js +123 -0
  274. package/dist/surface/tools/lease-tools.d.ts +3 -23
  275. package/dist/surface/tools/lease-tools.js +30 -131
  276. package/dist/surface/tools/live-call-text.js +2 -2
  277. package/dist/surface/tools/next-step.d.ts +19 -0
  278. package/dist/surface/tools/next-step.js +159 -0
  279. package/dist/surface/tools/observe-tools.js +2 -4
  280. package/dist/surface/tools/platform-moment.d.ts +12 -0
  281. package/dist/surface/tools/platform-moment.js +168 -0
  282. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  283. package/dist/surface/tools/query-hint-schema.js +10 -0
  284. package/dist/surface/tools/read-tools.js +8 -2
  285. package/dist/surface/tools/real-input-attempt.js +96 -13
  286. package/dist/surface/tools/tool-kit.d.ts +15 -1
  287. package/dist/surface/tools/tool-kit.js +4 -6
  288. package/dist/surface/tools/tools.js +20 -48
  289. package/dist/telemetry/hud-metrics.d.ts +2 -0
  290. package/dist/telemetry/hud-metrics.js +17 -0
  291. package/dist/telemetry/telemetry.d.ts +12 -0
  292. package/dist/telemetry/telemetry.js +43 -14
  293. package/dist/telemetry/verification-of.js +1 -0
  294. package/dist/wire-journal.js +4 -1
  295. package/docs/README.md +1 -1
  296. package/docs/agent-cheatsheet.md +1 -1
  297. package/docs/autodrive.md +29 -25
  298. package/docs/cli/doctor.mdx +6 -0
  299. package/docs/cli/drive.mdx +8 -2
  300. package/docs/cli/init.mdx +11 -13
  301. package/docs/cli/status.mdx +5 -1
  302. package/docs/cli/version.mdx +3 -3
  303. package/docs/cli.mdx +1 -1
  304. package/docs/desktop.mdx +5 -1
  305. package/docs/environment.mdx +3 -7
  306. package/docs/getting-started.md +13 -11
  307. package/docs/human-control.md +1 -2
  308. package/docs/index.mdx +4 -3
  309. package/docs/install-manual.mdx +2 -2
  310. package/docs/packages/tauri.mdx +2 -2
  311. package/docs/packages/vite-plugin.mdx +22 -1
  312. package/docs/packages.mdx +1 -1
  313. package/docs/quickstart.mdx +17 -7
  314. package/docs/telemetry.md +1 -1
  315. package/docs/tools/session-and-feedback.mdx +9 -9
  316. package/docs/usage.md +5 -2
  317. package/docs/what-is-recorded.md +13 -3
  318. package/docs/what-is-reticle.mdx +3 -1
  319. package/package.json +8 -12
  320. package/dist/features/harness/driver.d.ts +0 -50
  321. package/dist/features/harness/driver.js +0 -236
  322. package/dist/features/harness/jev-driver.d.ts +0 -188
  323. package/dist/features/harness/jev-driver.js +0 -678
  324. package/dist/features/harness/openai-driver.d.ts +0 -58
  325. package/dist/features/harness/openai-driver.js +0 -206
  326. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  327. package/dist/memory/project/dir/fill-value-store.js +0 -88
  328. package/docs/local-registry.md +0 -143
@@ -10,7 +10,7 @@
10
10
  * the thin Playwright adapter that satisfies these interfaces lives separately.
11
11
  */
12
12
  import { type SeedStorage } from '@reticlehq/core';
13
- import type { Launcher, PooledMockRule } from './pool-contract.js';
13
+ import type { Launcher, PooledMockRule, ScreenshotOptions } from './pool-contract.js';
14
14
  export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, } from './pool-contract.js';
15
15
  /** A leased context+page for one flow. `release` frees the slot and closes the context. */
16
16
  export interface Lease {
@@ -42,9 +42,17 @@ interface BrowserPoolOptions {
42
42
  * the ~450KB build is only read if some page ever needs it; undefined when there is none to give.
43
43
  */
44
44
  zeroInstallScript?: () => string | undefined;
45
+ /**
46
+ * Launches a browser with a window, for a lease somebody wants to watch. Launched only when the
47
+ * first headed lease is asked for. Absent: a headed lease is refused.
48
+ */
49
+ launchHeaded?: Launcher;
45
50
  }
51
+ /** What a headed lease is refused with where no window can be opened. */
52
+ export declare const NO_HEADED_BROWSER = "no headed browser can be opened here";
46
53
  /**
47
- * Owns a single browser and leases isolated contexts out of it, capped at `maxContexts`.
54
+ * Owns a headless browser (and, when asked, a headed one) and leases isolated contexts out of them,
55
+ * capped at `maxContexts` across both.
48
56
  * Not exported as a singleton — the daemon owns one instance.
49
57
  */
50
58
  export declare class BrowserPool {
@@ -60,15 +68,17 @@ export declare class BrowserPool {
60
68
  /** The sessionIds of every currently leased context — used to report/group leased sessions. */
61
69
  leasedSessionIds(): string[];
62
70
  /**
63
- * The public session id of an active lease on this origin, if we already hold one.
71
+ * The public session id of an active lease on this origin that `owner` took, if there is one.
64
72
  *
65
73
  * The lease TOOL reuses that id instead of minting a second context: a second acquire on the same
66
- * origin used to leave both tabs connected and poison default session resolution (#600). The pool's
67
- * own `acquire` still mints — the parallel suite needs isolation on purpose.
74
+ * origin used to leave both tabs connected and poison default session resolution (#600). Only the
75
+ * caller that took the lease gets it back: handing it to whoever asked put two agents in one tab,
76
+ * each one's verdicts resting on the other's actions (#1226). A caller with no owner cannot be told
77
+ * apart from another, so it always gets its own. The pool's own `acquire` still mints.
68
78
  *
69
79
  * Prefers the alias the agent was given, when the app registered under its own name.
70
80
  */
71
- leaseIdOnOrigin(origin: string): string | undefined;
81
+ leaseIdOnOrigin(origin: string, owner: string | undefined): string | undefined;
72
82
  /**
73
83
  * Release a lease by its sessionId (closes the context, frees the slot). No-op if unknown.
74
84
  *
@@ -106,9 +116,7 @@ export declare class BrowserPool {
106
116
  * Touches the lease like any other tool call, so capturing keeps it alive rather than letting the
107
117
  * reaper take it out from under a diff.
108
118
  */
109
- screenshotLease(sessionId: string, opts?: {
110
- fullPage?: boolean;
111
- }): Promise<Uint8Array | undefined>;
119
+ screenshotLease(sessionId: string, opts?: ScreenshotOptions): Promise<Uint8Array | undefined>;
112
120
  /**
113
121
  * Move the real pointer on a leased page, or false when this session is not a lease or cannot
114
122
  * hover. Same optional-capability contract as screenshotLease: absence must read as "could not
@@ -167,6 +175,9 @@ export declare class BrowserPool {
167
175
  signal?: AbortSignal;
168
176
  sessionId?: string;
169
177
  seedStorage?: SeedStorage;
178
+ owner?: string;
179
+ /** In a browser with a window the person can watch. */
180
+ headed?: boolean;
170
181
  }): Promise<Lease>;
171
182
  /**
172
183
  * The websocket address this lease's page reported it could not reach, if it reported one.
@@ -181,4 +192,6 @@ export declare class BrowserPool {
181
192
  lastDialogMessage(sessionId: string): string | undefined;
182
193
  /** Close every context and the browser. Pending waiters are rejected (the pool is terminal now). */
183
194
  shutdown(): Promise<void>;
195
+ /** Whether this lease lives in the headed browser. */
196
+ isHeaded(sessionId: string): boolean;
184
197
  }
@@ -20,20 +20,22 @@ const DEFAULT_NAV_TIMEOUT_MS = 30_000;
20
20
  * this is slack for a burst sweep, not a history.
21
21
  */
22
22
  const MAX_REMEMBERED_REAPED_LEASES = 64;
23
+ /** What a headed lease is refused with where no window can be opened. */
24
+ export const NO_HEADED_BROWSER = 'no headed browser can be opened here';
23
25
  /**
24
- * Owns a single browser and leases isolated contexts out of it, capped at `maxContexts`.
26
+ * Owns a headless browser (and, when asked, a headed one) and leases isolated contexts out of them,
27
+ * capped at `maxContexts` across both.
25
28
  * Not exported as a singleton — the daemon owns one instance.
26
29
  */
27
30
  export class BrowserPool {
28
- #launch;
29
31
  #max;
30
32
  #genId;
31
33
  #now;
32
34
  #ttl;
33
35
  #zeroInstall;
34
36
  #navTimeout;
35
- #browser;
36
- #launching;
37
+ #headless;
38
+ #headed;
37
39
  #closed = false;
38
40
  /** Leases reclaimed for going stale — see reapedLeaseCount(). */
39
41
  #reapedLeases = 0;
@@ -65,7 +67,8 @@ export class BrowserPool {
65
67
  constructor(launch, opts) {
66
68
  if (opts.maxContexts < 1)
67
69
  throw new Error('maxContexts must be >= 1');
68
- this.#launch = launch;
70
+ this.#headless = { launch, headed: false };
71
+ this.#headed = { launch: opts.launchHeaded, headed: true };
69
72
  this.#max = opts.maxContexts;
70
73
  this.#genId = opts.genSessionId;
71
74
  this.#now = opts.now ?? (() => Date.now());
@@ -93,16 +96,22 @@ export class BrowserPool {
93
96
  return [...this.#active.keys()];
94
97
  }
95
98
  /**
96
- * The public session id of an active lease on this origin, if we already hold one.
99
+ * The public session id of an active lease on this origin that `owner` took, if there is one.
97
100
  *
98
101
  * The lease TOOL reuses that id instead of minting a second context: a second acquire on the same
99
- * origin used to leave both tabs connected and poison default session resolution (#600). The pool's
100
- * own `acquire` still mints — the parallel suite needs isolation on purpose.
102
+ * origin used to leave both tabs connected and poison default session resolution (#600). Only the
103
+ * caller that took the lease gets it back: handing it to whoever asked put two agents in one tab,
104
+ * each one's verdicts resting on the other's actions (#1226). A caller with no owner cannot be told
105
+ * apart from another, so it always gets its own. The pool's own `acquire` still mints.
101
106
  *
102
107
  * Prefers the alias the agent was given, when the app registered under its own name.
103
108
  */
104
- leaseIdOnOrigin(origin) {
109
+ leaseIdOnOrigin(origin, owner) {
110
+ if (owner === undefined)
111
+ return undefined;
105
112
  for (const [leaseId, lease] of this.#active) {
113
+ if (lease.owner !== owner)
114
+ continue;
106
115
  let leaseOrigin;
107
116
  try {
108
117
  leaseOrigin = new URL(lease.url).origin;
@@ -314,6 +323,10 @@ export class BrowserPool {
314
323
  async acquire(url, opts = {}) {
315
324
  if (this.#closed)
316
325
  throw new Error('browser pool is shut down');
326
+ const slot = true === opts.headed ? this.#headed : this.#headless;
327
+ // Refused before a slot is claimed, so a refusal never holds one.
328
+ if (slot.launch === undefined)
329
+ throw new Error(NO_HEADED_BROWSER);
317
330
  // #waitForSlot claims the slot synchronously (bumps #occupied) before returning, so the cap holds
318
331
  // even when many acquires race through the gate in the same tick.
319
332
  await this.#waitForSlot(opts.signal);
@@ -322,7 +335,7 @@ export class BrowserPool {
322
335
  let pending;
323
336
  let pendingDialogMessage;
324
337
  try {
325
- const browser = await this.#ensureBrowser();
338
+ const browser = await this.#ensureBrowser(slot);
326
339
  context = await browser.newContext();
327
340
  const page = await context.newPage();
328
341
  // Per-page crash isolation: if THIS renderer dies, reclaim only this lease — the shared browser
@@ -375,7 +388,7 @@ export class BrowserPool {
375
388
  // #occupied. Registering the lease now would resurrect a dead entry against a crashed browser with
376
389
  // the slot count out of sync (drifting below #active.size, eventually exceeding the cap). If we're
377
390
  // no longer the live browser, bail — the catch below closes the context and returns the slot.
378
- if (this.#browser !== browser)
391
+ if (slot.browser !== browser)
379
392
  throw new Error('browser crashed during navigation');
380
393
  let navStatus;
381
394
  if (null !== navRes && 'object' === typeof navRes && 'status' in navRes) {
@@ -395,9 +408,11 @@ export class BrowserPool {
395
408
  }
396
409
  }
397
410
  this.#active.set(sessionId, {
411
+ headed: slot.headed,
398
412
  context,
399
413
  page,
400
414
  url,
415
+ ...(opts.owner === undefined ? {} : { owner: opts.owner }),
401
416
  touchedAt: this.#now(),
402
417
  ...(pending === undefined ? {} : { dialFailureUrl: pending }),
403
418
  ...(pendingDialogMessage === undefined ? {} : { lastDialogMessage: pendingDialogMessage }),
@@ -450,17 +465,19 @@ export class BrowserPool {
450
465
  await Promise.all(contexts.map((c) => c.close().catch(() => undefined)));
451
466
  // If a launch is in flight, await it so we can close the resulting browser — otherwise it
452
467
  // resolves after shutdown returns and the Chromium process is orphaned (hundreds of MB leaked).
453
- const inflight = this.#launching;
454
- if (inflight !== undefined) {
455
- const launched = await inflight.catch(() => undefined);
456
- if (launched !== undefined && launched !== this.#browser) {
457
- await launched.close().catch(() => undefined);
468
+ for (const slot of [this.#headless, this.#headed]) {
469
+ const inflight = slot.launching;
470
+ if (inflight !== undefined) {
471
+ const launched = await inflight.catch(() => undefined);
472
+ if (launched !== undefined && launched !== slot.browser) {
473
+ await launched.close().catch(() => undefined);
474
+ }
458
475
  }
476
+ const browser = slot.browser;
477
+ slot.browser = undefined;
478
+ if (browser !== undefined)
479
+ await browser.close().catch(() => undefined);
459
480
  }
460
- const browser = this.#browser;
461
- this.#browser = undefined;
462
- if (browser !== undefined)
463
- await browser.close().catch(() => undefined);
464
481
  for (const waiter of this.#waiters.splice(0))
465
482
  waiter();
466
483
  }
@@ -532,38 +549,56 @@ export class BrowserPool {
532
549
  if (next !== undefined)
533
550
  next();
534
551
  }
535
- async #ensureBrowser() {
552
+ async #ensureBrowser(slot) {
536
553
  if (this.#closed)
537
554
  throw new Error('browser pool is shut down');
538
- if (this.#browser !== undefined && this.#browser.isConnected())
539
- return this.#browser;
555
+ if (slot.browser !== undefined && slot.browser.isConnected())
556
+ return slot.browser;
557
+ const launch = slot.launch;
558
+ if (launch === undefined)
559
+ throw new Error(NO_HEADED_BROWSER);
540
560
  // De-dupe concurrent launches: the first acquire to find no browser starts one; the rest await it.
541
- if (this.#launching === undefined) {
561
+ if (slot.launching === undefined) {
542
562
  // The in-flight promise MUST be cleared on failure as well as success. It used to be cleared only
543
563
  // in the success callback, so the first failed launch — Chromium not downloaded, a transient
544
564
  // EAGAIN — left a permanently rejected promise here, and every subsequent acquire for the daemon's
545
565
  // lifetime re-returned that same stale rejection. Running `npx playwright install` did not help;
546
566
  // only restarting the daemon did. A retry must be allowed to actually retry.
547
- this.#launching = this.#launch()
567
+ slot.launching = launch()
548
568
  .then((b) => {
549
- b.onDisconnected(() => this.#onCrash(b));
550
- this.#browser = b;
569
+ b.onDisconnected(() => this.#onCrash(slot, b));
570
+ slot.browser = b;
551
571
  return b;
552
572
  })
553
573
  .finally(() => {
554
- this.#launching = undefined;
574
+ slot.launching = undefined;
555
575
  });
556
576
  }
557
- return this.#launching;
577
+ return slot.launching;
558
578
  }
559
- /** The browser process died: drop every lease (they're invalid) and clear so the next acquire relaunches. */
560
- #onCrash(crashed) {
561
- if (this.#browser !== crashed)
579
+ /**
580
+ * A browser process died: drop the leases it held (they're invalid) and clear its slot so the next
581
+ * acquire of that kind relaunches it. The other browser's leases live on.
582
+ */
583
+ #onCrash(slot, crashed) {
584
+ if (slot.browser !== crashed)
562
585
  return; // a stale handler from a prior browser
563
- this.#browser = undefined;
564
- this.#active.clear();
565
- this.#occupied = 0; // every slot is gone with the browser
586
+ slot.browser = undefined;
587
+ let freed = 0;
588
+ for (const [id, lease] of this.#active) {
589
+ if (lease.headed !== slot.headed)
590
+ continue;
591
+ this.#active.delete(id);
592
+ freed += 1;
593
+ }
594
+ // A lease mid-acquire on the dead browser holds a slot too; it frees it in its own catch. When
595
+ // every lease belonged to this browser, nothing else can be holding one.
596
+ this.#occupied = 0 === this.#active.size ? 0 : Math.max(0, this.#occupied - freed);
566
597
  for (const waiter of this.#waiters.splice(0))
567
598
  waiter(); // let them re-claim + relaunch
568
599
  }
600
+ /** Whether this lease lives in the headed browser. */
601
+ isHeaded(sessionId) {
602
+ return true === this.#active.get(this.#leaseIdOf(sessionId))?.headed;
603
+ }
569
604
  }
@@ -6,6 +6,19 @@
6
6
  * Headless by default — the pool exists for fault-tolerant headless multi-agent testing.
7
7
  */
8
8
  import type { Launcher } from './browser-pool.js';
9
+ import type { ScreenshotOptions } from './pool-contract.js';
10
+ /** Reticle's own panel, kept out of a picture of the app: it is ours, not what the app shows. */
11
+ export declare const HIDE_RETICLE_CHROME_CSS = "[data-reticle-overlay]{display:none !important}";
12
+ /**
13
+ * How Playwright captures a leased tab. A live picture (JPEG) leaves Reticle's panel out, as the
14
+ * visual captures do; a plain PNG capture is unchanged, so no existing baseline moves.
15
+ */
16
+ export declare function screenshotOptions(opts: ScreenshotOptions): {
17
+ fullPage: boolean;
18
+ type?: 'jpeg';
19
+ quality?: number;
20
+ style?: string;
21
+ };
9
22
  /**
10
23
  * How a leased tab navigates. Pure, and exported, because the decision in it is worth a test while
11
24
  * the rest of this file needs a real Chromium.
@@ -11,6 +11,22 @@ import { getSessionMetrics } from '../../telemetry/session-metrics.js';
11
11
  import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
12
12
  import { chromiumInstallCommand, chromiumInstallDepsCommand, bundledPlaywrightVersion, } from '../../command/cli/doctor/browser/chromium-hint.js';
13
13
  import { installNetworkMocks } from '../input/network-mock.js';
14
+ /** Reticle's own panel, kept out of a picture of the app: it is ours, not what the app shows. */
15
+ export const HIDE_RETICLE_CHROME_CSS = '[data-reticle-overlay]{display:none !important}';
16
+ /**
17
+ * How Playwright captures a leased tab. A live picture (JPEG) leaves Reticle's panel out, as the
18
+ * visual captures do; a plain PNG capture is unchanged, so no existing baseline moves.
19
+ */
20
+ export function screenshotOptions(opts) {
21
+ return opts.jpegQuality === undefined
22
+ ? { fullPage: true === opts.fullPage }
23
+ : {
24
+ fullPage: true === opts.fullPage,
25
+ type: 'jpeg',
26
+ quality: opts.jpegQuality,
27
+ style: HIDE_RETICLE_CHROME_CSS,
28
+ };
29
+ }
14
30
  /**
15
31
  * How a leased tab navigates. Pure, and exported, because the decision in it is worth a test while
16
32
  * the rest of this file needs a real Chromium.
@@ -43,7 +59,7 @@ function wrapBrowser(browser) {
43
59
  close: () => page.close(),
44
60
  evaluate: (script) => page.evaluate(script),
45
61
  // Playwright returns a Buffer; Uint8Array is what the visual store and differ take.
46
- screenshot: async (opts) => new Uint8Array(await page.screenshot({ fullPage: true === opts?.fullPage })),
62
+ screenshot: async (opts) => new Uint8Array(await page.screenshot(screenshotOptions(opts ?? {}))),
47
63
  // Three moves, same as performGesture: a single move to the center can be a no-op if
48
64
  // the pointer was already there, and CSS :hover needs a native hit-test to apply.
49
65
  hover: async (x, y) => {
@@ -20,14 +20,12 @@ export interface PooledPage {
20
20
  */
21
21
  onConsole?(handler: (text: string) => void): void;
22
22
  /**
23
- * Capture the page as a PNG. OPTIONAL, like `onConsole`: a fake that does not implement it makes
23
+ * Capture the page as a PNG, or a JPEG at `jpegQuality` (a live picture, where size matters more). OPTIONAL, like `onConsole`: a fake that does not implement it makes
24
24
  * the pool report "no provider", which is the correct degradation — an absent screenshotter must
25
25
  * read as "this context cannot be captured", never as a blank image that a visual diff would then
26
26
  * compare against and pass.
27
27
  */
28
- screenshot?(opts?: {
29
- fullPage?: boolean;
30
- }): Promise<Uint8Array>;
28
+ screenshot?(opts?: ScreenshotOptions): Promise<Uint8Array>;
31
29
  /**
32
30
  * Move the real pointer to (x, y) so CSS `:hover` applies. OPTIONAL, like `screenshot`: a fake
33
31
  * that does not implement it makes hover refuse rather than dispatch a synthetic mouseover that
@@ -116,3 +114,8 @@ export interface PooledBrowser {
116
114
  }
117
115
  /** Produces a freshly launched browser. Injected so tests can supply a fake. */
118
116
  export type Launcher = () => Promise<PooledBrowser>;
117
+ /** How a page is captured: the whole page or the viewport, and JPEG at a quality instead of PNG. */
118
+ export interface ScreenshotOptions {
119
+ fullPage?: boolean;
120
+ jpegQuality?: number;
121
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The command a session sends its page, as wire JSON.
3
+ *
4
+ * Marked `by: harness` when the Harness is the one calling, so the HUD can show the person watching
5
+ * which of the two is driving their app. Absent means the connected agent, which is also what an
6
+ * older page that does not know the field assumes.
7
+ */
8
+ export declare function commandPayload(id: string, sessionId: string, name: string, args: Record<string, unknown>): string;
@@ -0,0 +1,19 @@
1
+ import { CommandActor, MessageKind } from '@reticlehq/core';
2
+ import { currentDrivenBy } from '../../hooks/driven-by.js';
3
+ /**
4
+ * The command a session sends its page, as wire JSON.
5
+ *
6
+ * Marked `by: harness` when the Harness is the one calling, so the HUD can show the person watching
7
+ * which of the two is driving their app. Absent means the connected agent, which is also what an
8
+ * older page that does not know the field assumes.
9
+ */
10
+ export function commandPayload(id, sessionId, name, args) {
11
+ return JSON.stringify({
12
+ kind: MessageKind.COMMAND,
13
+ id,
14
+ sessionId,
15
+ name,
16
+ args,
17
+ ...(currentDrivenBy() === undefined ? {} : { by: CommandActor.HARNESS }),
18
+ });
19
+ }
@@ -16,6 +16,8 @@ export interface DevCommand {
16
16
  script?: string;
17
17
  /** The port the script PINS, when it pins one. Absent means the tool picks — never a guess. */
18
18
  port?: number;
19
+ /** The app's directory, when it is not the project root. The caller must run the command there. */
20
+ directory?: string;
19
21
  }
20
22
  /**
21
23
  * The dev command for the project in `directory`, or undefined when there is not one to be sure of.
@@ -24,3 +26,13 @@ export interface DevCommand {
24
26
  * default disk reader.
25
27
  */
26
28
  export declare function detectDevCommand(directory: string, read?: (path: string) => string | undefined): DevCommand | undefined;
29
+ /**
30
+ * The dev command for the project, looking one level down when the root has none.
31
+ *
32
+ * `reticle init` is run at the repo root, and in a monorepo the app is a directory down — so the
33
+ * daemon's "this project declares no dev script" was false for a root whose `frontend/package.json`
34
+ * held `"dev": "vite"`, and the agent asked a human for a command that was on disk (reticle#1368).
35
+ * init's own workspace listing answers which directories to consider; the root's lockfile picks the
36
+ * package manager, because a workspace package usually has no lockfile of its own.
37
+ */
38
+ export declare function detectDevCommandInProject(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[]): DevCommand | undefined;
@@ -8,9 +8,9 @@
8
8
  *
9
9
  * Pure apart from one injected reader, so every case is a unit test rather than a temp directory.
10
10
  */
11
- import { readFileSync } from 'node:fs';
11
+ import { readFileSync, readdirSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
- import { desktopLaunch } from '@reticlehq/init';
13
+ import { desktopLaunch, workspacePackageDirs } from '@reticlehq/init';
14
14
  /**
15
15
  * Script names checked, in order.
16
16
  *
@@ -28,6 +28,12 @@ const DEFAULT_PACKAGE_MANAGER = 'npm';
28
28
  const PACKAGE_JSON = 'package.json';
29
29
  /** `--port 4311` / `--port=4311`, the flag every dev server in this list accepts. */
30
30
  const PORT_FLAG = /--port[= ]\s*(\d+)/;
31
+ /**
32
+ * `-p 3005`, the short form Next, Nuxt and Astro accept (reticle#1367).
33
+ *
34
+ * Anchored to a start or a whitespace so the `-p` inside a longer flag (`--pretty`) is not a port.
35
+ */
36
+ const PORT_SHORT_FLAG = /(?:^|\s)-p\s*(\d+)/;
31
37
  /** `PORT=8080 remix dev` — the other half of how a project pins its port. */
32
38
  const PORT_ENV = /(?:^|\s)PORT=(\d+)/;
33
39
  /** Reads a file, or undefined if it is not there / not readable. */
@@ -41,7 +47,7 @@ function readTextFile(path) {
41
47
  }
42
48
  /** The port the script pins, when it pins one at all. */
43
49
  function pinnedPort(script) {
44
- const match = PORT_FLAG.exec(script) ?? PORT_ENV.exec(script);
50
+ const match = PORT_FLAG.exec(script) ?? PORT_SHORT_FLAG.exec(script) ?? PORT_ENV.exec(script);
45
51
  const digits = match?.[1];
46
52
  if (digits === undefined)
47
53
  return undefined;
@@ -68,17 +74,15 @@ function scriptsOf(manifest) {
68
74
  return {};
69
75
  return scripts;
70
76
  }
71
- /**
72
- * The dev command for the project in `directory`, or undefined when there is not one to be sure of.
73
- *
74
- * `read` is injected so the whole decision is testable without a filesystem; the daemon uses the
75
- * default disk reader.
76
- */
77
- export function detectDevCommand(directory, read = readTextFile) {
77
+ /** The package manager the lockfiles under `directory` identify. Anything else is npm. */
78
+ function packageManager(directory, read) {
79
+ return (LOCKFILES.find(({ file }) => read(join(directory, file)) !== undefined)?.manager ??
80
+ DEFAULT_PACKAGE_MANAGER);
81
+ }
82
+ /** The dev command for the package in `directory`, with the manager chosen by the caller. */
83
+ function devCommandFor(directory, manager, read) {
78
84
  const manifest = readManifest(read(join(directory, PACKAGE_JSON)));
79
85
  const scripts = scriptsOf(manifest);
80
- const manager = LOCKFILES.find(({ file }) => read(join(directory, file)) !== undefined)?.manager ??
81
- DEFAULT_PACKAGE_MANAGER;
82
86
  // A desktop shell's `dev` is its renderer's dev server, which no window ever opens onto. init's
83
87
  // rule decides which launcher it is, so the command the daemon hands an agent and the one init
84
88
  // spawns are the same.
@@ -105,3 +109,48 @@ export function detectDevCommand(directory, read = readTextFile) {
105
109
  }
106
110
  return undefined;
107
111
  }
112
+ /** Directory names under `path`, or none when it cannot be read. */
113
+ function readDirNames(path) {
114
+ try {
115
+ return readdirSync(path, { withFileTypes: true })
116
+ .filter((entry) => entry.isDirectory())
117
+ .map((entry) => entry.name);
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ }
123
+ /**
124
+ * The dev command for the project in `directory`, or undefined when there is not one to be sure of.
125
+ *
126
+ * `read` is injected so the whole decision is testable without a filesystem; the daemon uses the
127
+ * default disk reader.
128
+ */
129
+ export function detectDevCommand(directory, read = readTextFile) {
130
+ return devCommandFor(directory, packageManager(directory, read), read);
131
+ }
132
+ /**
133
+ * The dev command for the project, looking one level down when the root has none.
134
+ *
135
+ * `reticle init` is run at the repo root, and in a monorepo the app is a directory down — so the
136
+ * daemon's "this project declares no dev script" was false for a root whose `frontend/package.json`
137
+ * held `"dev": "vite"`, and the agent asked a human for a command that was on disk (reticle#1368).
138
+ * init's own workspace listing answers which directories to consider; the root's lockfile picks the
139
+ * package manager, because a workspace package usually has no lockfile of its own.
140
+ */
141
+ export function detectDevCommandInProject(directory, read = readTextFile, listDirs = readDirNames) {
142
+ const manager = packageManager(directory, read);
143
+ const here = devCommandFor(directory, manager, read);
144
+ if (here !== undefined)
145
+ return here;
146
+ const packages = workspacePackageDirs({
147
+ readFile: (path) => read(join(directory, path)) ?? null,
148
+ listDirs: (path) => listDirs(join(directory, path)),
149
+ });
150
+ for (const sub of packages) {
151
+ const found = devCommandFor(join(directory, sub), manager, read);
152
+ if (found !== undefined)
153
+ return { ...found, directory: sub };
154
+ }
155
+ return undefined;
156
+ }
@@ -0,0 +1,22 @@
1
+ import { type ReticleEvent } from '@reticlehq/core';
2
+ /**
3
+ * Where the SDK last saw the page heading.
4
+ *
5
+ * The SDK emits a navigation-initiated NET_PENDING while the old document is still alive — the
6
+ * one fact that can outrun the socket close (#1256). This keeps it off `Session.url` deliberately:
7
+ * successor matching and the 5xx probe need the page the tab was ON; this is where it was GOING.
8
+ * Timestamps are session-relative milliseconds, read from the session's injected clock.
9
+ */
10
+ export declare class DepartureNote {
11
+ #private;
12
+ /** Fold one inbound event into the note. */
13
+ observe(event: ReticleEvent, elapsedMs: number): void;
14
+ /**
15
+ * The navigation target, if it is fresh enough to be this departure's story.
16
+ *
17
+ * A navigation intent from long ago is not this departure: the document survived it — it kept
18
+ * talking — so attributing a later disconnect to it would invent a story. Read at tombstone
19
+ * time, when `elapsedMs` is the disconnect time.
20
+ */
21
+ read(elapsedMs: number): string | undefined;
22
+ }
@@ -0,0 +1,47 @@
1
+ import { EventType, NetInitiator } from '@reticlehq/core';
2
+ /**
3
+ * How long a navigation-departure note stays attributable to the disconnect that follows it.
4
+ *
5
+ * The socket close follows a real departure within seconds; a note older than this belongs to a
6
+ * navigation the document survived, and blaming a later disconnect on it would invent a story.
7
+ */
8
+ const DEPARTED_TO_FRESHNESS_MS = 30_000;
9
+ /**
10
+ * Where the SDK last saw the page heading.
11
+ *
12
+ * The SDK emits a navigation-initiated NET_PENDING while the old document is still alive — the
13
+ * one fact that can outrun the socket close (#1256). This keeps it off `Session.url` deliberately:
14
+ * successor matching and the 5xx probe need the page the tab was ON; this is where it was GOING.
15
+ * Timestamps are session-relative milliseconds, read from the session's injected clock.
16
+ */
17
+ export class DepartureNote {
18
+ #note;
19
+ /** Fold one inbound event into the note. */
20
+ observe(event, elapsedMs) {
21
+ if (event.type !== EventType.NET_PENDING)
22
+ return;
23
+ // Only navigation-initiated pendings count: a fetch that never settled is in-flight work,
24
+ // not a departure.
25
+ if (event.data['initiator'] !== NetInitiator.NAVIGATION)
26
+ return;
27
+ // A download never takes the document away, so it must not explain a later disconnect.
28
+ if (true === event.data['download'])
29
+ return;
30
+ const url = event.data['url'];
31
+ if ('string' !== typeof url || 0 === url.length)
32
+ return;
33
+ this.#note = { url, at: elapsedMs };
34
+ }
35
+ /**
36
+ * The navigation target, if it is fresh enough to be this departure's story.
37
+ *
38
+ * A navigation intent from long ago is not this departure: the document survived it — it kept
39
+ * talking — so attributing a later disconnect to it would invent a story. Read at tombstone
40
+ * time, when `elapsedMs` is the disconnect time.
41
+ */
42
+ read(elapsedMs) {
43
+ if (this.#note === undefined)
44
+ return undefined;
45
+ return elapsedMs - this.#note.at <= DEPARTED_TO_FRESHNESS_MS ? this.#note.url : undefined;
46
+ }
47
+ }
@@ -33,6 +33,11 @@ export declare function isSessionReplacedError(error: unknown): boolean;
33
33
  * `PendingCommands.rejectAll` documents, and a caller that CAN wait decides what it means.
34
34
  */
35
35
  export declare const SESSION_DISCONNECTED_REASON = "session disconnected";
36
+ /**
37
+ * The close an agent's `reticle_session { action: "end", disconnect: true }` sends. The page's SDK
38
+ * prints it and stops retrying, so it says how to undo it. A close reason is capped at 123 bytes.
39
+ */
40
+ export declare const SESSION_PARKED_REASON = "detached by the agent (session end, disconnect) \u2014 restart the daemon to attach this window again";
36
41
  /**
37
42
  * Is this failure the page's transport going away, rather than an answer from the page?
38
43
  *
@@ -38,6 +38,11 @@ export function isSessionReplacedError(error) {
38
38
  * `PendingCommands.rejectAll` documents, and a caller that CAN wait decides what it means.
39
39
  */
40
40
  export const SESSION_DISCONNECTED_REASON = 'session disconnected';
41
+ /**
42
+ * The close an agent's `reticle_session { action: "end", disconnect: true }` sends. The page's SDK
43
+ * prints it and stops retrying, so it says how to undo it. A close reason is capped at 123 bytes.
44
+ */
45
+ export const SESSION_PARKED_REASON = 'detached by the agent (session end, disconnect) — restart the daemon to attach this window again';
41
46
  /**
42
47
  * Is this failure the page's transport going away, rather than an answer from the page?
43
48
  *