@reticlehq/server 3.4.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 (350) 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/auth/cloud-session.d.ts +4 -4
  10. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  11. package/dist/command/cli/cli-flow-commands.js +4 -8
  12. package/dist/command/cli/cli-parse.d.ts +1 -1
  13. package/dist/command/cli/cli-parse.js +13 -11
  14. package/dist/command/cli/cli-verify.d.ts +1 -1
  15. package/dist/command/cli/cli-verify.js +35 -25
  16. package/dist/command/cli/cloud-cli.js +110 -12
  17. package/dist/command/cli/cloud-login.d.ts +9 -1
  18. package/dist/command/cli/cloud-login.js +43 -8
  19. package/dist/command/cli/demo/demo-page.js +11 -3
  20. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  21. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  22. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  23. package/dist/command/cli/drive/drive-attach.js +71 -4
  24. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  25. package/dist/command/cli/drive/drive-command.js +13 -7
  26. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  27. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  28. package/dist/command/cli/ports/port-holder.js +24 -0
  29. package/dist/command/cli/setup-mcp-cli.js +15 -5
  30. package/dist/command/cli/tutorial.d.ts +2 -2
  31. package/dist/command/cli/tutorial.js +6 -9
  32. package/dist/command/cli.d.ts +6 -0
  33. package/dist/command/cli.js +45 -23
  34. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  35. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  36. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  37. package/dist/command/daemon/daemon-resolve.js +33 -2
  38. package/dist/command/setup/agent-writer.d.ts +1 -1
  39. package/dist/command/setup/agent-writer.js +3 -2
  40. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  41. package/dist/command/setup/bringup/connect-lease.js +1 -0
  42. package/dist/command/setup/bringup/dev-server-wait.d.ts +10 -0
  43. package/dist/command/setup/bringup/dev-server-wait.js +20 -1
  44. package/dist/command/setup/init/init-port.d.ts +4 -0
  45. package/dist/command/setup/init/init-port.js +10 -0
  46. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  47. package/dist/command/setup/init/init-runtime.js +71 -9
  48. package/dist/command/setup/node-effects.js +8 -2
  49. package/dist/command/setup/remaining-steps.js +2 -1
  50. package/dist/command/setup/run-setup.d.ts +7 -0
  51. package/dist/command/setup/run-setup.js +38 -25
  52. package/dist/command/setup/setup-command.d.ts +2 -1
  53. package/dist/command/setup/setup-command.js +2 -0
  54. package/dist/command/setup/setup-mcp.d.ts +2 -0
  55. package/dist/command/setup/setup-mcp.js +14 -5
  56. package/dist/command/version/peer-announce.d.ts +6 -2
  57. package/dist/command/version/peer-announce.js +8 -6
  58. package/dist/command/version/version-nudge.d.ts +17 -3
  59. package/dist/command/version/version-nudge.js +25 -4
  60. package/dist/command/version/version-skew.js +11 -2
  61. package/dist/features/crawl/crawl-tools.js +4 -0
  62. package/dist/features/crawl/crawl.d.ts +6 -0
  63. package/dist/features/crawl/crawl.js +9 -1
  64. package/dist/features/exhaust/ledger.d.ts +7 -0
  65. package/dist/features/exhaust/ledger.js +35 -9
  66. package/dist/features/harness/app-findings.d.ts +2 -0
  67. package/dist/features/harness/app-findings.js +52 -0
  68. package/dist/features/harness/drive-report.d.ts +27 -1
  69. package/dist/features/harness/drive-report.js +162 -11
  70. package/dist/features/harness/drivers.d.ts +10 -14
  71. package/dist/features/harness/drivers.js +10 -14
  72. package/dist/features/harness/fill-values.d.ts +3 -69
  73. package/dist/features/harness/fill-values.js +3 -133
  74. package/dist/features/harness/goals.d.ts +21 -0
  75. package/dist/features/harness/goals.js +77 -0
  76. package/dist/features/harness/harness.d.ts +21 -0
  77. package/dist/features/harness/harness.js +69 -2
  78. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  79. package/dist/features/harness/platform/drive-target.js +81 -0
  80. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  81. package/dist/features/harness/platform/local-apps.js +204 -0
  82. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  83. package/dist/features/harness/platform/remote-drive.js +365 -0
  84. package/dist/features/harness/platform/script.d.ts +48 -0
  85. package/dist/features/harness/platform/script.js +128 -0
  86. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  87. package/dist/features/harness/platform/server-driver.js +179 -0
  88. package/dist/features/harness/platform-config.d.ts +7 -0
  89. package/dist/features/harness/platform-config.js +14 -2
  90. package/dist/features/harness/script/app-controls.d.ts +5 -0
  91. package/dist/features/harness/script/app-controls.js +40 -0
  92. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  93. package/dist/features/harness/script/lane-ids.js +16 -0
  94. package/dist/features/harness/script/script-run.d.ts +41 -0
  95. package/dist/features/harness/script/script-run.js +299 -0
  96. package/dist/features/license/license.d.ts +2 -2
  97. package/dist/features/visual/visual-tools.d.ts +10 -1
  98. package/dist/features/visual/visual-tools.js +40 -3
  99. package/dist/hooks/driven-by.d.ts +4 -0
  100. package/dist/hooks/driven-by.js +16 -0
  101. package/dist/hooks/hook-commands.js +15 -4
  102. package/dist/hooks/tool-log.d.ts +9 -0
  103. package/dist/hooks/tool-log.js +29 -0
  104. package/dist/index.d.ts +1 -0
  105. package/dist/index.js +91 -89
  106. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  107. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  108. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  109. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  110. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  111. package/dist/judgement/runs/artifact/run-store.js +26 -1
  112. package/dist/judgement/runs/drive-run.d.ts +22 -0
  113. package/dist/judgement/runs/drive-run.js +99 -1
  114. package/dist/judgement/runs/repair-prompt.js +1 -1
  115. package/dist/judgement/runs/reticle-runner.js +8 -1
  116. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  117. package/dist/judgement/runs/verification-sync.js +4 -3
  118. package/dist/language/flows/change/gate.d.ts +10 -0
  119. package/dist/language/flows/change/gate.js +22 -0
  120. package/dist/language/flows/decision.js +34 -0
  121. package/dist/language/flows/flow-anchor.d.ts +54 -8
  122. package/dist/language/flows/flow-anchor.js +88 -3
  123. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  124. package/dist/language/flows/flow-cross-step.js +21 -0
  125. package/dist/language/flows/flow-learning.js +8 -11
  126. package/dist/language/flows/flow-mutate-tools.js +5 -3
  127. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  128. package/dist/language/flows/flow-replay-run.js +101 -94
  129. package/dist/language/flows/flow-replay.d.ts +23 -1
  130. package/dist/language/flows/flow-replay.js +92 -19
  131. package/dist/language/flows/flow-scope.js +5 -1
  132. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  133. package/dist/language/flows/flow-step-runners.js +236 -42
  134. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  135. package/dist/language/flows/flow-store-for-session.js +2 -2
  136. package/dist/language/flows/flow-tools.js +58 -34
  137. package/dist/language/flows/flows.js +5 -2
  138. package/dist/language/flows/heal-run.js +6 -4
  139. package/dist/language/flows/recording/flake.d.ts +2 -2
  140. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  141. package/dist/language/flows/recording/tape/recordings.js +40 -8
  142. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  143. package/dist/language/flows/replay-from-hud.js +51 -0
  144. package/dist/language/flows/replay.d.ts +9 -5
  145. package/dist/language/flows/replay.js +27 -9
  146. package/dist/language/flows/suite/suite-flakes.js +2 -0
  147. package/dist/language/flows/suite-selection.d.ts +2 -15
  148. package/dist/language/flows/suite-selection.js +4 -3
  149. package/dist/language/flows/verify-change-tools.js +8 -4
  150. package/dist/launch-chromium.d.ts +27 -5
  151. package/dist/launch-chromium.js +38 -8
  152. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  153. package/dist/memory/cloud/cloud-config.js +15 -0
  154. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  155. package/dist/memory/cloud/cloud-sync.js +12 -1
  156. package/dist/memory/cloud/harness-config.d.ts +26 -1
  157. package/dist/memory/cloud/harness-config.js +99 -4
  158. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  159. package/dist/memory/cloud/hud-notices-source.js +128 -0
  160. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  161. package/dist/memory/cloud/memory-scope.js +205 -0
  162. package/dist/memory/cloud/sync-cycle.d.ts +21 -3
  163. package/dist/memory/cloud/sync-cycle.js +45 -5
  164. package/dist/memory/cloud/sync-daemon.js +7 -5
  165. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  166. package/dist/memory/cloud/sync-disk.js +5 -8
  167. package/dist/memory/impact/account-watch.d.ts +19 -0
  168. package/dist/memory/impact/account-watch.js +34 -0
  169. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  170. package/dist/memory/impact/impact-recorder.js +12 -2
  171. package/dist/memory/impact/impact-store.d.ts +16 -0
  172. package/dist/memory/impact/impact-store.js +142 -26
  173. package/dist/memory/intent/intent-shard.d.ts +2 -2
  174. package/dist/memory/intent/intent-tools.js +88 -5
  175. package/dist/memory/journal/envelope.d.ts +16 -16
  176. package/dist/memory/journal/journal-query.d.ts +14 -0
  177. package/dist/memory/journal/journal-query.js +15 -0
  178. package/dist/memory/journal/journal-recorder.js +7 -1
  179. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  180. package/dist/memory/journal/on-disk/retention.js +5 -0
  181. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  182. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  183. package/dist/memory/journal/session-end.js +6 -4
  184. package/dist/memory/project/artifact-root-resolver.d.ts +36 -1
  185. package/dist/memory/project/artifact-root-resolver.js +134 -26
  186. package/dist/memory/project/artifact-root.d.ts +42 -5
  187. package/dist/memory/project/artifact-root.js +87 -8
  188. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  189. package/dist/memory/project/dir/reticle-dir.js +3 -1
  190. package/dist/memory/project/project-tools.js +22 -3
  191. package/dist/memory/project/session-root.d.ts +24 -1
  192. package/dist/memory/project/session-root.js +41 -5
  193. package/dist/memory/project/sync-status.d.ts +39 -0
  194. package/dist/memory/project/sync-status.js +90 -0
  195. package/dist/memory/recall/memory-tools.js +2 -0
  196. package/dist/memory/recall/project-memory.d.ts +19 -0
  197. package/dist/memory/recall/project-memory.js +45 -6
  198. package/dist/portal/bridge/bridge.d.ts +7 -4
  199. package/dist/portal/bridge/bridge.js +25 -3
  200. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  201. package/dist/portal/bridge/pairing-token.js +17 -0
  202. package/dist/portal/input/real-input.d.ts +81 -10
  203. package/dist/portal/input/real-input.js +207 -22
  204. package/dist/portal/input/viewport-tools.js +19 -9
  205. package/dist/portal/pool/browser-pool.d.ts +22 -9
  206. package/dist/portal/pool/browser-pool.js +70 -35
  207. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  208. package/dist/portal/pool/playwright-launcher.js +17 -1
  209. package/dist/portal/pool/pool-contract.d.ts +7 -4
  210. package/dist/portal/session/command-payload.d.ts +8 -0
  211. package/dist/portal/session/command-payload.js +19 -0
  212. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  213. package/dist/portal/session/dev-server/dev-command.js +61 -12
  214. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  215. package/dist/portal/session/facts/departure-note.js +47 -0
  216. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  217. package/dist/portal/session/facts/session-replaced.js +5 -0
  218. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  219. package/dist/portal/session/first-run-wiring.js +132 -0
  220. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  221. package/dist/portal/session/human/notes-ledger.js +55 -0
  222. package/dist/portal/session/human/review-store.d.ts +12 -0
  223. package/dist/portal/session/human/review-store.js +18 -10
  224. package/dist/portal/session/live-control-tools.d.ts +0 -13
  225. package/dist/portal/session/live-control-tools.js +28 -3
  226. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  227. package/dist/portal/session/no-session-diagnosis.js +15 -0
  228. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  229. package/dist/portal/session/no-session-next-action.js +31 -5
  230. package/dist/portal/session/no-session-watch.d.ts +14 -1
  231. package/dist/portal/session/no-session-watch.js +63 -10
  232. package/dist/portal/session/presence/session-mirror.js +3 -0
  233. package/dist/portal/session/serving-directory.d.ts +27 -0
  234. package/dist/portal/session/serving-directory.js +45 -0
  235. package/dist/portal/session/session-health.d.ts +7 -0
  236. package/dist/portal/session/session-health.js +6 -0
  237. package/dist/portal/session/session-info.d.ts +8 -1
  238. package/dist/portal/session/session-info.js +6 -0
  239. package/dist/portal/session/session-manager.d.ts +21 -0
  240. package/dist/portal/session/session-manager.js +44 -8
  241. package/dist/portal/session/session-reaper.js +4 -1
  242. package/dist/portal/session/session-successor.d.ts +5 -0
  243. package/dist/portal/session/session-tools.js +26 -3
  244. package/dist/portal/session/session.d.ts +12 -10
  245. package/dist/portal/session/session.js +30 -31
  246. package/dist/start-options.d.ts +6 -0
  247. package/dist/status-payload.d.ts +10 -0
  248. package/dist/status-payload.js +23 -0
  249. package/dist/surface/http-server.d.ts +1 -1
  250. package/dist/surface/http-server.js +15 -4
  251. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  252. package/dist/surface/mcp/mcp-proxy.js +36 -5
  253. package/dist/surface/mcp/mcp.d.ts +11 -6
  254. package/dist/surface/mcp/mcp.js +28 -14
  255. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  256. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  257. package/dist/surface/mcp/server-instructions.js +14 -4
  258. package/dist/surface/mcp/shared-params.js +2 -2
  259. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  260. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  261. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  262. package/dist/surface/tools/act-sequence-tool.js +2 -2
  263. package/dist/surface/tools/act-tools.js +25 -9
  264. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  265. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  266. package/dist/surface/tools/browser-tools.js +8 -1
  267. package/dist/surface/tools/chat-drives.d.ts +26 -0
  268. package/dist/surface/tools/chat-drives.js +165 -0
  269. package/dist/surface/tools/dynamic-tools.js +29 -2
  270. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  271. package/dist/surface/tools/empty-session-list.js +80 -0
  272. package/dist/surface/tools/error-recovery.d.ts +6 -0
  273. package/dist/surface/tools/error-recovery.js +38 -6
  274. package/dist/surface/tools/explore-tools.d.ts +10 -1
  275. package/dist/surface/tools/explore-tools.js +82 -13
  276. package/dist/surface/tools/feedback-tools.js +14 -0
  277. package/dist/surface/tools/harness-explore.d.ts +96 -14
  278. package/dist/surface/tools/harness-explore.js +227 -202
  279. package/dist/surface/tools/harness-plan.d.ts +12 -0
  280. package/dist/surface/tools/harness-plan.js +31 -0
  281. package/dist/surface/tools/harness-script.d.ts +8 -0
  282. package/dist/surface/tools/harness-script.js +342 -0
  283. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  284. package/dist/surface/tools/harness-toolset.js +77 -6
  285. package/dist/surface/tools/invoke-tool.js +64 -9
  286. package/dist/surface/tools/lease-seed.d.ts +29 -0
  287. package/dist/surface/tools/lease-seed.js +123 -0
  288. package/dist/surface/tools/lease-tools.d.ts +3 -23
  289. package/dist/surface/tools/lease-tools.js +30 -131
  290. package/dist/surface/tools/live-call-text.js +2 -2
  291. package/dist/surface/tools/next-step.d.ts +19 -0
  292. package/dist/surface/tools/next-step.js +159 -0
  293. package/dist/surface/tools/observe-tools.js +2 -4
  294. package/dist/surface/tools/platform-moment.d.ts +12 -0
  295. package/dist/surface/tools/platform-moment.js +168 -0
  296. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  297. package/dist/surface/tools/query-hint-schema.js +10 -0
  298. package/dist/surface/tools/read-tools.js +8 -2
  299. package/dist/surface/tools/real-input-attempt.js +96 -13
  300. package/dist/surface/tools/tool-kit.d.ts +25 -1
  301. package/dist/surface/tools/tool-kit.js +4 -6
  302. package/dist/surface/tools/tools.js +20 -48
  303. package/dist/telemetry/argument-shape.js +1 -1
  304. package/dist/telemetry/hud-metrics.d.ts +2 -0
  305. package/dist/telemetry/hud-metrics.js +17 -0
  306. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  307. package/dist/telemetry/onboarding-firsts.js +2 -2
  308. package/dist/telemetry/telemetry.d.ts +12 -0
  309. package/dist/telemetry/telemetry.js +43 -14
  310. package/dist/telemetry/verification-of.js +1 -0
  311. package/dist/wire-journal.js +4 -1
  312. package/docs/README.md +1 -1
  313. package/docs/actions.mdx +1 -2
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/autodrive.md +29 -25
  316. package/docs/cli/cloud.mdx +6 -4
  317. package/docs/cli/doctor.mdx +6 -0
  318. package/docs/cli/drive.mdx +8 -2
  319. package/docs/cli/init.mdx +11 -13
  320. package/docs/cli/status.mdx +5 -1
  321. package/docs/cli/version.mdx +3 -3
  322. package/docs/cli.mdx +14 -8
  323. package/docs/desktop.mdx +5 -1
  324. package/docs/environment.mdx +3 -7
  325. package/docs/getting-started.md +14 -2
  326. package/docs/human-control.md +1 -2
  327. package/docs/index.mdx +4 -3
  328. package/docs/install-agentic.mdx +1 -1
  329. package/docs/install-manual.mdx +2 -2
  330. package/docs/packages/next.mdx +3 -0
  331. package/docs/packages/tauri.mdx +2 -2
  332. package/docs/packages/vite-plugin.mdx +24 -1
  333. package/docs/packages.mdx +1 -1
  334. package/docs/quickstart.mdx +23 -11
  335. package/docs/telemetry.md +1 -1
  336. package/docs/tools/act.mdx +1 -1
  337. package/docs/tools/session-and-feedback.mdx +9 -9
  338. package/docs/usage.md +5 -3
  339. package/docs/what-is-recorded.md +13 -3
  340. package/docs/what-is-reticle.mdx +3 -1
  341. package/package.json +8 -12
  342. package/dist/features/harness/driver.d.ts +0 -50
  343. package/dist/features/harness/driver.js +0 -236
  344. package/dist/features/harness/jev-driver.d.ts +0 -188
  345. package/dist/features/harness/jev-driver.js +0 -678
  346. package/dist/features/harness/openai-driver.d.ts +0 -46
  347. package/dist/features/harness/openai-driver.js +0 -194
  348. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  349. package/dist/memory/project/dir/fill-value-store.js +0 -88
  350. package/docs/local-registry.md +0 -143
@@ -22,14 +22,14 @@
22
22
  /** The short form left in the per-turn schema. Must still make sense with no instructions at all. */
23
23
  export const SHARED_PARAM_SHORT = {
24
24
  sessionId: 'Target a specific tab. Omit unless you mean one — see instructions.',
25
- since: 'Cursor from a prior reticle_act/reticle_observe.',
25
+ since: 'Cursor from a prior act/observe, or a git ref.',
26
26
  limit: 'Cap descriptors to the first N.',
27
27
  };
28
28
  /** The full text, sent once at initialize. Everything trimmed above is restated here. */
29
29
  export const SHARED_PARAM_GUIDANCE = 'Arguments that mean the same thing on every tool: `sessionId` — omit unless you mean a specific ' +
30
30
  'tab; Reticle scopes to your project, prefers the active one, and refuses rather than guesses when ' +
31
31
  'ambiguous. `since` — a cursor from a prior act/observe, bounding an observation to what came ' +
32
- 'after it. `limit` — caps returned descriptors, cutting tokens on broad queries.';
32
+ 'after it, or a git ref when verifying a change. `limit` — caps returned descriptors, cutting tokens on broad queries.';
33
33
  /** Is this a parameter whose guidance now lives in the instructions block? */
34
34
  export function isSharedParam(name) {
35
35
  return Object.hasOwn(SHARED_PARAM_SHORT, name);
@@ -59,6 +59,14 @@ export interface ArrivalScope {
59
59
  * "the app redirected the newly arrived document somewhere else."
60
60
  */
61
61
  navigatedFrom: string;
62
+ /**
63
+ * The DOCUMENT the session was on when the navigation was dispatched.
64
+ *
65
+ * A navigation to the page already shown replaces the document without changing the URL, so the
66
+ * old document is still the connected one until the replacement reports (#1320). Only a different
67
+ * document identity proves the replacement; `undefined` on both sides proves nothing and waits.
68
+ */
69
+ navigatedFromDocumentId?: string;
62
70
  /** Sessions already on the target before dispatch cannot prove this navigation arrived. */
63
71
  priorIds: ReadonlySet<string>;
64
72
  }
@@ -42,6 +42,21 @@ function samePage(a, b) {
42
42
  return false;
43
43
  }
44
44
  }
45
+ /**
46
+ * Same origin and path, different fragment: the browser changes the URL WITHOUT replacing the
47
+ * document. There is no replacement to wait for on this path, so it must not be treated as a
48
+ * same-page navigation (#1320).
49
+ */
50
+ function sameDocumentFragmentChange(a, b) {
51
+ if (!samePage(a, b))
52
+ return false;
53
+ try {
54
+ return new URL(a).hash !== new URL(b).hash;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
45
60
  /**
46
61
  * The navigation target as an absolute URL, resolved against the tab being navigated — the same
47
62
  * resolution the browser applies. An agent passes a path (`/orders?id=4`) as often as a full URL,
@@ -70,13 +85,39 @@ export function idsAtTarget(sessions, target) {
70
85
  }
71
86
  return ids;
72
87
  }
88
+ /**
89
+ * Whether `current` is a different document from the one the navigation was dispatched on.
90
+ *
91
+ * A new Session object under the same id is not enough: the SDK also reopens its socket in place,
92
+ * and that replaces the Session while the page stays exactly where it was. What a real unload
93
+ * changes is the document id — the one sampled before dispatch (`navigatedFromDocumentId`) against
94
+ * the one the live session is reporting (`currentDocumentId`). Either side not having reported one
95
+ * yet is "cannot tell", which is never reported as a new document.
96
+ */
97
+ function documentUnloaded(scope, current) {
98
+ const before = scope.navigatedFromDocumentId;
99
+ const after = current.currentDocumentId;
100
+ return before !== undefined && after !== undefined && before !== after;
101
+ }
73
102
  function findArrival(sessions, target, scope, currentSession) {
74
103
  let arrived = null;
75
- for (const s of sessions.all()) {
104
+ // One snapshot for the whole look, so "is the driven session still connected?" and "has a
105
+ // replacement reported?" are answered from the same moment.
106
+ const snapshot = sessions.all();
107
+ const drivenPresent = snapshot.some((s) => s.id === currentSession.id);
108
+ // Navigating to the page already shown replaces its document without moving the URL, so the
109
+ // OLD document is still the connected one at the first looks; its presence there is not
110
+ // arrival until the replacement reports a different document identity (#1320). A fragment-only
111
+ // change keeps the document, so it is an arrival as soon as the URL matches.
112
+ const replacesDocument = samePage(scope.navigatedFrom, target) &&
113
+ !sameDocumentFragmentChange(scope.navigatedFrom, target);
114
+ for (const s of snapshot) {
76
115
  // The currently tracked document gets priority because it is the strongest
77
116
  // evidence of where this navigation went.
78
117
  if (s.id === currentSession.id) {
79
118
  if (samePage(s.url, target)) {
119
+ if (replacesDocument && s.currentDocumentId === scope.navigatedFromDocumentId)
120
+ continue;
80
121
  return { sessionId: s.id };
81
122
  }
82
123
  // Intermediate routes are not necessarily redirects. Keep polling so
@@ -87,6 +128,12 @@ function findArrival(sessions, target, scope, currentSession) {
87
128
  // navigation arrived.
88
129
  if (scope.priorIds.has(s.id))
89
130
  continue;
131
+ // While the replaced document is still connected, a DIFFERENT session at the target is an
132
+ // unrelated arrival (a second tab, a neighbour app): the replacement this navigation waits
133
+ // for has not reported yet, and letting the newcomer stand in would confirm too early and
134
+ // name the wrong session (#1320).
135
+ if (replacesDocument && drivenPresent)
136
+ continue;
90
137
  if (samePage(s.url, target)) {
91
138
  if (null === arrived)
92
139
  arrived = { sessionId: s.id };
@@ -122,7 +169,11 @@ export async function awaitArrival(sessions, target, scope, timeoutMs = ARRIVAL_
122
169
  // landed when the target was never observed.
123
170
  const current = sessions.get(scope.navigatedSession.id);
124
171
  if (current !== undefined) {
125
- if (!samePage(current.url, scope.navigatedFrom) && !samePage(current.url, target)) {
172
+ // Sitting on `navigatedFrom` means two different things: a tab that has not moved, or a page
173
+ // that unloaded and came back here (an auth guard bouncing `/checkout` to `/login` while the
174
+ // tab started on `/login`). Only a document that is provably new tells them apart.
175
+ if (!samePage(current.url, target) &&
176
+ (!samePage(current.url, scope.navigatedFrom) || documentUnloaded(scope, current))) {
126
177
  return {
127
178
  sessionId: current.id,
128
179
  landedOn: current.url,
@@ -131,6 +182,9 @@ export async function awaitArrival(sessions, target, scope, timeoutMs = ARRIVAL_
131
182
  return null;
132
183
  }
133
184
  const successor = await awaitDocumentSuccessor(sessions, scope.navigatedSession, 0, clock);
185
+ // A successor under another id is only "unique at the origin", which a tab opened by someone else
186
+ // after the driven one went away satisfies equally. On the starting page that guess cannot be told
187
+ // from a redirect, so it is not reported as one.
134
188
  if (successor !== null &&
135
189
  !samePage(successor.url, target) &&
136
190
  !samePage(successor.url, scope.navigatedFrom)) {
@@ -27,7 +27,26 @@
27
27
  * - still in flight when the budget genuinely runs out → `unsettled`, as before, but now an honest
28
28
  * limit instead of an early exit.
29
29
  */
30
- import { EventType, NetInitiator, isDevToolingUrl, isForeignTraffic } from '@reticlehq/core';
30
+ import { EventType, NetInitiator, isDevToolingUrl, isForeignTraffic, urlForMatch, } from '@reticlehq/core';
31
+ /**
32
+ * What a URL is matched against, as opposed to what is shown.
33
+ *
34
+ * Redaction runs at emit time and rewrites a sensitive path segment in place: `/verify/refresh-token`
35
+ * becomes `/verify/[REDACTED]`, and the original survives only in `urlRaw`. A project that DECLARED
36
+ * `/verify/refresh-token` in `.reticle.json` `background` is matched with `String.includes`, so
37
+ * classifying on the displayed URL silently put the endpoint back into the in-flight count and held
38
+ * the settle window open — the exact defect this file exists to prevent, reached through the one
39
+ * field the classifier did not consult.
40
+ *
41
+ * The same division every other URL comparison in the product already uses (`urlContains`, `netCall`):
42
+ * match against `urlForMatch`, DISCLOSE `url`. The transcript keeps the redacted spelling.
43
+ */
44
+ const matchUrlOf = (data) => {
45
+ // `urlForMatch` answers with the empty string when the event carries no URL at all, and every
46
+ // classifier below takes `undefined` to mean "nothing to judge". Fails OPEN either way.
47
+ const url = urlForMatch(data);
48
+ return 0 === url.length ? undefined : url;
49
+ };
31
50
  /**
32
51
  * The dev toolchain talking about ITSELF is not the app finishing its work.
33
52
  *
@@ -36,7 +55,7 @@ import { EventType, NetInitiator, isDevToolingUrl, isForeignTraffic } from '@ret
36
55
  * deliberately disabled control — no state change, no storage change, zero application requests —
37
56
  * was graded on the strength of a Next webpack hot-update the same payload printed as ignored.
38
57
  */
39
- const isDevTooling = (data) => isDevToolingUrl('string' === typeof data['url'] ? data['url'] : undefined);
58
+ const isDevTooling = (data) => isDevToolingUrl(matchUrlOf(data));
40
59
  /**
41
60
  * Somebody else's host is not the app finishing its work.
42
61
  *
@@ -55,12 +74,15 @@ const isDevTooling = (data) => isDevToolingUrl('string' === typeof data['url'] ?
55
74
  * SAME-ORIGIN background traffic is never guessed: nothing in a URL separates the app's telemetry
56
75
  * from the app's work. It is excluded only when the project DECLARED it (`background`).
57
76
  */
58
- const isForeign = (data, appOrigin, background) => isForeignTraffic('string' === typeof data['url'] ? data['url'] : undefined, appOrigin, background);
77
+ const isForeign = (data, appOrigin, background) => isForeignTraffic(matchUrlOf(data), appOrigin, background);
59
78
  /**
60
79
  * A departure records where the browser was SENT, not a request whose result we will see — the
61
80
  * response is delivered to a document this session does not live into. It can never be matched by a
62
81
  * NET_REQUEST, so counting it as outstanding would mean no page containing an outbound link or a
63
82
  * download button ever settles again. See `NetInitiator.NAVIGATION`.
83
+ *
84
+ * The `settled` predicate asks the same question in `evalSettled`; it spells it against this same
85
+ * constant rather than against a string, which is what keeps the two answers one vocabulary.
64
86
  */
65
87
  const isDeparture = (data) => data['initiator'] === NetInitiator.NAVIGATION;
66
88
  /** How often to re-read the buffer. Short enough to return promptly, long enough not to spin. */
@@ -303,8 +303,8 @@ export const ACT_SEQUENCE_TOOL = {
303
303
  count: inputSteps.length,
304
304
  steps: stepResults,
305
305
  });
306
- deps.recordings.capture(pageBefore === undefined ? recorded : { ...recorded, page: pageBefore });
307
- deps.recordings.markEnded(pathOf(session.url));
306
+ deps.recordings.capture(pageBefore === undefined ? recorded : { ...recorded, page: pageBefore }, undefined, session.id);
307
+ deps.recordings.markEnded(pathOf(session.url), session.id);
308
308
  }
309
309
  /*
310
310
  * The grade, and the coverage it is never allowed to hide.
@@ -22,6 +22,7 @@ import { intentDebt, openSessionIntents } from '../../memory/intent/open-intents
22
22
  import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../../memory/intent/inline-intent.js';
23
23
  import { declaresState } from '@reticlehq/engine/question/predicate/predicate-asks.js';
24
24
  import { isStateUnwatched } from '@reticlehq/engine/evidence/blind-spots.js';
25
+ import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
25
26
  import { inFlightRequestLabels, repeatedRequestLabels } from './act/settle-in-flight.js';
26
27
  import { finishAfterMatch } from './act/after-match.js';
27
28
  import { noteProved } from './act/proved-controls.js';
@@ -62,8 +63,8 @@ import { readAlreadyTrue } from './act/already-true.js';
62
63
  /**
63
64
  * The action vocabulary, derived from ActionType — never retyped.
64
65
  *
65
- * The description used to list thirteen actions while ActionType had seventeen: blur, upload, drag
66
- * and webmcp were real, callable, and undocumented, because a hand-copied list drifts the moment
66
+ * The description used to omit callable actions such as blur, upload and drag,
67
+ * because a hand-copied list drifts the moment
67
68
  * someone adds an arm. Deriving both the schema and the prose from the enum makes that impossible.
68
69
  *
69
70
  * The handler already refused an unknown action with a good message; the schema now refuses it
@@ -124,7 +125,7 @@ export const ACT_TOOLS = [
124
125
  args: z
125
126
  .record(z.unknown())
126
127
  .optional()
127
- .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
128
+ .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer or the key DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For press: { keys: ["Control","k"] } holds several keys down TOGETHER (pressed in order, released in reverse) and { code } names the physical key — both stay on the in-page dispatcher, the only path that can express them. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
128
129
  refuseWhenThrottled: z
129
130
  .boolean()
130
131
  .optional()
@@ -193,7 +194,7 @@ export const ACT_TOOLS = [
193
194
  // drive native pointer input when a provider is available; otherwise fall back.
194
195
  const real = await tryRealInput(deps, session, ref, action, args);
195
196
  if (real.result !== undefined) {
196
- captureAct(deps.recordings, args, real.result, routeBefore);
197
+ captureAct(deps.recordings, args, real.result, routeBefore, session.id);
197
198
  settledOutcome = real.settled ?? undefined;
198
199
  // Native input reports no synthetic effect block, so nothing measured in-target: undefined
199
200
  // (the weaker empty-window test), never a fabricated zero.
@@ -217,7 +218,7 @@ export const ACT_TOOLS = [
217
218
  });
218
219
  if (!result.ok)
219
220
  throw new Error(result.error ?? 'act failed');
220
- captureAct(deps.recordings, args, result.result, routeBefore);
221
+ captureAct(deps.recordings, args, result.result, routeBefore, session.id);
221
222
  // lift dispatch/settle status to the envelope (a settle timeout is NOT a failure).
222
223
  const r = asRecord(result.result);
223
224
  if ('boolean' === typeof r['settled'])
@@ -240,7 +241,7 @@ export const ACT_TOOLS = [
240
241
  });
241
242
  }
242
243
  finally {
243
- deps.recordings.markEnded(pathOf(session.url));
244
+ deps.recordings.markEnded(pathOf(session.url), session.id);
244
245
  // Close the window on every exit (settle or throw), recording the action + settle outcome.
245
246
  session.finishAction(undefined, settledOutcome, true === settledOutcome ? session.elapsed() - since : undefined);
246
247
  }
@@ -524,6 +525,8 @@ export const ACT_TOOLS = [
524
525
  const spots = blindSpotsFromState(session.blindSpots(), session.runtime);
525
526
  const coverage = buildCoverageStatement(spots);
526
527
  const absenceBlindSpot = absenceBlindSpotNote(until, spots);
528
+ // A green resting on hidden matches says the node exists, not that it shows (#1408).
529
+ const hiddenMatch = hiddenMatchNote(until, verdict.evidence);
527
530
  // Nothing subscribed ⇒ the state channel is dark, and the summary must say so rather than
528
531
  // report an empty diff list that reads like a fact about the app. See CausalSummary.
529
532
  const stateUnwatched = isStateUnwatched(spots);
@@ -569,6 +572,10 @@ export const ACT_TOOLS = [
569
572
  ...(0 === impeachingNotes.length ? {} : { blindSpots: impeachingNotes }),
570
573
  losses,
571
574
  });
575
+ // A refuted expectation must not ride into a regression flow cut from the ambient tape; it is
576
+ // kept as the capsule below instead. See RecordingStore.unassertLast.
577
+ if (!verdict.pass && actResult !== null)
578
+ deps.recordings.unassertLast();
572
579
  const capsuleSaved = await saveFailedAssertCapsule({
573
580
  deps,
574
581
  verdict,
@@ -606,6 +613,7 @@ export const ACT_TOOLS = [
606
613
  prior,
607
614
  actionSince: since,
608
615
  expectedFailures: declared.netFailures,
616
+ namedNetUrls: declared.netUrls,
609
617
  // A consequence that was already true before the action proves nothing about it, so it is
610
618
  // not evidence the destination rendered either — `alreadyTrue` decides that, once.
611
619
  renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
@@ -617,7 +625,14 @@ export const ACT_TOOLS = [
617
625
  });
618
626
  // The single field an agent reads. Everything below it is the evidence it was derived from;
619
627
  // this is the only one that has to be interpreted, and now it interprets itself.
620
- const outcomePending = acceptedWriteLabels(windowEvents);
628
+ // Same routing as the assert path (#1120), fed by the links this verdict PROVED. On red,
629
+ // `gradedLinks` is the declared surface (every anyOf branch), which must not exempt a 202
630
+ // the verdict never rested on — so only a green passes it through.
631
+ const outcomePending = acceptedWriteLabels(windowEvents, {
632
+ appUrl: session.url,
633
+ background: session.background,
634
+ asserted: verdict.pass ? gradedLinks : [],
635
+ });
621
636
  const outcomeUnread = unreadWriteLabels(windowEvents);
622
637
  const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
623
638
  const { decision, durable } = await withDurability(args['durable'], until, decideVerified({
@@ -643,6 +658,7 @@ export const ACT_TOOLS = [
643
658
  ? { observationLost: true, lastUrl: session.url }
644
659
  : {}),
645
660
  ...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
661
+ ...(hiddenMatch === undefined ? {} : { hiddenMatch }),
646
662
  honesty,
647
663
  contradictions,
648
664
  ...(0 === outcomePending.length ? {} : { outcomePending }),
@@ -802,8 +818,8 @@ export const ACT_TOOLS = [
802
818
  }
803
819
  finally {
804
820
  if (dispatched !== undefined)
805
- captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, recordedVerdict);
806
- deps.recordings.markEnded(pathOf(currentOf(deps.sessions, session).url));
821
+ captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, recordedVerdict, session.id);
822
+ deps.recordings.markEnded(pathOf(currentOf(deps.sessions, session).url), session.id);
807
823
  acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
808
824
  }
809
825
  },
@@ -1,5 +1,5 @@
1
1
  import type { InstrumentationGap, JournalVerdictEffect } from '@reticlehq/core/artifacts';
2
- import type { Predicate } from '@reticlehq/engine/question/predicate/predicate.js';
2
+ import { type Predicate } from '@reticlehq/engine/question/predicate/predicate.js';
3
3
  import type { Session } from '../../../portal/session/session.js';
4
4
  import { type Contradiction } from '@reticlehq/engine/disagreement/contradictions.js';
5
5
  /**
@@ -4,12 +4,14 @@ import { gapsForAction } from '@reticlehq/engine/evidence/instrumentation-gaps.j
4
4
  import { noteSessionGaps } from '@reticlehq/engine/evidence/gap-ledger.js';
5
5
  import { declaresState } from '@reticlehq/engine/question/predicate/predicate-asks.js';
6
6
  import { isStateUnwatched } from '@reticlehq/engine/evidence/blind-spots.js';
7
+ import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
8
+ import { provenExpectedLinks, } from '@reticlehq/engine/question/predicate/predicate.js';
7
9
  import { findContradictions, } from '@reticlehq/engine/disagreement/contradictions.js';
8
10
  import { crashedRuleNotes } from '@reticlehq/engine/disagreement/contradiction-folds.js';
9
11
  import { declaredExpectations, declaresBodyIndependentChannel, } from '@reticlehq/engine/question/declared.js';
10
12
  import { absenceBlindSpotNote, blindSpotsFromState, buildCoverageStatement, restsOnCompleteWindow, Coverage, impeachesCapture, transportGapNote, } from '@reticlehq/engine/evidence/blind-spots.js';
11
13
  import { buildHonestyBlock } from '@reticlehq/engine/evidence/honesty.js';
12
- import { acceptedWriteLabels } from '@reticlehq/engine/evidence/accepted-write.js';
14
+ import { acceptedWriteLabels, hasAcceptedWrite, } from '@reticlehq/engine/evidence/accepted-write.js';
13
15
  import { unreadWriteLabels } from '@reticlehq/engine/evidence/unread-outcome.js';
14
16
  import { decideVerified } from '@reticlehq/engine/evidence/verified.js';
15
17
  import { describeWaitTarget, namedNetIsInFlight } from '@reticlehq/engine/evidence/unsettled.js';
@@ -58,6 +60,8 @@ changeUndeclared) {
58
60
  const spots = blindSpotsFromState(session.blindSpots(), session.runtime);
59
61
  const statement = buildCoverageStatement(spots);
60
62
  const absenceBlindSpot = absenceBlindSpotNote(predicate, spots);
63
+ // A green resting on hidden matches says the node exists, not that it shows (#1408).
64
+ const hiddenMatch = hiddenMatchNote(predicate, evidence);
61
65
  // Omitted entirely when coverage is full, so an intact page pays nothing and the field's PRESENCE
62
66
  // is the warning.
63
67
  const coverage = Coverage.PARTIAL === statement.coverage
@@ -115,7 +119,17 @@ changeUndeclared) {
115
119
  // A consumer rule that crashed belongs here too: the engine ran with fewer rules than it claims,
116
120
  // so part of what happened may simply not have been looked for.
117
121
  const impeachingNotes = [impeaching.note, gap, ...crashedRuleNotes()].filter((n) => n !== undefined);
118
- const outcomePending = acceptedWriteLabels(windowEvents);
122
+ // Which 202s are still the claim's outcome to wait for (#1120). The proven links decide the
123
+ // exemption, so a 200 branch of an `anyOf` cannot exempt the 202 that also fired; and the walk
124
+ // only runs when a 202 is actually waiting, because that is the only case it can change.
125
+ const assertedAccepts = pass && hasAcceptedWrite(windowEvents)
126
+ ? await provenExpectedLinks(session, predicate, since)
127
+ : [];
128
+ const outcomePending = acceptedWriteLabels(windowEvents, {
129
+ appUrl: session.url,
130
+ background: session.background,
131
+ asserted: assertedAccepts,
132
+ });
119
133
  const outcomeUnread = unreadWriteLabels(windowEvents);
120
134
  const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
121
135
  const effectiveInconclusive = inconclusive ?? (!pass ? session.preconditionFailure?.() : undefined);
@@ -160,6 +174,7 @@ changeUndeclared) {
160
174
  ...(effectiveInconclusive === undefined ? {} : { inconclusive: effectiveInconclusive }),
161
175
  ...(true === observationLost ? { observationLost: true, lastUrl: session.url } : {}),
162
176
  ...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
177
+ ...(hiddenMatch === undefined ? {} : { hiddenMatch }),
163
178
  ...(namedNetIsInFlight(predicate, stillInFlight) ? { namedRequestInFlight: true } : {}),
164
179
  honesty: buildHonestyBlock({
165
180
  grade: gradeOfPredicate(predicate),
@@ -96,6 +96,10 @@ export const BROWSER_TOOLS = [
96
96
  // which reads whatever is at the target afterwards, cannot tell the two apart on its own.
97
97
  const target = absoluteTarget(url, session.url);
98
98
  const priorIds = idsAtTarget(deps.sessions, target);
99
+ // The document the tab is on NOW. Sampled before dispatch because the replacement can
100
+ // connect while the NAVIGATE reply is still pending, and reading it after the await would
101
+ // record the replacement as the starting document (#1320).
102
+ const navigatedFromDocumentId = session.currentDocumentId;
99
103
  session.beginAction(ReticleTool.NAVIGATE, { url });
100
104
  // Same floor as the reload path above, for the same reason: going to a new URL replaces the
101
105
  // document just as thoroughly. `beginAction` attributes events to this action; it does not move
@@ -112,9 +116,12 @@ export const BROWSER_TOOLS = [
112
116
  const arrival = true === result.ok
113
117
  ? await awaitArrival(deps.sessions, target, {
114
118
  // Keep the original Session object and URL so arrival detection can distinguish
115
- // an in-progress navigation from a document that arrived somewhere else.
119
+ // an in-progress navigation from a document that arrived somewhere else. The
120
+ // document identity lets a same-URL navigation wait for its replacement instead
121
+ // of confirming the document it is about to destroy (#1320).
116
122
  navigatedSession: session,
117
123
  navigatedFrom: session.url,
124
+ ...(navigatedFromDocumentId === undefined ? {} : { navigatedFromDocumentId }),
118
125
  priorIds,
119
126
  }, timeoutMs)
120
127
  : null;
@@ -0,0 +1,26 @@
1
+ import type { SessionInfo } from '../../portal/session/session-info.js';
2
+ import { LinkCapability, type PresenterTone } from '@reticlehq/core';
3
+ import type { ToolDeps } from './tool-kit.js';
4
+ import type { FileSystemPort } from '../../memory/project/fs/fs-port.js';
5
+ /** What this daemon does for the platform's chat. Each one is listed only once it is built. */
6
+ export declare const CHAT_CAPABILITIES: readonly LinkCapability[];
7
+ /** The slice of the session manager this needs. */
8
+ export interface ChatDriveSessions {
9
+ list(): SessionInfo[];
10
+ count(): number;
11
+ get(id: string): {
12
+ autoEnd(text: string, tone: PresenterTone): void;
13
+ pushNarration(text: string): void;
14
+ } | undefined;
15
+ }
16
+ export interface ChatDriveCloud {
17
+ /** The platform and key a tab's own project is linked with, or undefined when it is not. */
18
+ sessionCloud: (sessionId: string) => Promise<{
19
+ url: string;
20
+ apiKey: string;
21
+ } | undefined>;
22
+ version: string;
23
+ }
24
+ export declare function startChatDrives(deps: ToolDeps, sessions: ChatDriveSessions, fs: FileSystemPort, cloud: ChatDriveCloud, log: (line: string) => void): {
25
+ stop: () => void;
26
+ };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The daemon's side of the platform chat: the apps it reports, and the drives it takes.
3
+ *
4
+ * Out of `index.ts`, which only calls this and hands it the cloud credentials (the tool surface does
5
+ * not reach `memory/cloud`; see `withLinkedCredential`): both halves share the same view of which tab is which
6
+ * app, and keeping them together is what stops a drive attached to one app landing in another that
7
+ * happens to be on the same port. See `local-apps.ts` and `remote-drive.ts` for each half.
8
+ */
9
+ import { basename, join } from 'node:path';
10
+ import { homedir, hostname } from 'node:os';
11
+ import { LinkCapability, ReticleTool, asRecord } from '@reticlehq/core';
12
+ import { prepareDrive } from '../../features/harness/platform/drive-target.js';
13
+ import { reticleToolset } from './harness-toolset.js';
14
+ import { projectDirFor } from '../../memory/project/session-root.js';
15
+ import { driveFrame } from '../../features/visual/visual-tools.js';
16
+ import { LEASE_ACQUIRE_TOOL } from './lease-tools.js';
17
+ import { REMOTE_DRIVE_JPEG_QUALITY, endDrivenTab, pickOwnDriveSession, startRemoteDrives, } from '../../features/harness/platform/remote-drive.js';
18
+ import { appKeyOf, appsByPlatform, machineOf, mayOpen, NOT_AN_APP_ADDRESS, nameFromProjectId, pickAppTab, startAppReports, } from '../../features/harness/platform/local-apps.js';
19
+ import { serverOptionsFromEnv } from '../../features/harness/platform/server-driver.js';
20
+ import { driveForChat } from './explore-tools.js';
21
+ import { withLinkedCredential } from './harness-explore.js';
22
+ import { runTool } from './invoke-tool.js';
23
+ /** Why a platform drive with no tab of its own runs no tool. */
24
+ const NO_DRIVEN_TAB = 'No tab was picked for this drive, so no tool runs.';
25
+ /** What this daemon does for the platform's chat. Each one is listed only once it is built. */
26
+ export const CHAT_CAPABILITIES = [
27
+ LinkCapability.DRIVE_SPEC,
28
+ LinkCapability.TARGET_TAB,
29
+ LinkCapability.TARGET_HEADLESS,
30
+ LinkCapability.TARGET_HEADED,
31
+ LinkCapability.HUD,
32
+ LinkCapability.TOOL_SESSION,
33
+ LinkCapability.FRAMES,
34
+ ];
35
+ /** A project's own name: its package's, else its id's, else its folder's. */
36
+ const nameOf = async (fs, dir, projectId) => {
37
+ try {
38
+ const name = JSON.parse(await fs.readFile(join(dir, 'package.json')))
39
+ .name;
40
+ if ('string' === typeof name && 0 < name.length)
41
+ return name;
42
+ }
43
+ catch {
44
+ // No package, or not JSON: the folder names it.
45
+ }
46
+ // Not mapped to a checkout (the daemon's shared `unmatched` directory): the id the SDK stamped.
47
+ if (undefined !== projectId)
48
+ return nameFromProjectId(projectId);
49
+ return basename(dir) || dir;
50
+ };
51
+ export function startChatDrives(deps, sessions, fs, cloud, log) {
52
+ const env = process.env;
53
+ const machine = machineOf(hostname(), homedir());
54
+ const sessionCloud = cloud.sessionCloud;
55
+ const sessionKey = async (id) => (await sessionCloud(id))?.apiKey;
56
+ // ponytail: a tab whose project resolves to nothing counts as the daemon's own project here, as it
57
+ // does for flows and runs; a per-tab "unresolved" app is the upgrade if that ever misleads.
58
+ const appOf = (id) => {
59
+ try {
60
+ const projectId = sessions.list().find((tab) => tab.sessionId === id)?.projectId;
61
+ return appKeyOf(machine.id, projectDirFor(deps, id), projectId);
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ };
67
+ // Opened headless, like `reticle drive <url>`, through runTool so it is counted like any call.
68
+ const openable = (url) => {
69
+ if (!mayOpen(url, sessions.list().map((tab) => tab.url)))
70
+ throw new Error(NOT_AN_APP_ADDRESS);
71
+ };
72
+ const open = async (url) => {
73
+ openable(url);
74
+ await runTool(LEASE_ACQUIRE_TOOL, deps, { url });
75
+ };
76
+ const pick = (goal, key, app) => undefined === app
77
+ ? pickOwnDriveSession(sessions.list(), goal, key, sessionKey)
78
+ : pickAppTab(app, goal, () => sessions.list(), appOf, open).then(async (id) =>
79
+ // The app the platform named must be one this credential's project owns.
80
+ 'string' === typeof id && (await sessionKey(id)) !== key ? null : id);
81
+ const drives = startRemoteDrives({
82
+ env: () => withLinkedCredential(deps, env),
83
+ connected: () => 0 < sessions.count(),
84
+ pick,
85
+ // The spec's target and HUD, applied through the same counted tools an agent would call.
86
+ prepare: ({ goal, apiKey, app, spec }) => prepareDrive(spec, app?.url ?? undefined, {
87
+ pickTab: () => pick(goal, apiKey, app),
88
+ open: async (url, headed, hud) => {
89
+ openable(url);
90
+ const lease = asRecord(await runTool(LEASE_ACQUIRE_TOOL, deps, {
91
+ url,
92
+ ...(headed ? { headed } : {}),
93
+ ...(hud === undefined ? {} : { hud }),
94
+ }));
95
+ const id = lease['sessionId'];
96
+ if ('string' !== typeof id)
97
+ throw new Error(`could not open ${url}`);
98
+ return id;
99
+ },
100
+ tuneHud: async (sessionId, hud) => {
101
+ await reticleToolset(deps, { sessionId }).invoke(ReticleTool.SESSION_TUNE, { hud });
102
+ },
103
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
104
+ }),
105
+ // A platform drive: Reticle's own tools on the driven tab, and nothing else. Locked, so a call
106
+ // naming another tab still lands on this one; with no tab picked, nothing runs at all.
107
+ session: (sessionId) => {
108
+ if (sessionId === undefined)
109
+ return { invoke: () => Promise.reject(new Error(NO_DRIVEN_TAB)) };
110
+ const tools = reticleToolset(deps, { sessionId, locked: true });
111
+ return { tools: tools.tools, invoke: (tool, args) => tools.invoke(tool, args) };
112
+ },
113
+ drive: async (goal, sessionId) => {
114
+ const url = sessions.list().find((tab) => tab.sessionId === sessionId)?.url;
115
+ const outcome = await driveForChat(deps, goal, sessionId);
116
+ return url === undefined ? outcome : { ...outcome, url };
117
+ },
118
+ // The leased tab, or a desktop window's own capture. A web tab only the SDK reaches has no camera.
119
+ frame: (sessionId) => sessionId === undefined
120
+ ? Promise.resolve(undefined)
121
+ : driveFrame(deps, sessionId, REMOTE_DRIVE_JPEG_QUALITY),
122
+ settle: (id, outcome) => endDrivenTab(id === undefined ? undefined : sessions.get(id), outcome),
123
+ log,
124
+ });
125
+ const reports = startAppReports({
126
+ machine,
127
+ version: cloud.version,
128
+ capabilities: CHAT_CAPABILITIES,
129
+ apps: async () => appsByPlatform(sessions.list(), serverOptionsFromEnv(await withLinkedCredential(deps, env)), machine.id, async (tab) => {
130
+ const dir = projectDirFor(deps, tab.sessionId);
131
+ const platform = await sessionCloud(tab.sessionId);
132
+ return {
133
+ dir,
134
+ name: await nameOf(fs, dir, tab.projectId),
135
+ ...(undefined === platform
136
+ ? {}
137
+ : { platform: { url: platform.url.replace(/\/+$/, ''), apiKey: platform.apiKey } }),
138
+ };
139
+ }),
140
+ open,
141
+ drivePending: (platform) => void drives.tick(platform),
142
+ // What the platform wants the person told: in the daemon's log, and on the HUD of each open tab
143
+ // of that project, so somebody who never reads the log still sees "update Reticle".
144
+ notify: (notice, platform) => {
145
+ const line = `${notice.text}${notice.url === undefined ? '' : ` ${notice.url}`}`;
146
+ log(`reticle: from the platform: ${line}`);
147
+ void (async () => {
148
+ for (const tab of sessions.list()) {
149
+ const key = await sessionKey(tab.sessionId).catch(() => undefined);
150
+ if (key === platform.apiKey)
151
+ sessions.get(tab.sessionId)?.pushNarration(line);
152
+ }
153
+ })();
154
+ },
155
+ log,
156
+ });
157
+ // Once now, so the chat sees this machine's apps as soon as the daemon is up.
158
+ void reports.tick();
159
+ return {
160
+ stop: () => {
161
+ drives.stop();
162
+ reports.stop();
163
+ },
164
+ };
165
+ }