@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
@@ -0,0 +1,21 @@
1
+ /** A contradiction's identity for de-duplication: the rule that fired, and the evidence it fired on. */
2
+ function contradictionId(found) {
3
+ return `${found.kind}|${found.detail}`;
4
+ }
5
+ /** Cross-step findings belong to no single step; -1 is the address the suite verdict already uses. */
6
+ export const CROSS_STEP_INDEX = -1;
7
+ /**
8
+ * The contradictions the whole-span pass found that no individual step could.
9
+ *
10
+ * A step's window closes when the step ends, so a request fired at step 2 and still unanswered at
11
+ * step 5 is invisible to every per-step window: step 2's closed before the answer came and step 5
12
+ * never saw it start. Re-running the detectors over the whole replay span finds those — and re-finds
13
+ * everything the steps already reported, which is what the subtraction is for. Reporting a finding
14
+ * twice teaches a reader that the count is noise.
15
+ *
16
+ * Exported for its own test: the subtraction is the whole rule, and it is pure.
17
+ */
18
+ export function crossStepOnly(whole, steps) {
19
+ const seen = new Set(steps.flatMap((step) => (step.contradictions ?? []).map(contradictionId)));
20
+ return whole.filter((found) => !seen.has(contradictionId(found)));
21
+ }
@@ -1,15 +1,11 @@
1
1
  import { asString } from '@reticlehq/core';
2
2
  import { flowsForSession } from './flow-store-for-session.js';
3
+ import { safeSessionTarget } from '../../memory/project/session-root.js';
3
4
  import { replayNamedFlow } from './flow-replay-run.js';
4
5
  import { recordingSources, withLearnedSources } from './learned-sources.js';
5
6
  import { ReplayStatus } from '@reticlehq/core';
6
- function projectOf(deps, args) {
7
- try {
8
- return deps.sessions.resolve(asString(args['sessionId'])).projectId;
9
- }
10
- catch {
11
- return undefined;
12
- }
7
+ function targetOf(deps, args) {
8
+ return safeSessionTarget(deps, asString(args['sessionId']));
13
9
  }
14
10
  /**
15
11
  * Write back what a replay taught the flow, once the replay has finished writing its own.
@@ -36,8 +32,8 @@ export async function persistLearning(deps, args, result) {
36
32
  if (0 === name.length)
37
33
  return result;
38
34
  try {
39
- const projectId = projectOf(deps, args);
40
- await flowsForSession(deps, projectId).flows.recordLearned(name, learned, projectId);
35
+ const target = targetOf(deps, args);
36
+ await flowsForSession(deps, target).flows.recordLearned(name, learned, target.projectId);
41
37
  }
42
38
  catch {
43
39
  // Bookkeeping only.
@@ -93,8 +89,9 @@ export async function persistSources(deps, args, sources) {
93
89
  if (0 === name.length)
94
90
  return;
95
91
  try {
96
- const projectId = projectOf(deps, args);
97
- const store = flowsForSession(deps, projectId).flows;
92
+ const target = targetOf(deps, args);
93
+ const { projectId } = target;
94
+ const store = flowsForSession(deps, target).flows;
98
95
  const current = await store.load(name, projectId);
99
96
  // Read first so a flow that already names every file is never rewritten.
100
97
  if (!current.ok || withLearnedSources(current.value, sources) === undefined)
@@ -15,7 +15,8 @@ import { MutationOutcome } from 'open-verification';
15
15
  import { z } from 'zod';
16
16
  import { sessionMutationPort } from '../../surface/tools/lease-tools.js';
17
17
  import { mutationTest } from './mutation-run.js';
18
- import { replayNamedFlow, sessionProjectId } from './flow-replay-run.js';
18
+ import { replayNamedFlow } from './flow-replay-run.js';
19
+ import { sessionTarget } from '../../memory/project/session-root.js';
19
20
  import { flowsForSession } from './flow-store-for-session.js';
20
21
  import { ReplayStatus } from '@reticlehq/core';
21
22
  const NOTHING_DECLARED = 'this flow declares no network consequence, so it has named nothing whose breakage it could be ' +
@@ -48,8 +49,9 @@ export const FLOW_MUTATE_TOOL = {
48
49
  handler: async (deps, args) => {
49
50
  const name = asString(args['flowName']) ?? asString(args['flow']) ?? '';
50
51
  const sessionId = asString(args['sessionId']);
51
- const projectId = sessionProjectId(deps, sessionId);
52
- const loaded = await flowsForSession(deps, projectId).flows.load(name, projectId);
52
+ const project = sessionTarget(deps, sessionId);
53
+ const { projectId } = project;
54
+ const loaded = await flowsForSession(deps, project).flows.load(name, projectId);
53
55
  if (!loaded.ok) {
54
56
  return { flow: name, because: `could not load "${name}": ${String(loaded.code)}` };
55
57
  }
@@ -1,4 +1,4 @@
1
- import { type CommandResult, type FlowFile, type FlowReplayResult, type Contradiction, type FlowStepResult, type ReticleEvent } from '@reticlehq/core';
1
+ import { type CommandResult, type FlowFile, type FlowReplayResult, type ReticleEvent } from '@reticlehq/core';
2
2
  import type { ArrivalClock } from '../../surface/tools/act/navigation/navigate-arrival.js';
3
3
  import type { SessionManager } from '../../portal/session/session-manager.js';
4
4
  import type { Session } from '../../portal/session/session.js';
@@ -20,8 +20,8 @@ interface StartPathSession {
20
20
  * anchor simply isn't on this page yet. Detect that so the decision says "navigate there first"
21
21
  * instead of a mystifying "a step no longer matches". Returns undefined when the routes agree or the
22
22
  * current route is unobservable — never a false alarm. Replay normally never gets here on a
23
- * wrong page (arriveAtStartPath navigates before step 1); this is the fallback for when that
24
- * navigation was refused or the SDK never reconnected in the window.
23
+ * wrong page (arriveAtStartPath navigates before step 1); this is the fallback when navigation
24
+ * was refused. A page that never reconnected is reported as unverifiable before step 1.
25
25
  */
26
26
  export declare function startPathMismatchHint(flow: FlowFile, session: StartPathSession): string | undefined;
27
27
  /** What replay learned on its way to step 0. Both fields absent = the tab was left exactly as it was. */
@@ -30,6 +30,10 @@ export interface StartPathArrival {
30
30
  session?: Session;
31
31
  /** Set when the RESET itself is why step 1 cannot start — the reason a step-1 red is not drift. */
32
32
  resetCost?: string;
33
+ /** Set when the tab was sent away and nothing reconnected: the old handle will never answer (#1129). */
34
+ lost?: string;
35
+ /** A live successor arrived on a different route, so replay must not use the dead old handle. */
36
+ redirected?: string;
33
37
  }
34
38
  /**
35
39
  * Replay's half of the FlowFile contract: put the tab on the flow's `startPath` before step 1, and
@@ -48,10 +52,9 @@ export interface StartPathArrival {
48
52
  * A full-page load tears down the session socket, so the navigation must happen here — before any
49
53
  * step runs — and the replay continues on the session the SDK reconnects as (found via the same
50
54
  * tombstone rebind that lets `resolve(oldId)` answer after any navigation). Best-effort by design:
51
- * when the flow carries no startPath, the current route is unobservable, the navigation is refused,
52
- * or the SDK never reconnects in the window, replay proceeds on the connected session as before —
53
- * with startPathMismatchHint turning any resulting drift into an actionable next move rather than a
54
- * mystifying one.
55
+ * when the flow carries no startPath, the current route is unobservable, or navigation is refused,
56
+ * replay proceeds on the connected session. A missing successor stops before step 1; a redirect
57
+ * continues on the live successor with a route-specific hint.
55
58
  */
56
59
  export declare function arriveAtStartPath(sessions: SessionManager, session: StartPathSession & {
57
60
  id: string;
@@ -69,6 +72,18 @@ export declare function arriveAtStartPath(sessions: SessionManager, session: Sta
69
72
  * to move and this decides how, which is also why the deliberate short-circuits live up there and
70
73
  * not in here: a caller that has already decided it must move should not have to argue with them.
71
74
  */
75
+ /** How a navigate ended: target, redirect, refusal (untouched tab), or no successor. */
76
+ export type NavigateOutcome = {
77
+ kind: 'arrived';
78
+ session: Session;
79
+ } | {
80
+ kind: 'redirected';
81
+ session: Session;
82
+ } | {
83
+ kind: 'refused';
84
+ } | {
85
+ kind: 'lost';
86
+ };
72
87
  export declare function navigateAndAwait(sessions: SessionManager, session: {
73
88
  id: string;
74
89
  /** Optional because a session's URL is, and `carryReticleIdentity` already handles its absence. */
@@ -76,7 +91,7 @@ export declare function navigateAndAwait(sessions: SessionManager, session: {
76
91
  command(name: string, args?: Record<string, unknown>): Promise<CommandResult>;
77
92
  }, destination: string, expectedPath: string, timeoutMs?: number, clock?: ArrivalClock,
78
93
  /** True when the destination is the page we are already on — see `arrivedSuccessor`. */
79
- isReload?: boolean): Promise<Session | undefined>;
94
+ isReload?: boolean): Promise<NavigateOutcome>;
80
95
  /**
81
96
  * The honest answer when the document replay was driving went away mid-run.
82
97
  *
@@ -97,7 +112,6 @@ export declare function lostDocumentResult(name: string, lost: DocumentLostDurin
97
112
  export declare function replayNamedFlow(deps: ToolDeps, args: Record<string, unknown>,
98
113
  /** Wraps the session the steps run on, so a caller can watch what they resolved. */
99
114
  observe?: (session: FlowReplaySession) => FlowReplaySession): Promise<FlowReplayResult>;
100
- export declare function crossStepOnly(whole: readonly Contradiction[], steps: readonly FlowStepResult[]): Contradiction[];
101
115
  /**
102
116
  * The connecting session's project, or undefined when no browser is attached. Flow tools use it to
103
117
  * scope storage to the current app on a shared daemon; resolving must NOT throw here (list/load are
@@ -6,8 +6,8 @@ import { establishedState, samePath } from './flow-journey.js';
6
6
  import { asRecord, asString } from '@reticlehq/core';
7
7
  import { routeOfEvent, routeOfUrl } from '@reticlehq/engine/question/predicate/predicate-route.js';
8
8
  import { carryReticleIdentity } from '../../surface/tools/lease-tools.js';
9
- import { replayFlow } from './flow-replay.js';
10
- import { anchorPrecondition, anchorQueryArgs } from './flow-step-runners.js';
9
+ import { partialStatus, replayFlow, replayWindow } from './flow-replay.js';
10
+ import { anchorPrecondition, anchorQueryArgs, staleTargetResult } from './flow-step-runners.js';
11
11
  import { queryRefs } from './replay.js';
12
12
  import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
13
13
  import { buildDecision, unverifiableReason } from './decision.js';
@@ -17,7 +17,7 @@ import { isDocumentGoneError } from '../../portal/session/facts/session-replaced
17
17
  import { classifyFlowAssertions, flattenSteps } from './flow-classify.js';
18
18
  import { dischargeFlowIntent, flowIntentStatement, flowReplayVerdictId } from './flow-intent.js';
19
19
  import { IntentStore } from '../../memory/intent/intent-store.js';
20
- import { sessionRoot } from '../../memory/project/session-root.js';
20
+ import { safeSessionTarget, sessionRoot, } from '../../memory/project/session-root.js';
21
21
  import { waitForPredicate } from '@reticlehq/engine/question/predicate/predicate.js';
22
22
  import { computeSegments } from '../../memory/journal/rollups.js';
23
23
  import { stepEffect } from '@reticlehq/engine/evidence/step-effect.js';
@@ -28,10 +28,12 @@ import { EnvelopeStore } from '../../memory/journal/envelope-store.js';
28
28
  import { homedir } from 'node:os';
29
29
  import { cloudFetch, syncRunRecordToCloud, SyncOutcome } from '../../memory/cloud/cloud-sync.js';
30
30
  import { resolveProjectCloud } from '../../memory/cloud/cloud-config.js';
31
+ import { memoryReadUrl, scopedMemoryEntries } from '../../memory/cloud/memory-scope.js';
31
32
  import { consultSubjectFor, selectConsulted } from './flow-memory-consult.js';
32
33
  import { log } from '../../log.js';
33
34
  import { flowsForSession } from './flow-store-for-session.js';
34
35
  import { projectForRoot } from '../../memory/project/project-for-root.js';
36
+ import { CROSS_STEP_INDEX, crossStepOnly } from './flow-cross-step.js';
35
37
  export function latestRecordedFlow(events) {
36
38
  for (let i = events.length - 1; i >= 0; i--) {
37
39
  const event = events[i];
@@ -138,7 +140,10 @@ async function consultProjectMemory(deps, flow, root) {
138
140
  const cloud = await resolveProjectCloud(deps.fs, root, homedir(), process.env);
139
141
  if (null === cloud.config || !cloud.policy.memory)
140
142
  return undefined;
141
- const url = `${cloud.config.url}/v1/memory?subject=${encodeURIComponent(subject)}`;
143
+ // Scoped to the linked project. The key covers a whole workspace, so a read that named no
144
+ // project could be answered with ANOTHER repo's knowledge and this replay would carry it into a
145
+ // verdict as if it were this app's — see `memory-scope.ts` for the whole shape.
146
+ const url = memoryReadUrl(cloud.config.url, { projectId: cloud.projectId, subject });
142
147
  const res = await cloudFetch(url, {
143
148
  method: 'GET',
144
149
  headers: { authorization: `Bearer ${cloud.config.apiKey}` },
@@ -149,9 +154,14 @@ async function consultProjectMemory(deps, flow, root) {
149
154
  // property yields the function itself, `.entries` on it is undefined, and the whole feature
150
155
  // fails silently to "the project knows nothing" — which is indistinguishable from the honest
151
156
  // empty case and is why this took a live drive to notice at all.
152
- const body = (await res.json());
153
- const entries = body?.entries;
154
- if (!Array.isArray(entries))
157
+ const body = await res.json();
158
+ // The envelope is the check that works: the platform names the project on the RESPONSE and not
159
+ // on each entry, so the entry-level filter alone kept a whole sibling workspace while looking
160
+ // like it did something. `undefined` means the response could not be shown to be this project's
161
+ // — nothing is attached, because a verdict carrying another repo's "established knowledge" is
162
+ // worse than one carrying none. See `memory-scope.ts`.
163
+ const entries = scopedMemoryEntries(body, cloud.projectId);
164
+ if (entries === undefined)
155
165
  return undefined;
156
166
  const picked = selectConsulted(entries);
157
167
  return 0 === picked.length ? undefined : picked;
@@ -167,8 +177,8 @@ async function consultProjectMemory(deps, flow, root) {
167
177
  * anchor simply isn't on this page yet. Detect that so the decision says "navigate there first"
168
178
  * instead of a mystifying "a step no longer matches". Returns undefined when the routes agree or the
169
179
  * current route is unobservable — never a false alarm. Replay normally never gets here on a
170
- * wrong page (arriveAtStartPath navigates before step 1); this is the fallback for when that
171
- * navigation was refused or the SDK never reconnected in the window.
180
+ * wrong page (arriveAtStartPath navigates before step 1); this is the fallback when navigation
181
+ * was refused. A page that never reconnected is reported as unverifiable before step 1.
172
182
  */
173
183
  export function startPathMismatchHint(flow, session) {
174
184
  const startPath = flow.startPath;
@@ -188,33 +198,25 @@ const REAL_ARRIVAL_CLOCK = {
188
198
  };
189
199
  /**
190
200
  * The session `oldId` denotes right now — itself, or the tombstone successor `resolve` rebinds to
191
- * after a full-document navigation — but only once it actually sits on `target`. Undefined while
192
- * the tab is still travelling (or in the gap between teardown and the successor's HELLO, when
193
- * `resolve` throws). Keyed to the navigated tab's own identity on purpose: matching "any session at
194
- * the target page" would happily hand replay an unrelated tab that was already sitting there.
201
+ * after a full-document navigation. A different instance proves the SDK reconnected even when the
202
+ * app redirected to another route. Keyed to the navigated tab's own identity on purpose: matching
203
+ * "any session at the target page" could hand replay an unrelated tab already sitting there.
195
204
  */
196
205
  function arrivedSuccessor(sessions, oldId, target,
197
- /** The handle that issued the navigation, when only a genuinely new one counts as arrival. */
198
- mustReplace) {
206
+ /** The handle that issued the navigation. */
207
+ issuedBy, isReload) {
199
208
  try {
200
209
  const candidate = sessions.resolve(oldId);
201
- /*
202
- * A RELOAD lands on the path it left, so the path cannot tell the successor from the tab that
203
- * is still tearing down — `resolve(oldId)` answers with the dying one while it is registered,
204
- * which looks like instant arrival and hands replay a socket that will never answer again.
205
- *
206
- * The discriminator is the OBJECT, not the id. A leased tab carries its identity across a page
207
- * load (that is what the identity params are for), so it reconnects under the SAME id — waiting
208
- * for a new id there waits forever, and the first lease this was tried on timed out at step 1
209
- * on a tab that was perfectly healthy. `SessionManager.add` replaces the instance on every
210
- * reconnect, so a different instance is exactly "the socket came back".
211
- *
212
- * A navigation to a different route has the path as its discriminator and does not need this.
213
- */
214
- if (mustReplace !== undefined && candidate === mustReplace)
210
+ // A reload can return the dying handle on the same path. Compare instances, not ids: a leased
211
+ // tab can reconnect under the same id. A redirect counts only on a new instance.
212
+ if (isReload && candidate === issuedBy)
215
213
  return undefined;
216
214
  const path = currentPathOf(candidate);
217
- return path !== undefined && samePath(path, target) ? candidate : undefined;
215
+ if (path === undefined)
216
+ return undefined;
217
+ const matches = samePath(path, target);
218
+ // A still-connected old tab on the wrong route is not a redirected successor.
219
+ return matches || candidate !== issuedBy ? { session: candidate, matches } : undefined;
218
220
  }
219
221
  catch {
220
222
  return undefined;
@@ -267,10 +269,9 @@ async function firstStepResolvesHere(session, flow) {
267
269
  * A full-page load tears down the session socket, so the navigation must happen here — before any
268
270
  * step runs — and the replay continues on the session the SDK reconnects as (found via the same
269
271
  * tombstone rebind that lets `resolve(oldId)` answer after any navigation). Best-effort by design:
270
- * when the flow carries no startPath, the current route is unobservable, the navigation is refused,
271
- * or the SDK never reconnects in the window, replay proceeds on the connected session as before —
272
- * with startPathMismatchHint turning any resulting drift into an actionable next move rather than a
273
- * mystifying one.
272
+ * when the flow carries no startPath, the current route is unobservable, or navigation is refused,
273
+ * replay proceeds on the connected session. A missing successor stops before step 1; a redirect
274
+ * continues on the live successor with a route-specific hint.
274
275
  */
275
276
  export async function arriveAtStartPath(sessions, session, flow, timeoutMs = START_PATH_ARRIVAL_TIMEOUT_MS, clock = REAL_ARRIVAL_CLOCK) {
276
277
  const target = flow.startPath;
@@ -304,9 +305,24 @@ export async function arriveAtStartPath(sessions, session, flow, timeoutMs = STA
304
305
  // gone after is the reload's doing, not the flow file's. Without this, an app holding its session
305
306
  // in memory comes back signed out and step 1 blames a component that is completely fine.
306
307
  const resolvedBefore = here && (await firstStepResolvesHere(session, flow));
307
- const arrived = await navigateAndAwait(sessions, session, destination, target, timeoutMs, clock, here);
308
- if (arrived === undefined)
308
+ const outcome = await navigateAndAwait(sessions, session, destination, target, timeoutMs, clock, here);
309
+ if ('lost' === outcome.kind) {
310
+ return {
311
+ lost: `replay ${here ? 'reloaded' : 'navigated to'} ${target} before step 1 and no page was connected ` +
312
+ `after ${String(timeoutMs)}ms, so the session it was driving is gone and nothing ran. A slow ` +
313
+ `or backgrounded tab can take longer to come back: bring it to the front and replay`,
314
+ };
315
+ }
316
+ if ('refused' === outcome.kind)
309
317
  return {};
318
+ if ('redirected' === outcome.kind) {
319
+ const path = currentPathOf(outcome.session) ?? 'another route';
320
+ return {
321
+ session: outcome.session,
322
+ redirected: `replay ${here ? 'reloaded' : 'navigated to'} ${target} before step 1, but the tab reconnected on ${path}. Check the redirect or sign in before replaying`,
323
+ };
324
+ }
325
+ const arrived = outcome.session;
310
326
  if (!resolvedBefore || (await firstStepResolvesHere(arrived, flow)))
311
327
  return { session: arrived };
312
328
  return {
@@ -329,18 +345,6 @@ function resetCostHint(flow, target) {
329
345
  return (`replay reloaded ${target} before step 1 and the first anchor did not come back — this flow ` +
330
346
  `starts from state a page load discards. ${optOut}, or record it from a cold page`);
331
347
  }
332
- /**
333
- * Send a tab somewhere and wait for the session that comes back.
334
- *
335
- * A navigation is a full page load: the session that issued it DIES and a successor connects with a
336
- * new id. Every caller therefore has to wait for that successor rather than reuse the handle it
337
- * already holds, and getting this wrong does not error — it leaves the caller talking to a session
338
- * that will never answer again.
339
- *
340
- * Extracted so there is ONE of these rather than one per caller. `arriveAtStartPath` decides WHETHER
341
- * to move and this decides how, which is also why the deliberate short-circuits live up there and
342
- * not in here: a caller that has already decided it must move should not have to argue with them.
343
- */
344
348
  export async function navigateAndAwait(sessions, session, destination, expectedPath, timeoutMs = START_PATH_ARRIVAL_TIMEOUT_MS, clock = REAL_ARRIVAL_CLOCK,
345
349
  /** True when the destination is the page we are already on — see `arrivedSuccessor`. */
346
350
  isReload = false) {
@@ -349,7 +353,7 @@ isReload = false) {
349
353
  try {
350
354
  const outcome = await session.command(ReticleCommand.NAVIGATE, { url });
351
355
  if (!outcome.ok || true !== asRecord(outcome.result)['ok'])
352
- return undefined;
356
+ return { kind: 'refused' };
353
357
  }
354
358
  catch (error) {
355
359
  // A navigation that unloads the page rejects the very command that asked for it — the transport
@@ -357,15 +361,18 @@ isReload = false) {
357
361
  // and left replay driving a handle that could never answer again, which is how a flow with a
358
362
  // startPath died at step 1 on an app that had loaded perfectly. Anything else is a real refusal.
359
363
  if (!isDocumentGoneError(error))
360
- return undefined;
364
+ return { kind: 'refused' };
361
365
  }
362
366
  const deadline = clock.now() + timeoutMs;
363
367
  for (;;) {
364
- const arrived = arrivedSuccessor(sessions, session.id, expectedPath, isReload ? session : undefined);
365
- if (arrived !== undefined)
366
- return arrived;
368
+ const arrived = arrivedSuccessor(sessions, session.id, expectedPath, session, isReload);
369
+ if (true === arrived?.matches)
370
+ return { kind: 'arrived', session: arrived.session };
371
+ // A live redirect is a different result from no SDK reconnection at all.
367
372
  if (clock.now() >= deadline)
368
- return undefined;
373
+ return arrived === undefined
374
+ ? { kind: 'lost' }
375
+ : { kind: 'redirected', session: arrived.session };
369
376
  await clock.sleep(START_PATH_POLL_MS);
370
377
  }
371
378
  }
@@ -382,7 +389,7 @@ isReload = false) {
382
389
  * conservative direction, because a composite graded on a flow nobody could read is graded on a
383
390
  * promise.
384
391
  */
385
- async function loadInvokedFlows(deps, flow, projectId) {
392
+ async function loadInvokedFlows(deps, flow, target) {
386
393
  const out = new Map();
387
394
  const queue = [flow];
388
395
  const seen = new Set([flow.name]);
@@ -395,7 +402,7 @@ async function loadInvokedFlows(deps, flow, projectId) {
395
402
  if (name === undefined || seen.has(name))
396
403
  continue;
397
404
  seen.add(name);
398
- const sub = await flowsForSession(deps, projectId).flows.load(name, projectId);
405
+ const sub = await flowsForSession(deps, target).flows.load(name, target.projectId);
399
406
  if (!sub.ok)
400
407
  continue;
401
408
  out.set(name, sub.value);
@@ -453,7 +460,7 @@ async function firstUnmetPrecondition(session, flow, since) {
453
460
  export function lostDocumentResult(name, lost) {
454
461
  return {
455
462
  name,
456
- status: ReplayStatus.OK,
463
+ status: ReplayStatus.UNVERIFIABLE,
457
464
  steps: lost.steps,
458
465
  unverifiable: {
459
466
  reason: `the page's connection to Reticle was replaced while step ${String(lost.atStep)} was running ` +
@@ -473,16 +480,11 @@ observe = (s) => s) {
473
480
  // Resolve within the connecting app's scope so a shared daemon replays THIS project's flow, not a
474
481
  // same-named flow from another app. Safe-resolve: a missing session degrades to the global store,
475
482
  // and the load-then-session order (unchanged) still surfaces a not-found before a no-session error.
476
- let projectId;
477
- try {
478
- projectId = deps.sessions.resolve(asString(args['sessionId'])).projectId;
479
- }
480
- catch {
481
- projectId = undefined;
482
- }
483
+ const target = safeSessionTarget(deps, asString(args['sessionId']));
484
+ const { projectId } = target;
483
485
  // The app's store, not the daemon's: this load answering `flow_not_found` for a flow plainly on
484
486
  // disk is what made replay unusable from a daemon started outside the project.
485
- const loaded = await flowsForSession(deps, projectId).flows.load(name, projectId);
487
+ const loaded = await flowsForSession(deps, target).flows.load(name, projectId);
486
488
  if (!loaded.ok) {
487
489
  await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt, projectId, sessionRoot(deps, asString(args['sessionId'])));
488
490
  return {
@@ -507,11 +509,23 @@ observe = (s) => s) {
507
509
  // The FlowFile contract: replay navigates to the flow's startPath before step 1, and the steps run
508
510
  // on the session the SDK reconnects as. When arrival can't be confirmed, replay proceeds on the
509
511
  // connected session — and the hint below turns the wrong-page drift into an actionable next move.
510
- const arrival = await arriveAtStartPath(deps.sessions, connected, loaded.value);
512
+ // Continuing at a step means the page is already there; going to the start would undo it.
513
+ const { to, at: continueAt } = replayWindow(args);
514
+ const arrival = continueAt === undefined ? await arriveAtStartPath(deps.sessions, connected, loaded.value) : {};
515
+ // Continuing on `connected` after the tab left timed every step out on a dead handle (#1129).
516
+ if (arrival.lost !== undefined) {
517
+ const reason = arrival.lost;
518
+ return {
519
+ name: loaded.value.name,
520
+ status: ReplayStatus.UNVERIFIABLE,
521
+ steps: [],
522
+ unverifiable: { reason },
523
+ };
524
+ }
511
525
  const session = arrival.session ?? connected;
512
526
  // The reset's own cost outranks the wrong-page hint: if the reload is why step 1 cannot start,
513
527
  // "navigate there and replay" is advice that would do the same thing again.
514
- const startPathHint = arrival.resetCost ?? startPathMismatchHint(loaded.value, session);
528
+ const startPathHint = arrival.redirected ?? arrival.resetCost ?? startPathMismatchHint(loaded.value, session);
515
529
  // Floor the success oracle at the start of THIS replay so a stale signal from a prior run
516
530
  // in the same session can't fake a pass.
517
531
  const replayFloor = session.elapsed();
@@ -521,7 +535,7 @@ observe = (s) => s) {
521
535
  // Loaded once and used for BOTH the replay and the grading below. A composite asserts through
522
536
  // what it runs, and a grader that cannot see the sub-flows reports `unverifiable` on a journey
523
537
  // that checks itself thoroughly — right about the file, wrong about the journey.
524
- const invokedFlows = await loadInvokedFlows(deps, replayable, projectId);
538
+ const invokedFlows = await loadInvokedFlows(deps, replayable, target);
525
539
  /*
526
540
  * The flow's own preconditions, before a single step runs.
527
541
  *
@@ -557,7 +571,7 @@ observe = (s) => s) {
557
571
  const names = missing.map((each) => each.envKey).join(', ');
558
572
  return {
559
573
  name: replayable.name,
560
- status: ReplayStatus.OK,
574
+ status: ReplayStatus.UNVERIFIABLE,
561
575
  steps: [],
562
576
  unverifiable: {
563
577
  reason: `this flow fills a redacted field and nothing supplied it, so nothing ran and nothing ` +
@@ -577,7 +591,7 @@ observe = (s) => s) {
577
591
  if (unmet !== undefined) {
578
592
  return {
579
593
  name: replayable.name,
580
- status: ReplayStatus.OK,
594
+ status: ReplayStatus.UNVERIFIABLE,
581
595
  steps: [],
582
596
  unverifiable: { reason: unmet },
583
597
  };
@@ -588,13 +602,20 @@ observe = (s) => s) {
588
602
  // How an `invoke` step finds the flow it runs. Scoped to the same project as the flow being
589
603
  // replayed, so a composite cannot reach into another app's store for a same-named sub-journey.
590
604
  resolveFlow: async (invoked) => {
591
- const sub = await flowsForSession(deps, projectId).flows.load(invoked, projectId);
605
+ const sub = await flowsForSession(deps, target).flows.load(invoked, projectId);
592
606
  return sub.ok ? await resolveFlowUploads(deps, sub.value) : undefined;
593
607
  },
594
608
  // Bug-sweep mode: keep going past a step whose action ran and whose consequence merely did
595
609
  // not hold, so one flow reports one verdict per step instead of stopping at the first defect.
596
610
  sweep: true === args['sweep'],
597
611
  ...(from === undefined ? {} : { from }),
612
+ ...(to === undefined ? {} : { to }),
613
+ ...(continueAt === undefined ? {} : { at: continueAt }),
614
+ // The HUD's replay chip, when the human pressed ▶. In-process only: an agent's arguments
615
+ // arrive as JSON and can never carry a function.
616
+ ...('function' === typeof args['onStep']
617
+ ? { onStep: args['onStep'] }
618
+ : {}),
598
619
  });
599
620
  }
600
621
  catch (error) {
@@ -602,6 +623,9 @@ observe = (s) => s) {
602
623
  throw error;
603
624
  return lostDocumentResult(name, error);
604
625
  }
626
+ // Only part of the journey was driven here: its steps were checked, nothing about the whole flow.
627
+ if (continueAt !== undefined || (to !== undefined && to < loaded.value.steps.length))
628
+ return { name, status: partialStatus(steps), steps };
605
629
  // Computed HERE, before the synthetic success row is appended below: once that row is pushed,
606
630
  // `steps.length` no longer counts only the flow's own steps and the arithmetic is wrong.
607
631
  const halted = haltedFrom(steps, loaded.value.steps.length);
@@ -639,6 +663,9 @@ observe = (s) => s) {
639
663
  };
640
664
  steps.push(row);
641
665
  }
666
+ const staleTarget = staleTargetResult(name, steps);
667
+ if (staleTarget !== undefined)
668
+ return staleTarget;
642
669
  const driftSteps = steps.filter((s) => s.drift !== undefined).length;
643
670
  const allOk = steps.every((s) => s.ok);
644
671
  const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.ERROR;
@@ -674,8 +701,9 @@ observe = (s) => s) {
674
701
  * moment somebody needs to know what this feature is supposed to do and who established it. A
675
702
  * knowledge base you only see when everything is already fine is decoration.
676
703
  */
677
- // No projectId argument: the API key is already bound to one project server-side, so passing a
678
- // second opinion about which project this is would only create a way for the two to disagree.
704
+ // The scope comes from the link file, inside `consultProjectMemory`. It is NOT the API key's
705
+ // job: a key covers a whole workspace and a workspace holds more than one repo, so "bound to one
706
+ // project server-side" was an assumption that handed this replay another app's knowledge.
679
707
  const knows = await consultProjectMemory(deps, loaded.value, replayRoot);
680
708
  const failed = steps.find((step) => !step.ok && step.drift === undefined);
681
709
  if (failed !== undefined) {
@@ -768,27 +796,6 @@ observe = (s) => s) {
768
796
  */
769
797
  return result;
770
798
  }
771
- /** A contradiction's identity for de-duplication: the rule that fired, and the evidence it fired on. */
772
- function contradictionId(found) {
773
- return `${found.kind}|${found.detail}`;
774
- }
775
- /**
776
- * The contradictions the whole-span pass found that no individual step could.
777
- *
778
- * A step's window closes when the step ends, so a request fired at step 2 and still unanswered at
779
- * step 5 is invisible to every per-step window: step 2's closed before the answer came and step 5
780
- * never saw it start. Re-running the detectors over the whole replay span finds those — and re-finds
781
- * everything the steps already reported, which is what the subtraction is for. Reporting a finding
782
- * twice teaches a reader that the count is noise.
783
- *
784
- * Exported for its own test: the subtraction is the whole rule, and it is pure.
785
- */
786
- /** Cross-step findings belong to no single step; -1 is the address the suite verdict already uses. */
787
- const CROSS_STEP_INDEX = -1;
788
- export function crossStepOnly(whole, steps) {
789
- const seen = new Set(steps.flatMap((step) => (step.contradictions ?? []).map(contradictionId)));
790
- return whole.filter((found) => !seen.has(contradictionId(found)));
791
- }
792
799
  /**
793
800
  * The deviation report over a replay's route segments — compared against the learned envelope, which is
794
801
  * then updated. Best-effort: any failure (disk, empty window) yields undefined, never breaking the replay.
@@ -1,7 +1,7 @@
1
1
  export { nearestIsAmbiguous, nearestTestid, resolveQuery } from './flow-anchor.js';
2
2
  import type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
3
3
  export type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
4
- import { type Drift, type FlowFile, type FlowStep, type FlowStepResult } from '@reticlehq/core';
4
+ import { type Drift, type FlowFile, type FlowStep, type FlowStepResult, ReplayStatus } from '@reticlehq/core';
5
5
  /**
6
6
  * The document this replay was driving went away mid-run.
7
7
  *
@@ -53,6 +53,11 @@ export declare function assertStepExpect(session: FlowReplaySession, expect: Non
53
53
  * How a replay is loaded, where it starts reporting, and how far it goes.
54
54
  */
55
55
  export interface ReplayFromOptions {
56
+ /**
57
+ * Told after every step it reports: how many are done, of how many, and whether this one held.
58
+ * The HUD's replay chip draws its progress from this; it must never throw into the replay.
59
+ */
60
+ onStep?: (done: number, total: number, held: boolean) => void;
56
61
  /**
57
62
  * Resume at this step: an index, or a step's `id` (which survives edits that shift indices).
58
63
  *
@@ -62,6 +67,16 @@ export interface ReplayFromOptions {
62
67
  * for, and swallowing that would report the run as starting where it did not.
63
68
  */
64
69
  from?: number | string;
70
+ /**
71
+ * Stop before this step index: drive the journey only as far as a point, such as a dialog that
72
+ * several journeys branch from. Absent means the whole flow.
73
+ */
74
+ to?: number;
75
+ /**
76
+ * Continue at this step on the page as it is: earlier steps are not driven at all, because the
77
+ * caller already drove an identical prefix (another flow replayed `to` this step).
78
+ */
79
+ at?: number;
65
80
  /**
66
81
  * How to load a flow this one INVOKES. Absent means invocations cannot be followed.
67
82
  *
@@ -99,6 +114,13 @@ export interface ReplayFromOptions {
99
114
  * is not setup, it is a second charge, a second email, a second record.
100
115
  */
101
116
  export declare function resumeIndex(from: number | string | undefined, steps: readonly FlowStep[]): number | undefined;
117
+ /** How far a replay drives: `to` stops before a step, `at` picks up at one. Positive indices only. */
118
+ export declare function replayWindow(args: Record<string, unknown>): {
119
+ to?: number;
120
+ at?: number;
121
+ };
122
+ /** A part-way replay's status, from the steps it drove. */
123
+ export declare function partialStatus(steps: readonly FlowStepResult[]): ReplayStatus;
102
124
  /** The `from` argument, narrowed: a step index or a step id. */
103
125
  export declare function resumeArg(raw: unknown): number | string | undefined;
104
126
  /** Why `from` cannot be resumed at, or undefined when it can (or was not given). */