@reticlehq/server 3.9.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +43 -2
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/init.mdx +2 -2
  322. package/docs/cli/license.mdx +9 -7
  323. package/docs/cli/restart.mdx +7 -5
  324. package/docs/cli/rollback.mdx +5 -5
  325. package/docs/cli/stop.mdx +3 -3
  326. package/docs/cli/update.mdx +2 -2
  327. package/docs/cli/verify.mdx +23 -1
  328. package/docs/cli.mdx +33 -23
  329. package/docs/deploy-checks.md +1 -1
  330. package/docs/desktop.mdx +1 -1
  331. package/docs/enterprise.md +2 -2
  332. package/docs/environment.mdx +1 -1
  333. package/docs/flows.md +11 -1
  334. package/docs/frameworks.mdx +9 -9
  335. package/docs/getting-started.md +1 -1
  336. package/docs/install-manual.mdx +1 -1
  337. package/docs/license-activation.mdx +3 -3
  338. package/docs/packages/tauri.mdx +1 -1
  339. package/docs/packages.mdx +1 -1
  340. package/docs/predicates.mdx +1 -1
  341. package/docs/quickstart.mdx +68 -49
  342. package/docs/skill-setup.md +1 -1
  343. package/docs/skill.md +14 -13
  344. package/docs/telemetry.md +2 -2
  345. package/docs/tools/navigate.mdx +1 -1
  346. package/docs/tools/verify.mdx +4 -1
  347. package/docs/troubleshooting.mdx +30 -1
  348. package/docs/usage.md +17 -14
  349. package/package.json +11 -14
  350. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  351. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -146,11 +146,13 @@ export class SessionJournal {
146
146
  #droppedBytes = 0;
147
147
  /** Parsed records evicted from the retained set. Exact — these were counted, not estimated. */
148
148
  #droppedEvents = 0;
149
- /**
150
- * Latest parsed eviction timestamp, or +Infinity after unparsed bytes were skipped.
151
- * -Infinity means no loss. Later intact reads cannot narrow an unknown discarded time range.
152
- */
149
+ /** Latest `t` that a cut or an eviction can reach, inclusive. -Infinity means no loss. */
153
150
  #lostThroughT = Number.NEGATIVE_INFINITY;
151
+ /** A cut skipped bytes and no record after it has been parsed yet, so its bound is unknown. */
152
+ #cutPending = false;
153
+ /** The last record parsed, so a cut can tell whether the clock started again across it. */
154
+ #lastParsed;
155
+ #now;
154
156
  constructor(fs, root, sessionId, options = {}) {
155
157
  if (!isValidSessionId(sessionId)) {
156
158
  throw new Error(`refusing to journal an unsafe session id: ${sessionId}`);
@@ -171,6 +173,7 @@ export class SessionJournal {
171
173
  this.#maxRetainedBytes = boundedOption(options.maxRetainedBytes, JOURNAL_READ_LIMITS.MAX_RETAINED_BYTES, 'maxRetainedBytes');
172
174
  this.#maxRetainedEvents = boundedOption(options.maxRetainedEvents, JOURNAL_READ_LIMITS.MAX_RETAINED_EVENTS, 'maxRetainedEvents');
173
175
  this.#eventBytesCap = cap;
176
+ this.#now = options.now;
174
177
  }
175
178
  /**
176
179
  * Append a batch, or refuse it at the byte cap and say so.
@@ -416,10 +419,15 @@ export class SessionJournal {
416
419
  // never skips, so its start IS the offset we asked for.
417
420
  const from = chunk.from ?? this.#eventBytesConsumed;
418
421
  const skipped = Math.max(0, from - this.#eventBytesConsumed);
419
- this.#ingestTail(chunk.text);
422
+ // Before the ingest, so the first record parsed after the cut bounds it.
420
423
  if (skipped > 0) {
421
424
  this.#noteCut(skipped);
422
425
  }
426
+ this.#ingestTail(chunk.text);
427
+ const now = this.#now;
428
+ if (this.#cutPending && now !== undefined) {
429
+ this.#boundCut({ t: now() });
430
+ }
423
431
  // Resume at the end of the last COMPLETE line, counted forward over the bytes that actually
424
432
  // ARRIVED. Not from `chunk.size`: that is the file's end as `fstat` reported it, and a read
425
433
  // truncated or cut short returns less — a cursor there would skip every unread byte, silently
@@ -494,6 +502,10 @@ export class SessionJournal {
494
502
  }
495
503
  const result = ReticleEventSchema.safeParse(parsed);
496
504
  if (result.success) {
505
+ if (this.#cutPending) {
506
+ this.#boundCut(result.data);
507
+ }
508
+ this.#lastParsed = result.data;
497
509
  this.#eventCache.push(result.data);
498
510
  // Its cost in the file, newline included — so the retained bound is in the same unit the
499
511
  // read ceiling and the loss report are.
@@ -543,13 +555,37 @@ export class SessionJournal {
543
555
  }
544
556
  }
545
557
  /**
546
- * Skipped bytes have unknown timestamps: page clocks can reset while the journal id survives.
547
- * A surviving tail cannot bound records that were never parsed. Later intact reads cannot
548
- * reconstruct that history; finite bounds apply only to parsed cache evictions.
558
+ * Skipped bytes are not parsed, so their `t` is not known at the cut. The first record parsed
559
+ * after the cut, or the time of the read, can bound them (see `#boundCut`). Until then, the cut
560
+ * reaches every window.
549
561
  */
550
562
  #noteCut(skippedBytes) {
551
563
  this.#droppedBytes += skippedBytes;
552
- this.#lostThroughT = Number.POSITIVE_INFINITY;
564
+ this.#cutPending = true;
565
+ }
566
+ /**
567
+ * Bound the pending cut at `next`: the first record parsed after it, or the time of the read.
568
+ *
569
+ * The file is append-only, and one connection stamps `t` from one monotonic clock
570
+ * (`Session.elapsed`). So when the clock did not start again across the cut, each skipped record
571
+ * has a `t` at or before `next.t`.
572
+ *
573
+ * A reload or a reconnect keeps the session id and starts the clock again, and one window can
574
+ * span it: a window opened at t=150 on the old connection can hold skipped old records at t=200
575
+ * while the new connection's records start at t=10. A bound from the new clock then clears a
576
+ * window that lost evidence. So the cut stays unbounded, for good, when the reset cannot be
577
+ * ruled out: no record was parsed before the cut, or `t` or `seq` went down, or the document
578
+ * changed. This check is conservative but not complete. A reconnect in the same document whose
579
+ * new clock already passed the old `t` shows none of the three signs.
580
+ */
581
+ #boundCut(next) {
582
+ const last = this.#lastParsed;
583
+ this.#cutPending = false;
584
+ if (last === undefined || clockStartedAgain(last, next)) {
585
+ this.#lostThroughT = Number.POSITIVE_INFINITY;
586
+ return;
587
+ }
588
+ this.#lostThroughT = Math.max(this.#lostThroughT, next.t);
553
589
  }
554
590
  /**
555
591
  * What these reads could not produce, or `undefined` when nothing was lost.
@@ -562,7 +598,7 @@ export class SessionJournal {
562
598
  if (0 === this.#droppedBytes && 0 === this.#droppedEvents) {
563
599
  return undefined;
564
600
  }
565
- const bounded = Number.isFinite(this.#lostThroughT);
601
+ const bounded = !this.#cutPending && Number.isFinite(this.#lostThroughT);
566
602
  return {
567
603
  droppedBytes: this.#droppedBytes,
568
604
  droppedEvents: this.#droppedEvents,
@@ -621,6 +657,18 @@ export class SessionJournal {
621
657
  return out;
622
658
  }
623
659
  }
660
+ /** Did the clock start again between `last` and `next`? A field one record lacks proves nothing. */
661
+ function clockStartedAgain(last, next) {
662
+ if (next.t < last.t) {
663
+ return true;
664
+ }
665
+ if (last.seq !== undefined && next.seq !== undefined && next.seq < last.seq) {
666
+ return true;
667
+ }
668
+ return (last.documentId !== undefined &&
669
+ next.documentId !== undefined &&
670
+ next.documentId !== last.documentId);
671
+ }
624
672
  /**
625
673
  * The closure report a session's sidecar holds — details when it can be read, the bare fact when not.
626
674
  *
@@ -1,3 +1,12 @@
1
+ /**
2
+ * The `.reticle` folder a caller meant by `root`. Agents pass the project folder, the one they work
3
+ * in, so a folder that holds a `.reticle` stands for it — and so does any existing directory, since
4
+ * a fresh worktree has none until the first run writes one, the same way a `.reticle` path is
5
+ * accepted before it exists. A path nowhere on disk is the caller's to fix, and the refusals name
6
+ * the path to pass. Openings exported so error-recovery classifies both as the caller's mistake.
7
+ */
8
+ export declare const ARTIFACT_ROOT_ABSOLUTE_REFUSAL: string;
9
+ export declare const ARTIFACT_ROOT_MISSING_REFUSAL: string;
1
10
  /** Claim `root` for the session a lease registers. Takes a `.reticle` folder or the project folder holding one. */
2
11
  export declare function claimArtifactRoot(leaseId: string, root: string): void;
3
12
  /** Every root a lease claimed in this process, released or not. Sync reads it to find the work. */
@@ -17,20 +17,25 @@ const claims = new Map();
17
17
  const everClaimed = new Set();
18
18
  /**
19
19
  * The `.reticle` folder a caller meant by `root`. Agents pass the project folder, the one they work
20
- * in, so a folder that holds a `.reticle` stands for it. Anything else is refused with the path to pass.
20
+ * in, so a folder that holds a `.reticle` stands for it — and so does any existing directory, since
21
+ * a fresh worktree has none until the first run writes one, the same way a `.reticle` path is
22
+ * accepted before it exists. A path nowhere on disk is the caller's to fix, and the refusals name
23
+ * the path to pass. Openings exported so error-recovery classifies both as the caller's mistake.
21
24
  */
25
+ export const ARTIFACT_ROOT_ABSOLUTE_REFUSAL = `root must be an absolute path to a ${ReticleDir.ROOT} folder`;
26
+ export const ARTIFACT_ROOT_MISSING_REFUSAL = `root must be an existing directory, or the ${ReticleDir.ROOT} folder inside one`;
22
27
  function resolveArtifactRoot(root) {
23
28
  if (!isAbsolute(root)) {
24
- throw new Error(`root must be an absolute path to a ${ReticleDir.ROOT} folder, got ${root}`);
29
+ throw new Error(`${ARTIFACT_ROOT_ABSOLUTE_REFUSAL}, got ${root}`);
25
30
  }
26
31
  if (ReticleDir.ROOT === basename(root)) {
27
32
  return root;
28
33
  }
29
34
  const inside = join(root, ReticleDir.ROOT);
30
- if (isDirectory(inside)) {
35
+ if (isDirectory(inside) || isDirectory(root)) {
31
36
  return inside;
32
37
  }
33
- throw new Error(`root must be the ${ReticleDir.ROOT} folder, or a project folder that contains one, got ${root}. Pass ${inside}`);
38
+ throw new Error(`${ARTIFACT_ROOT_MISSING_REFUSAL}, got ${root}. Pass ${inside}`);
34
39
  }
35
40
  function isDirectory(path) {
36
41
  try {
@@ -0,0 +1,7 @@
1
+ import { type NetworkDetail } from './network-detail.js';
2
+ /** The part of a CDP session this needs, kept structural so it can be tested with a fake. */
3
+ export interface CdpLike {
4
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
5
+ on(event: string, handler: (event: unknown) => void): void;
6
+ }
7
+ export declare function attachDownloadCapture(cdp: CdpLike, emit: (detail: NetworkDetail) => void, pageUrl: () => string): Promise<void>;
@@ -0,0 +1,92 @@
1
+ import { buildNetworkDetail } from './network-detail.js';
2
+ import { drivenRedactionPolicy } from './driven-redaction.js';
3
+ /**
4
+ * Capture a file the server sends as a download, on a page Reticle drives.
5
+ *
6
+ * A click that becomes a download leaves the page as a navigation the browser turns into a file.
7
+ * Measured on Chromium, nothing the page can see records it: no page `request`/`response` event, no
8
+ * CDP `Network.responseReceived` on the page target, and a Playwright route does not intercept it.
9
+ * So a `net` check on a working export missed. CDP request interception at the response stage does
10
+ * see it, with the status and headers, so this listens there.
11
+ *
12
+ * Document responses only, and every paused response is let through at once: the capture must never
13
+ * change or delay what the page receives. Chromium only; a browser without the Fetch domain gets no
14
+ * capture rather than an error.
15
+ */
16
+ const DOCUMENT_RESPONSES = { patterns: [{ resourceType: 'Document', requestStage: 'Response' }] };
17
+ /**
18
+ * The body is read only for a text attachment of known, small size. Reading it holds the whole file
19
+ * in memory, so a large or binary download keeps its status and headers and gets no body: a body
20
+ * check on it stays undecidable rather than costing the run its memory.
21
+ */
22
+ const MAX_BODY_READ_BYTES = 1024 * 1024;
23
+ const TEXTUAL = /text\/|json|csv|xml/i;
24
+ function readableBodySize(headers) {
25
+ const length = Number(headers['content-length']);
26
+ return (Number.isFinite(length) &&
27
+ length <= MAX_BODY_READ_BYTES &&
28
+ TEXTUAL.test(headers['content-type'] ?? ''));
29
+ }
30
+ async function readBody(cdp, requestId) {
31
+ try {
32
+ const got = (await cdp.send('Fetch.getResponseBody', { requestId }));
33
+ if ('string' !== typeof got.body) {
34
+ return undefined;
35
+ }
36
+ return true === got.base64Encoded ? Buffer.from(got.body, 'base64').toString('utf8') : got.body;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ function headersOf(raw) {
43
+ const headers = {};
44
+ if (!Array.isArray(raw)) {
45
+ return headers;
46
+ }
47
+ for (const entry of raw) {
48
+ const { name, value } = (entry ?? {});
49
+ if ('string' === typeof name && 'string' === typeof value) {
50
+ headers[name.toLowerCase()] = value;
51
+ }
52
+ }
53
+ return headers;
54
+ }
55
+ function isAttachment(headers) {
56
+ return /^\s*attachment\b/i.test(headers['content-disposition'] ?? '');
57
+ }
58
+ async function onPaused(cdp, paused, emit, pageUrl) {
59
+ const requestId = 'string' === typeof paused.requestId ? paused.requestId : undefined;
60
+ try {
61
+ const headers = headersOf(paused.responseHeaders);
62
+ const url = paused.request?.url;
63
+ const status = paused.responseStatusCode;
64
+ if (isAttachment(headers) && 'string' === typeof url && 'number' === typeof status) {
65
+ const method = paused.request?.method;
66
+ const responseBody = requestId !== undefined && readableBodySize(headers)
67
+ ? await readBody(cdp, requestId)
68
+ : undefined;
69
+ emit(buildNetworkDetail({
70
+ url,
71
+ ...('string' === typeof method ? { method } : {}),
72
+ status,
73
+ headers,
74
+ resourceType: 'document',
75
+ ...(responseBody === undefined ? {} : { responseBody }),
76
+ pageUrl: pageUrl(),
77
+ }, drivenRedactionPolicy()));
78
+ }
79
+ }
80
+ catch {
81
+ // Recording is best-effort; letting the response through below is not.
82
+ }
83
+ if (requestId !== undefined) {
84
+ await cdp.send('Fetch.continueRequest', { requestId }).catch(() => undefined);
85
+ }
86
+ }
87
+ export async function attachDownloadCapture(cdp, emit, pageUrl) {
88
+ cdp.on('Fetch.requestPaused', (event) => {
89
+ void onPaused(cdp, event ?? {}, emit, pageUrl);
90
+ });
91
+ await cdp.send('Fetch.enable', DOCUMENT_RESPONSES);
92
+ }
@@ -49,6 +49,10 @@ export interface NetworkDetail {
49
49
  * Omitted when the whole body fits, so the caveat means something when it appears.
50
50
  */
51
51
  requestBodyTruncated?: boolean;
52
+ /** The response body, captured only where the page cannot (a server-sent download); capped. */
53
+ responseBody?: string;
54
+ /** True when `responseBody` was cut to the cap: a body check on the rest cannot be decided. */
55
+ responseBodyTruncated?: boolean;
52
56
  /**
53
57
  * The document that ISSUED the request, which is what identifies the session it belongs to.
54
58
  *
@@ -73,6 +77,8 @@ export declare function buildNetworkDetail(raw: {
73
77
  headers: Record<string, string>;
74
78
  resourceType?: string;
75
79
  requestBody?: string;
80
+ /** The response body, when the capture could read it (a server-sent download). */
81
+ responseBody?: string;
76
82
  pageUrl?: string;
77
83
  }, policy?: RedactionPolicy): NetworkDetail;
78
84
  /** The minimal Playwright surfaces this attachment reads — kept structural so it's fake-testable. */
@@ -19,8 +19,9 @@ const DEFAULT_POLICY = { isSensitiveKey: defaultIsSensitiveKey };
19
19
  * which is key-based over a parsed object — not string-shaped.
20
20
  */
21
21
  function redactByKey(value, policy) {
22
- if (Array.isArray(value))
22
+ if (Array.isArray(value)) {
23
23
  return value.map((v) => redactByKey(v, policy));
24
+ }
24
25
  if (value !== null && 'object' === typeof value) {
25
26
  const out = {};
26
27
  for (const [k, v] of Object.entries(value)) {
@@ -86,6 +87,10 @@ function projectHeaders(headers, policy) {
86
87
  * about the function's inputs.
87
88
  */
88
89
  export function buildNetworkDetail(raw, policy = DEFAULT_POLICY) {
90
+ // Capped and redacted exactly like the request body, and the cut is declared the same way.
91
+ const responseBody = raw.responseBody === undefined || 0 === raw.responseBody.length
92
+ ? undefined
93
+ : projectWireBody(raw.responseBody, policy);
89
94
  const wireBody = raw.requestBody === undefined || 0 === raw.requestBody.length
90
95
  ? undefined
91
96
  : projectWireBody(raw.requestBody, policy);
@@ -101,6 +106,12 @@ export function buildNetworkDetail(raw, policy = DEFAULT_POLICY) {
101
106
  requestBody: wireBody.body,
102
107
  ...(wireBody.truncated ? { requestBodyTruncated: true } : {}),
103
108
  }),
109
+ ...(responseBody === undefined
110
+ ? {}
111
+ : {
112
+ responseBody: responseBody.body,
113
+ ...(responseBody.truncated ? { responseBodyTruncated: true } : {}),
114
+ }),
104
115
  ...(raw.pageUrl === undefined || 0 === raw.pageUrl.length ? {} : { pageUrl: raw.pageUrl }),
105
116
  };
106
117
  }
@@ -8,6 +8,17 @@ function mockProvider(deps) {
8
8
  const p = deps.realInput;
9
9
  return p !== undefined && 'function' === typeof p.setMocks ? p : undefined;
10
10
  }
11
+ /**
12
+ * Whether this call can mock the session: a lease of this caller, or a page the driven browser has.
13
+ * Having a driven browser is not enough: it mocks its own page, whatever session was named.
14
+ */
15
+ async function canMock(deps, session) {
16
+ if (true === deps.pool?.heldBy(session.id, deps.attachId)) {
17
+ return true;
18
+ }
19
+ const driven = mockProvider(deps);
20
+ return driven !== undefined && driven.isAvailableFor(session.url);
21
+ }
11
22
  const ruleShape = z.object({
12
23
  urlContains: z
13
24
  .string()
@@ -22,6 +33,11 @@ const ruleShape = z.object({
22
33
  .boolean()
23
34
  .optional()
24
35
  .describe('Simulate a network failure (offline) instead of a response.'),
36
+ forwardTo: z
37
+ .string()
38
+ .url()
39
+ .optional()
40
+ .describe('Send the matching request on to this origin, e.g. "http://127.0.0.1:15500"; the path and query stay and the real server answers.'),
25
41
  });
26
42
  /**
27
43
  * Narrow the validated tool args into MockRule[], omitting undefined keys (exactOptionalPropertyTypes).
@@ -41,8 +57,9 @@ const ruleShape = z.object({
41
57
  * to parse.
42
58
  */
43
59
  export function toRules(value) {
44
- if (value === undefined)
60
+ if (value === undefined) {
45
61
  return [];
62
+ }
46
63
  const parsed = z.array(ruleShape).safeParse(value);
47
64
  if (!parsed.success) {
48
65
  const problem = parsed.error.issues
@@ -50,23 +67,32 @@ export function toRules(value) {
50
67
  .join('; ');
51
68
  throw new Error(`reticle_network_mock could not read \`mocks\` (${problem}). Nothing was applied and mocking ` +
52
69
  'was left unchanged — an unreadable rule is NOT treated as a request to clear. A rule is ' +
53
- '{ urlContains, method?, status?, body?, contentType?, delayMs?, abort? }; pass `clear: true` ' +
70
+ '{ urlContains, method?, status?, body?, contentType?, delayMs?, abort?, forwardTo? }; pass `clear: true` ' +
54
71
  'or an empty array if you meant to turn mocking off.');
55
72
  }
56
73
  return parsed.data.map((r) => {
57
74
  const rule = { urlContains: r.urlContains };
58
- if (r.method !== undefined)
75
+ if (r.method !== undefined) {
59
76
  rule.method = r.method;
60
- if (r.status !== undefined)
77
+ }
78
+ if (r.status !== undefined) {
61
79
  rule.status = r.status;
62
- if (r.body !== undefined)
80
+ }
81
+ if (r.body !== undefined) {
63
82
  rule.body = r.body;
64
- if (r.contentType !== undefined)
83
+ }
84
+ if (r.contentType !== undefined) {
65
85
  rule.contentType = r.contentType;
66
- if (r.delayMs !== undefined)
86
+ }
87
+ if (r.delayMs !== undefined) {
67
88
  rule.delayMs = r.delayMs;
68
- if (r.abort !== undefined)
89
+ }
90
+ if (r.abort !== undefined) {
69
91
  rule.abort = r.abort;
92
+ }
93
+ if (r.forwardTo !== undefined) {
94
+ rule.forwardTo = r.forwardTo;
95
+ }
70
96
  return rule;
71
97
  });
72
98
  }
@@ -95,7 +121,7 @@ export const NETWORK_MOCK_TOOLS = [
95
121
  mocks: z
96
122
  .array(ruleShape)
97
123
  .optional()
98
- .describe('Interception rules, first match wins: { urlContains, method?, status?, body?, contentType?, delayMs?, abort? }. Omit or pass [] to clear.'),
124
+ .describe('Interception rules, first match wins: { urlContains, method?, status?, body?, contentType?, delayMs?, abort?, forwardTo? }. Omit or pass [] to clear.'),
99
125
  clear: z.boolean().optional().describe('Clear all active mocks (same as mocks: []).'),
100
126
  ...sessionIdShape,
101
127
  },
@@ -107,20 +133,30 @@ export const NETWORK_MOCK_TOOLS = [
107
133
  recommendation: z.string().optional(),
108
134
  },
109
135
  handler: async (deps, args) => {
110
- const session = deps.sessions.resolve(asString(args['sessionId']));
136
+ const session = await deps.sessions.resolveServing(asString(args['sessionId']), (s) => canMock(deps, s));
111
137
  const rules = true === args['clear'] ? [] : toRules(args['mocks']);
112
- const provider = mockProvider(deps);
138
+ // A lease is mocked as the lease, and the driven browser mocks only a page it has. The driven
139
+ // provider mocks its own page whatever session was named: taken first, it mocked another tab,
140
+ // answered `applied: true`, and the lease's real write reached the backend.
141
+ const driven = mockProvider(deps);
142
+ const provider = driven !== undefined &&
143
+ deps.pool?.leaseUrl(session.id) === undefined &&
144
+ (await driven.isAvailableFor(session.url))
145
+ ? driven
146
+ : undefined;
113
147
  if (provider !== undefined) {
114
148
  const applied = await provider.setMocks(session.url, rules);
115
- if (applied)
149
+ if (applied) {
116
150
  return mocksApplied(deps, session.id, rules.length);
151
+ }
117
152
  }
118
153
  // A lease is a Playwright-owned page — CDP intercept was always there, this tool just had no
119
154
  // route to it. Tried after the driven provider: when both exist, drive is the page the caller
120
155
  // means, and a lease is the fallback rather than a competitor.
121
156
  const leased = await deps.pool?.setMocksLease(session.id, rules);
122
- if (true === leased)
157
+ if (true === leased) {
123
158
  return mocksApplied(deps, session.id, rules.length);
159
+ }
124
160
  return {
125
161
  applied: false,
126
162
  count: 0,
@@ -26,6 +26,12 @@ export interface MockRule {
26
26
  delayMs?: number;
27
27
  /** Simulate a network failure (offline / connection refused) instead of a response. */
28
28
  abort?: boolean;
29
+ /**
30
+ * Send the request on to this origin (protocol, host and port; the path and query stay). An app that
31
+ * calls a fixed address (`http://localhost:5000`) can then reach its API on any port, for example when
32
+ * the host already uses that port. The real server answers.
33
+ */
34
+ forwardTo?: string;
29
35
  }
30
36
  /** What to do with a request after matching: fulfill a canned response, abort it, or let it through. */
31
37
  interface MockOutcome {
@@ -34,6 +40,8 @@ interface MockOutcome {
34
40
  body?: string;
35
41
  contentType?: string;
36
42
  delayMs?: number;
43
+ /** For `continue`: the url the request goes on with (a `forwardTo` rule). */
44
+ url?: string;
37
45
  }
38
46
  /**
39
47
  * Decide the outcome for a request from the active rules. Pure: no IO, no clock. The first rule
@@ -6,12 +6,26 @@ const DEFAULT_CONTENT_TYPE = 'application/json';
6
6
  */
7
7
  export function matchMock(rules, req) {
8
8
  for (const rule of rules) {
9
- if (!req.url.includes(rule.urlContains))
9
+ if (!req.url.includes(rule.urlContains)) {
10
10
  continue;
11
- if (rule.method !== undefined && rule.method.toUpperCase() !== req.method.toUpperCase())
11
+ }
12
+ if (rule.method !== undefined && rule.method.toUpperCase() !== req.method.toUpperCase()) {
12
13
  continue;
13
- if (true === rule.abort)
14
+ }
15
+ if (true === rule.abort) {
14
16
  return { kind: 'abort' };
17
+ }
18
+ if (rule.forwardTo !== undefined) {
19
+ const to = new URL(rule.forwardTo);
20
+ const next = new URL(req.url);
21
+ next.protocol = to.protocol;
22
+ next.host = to.host;
23
+ const outcome = { kind: 'continue', url: next.toString() };
24
+ if (rule.delayMs !== undefined) {
25
+ outcome.delayMs = rule.delayMs;
26
+ }
27
+ return outcome;
28
+ }
15
29
  /*
16
30
  * A rule that says only WHEN says nothing about WHAT.
17
31
  *
@@ -35,10 +49,12 @@ export function matchMock(rules, req) {
35
49
  status: rule.status ?? DEFAULT_STATUS,
36
50
  contentType: rule.contentType ?? DEFAULT_CONTENT_TYPE,
37
51
  };
38
- if (rule.body !== undefined)
52
+ if (rule.body !== undefined) {
39
53
  outcome.body = rule.body;
40
- if (rule.delayMs !== undefined)
54
+ }
55
+ if (rule.delayMs !== undefined) {
41
56
  outcome.delayMs = rule.delayMs;
57
+ }
42
58
  return outcome;
43
59
  }
44
60
  return { kind: 'continue' };
@@ -49,17 +65,19 @@ export async function applyOutcome(route, outcome, sleep) {
49
65
  // Sleep FIRST, then let it through: the delay is the whole perturbation, and the response that
50
66
  // comes back is the server's own. Fallback, not continue: a lease's context route (the CSP
51
67
  // widening, #2016) must still see the request.
52
- if (outcome.delayMs !== undefined && outcome.delayMs > 0)
68
+ if (outcome.delayMs !== undefined && outcome.delayMs > 0) {
53
69
  await sleep(outcome.delayMs);
54
- await route.fallback();
70
+ }
71
+ await (outcome.url === undefined ? route.fallback() : route.fallback({ url: outcome.url }));
55
72
  return;
56
73
  }
57
74
  if ('abort' === outcome.kind) {
58
75
  await route.abort('failed');
59
76
  return;
60
77
  }
61
- if (outcome.delayMs !== undefined && outcome.delayMs > 0)
78
+ if (outcome.delayMs !== undefined && outcome.delayMs > 0) {
62
79
  await sleep(outcome.delayMs);
80
+ }
63
81
  await route.fulfill({
64
82
  status: outcome.status ?? DEFAULT_STATUS,
65
83
  contentType: outcome.contentType ?? DEFAULT_CONTENT_TYPE,
@@ -74,8 +92,9 @@ const realSleep = (ms) => new Promise((res) => setTimeout(res, ms));
74
92
  */
75
93
  export async function installNetworkMocks(page, rules, sleep = realSleep) {
76
94
  await page.unroute('**/*').catch(() => undefined);
77
- if (0 === rules.length)
95
+ if (0 === rules.length) {
78
96
  return;
97
+ }
79
98
  await page.route('**/*', (route) => {
80
99
  const req = route.request();
81
100
  const outcome = matchMock(rules, { url: req.url(), method: req.method() });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A screenshot of a Playwright page, shared by the CDP and launched providers so the capture path
3
+ * lives in one place.
4
+ */
5
+ import type { Page } from 'playwright';
6
+ import type { ElementBox } from './real-input.js';
7
+ /** Options for a page screenshot — full-page scroll capture and/or a clip box. */
8
+ export interface ScreenshotOpts {
9
+ fullPage?: boolean;
10
+ /** Restrict the capture to one element/region (viewport CSS px). */
11
+ clip?: ElementBox;
12
+ /**
13
+ * A live picture, as a JPEG at this quality: what a person is watching, taken as it is. The
14
+ * baseline tweaks (hiding the HUD, stopping animations) would make their window flicker each second.
15
+ */
16
+ jpegQuality?: number;
17
+ /** One image pixel per CSS pixel, so a high-density display does not double a step screenshot. */
18
+ cssScale?: boolean;
19
+ }
20
+ /**
21
+ * Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
22
+ * screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
23
+ */
24
+ export declare function capturePage(page: Page, opts: ScreenshotOpts): Promise<Uint8Array>;
@@ -0,0 +1,33 @@
1
+ import { SCREENSHOT_DETERMINISM, captureWithChromeHidden, } from '../pool/playwright-launcher.js';
2
+ /**
3
+ * Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
4
+ * screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
5
+ */
6
+ export async function capturePage(page, opts) {
7
+ // A step capture injects nothing into the page, so Reticle's own hosts are hidden another way.
8
+ if (true === opts.cssScale) {
9
+ return captureWithChromeHidden({ evaluate: (script) => page.evaluate(script) }, () => shoot(page, opts));
10
+ }
11
+ return shoot(page, opts);
12
+ }
13
+ /** Playwright screenshot options for one capture: a JPEG, a clip, the full page, or the viewport. */
14
+ function screenshotOptions(opts) {
15
+ if (opts.jpegQuality !== undefined) {
16
+ return {
17
+ type: 'jpeg',
18
+ quality: opts.jpegQuality,
19
+ ...(true === opts.cssScale ? { scale: 'css' } : {}),
20
+ };
21
+ }
22
+ if (opts.clip !== undefined) {
23
+ return { ...SCREENSHOT_DETERMINISM, clip: opts.clip };
24
+ }
25
+ if (true === opts.fullPage) {
26
+ return { ...SCREENSHOT_DETERMINISM, fullPage: true };
27
+ }
28
+ return { ...SCREENSHOT_DETERMINISM };
29
+ }
30
+ async function shoot(page, opts) {
31
+ const buf = await page.screenshot(screenshotOptions(opts));
32
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
33
+ }