@reticlehq/server 3.1.0 → 3.2.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 (228) hide show
  1. package/SKILL.md +17 -18
  2. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  3. package/dist/command/cli/cli-doctor.js +13 -1
  4. package/dist/command/cli/cli-flow-commands.d.ts +2 -1
  5. package/dist/command/cli/cli-kill.js +1 -1
  6. package/dist/command/cli/cli-parse.d.ts +2 -1
  7. package/dist/command/cli/cli-parse.js +5 -2
  8. package/dist/command/cli/cli-verify.js +2 -2
  9. package/dist/command/cli/cloud-cli.js +7 -6
  10. package/dist/command/cli/cloud-kit.js +2 -6
  11. package/dist/command/cli/config/config-discovery.d.ts +12 -0
  12. package/dist/command/cli/config/config-discovery.js +19 -0
  13. package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
  14. package/dist/command/cli/doctor/doctor-rows.js +12 -0
  15. package/dist/command/cli/drive/drive-command.js +1 -1
  16. package/dist/command/cli/launch/cli-launch.d.ts +10 -2
  17. package/dist/command/cli/launch/cli-launch.js +32 -39
  18. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
  19. package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
  20. package/dist/command/cli/mcp-command.js +5 -3
  21. package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
  22. package/dist/command/cli/ports/resolve/cli-port.js +5 -1
  23. package/dist/command/cli/status/status-lines.d.ts +28 -0
  24. package/dist/command/cli/status/status-lines.js +117 -0
  25. package/dist/command/cli.d.ts +1 -1
  26. package/dist/command/cli.js +24 -177
  27. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  28. package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
  29. package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
  30. package/dist/command/daemon/binding/loopback-reach.js +63 -0
  31. package/dist/command/daemon/daemon-resilience.d.ts +13 -1
  32. package/dist/command/daemon/daemon-resilience.js +12 -1
  33. package/dist/command/daemon/daemon.d.ts +2 -11
  34. package/dist/command/daemon/daemon.js +8 -11
  35. package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
  36. package/dist/command/daemon/lifetime/heartbeat.js +10 -0
  37. package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
  38. package/dist/command/daemon/lifetime/log-cap.js +115 -0
  39. package/dist/command/setup/agent-notice-memory.d.ts +16 -0
  40. package/dist/command/setup/agent-notice-memory.js +65 -0
  41. package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
  42. package/dist/command/setup/bringup/dev-server-wait.js +16 -0
  43. package/dist/command/setup/bringup/ensure-daemon.js +1 -1
  44. package/dist/command/setup/init/init-runtime.js +13 -1
  45. package/dist/command/setup/node-effects.js +1 -1
  46. package/dist/command/setup/probe/page-probe.js +9 -2
  47. package/dist/command/setup/run-setup.d.ts +25 -7
  48. package/dist/command/setup/run-setup.js +51 -9
  49. package/dist/command/setup/setup-command.js +31 -3
  50. package/dist/command/setup/terminal/confirm.js +2 -1
  51. package/dist/command/update/updater.d.ts +27 -0
  52. package/dist/command/update/updater.js +35 -2
  53. package/dist/features/crawl/crawl-tools.js +3 -1
  54. package/dist/features/crawl/crawl.js +2 -1
  55. package/dist/features/harness/drive-report.d.ts +73 -0
  56. package/dist/features/harness/drive-report.js +196 -0
  57. package/dist/features/harness/driver.js +29 -8
  58. package/dist/features/harness/drivers.d.ts +19 -0
  59. package/dist/features/harness/drivers.js +19 -0
  60. package/dist/features/harness/fill-values.d.ts +67 -0
  61. package/dist/features/harness/fill-values.js +153 -0
  62. package/dist/features/harness/jev-driver.d.ts +188 -0
  63. package/dist/features/harness/jev-driver.js +664 -0
  64. package/dist/features/harness/openai-driver.d.ts +46 -0
  65. package/dist/features/harness/openai-driver.js +197 -0
  66. package/dist/features/harness/platform-config.d.ts +52 -0
  67. package/dist/features/harness/platform-config.js +89 -0
  68. package/dist/features/visual/visual-tools.js +26 -2
  69. package/dist/index.js +60 -55
  70. package/dist/judgement/domain/domain-tools.js +4 -2
  71. package/dist/judgement/runs/verification-sync.d.ts +2 -2
  72. package/dist/judgement/runs/verification-sync.js +9 -2
  73. package/dist/language/flows/affected-tools.js +6 -3
  74. package/dist/language/flows/flow-replay-run.d.ts +25 -2
  75. package/dist/language/flows/flow-replay-run.js +73 -27
  76. package/dist/language/flows/flow-replay.d.ts +17 -0
  77. package/dist/language/flows/flow-replay.js +148 -100
  78. package/dist/language/flows/flow-result.d.ts +18 -1
  79. package/dist/language/flows/flow-result.js +18 -2
  80. package/dist/language/flows/flow-store-for-session.d.ts +2 -1
  81. package/dist/language/flows/flow-tools.js +8 -4
  82. package/dist/language/flows/flows.d.ts +8 -7
  83. package/dist/language/flows/flows.js +7 -3
  84. package/dist/language/flows/narrow-write.d.ts +4 -4
  85. package/dist/language/flows/replay.js +1 -1
  86. package/dist/language/flows/suite-selection.d.ts +15 -1
  87. package/dist/language/flows/suite-selection.js +1 -1
  88. package/dist/language/flows/verify-change-tools.js +7 -4
  89. package/dist/memory/cloud/account-state.d.ts +1 -1
  90. package/dist/memory/cloud/account-state.js +5 -4
  91. package/dist/memory/cloud/cloud-config.d.ts +9 -0
  92. package/dist/memory/cloud/cloud-config.js +9 -0
  93. package/dist/memory/cloud/cloud-sync.d.ts +7 -2
  94. package/dist/memory/cloud/cloud-sync.js +10 -5
  95. package/dist/memory/cloud/harness-config.d.ts +10 -0
  96. package/dist/memory/cloud/harness-config.js +58 -0
  97. package/dist/memory/cloud/harness-offer.d.ts +44 -0
  98. package/dist/memory/cloud/harness-offer.js +146 -0
  99. package/dist/memory/cloud/harness-switch.d.ts +11 -0
  100. package/dist/memory/cloud/harness-switch.js +53 -0
  101. package/dist/memory/cloud/sync-daemon.d.ts +22 -2
  102. package/dist/memory/cloud/sync-daemon.js +24 -0
  103. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  104. package/dist/memory/impact/impact-recorder.js +2 -1
  105. package/dist/memory/impact/impact-store.d.ts +5 -0
  106. package/dist/memory/impact/impact-store.js +19 -0
  107. package/dist/memory/intent/intent-shard-store.js +2 -2
  108. package/dist/memory/intent/intent-shard.d.ts +12 -12
  109. package/dist/memory/journal/attach-journal.d.ts +11 -0
  110. package/dist/memory/journal/attach-journal.js +1 -1
  111. package/dist/memory/journal/journal-query.d.ts +35 -1
  112. package/dist/memory/journal/journal-query.js +46 -0
  113. package/dist/memory/journal/journal-recorder.d.ts +50 -2
  114. package/dist/memory/journal/journal-recorder.js +45 -7
  115. package/dist/memory/journal/on-disk/retention.d.ts +87 -2
  116. package/dist/memory/journal/on-disk/retention.js +177 -8
  117. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
  118. package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
  119. package/dist/memory/journal/self-observation.d.ts +3 -0
  120. package/dist/memory/journal/self-observation.js +67 -0
  121. package/dist/memory/journal/session-end.d.ts +13 -3
  122. package/dist/memory/journal/session-end.js +24 -4
  123. package/dist/memory/journal/session-journal.d.ts +115 -5
  124. package/dist/memory/journal/session-journal.js +479 -32
  125. package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
  126. package/dist/memory/project/artifact-root-resolver.js +89 -0
  127. package/dist/memory/project/artifact-root.d.ts +39 -2
  128. package/dist/memory/project/artifact-root.js +58 -1
  129. package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
  130. package/dist/memory/project/dir/fill-value-store.js +88 -0
  131. package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
  132. package/dist/memory/project/dir/reticle-dir.js +11 -0
  133. package/dist/memory/project/fs/fs-port.d.ts +22 -1
  134. package/dist/memory/project/fs/fs-port.js +33 -5
  135. package/dist/memory/project/learned-routes.d.ts +16 -2
  136. package/dist/memory/project/learned-routes.js +26 -10
  137. package/dist/memory/project/project-for-root.d.ts +30 -0
  138. package/dist/memory/project/project-for-root.js +43 -0
  139. package/dist/memory/project/project-tools.js +10 -5
  140. package/dist/memory/project/session-root.d.ts +35 -6
  141. package/dist/memory/project/session-root.js +70 -8
  142. package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
  143. package/dist/portal/bridge/auth-failure-reason.js +15 -1
  144. package/dist/portal/bridge/bridge.d.ts +6 -1
  145. package/dist/portal/bridge/bridge.js +36 -2
  146. package/dist/portal/session/facts/session-replaced.d.ts +19 -0
  147. package/dist/portal/session/facts/session-replaced.js +22 -0
  148. package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
  149. package/dist/portal/session/no-session-diagnosis.js +67 -60
  150. package/dist/portal/session/no-session-next-action.js +3 -2
  151. package/dist/portal/session/no-session-watch.js +14 -10
  152. package/dist/portal/session/rebind.d.ts +18 -0
  153. package/dist/portal/session/rebind.js +38 -0
  154. package/dist/portal/session/session-manager.d.ts +4 -0
  155. package/dist/portal/session/session-manager.js +19 -1
  156. package/dist/portal/session/session.d.ts +21 -10
  157. package/dist/portal/session/session.js +29 -42
  158. package/dist/status-payload.d.ts +12 -1
  159. package/dist/status-payload.js +4 -1
  160. package/dist/surface/http-server.js +3 -5
  161. package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
  162. package/dist/surface/mcp/faults/mcp-outage.js +20 -4
  163. package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
  164. package/dist/surface/mcp/mcp-proxy.js +20 -9
  165. package/dist/surface/mcp/mcp.d.ts +10 -1
  166. package/dist/surface/mcp/mcp.js +59 -29
  167. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
  168. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
  169. package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
  170. package/dist/surface/mcp/proxy/proxy-log.js +7 -22
  171. package/dist/surface/tools/act/act-preflight.js +6 -1
  172. package/dist/surface/tools/act/sequence-grade.js +13 -0
  173. package/dist/surface/tools/act-capsule.js +27 -1
  174. package/dist/surface/tools/assert/assert-verdict.js +44 -17
  175. package/dist/surface/tools/coverage-tools.js +5 -2
  176. package/dist/surface/tools/dynamic-tools.js +37 -46
  177. package/dist/surface/tools/error-recovery.d.ts +6 -0
  178. package/dist/surface/tools/error-recovery.js +15 -0
  179. package/dist/surface/tools/explore-tools.js +80 -7
  180. package/dist/surface/tools/feedback-tools.js +49 -2
  181. package/dist/surface/tools/first-sentence.d.ts +24 -0
  182. package/dist/surface/tools/first-sentence.js +0 -0
  183. package/dist/surface/tools/harness-explore.d.ts +125 -1
  184. package/dist/surface/tools/harness-explore.js +401 -15
  185. package/dist/surface/tools/harness-plan.d.ts +76 -0
  186. package/dist/surface/tools/harness-plan.js +156 -0
  187. package/dist/surface/tools/harness-toolset.js +12 -1
  188. package/dist/surface/tools/invoke-tool.js +65 -5
  189. package/dist/surface/tools/live-call-text.d.ts +6 -0
  190. package/dist/surface/tools/live-call-text.js +36 -2
  191. package/dist/surface/tools/observe-tools.js +55 -9
  192. package/dist/surface/tools/read/query-windowed.d.ts +1 -1
  193. package/dist/surface/tools/read/query-windowed.js +1 -1
  194. package/dist/surface/tools/read-tools.js +1 -2
  195. package/dist/surface/tools/tool-kit.d.ts +18 -3
  196. package/dist/surface/tools/tools-helpers.d.ts +8 -7
  197. package/dist/surface/tools/tools-helpers.js +11 -13
  198. package/dist/surface/tools/tools.js +6 -1
  199. package/dist/telemetry/bug-found.js +32 -2
  200. package/dist/telemetry/feedback-context.js +3 -3
  201. package/dist/telemetry/harness-drive.d.ts +6 -0
  202. package/dist/telemetry/harness-drive.js +39 -0
  203. package/dist/telemetry/mcp-connection.d.ts +14 -1
  204. package/dist/telemetry/mcp-connection.js +17 -2
  205. package/dist/telemetry/onboarding-firsts.d.ts +30 -1
  206. package/dist/telemetry/onboarding-firsts.js +54 -12
  207. package/dist/telemetry/verification-of.js +31 -3
  208. package/docs/agent-cheatsheet.md +7 -3
  209. package/docs/autodrive.md +42 -2
  210. package/docs/benchmarks.md +1 -1
  211. package/docs/capabilities.mdx +1 -1
  212. package/docs/cli/cloud.mdx +3 -3
  213. package/docs/cli/doctor.mdx +1 -0
  214. package/docs/cli/status.mdx +35 -20
  215. package/docs/cli.mdx +8 -18
  216. package/docs/desktop.mdx +1 -1
  217. package/docs/faq.mdx +1 -1
  218. package/docs/install-manual.mdx +1 -1
  219. package/docs/packages/tauri.mdx +1 -1
  220. package/docs/packages.mdx +1 -1
  221. package/docs/predicates.mdx +16 -0
  222. package/docs/system-map.md +6 -4
  223. package/docs/telemetry.md +2 -1
  224. package/docs/token-efficiency.md +2 -2
  225. package/docs/tools/overview.mdx +1 -1
  226. package/docs/troubleshooting.mdx +8 -2
  227. package/docs/usage.md +1 -1
  228. package/package.json +6 -6
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { projectForRoot } from '../../memory/project/project-for-root.js';
2
3
  import { sessionRoot } from '../../memory/project/session-root.js';
3
4
  import { asNumber, asString } from '@reticlehq/core';
4
5
  import { ReticleTool } from '@reticlehq/core';
@@ -75,8 +76,9 @@ export const DOMAIN_TOOLS = [
75
76
  if (loaded.ok)
76
77
  flows.push(loaded.value);
77
78
  }
78
- const contract = await readContract(deps.fs, sessionRoot(deps, asString(args['sessionId'])));
79
- const project = await deps.project.read();
79
+ const root = sessionRoot(deps, asString(args['sessionId']));
80
+ const contract = await readContract(deps.fs, root);
81
+ const project = await projectForRoot(deps, root).read();
80
82
  const runs = project.ok ? project.file.runs : [];
81
83
  const model = buildDomainModel(flows, contract.ok ? contract.capabilities : null, runs);
82
84
  // Self-instrumentation: turn located gaps (unasserted flows with a source stamp) into apply-ready diffs.
@@ -1,4 +1,4 @@
1
- import { type FlowReplayResult } from '@reticlehq/core';
1
+ import { type ProjectId, type FlowReplayResult } from '@reticlehq/core';
2
2
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
3
3
  /** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
4
4
  export interface TimedReplay {
@@ -10,4 +10,4 @@ export interface TimedReplay {
10
10
  * tool — a disk or network failure is logged and swallowed so the verdict the agent asked for is
11
11
  * unaffected. Returns the runId (for logging/tests); undefined if nothing was produced (empty suite).
12
12
  */
13
- export declare function persistAndSyncVerificationRun(deps: ToolDeps, timed: TimedReplay[], projectId: string | undefined): Promise<string | undefined>;
13
+ export declare function persistAndSyncVerificationRun(deps: ToolDeps, timed: TimedReplay[], projectId: ProjectId | undefined): Promise<string | undefined>;
@@ -18,6 +18,7 @@ import { RunStore } from './artifact/run-store.js';
18
18
  import { cloudFetch, syncRunToCloud, SyncOutcome } from '../../memory/cloud/cloud-sync.js';
19
19
  import { resolveProjectCloud } from '../../memory/cloud/cloud-config.js';
20
20
  import { log } from '../../log.js';
21
+ import { rootForProjectId } from '../../memory/project/session-root.js';
21
22
  /** The author of record when no MCP peer introduced itself — a CLI run, or a client that skipped the handshake. */
22
23
  const RETICLE_MCP_AGENT_ID = 'reticle-mcp';
23
24
  /** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
@@ -60,9 +61,15 @@ export async function persistAndSyncVerificationRun(deps, timed, projectId) {
60
61
  if (0 === timed.length)
61
62
  return undefined;
62
63
  let run;
64
+ // The project the suite ran for, not the directory the daemon was started in. Both halves below
65
+ // take it: the artifact `reticle_run` reads back through `sessionRoot`, and the cloud link that
66
+ // decides WHICH DASHBOARD this run is pushed to. The daemon's own directory has no link file, so
67
+ // a misrouted read silently meant "not attached" — and where it DID have one, the run was pushed
68
+ // to a dashboard belonging to whoever that checkout was linked to.
69
+ const root = rootForProjectId(deps, projectId);
63
70
  try {
64
71
  run = assembleRun(deps, timed, projectId);
65
- await new RunStore(deps.fs, deps.reticleRoot).write(run);
72
+ await new RunStore(deps.fs, root).write(run);
66
73
  }
67
74
  catch (error) {
68
75
  log('verification-run-persist-failed', {
@@ -71,7 +78,7 @@ export async function persistAndSyncVerificationRun(deps, timed, projectId) {
71
78
  return undefined;
72
79
  }
73
80
  // Per-project cloud: only push when THIS project has cloud attached AND its policy allows runs.
74
- const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
81
+ const cloud = await resolveProjectCloud(deps.fs, root, homedir(), process.env);
75
82
  if (null === cloud.config || !cloud.policy.runs)
76
83
  return run.runId; // not attached / runs disabled → local only
77
84
  const result = await syncRunToCloud(run, cloud.config, cloudFetch);
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { ReticleTool } from '@reticlehq/core';
3
3
  import { loadNamedFlows, resolveChangedFiles } from '../../command/cli/cli-flow-commands.js';
4
+ import { projectDirFor } from '../../memory/project/session-root.js';
4
5
  import { sessionRoot, sessionProjectId } from '../../memory/project/session-root.js';
5
6
  import { affectedSavedFlows } from './change/flow-sources.js';
6
7
  /**
@@ -44,9 +45,11 @@ export const AFFECTED_TOOLS = [
44
45
  ? args['files'].filter((f) => 'string' === typeof f)
45
46
  : [];
46
47
  const since = 'string' === typeof args['since'] ? args['since'] : undefined;
47
- // Same address as verify_change, resolved the same way: `deps.reticleRoot` is the configured
48
- // project directory (the daemon's cwd is not), and the session names which project's flows.
49
- const changed = await resolveChangedFiles(files, since, deps.reticleRoot);
48
+ // Same address as verify_change, resolved the same way. `deps.reticleRoot` is NOT the project
49
+ // directory this line used to claim it was — it is the daemon's `.reticle` subdirectory, in
50
+ // whichever tree the MCP host was started in. `undefined` here resolves the single connected
51
+ // session, which is the same rule the flow lookup below already follows.
52
+ const changed = await resolveChangedFiles(files, since, projectDirFor(deps, undefined));
50
53
  const changedFiles = changed.files;
51
54
  // No `sessionId` on this tool's surface, and it does not need one: passing `undefined`
52
55
  // resolves the single connected session, and falls back exactly as before when it cannot.
@@ -2,6 +2,7 @@ import { FlowErrorCode, type CommandResult, type FlowFile, type FlowReplayResult
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';
5
+ import { DocumentLostDuringReplay } from './flow-replay.js';
5
6
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
6
7
  export declare function latestRecordedFlow(events: ReticleEvent[]): {
7
8
  name: string;
@@ -57,12 +58,34 @@ export declare function navigateAndAwait(sessions: SessionManager, session: {
57
58
  url?: string;
58
59
  command(name: string, args?: Record<string, unknown>): Promise<CommandResult>;
59
60
  }, destination: string, expectedPath: string, timeoutMs?: number, clock?: ArrivalClock): Promise<Session | undefined>;
61
+ /**
62
+ * The honest answer when the document replay was driving went away mid-run.
63
+ *
64
+ * Not a pass: nothing graded the journey, and the steps in hand are only the ones the departed
65
+ * document answered. Not a failure either — the app was never observed doing anything wrong, and
66
+ * reporting one would be the false red that sends a reader into product code that is fine. So it
67
+ * lands in the bucket this engine already has for "nothing was proved", the same one an unmet
68
+ * precondition uses, with the steps that DID answer attached and no row invented for the one that
69
+ * did not.
70
+ *
71
+ * Why replay reports rather than follows: the successor cannot be identified at the moment the
72
+ * socket closes. `SessionManager.remove` runs from the close handler, writes the tombstone a
73
+ * successor is later matched against, and has nothing to match yet — so the rejection is a plain
74
+ * transport failure by design, and a step in the middle of a journey has no budget to sit and poll
75
+ * with. `navigateAndAwait` CAN wait, because it knows it asked for the navigation, and it does.
76
+ */
77
+ export declare function lostDocumentResult(name: string, lost: DocumentLostDuringReplay): FlowReplayResult;
60
78
  export declare function replayNamedFlow(deps: ToolDeps, args: Record<string, unknown>): Promise<FlowReplayResult>;
61
79
  export declare function crossStepOnly(whole: readonly Contradiction[], steps: readonly FlowStepResult[]): Contradiction[];
62
80
  /**
63
81
  * The connecting session's project, or undefined when no browser is attached. Flow tools use it to
64
82
  * scope storage to the current app on a shared daemon; resolving must NOT throw here (list/load are
65
83
  * documented to work headless), so a missing/unknown session degrades to the global/legacy store.
84
+ *
85
+ * Re-exported, not re-implemented. This file carried a BYTE-IDENTICAL second copy, and the two
86
+ * drifted the moment `ProjectId` was threaded: `session-root`'s copy returned the brand while this
87
+ * one returned `string`, so every flow tool importing from here was handed a widened value and lost
88
+ * the brand one line after it was minted. Exactly the weak-annotation failure the brand's own doc
89
+ * records against `RunStore.list()`.
66
90
  */
67
- export declare function sessionProjectId(deps: ToolDeps, sessionId: string | undefined): string | undefined;
68
- export {};
91
+ export { sessionProjectId } from '../../memory/project/session-root.js';
@@ -11,7 +11,8 @@ import { queryRefs } from './replay.js';
11
11
  import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
12
12
  import { buildDecision, unverifiableReason } from './decision.js';
13
13
  import { unsuppliedSecrets } from './fields/flow-secret-field.js';
14
- import { assertStepExpect } from './flow-replay.js';
14
+ import { assertStepExpect, DocumentLostDuringReplay, } from './flow-replay.js';
15
+ import { isDocumentGoneError } from '../../portal/session/facts/session-replaced.js';
15
16
  import { classifyFlowAssertions, flattenSteps } from './flow-classify.js';
16
17
  import { dischargeFlowIntent, flowIntentStatement, flowReplayVerdictId } from './flow-intent.js';
17
18
  import { IntentStore } from '../../memory/intent/intent-store.js';
@@ -29,6 +30,7 @@ import { resolveProjectCloud } from '../../memory/cloud/cloud-config.js';
29
30
  import { consultSubjectFor, selectConsulted } from './flow-memory-consult.js';
30
31
  import { log } from '../../log.js';
31
32
  import { flowsForSession } from './flow-store-for-session.js';
33
+ import { projectForRoot } from '../../memory/project/project-for-root.js';
32
34
  import { FlowParseNote } from './flow-expect-grammar.js';
33
35
  export function latestRecordedFlow(events) {
34
36
  for (let i = events.length - 1; i >= 0; i--) {
@@ -76,7 +78,7 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
76
78
  /** The APP's `.reticle`, resolved from the session — never the daemon's own. */
77
79
  recordRoot) {
78
80
  const runStatus = replayToRunStatus(status);
79
- await deps.project.recordRun({
81
+ await projectForRoot(deps, recordRoot).recordRun({
80
82
  kind: RunKind.FLOW_REPLAY,
81
83
  name,
82
84
  status: runStatus,
@@ -291,8 +293,13 @@ export async function navigateAndAwait(sessions, session, destination, expectedP
291
293
  if (!outcome.ok || true !== asRecord(outcome.result)['ok'])
292
294
  return undefined;
293
295
  }
294
- catch {
295
- return undefined;
296
+ catch (error) {
297
+ // A navigation that unloads the page rejects the very command that asked for it — the transport
298
+ // dies with the document. Reading that as "the navigate failed" skipped the arrival poll below
299
+ // and left replay driving a handle that could never answer again, which is how a flow with a
300
+ // startPath died at step 1 on an app that had loaded perfectly. Anything else is a real refusal.
301
+ if (!isDocumentGoneError(error))
302
+ return undefined;
296
303
  }
297
304
  const deadline = clock.now() + timeoutMs;
298
305
  for (;;) {
@@ -361,6 +368,36 @@ async function firstUnmetPrecondition(session, flow, since) {
361
368
  }
362
369
  return undefined;
363
370
  }
371
+ /**
372
+ * The honest answer when the document replay was driving went away mid-run.
373
+ *
374
+ * Not a pass: nothing graded the journey, and the steps in hand are only the ones the departed
375
+ * document answered. Not a failure either — the app was never observed doing anything wrong, and
376
+ * reporting one would be the false red that sends a reader into product code that is fine. So it
377
+ * lands in the bucket this engine already has for "nothing was proved", the same one an unmet
378
+ * precondition uses, with the steps that DID answer attached and no row invented for the one that
379
+ * did not.
380
+ *
381
+ * Why replay reports rather than follows: the successor cannot be identified at the moment the
382
+ * socket closes. `SessionManager.remove` runs from the close handler, writes the tombstone a
383
+ * successor is later matched against, and has nothing to match yet — so the rejection is a plain
384
+ * transport failure by design, and a step in the middle of a journey has no budget to sit and poll
385
+ * with. `navigateAndAwait` CAN wait, because it knows it asked for the navigation, and it does.
386
+ */
387
+ export function lostDocumentResult(name, lost) {
388
+ return {
389
+ name,
390
+ status: ReplayStatus.OK,
391
+ steps: lost.steps,
392
+ unverifiable: {
393
+ reason: `the page loaded a new document while step ${String(lost.atStep)} was running, so the run ` +
394
+ `could not be graded — the steps before it are reported and nothing after it was observed. ` +
395
+ `Nothing here says the app failed. If that navigation is part of the journey, record the ` +
396
+ `step's consequence with \`expect\` and replay again; if it was not, the flow started ` +
397
+ `somewhere it no longer belongs.`,
398
+ },
399
+ };
400
+ }
364
401
  export async function replayNamedFlow(deps, args) {
365
402
  const startedAt = deps.now();
366
403
  const name = asString(args['flowName']) ?? '';
@@ -467,17 +504,25 @@ export async function replayNamedFlow(deps, args) {
467
504
  unverifiable: { reason: unmet },
468
505
  };
469
506
  }
470
- const steps = await replayFlow(session, replayable, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous'], undefined, {
471
- // How an `invoke` step finds the flow it runs. Scoped to the same project as the flow being
472
- // replayed, so a composite cannot reach into another app's store for a same-named sub-journey.
473
- resolveFlow: async (invoked) => {
474
- const sub = await flowsForSession(deps, projectId).flows.load(invoked, projectId);
475
- return sub.ok ? await resolveFlowUploads(deps, sub.value) : undefined;
476
- },
477
- // Bug-sweep mode: keep going past a step whose action ran and whose consequence merely did
478
- // not hold, so one flow reports one verdict per step instead of stopping at the first defect.
479
- sweep: true === args['sweep'],
480
- });
507
+ let steps;
508
+ try {
509
+ steps = await replayFlow(session, replayable, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, true === args['confirmDangerous'], undefined, {
510
+ // How an `invoke` step finds the flow it runs. Scoped to the same project as the flow being
511
+ // replayed, so a composite cannot reach into another app's store for a same-named sub-journey.
512
+ resolveFlow: async (invoked) => {
513
+ const sub = await flowsForSession(deps, projectId).flows.load(invoked, projectId);
514
+ return sub.ok ? await resolveFlowUploads(deps, sub.value) : undefined;
515
+ },
516
+ // Bug-sweep mode: keep going past a step whose action ran and whose consequence merely did
517
+ // not hold, so one flow reports one verdict per step instead of stopping at the first defect.
518
+ sweep: true === args['sweep'],
519
+ });
520
+ }
521
+ catch (error) {
522
+ if (!(error instanceof DocumentLostDuringReplay))
523
+ throw error;
524
+ return lostDocumentResult(name, error);
525
+ }
481
526
  // Computed HERE, before the synthetic success row is appended below: once that row is pushed,
482
527
  // `steps.length` no longer counts only the flow's own steps and the arithmetic is wrong.
483
528
  const halted = haltedFrom(steps, loaded.value.steps.length);
@@ -522,7 +567,7 @@ export async function replayNamedFlow(deps, args) {
522
567
  // Anti-reward-hacking baseline: record what this flow asserted ONLY when it passed clean. A
523
568
  // failing run must never become the baseline a later weakening is measured against.
524
569
  if (status === ReplayStatus.OK) {
525
- await new AssertionTiersStore(deps.fs, deps.reticleRoot).recordPassing(name, loaded.value.steps.map((s, i) => ({
570
+ await new AssertionTiersStore(deps.fs, replayRoot).recordPassing(name, loaded.value.steps.map((s, i) => ({
526
571
  step: i,
527
572
  ...(s.expect === undefined ? {} : { expect: s.expect }),
528
573
  })),
@@ -542,7 +587,7 @@ export async function replayNamedFlow(deps, args) {
542
587
  }
543
588
  }
544
589
  // Push-default: the deviation report over this drive's segments, learned across runs. Best-effort.
545
- const deviation = await computeReplayDeviation(deps, session, replayFloor);
590
+ const deviation = await computeReplayDeviation(deps, session, replayFloor, replayRoot);
546
591
  /*
547
592
  * What the team already knows about this flow, fetched on the agent's behalf.
548
593
  *
@@ -669,12 +714,14 @@ export function crossStepOnly(whole, steps) {
669
714
  * The deviation report over a replay's route segments — compared against the learned envelope, which is
670
715
  * then updated. Best-effort: any failure (disk, empty window) yields undefined, never breaking the replay.
671
716
  */
672
- async function computeReplayDeviation(deps, session, floor) {
717
+ async function computeReplayDeviation(deps, session, floor,
718
+ /** The APP's root — the same one the replay resolved eleven lines above the old call. */
719
+ root) {
673
720
  try {
674
721
  const segments = computeSegments(session.eventsSince(floor));
675
722
  if (0 === segments.length)
676
723
  return undefined;
677
- return await reportAndAccumulate(new EnvelopeStore(deps.fs, deps.reticleRoot), segments);
724
+ return await reportAndAccumulate(new EnvelopeStore(deps.fs, root), segments);
678
725
  }
679
726
  catch {
680
727
  return undefined;
@@ -691,12 +738,11 @@ function applyStartPathHint(result, hint) {
691
738
  * The connecting session's project, or undefined when no browser is attached. Flow tools use it to
692
739
  * scope storage to the current app on a shared daemon; resolving must NOT throw here (list/load are
693
740
  * documented to work headless), so a missing/unknown session degrades to the global/legacy store.
741
+ *
742
+ * Re-exported, not re-implemented. This file carried a BYTE-IDENTICAL second copy, and the two
743
+ * drifted the moment `ProjectId` was threaded: `session-root`'s copy returned the brand while this
744
+ * one returned `string`, so every flow tool importing from here was handed a widened value and lost
745
+ * the brand one line after it was minted. Exactly the weak-annotation failure the brand's own doc
746
+ * records against `RunStore.list()`.
694
747
  */
695
- export function sessionProjectId(deps, sessionId) {
696
- try {
697
- return deps.sessions.resolve(sessionId).projectId;
698
- }
699
- catch {
700
- return undefined;
701
- }
702
- }
748
+ export { sessionProjectId } from '../../memory/project/session-root.js';
@@ -3,6 +3,23 @@ export { anchorLabel, componentLabel, componentQueryArgs, editDistance, expectEl
3
3
  import type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
4
4
  export type { FlowReplaySession, WaitForSignal, Sleep } from './flow-replay-types.js';
5
5
  import { type Drift, type FlowFile, type FlowStep, type FlowStepResult } from '@reticlehq/core';
6
+ /**
7
+ * The document this replay was driving went away mid-run.
8
+ *
9
+ * Almost always because the replay itself navigated it — a `startPath` goto, or a step that clicks a
10
+ * link. The steps already in hand are real answers from the document that WAS there, so they travel
11
+ * with the error; the step that was in flight has no answer at all and is deliberately absent.
12
+ * Synthesising one would be a verdict about a page nobody observed, which is the one thing worse
13
+ * than returning no verdict.
14
+ *
15
+ * Typed rather than a message, because the caller has to tell it from a step that genuinely failed:
16
+ * one says the app is wrong, the other says we stopped watching.
17
+ */
18
+ export declare class DocumentLostDuringReplay extends Error {
19
+ readonly steps: FlowStepResult[];
20
+ readonly atStep: number;
21
+ constructor(steps: FlowStepResult[], atStep: number, lostTo?: unknown);
22
+ }
6
23
  /**
7
24
  * After a step's anchor resolves and its action runs, its `expect` is EVALUATED — every kind of it.
8
25
  *
@@ -11,7 +11,36 @@ import { replayActionArgs, ambiguousTestidNote } from './replay.js';
11
11
  import { anchorFieldName } from './fields/flow-secret-field.js';
12
12
  import { degradedStepResult, isDegradedAnchor, runComponentStep, runRoleStep, runSequenceStep, } from './flow-step-runners.js';
13
13
  import { successToPredicate } from './flow-success.js';
14
+ import { inFlightRequestLabels } from '../../surface/tools/act/settle-in-flight.js';
15
+ import { namedNetIsInFlight } from '@reticlehq/engine/evidence/unsettled.js';
16
+ /** What a still-open named request means, in the drift's own words. */
17
+ const IN_FLIGHT_AT_BUDGET_END = 'the request this step declared had not come back when the budget ended — it is still in flight, ' +
18
+ 'so nothing here says the app failed. Raise the step timeout, or look at the endpoint';
14
19
  import { ReticleTool } from '@reticlehq/core';
20
+ import { isDocumentGoneError } from '../../portal/session/facts/session-replaced.js';
21
+ /**
22
+ * The document this replay was driving went away mid-run.
23
+ *
24
+ * Almost always because the replay itself navigated it — a `startPath` goto, or a step that clicks a
25
+ * link. The steps already in hand are real answers from the document that WAS there, so they travel
26
+ * with the error; the step that was in flight has no answer at all and is deliberately absent.
27
+ * Synthesising one would be a verdict about a page nobody observed, which is the one thing worse
28
+ * than returning no verdict.
29
+ *
30
+ * Typed rather than a message, because the caller has to tell it from a step that genuinely failed:
31
+ * one says the app is wrong, the other says we stopped watching.
32
+ */
33
+ export class DocumentLostDuringReplay extends Error {
34
+ steps;
35
+ atStep;
36
+ constructor(steps, atStep, lostTo) {
37
+ super(`the document was replaced while step ${String(atStep)} was running`, {
38
+ cause: lostTo,
39
+ });
40
+ this.steps = steps;
41
+ this.atStep = atStep;
42
+ }
43
+ }
15
44
  const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
45
  /**
17
46
  * The route (pathname) currently in effect — the page a step runs on. Reads the latest ROUTE_CHANGE
@@ -170,11 +199,23 @@ export async function assertStepExpect(session, expect, dynamic, waitForSignal,
170
199
  const verdict = await waitForSignal(session, predicate, timeoutMs, since);
171
200
  if (verdict.pass)
172
201
  return undefined;
202
+ // A request the step NAMED that had not come back yet is a blind spot, not an assertion the app
203
+ // failed. The live verdict path already draws this line (namedNetIsInFlight); replay read the
204
+ // same window and never asked, so a slow endpoint was reported as a consequence that never fired.
205
+ // An unrelated open request does not pardon a named URL that never started — matching decides it.
206
+ const openRequests = expect.state === undefined ? inFlightRequestLabels(session.eventsSince(since)) : [];
207
+ const namedInFlight = namedNetIsInFlight(predicate, openRequests);
173
208
  return {
174
209
  // The store case keeps its own kind because heal and the run report branch on it; everything
175
210
  // else is a consequence that did not hold, and the reason carries observed-vs-expected.
176
- reasonKind: expect.state !== undefined ? DriftReason.STATE_MISMATCH : DriftReason.SIGNAL_NOT_OBSERVED,
177
- reason: verdict.failureReason ?? "the step's declared consequence did not hold",
211
+ reasonKind: expect.state !== undefined
212
+ ? DriftReason.STATE_MISMATCH
213
+ : namedInFlight
214
+ ? DriftReason.NET_STILL_IN_FLIGHT
215
+ : DriftReason.SIGNAL_NOT_OBSERVED,
216
+ reason: namedInFlight
217
+ ? `${IN_FLIGHT_AT_BUDGET_END} (${openRequests.join(', ')})`
218
+ : (verdict.failureReason ?? "the step's declared consequence did not hold"),
178
219
  anchor: expectLabel(expect),
179
220
  nearest: null,
180
221
  };
@@ -332,108 +373,115 @@ export async function replayFlow(session, flow, waitForSignal, signalTimeoutMs,
332
373
  // wait for the slowest step would trade a false red for a slow suite. See FlowStep.timeoutMs.
333
374
  const waitFor = (step) => step.timeoutMs ?? flow.signalTimeoutMs ?? signalTimeoutMs;
334
375
  let index = 0;
335
- for (const step of flow.steps) {
336
- if (step.invoke !== undefined) {
337
- results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options));
338
- const last = results[results.length - 1];
339
- if (last !== undefined && false === last.ok)
340
- break;
341
- index += 1;
342
- continue;
343
- }
344
- const label = anchorLabel(step.anchor);
345
- // The page this step runs on (the journey's "which page") — captured before the action.
346
- const page = currentRoute(session);
347
- // Event-time floor so the consequence reflects only THIS step's aftermath, not prior steps'.
348
- const cursorBefore = session.elapsed();
349
- const subSteps = step.steps;
350
- // Traced per step, so a slow replay says WHICH step and which anchor kind spent the time. The
351
- // per-step `durationMs` below is what the agent gets back; this is what a developer profiling
352
- // the replay engine gets, nested under the tool call with the browser round-trips beneath it.
353
- const result = await span('flow.step', { index, anchor: step.anchor.kind, label }, async () => {
354
- if (isDegradedAnchor(step.anchor)) {
355
- // Never QUERY the sentinel — it marks "no anchor was determined", not an element to find.
356
- return degradedStepResult(step, index, label);
357
- }
358
- if (subSteps !== undefined && subSteps.length > 0) {
359
- return runSequenceStep(session, step, index, subSteps, confirmDangerous, sleep, dynamic);
360
- }
361
- if (step.anchor.kind === AnchorKind.SIGNAL) {
362
- return runSignalStep(session, step, index, label, waitForSignal, waitFor(step), replayFloor);
363
- }
364
- if (step.anchor.kind === AnchorKind.COMPONENT) {
365
- return runComponentStep(session, step, index, step.anchor, confirmDangerous, sleep);
376
+ try {
377
+ for (const step of flow.steps) {
378
+ if (step.invoke !== undefined) {
379
+ results.push(await runInvokeStep(session, flow, step, index, waitForSignal, signalTimeoutMs, confirmDangerous, sleep, options));
380
+ const last = results[results.length - 1];
381
+ if (last !== undefined && false === last.ok)
382
+ break;
383
+ index += 1;
384
+ continue;
366
385
  }
367
- if (step.anchor.kind === AnchorKind.ROLE && step.anchor.name !== undefined) {
368
- // A NAMED role anchor addresses one element. The nameless one is the degraded placeholder
369
- // and keeps its old path, where it fails legibly rather than querying a role as a testid.
370
- return runRoleStep(session, step, index, step.anchor, confirmDangerous, sleep);
386
+ const label = anchorLabel(step.anchor);
387
+ // The page this step runs on (the journey's "which page") — captured before the action.
388
+ const page = currentRoute(session);
389
+ // Event-time floor so the consequence reflects only THIS step's aftermath, not prior steps'.
390
+ const cursorBefore = session.elapsed();
391
+ const subSteps = step.steps;
392
+ // Traced per step, so a slow replay says WHICH step and which anchor kind spent the time. The
393
+ // per-step `durationMs` below is what the agent gets back; this is what a developer profiling
394
+ // the replay engine gets, nested under the tool call with the browser round-trips beneath it.
395
+ const result = await span('flow.step', { index, anchor: step.anchor.kind, label }, async () => {
396
+ if (isDegradedAnchor(step.anchor)) {
397
+ // Never QUERY the sentinel — it marks "no anchor was determined", not an element to find.
398
+ return degradedStepResult(step, index, label);
399
+ }
400
+ if (subSteps !== undefined && subSteps.length > 0) {
401
+ return runSequenceStep(session, step, index, subSteps, confirmDangerous, sleep, dynamic);
402
+ }
403
+ if (step.anchor.kind === AnchorKind.SIGNAL) {
404
+ return runSignalStep(session, step, index, label, waitForSignal, waitFor(step), replayFloor);
405
+ }
406
+ if (step.anchor.kind === AnchorKind.COMPONENT) {
407
+ return runComponentStep(session, step, index, step.anchor, confirmDangerous, sleep);
408
+ }
409
+ if (step.anchor.kind === AnchorKind.ROLE && step.anchor.name !== undefined) {
410
+ // A NAMED role anchor addresses one element. The nameless one is the degraded placeholder
411
+ // and keeps its old path, where it fails legibly rather than querying a role as a testid.
412
+ return runRoleStep(session, step, index, step.anchor, confirmDangerous, sleep);
413
+ }
414
+ return runTestidStep(session, step, index, label, dynamic, confirmDangerous, sleep);
415
+ });
416
+ // Once the anchor resolved and the action ran, the step's own expect is evaluated — signal, net,
417
+ // console and store truth alike — deterministically, in the same cheap replay loop, with no LLM.
418
+ /*
419
+ * The step's own expect, and then every sub-step's.
420
+ *
421
+ * `classifyFlowAssertions` already walks act_sequence sub-steps and counts an expect on either
422
+ * level, and states the invariant plainly: this and what replay enforces must move together, or
423
+ * "the difference is a false green or a lost verification". Enforcing only the top level was
424
+ * that difference -- a sequence whose sub-step declared a consequence was GRADED asserted and
425
+ * checked by nothing, so it could not go red while wearing the grade that says it could.
426
+ *
427
+ * WEAKER THAN THE LIVE PATH, and deliberately so rather than silently. A recorded sequence is
428
+ * dispatched as ONE batched command, so there are no per-sub-step cursors to open a window with
429
+ * -- every sub-step expect is evaluated against the window the whole sequence opened, which
430
+ * means sub-step three's claim can be satisfied by sub-step one's consequence. The live
431
+ * `act_sequence` path takes a cursor before each dispatch and does not have this. Narrowing it
432
+ * here means dispatching sub-steps individually on replay, which is a behaviour change to the
433
+ * replay path and belongs in its own commit.
434
+ */
435
+ const declared = [step.expect, ...(step.steps ?? []).map((sub) => sub.expect)].filter((expectation) => expectation !== undefined);
436
+ for (const expectation of declared) {
437
+ if (!result.ok || result.drift !== undefined)
438
+ break;
439
+ const expectDrift = await assertStepExpect(session, expectation, dynamic, waitForSignal, waitFor(step), cursorBefore);
440
+ if (expectDrift !== undefined) {
441
+ result.ok = false;
442
+ result.drift = expectDrift;
443
+ }
371
444
  }
372
- return runTestidStep(session, step, index, label, dynamic, confirmDangerous, sleep);
373
- });
374
- // Once the anchor resolved and the action ran, the step's own expect is evaluated — signal, net,
375
- // console and store truth alike — deterministically, in the same cheap replay loop, with no LLM.
376
- /*
377
- * The step's own expect, and then every sub-step's.
378
- *
379
- * `classifyFlowAssertions` already walks act_sequence sub-steps and counts an expect on either
380
- * level, and states the invariant plainly: this and what replay enforces must move together, or
381
- * "the difference is a false green or a lost verification". Enforcing only the top level was
382
- * that difference -- a sequence whose sub-step declared a consequence was GRADED asserted and
383
- * checked by nothing, so it could not go red while wearing the grade that says it could.
384
- *
385
- * WEAKER THAN THE LIVE PATH, and deliberately so rather than silently. A recorded sequence is
386
- * dispatched as ONE batched command, so there are no per-sub-step cursors to open a window with
387
- * -- every sub-step expect is evaluated against the window the whole sequence opened, which
388
- * means sub-step three's claim can be satisfied by sub-step one's consequence. The live
389
- * `act_sequence` path takes a cursor before each dispatch and does not have this. Narrowing it
390
- * here means dispatching sub-steps individually on replay, which is a behaviour change to the
391
- * replay path and belongs in its own commit.
392
- */
393
- const declared = [step.expect, ...(step.steps ?? []).map((sub) => sub.expect)].filter((expectation) => expectation !== undefined);
394
- for (const expectation of declared) {
395
- if (!result.ok || result.drift !== undefined)
445
+ if (page !== undefined)
446
+ result.page = page;
447
+ const windowEvents = session.eventsSince(cursorBefore).filter((e) => e.t >= cursorBefore);
448
+ const consequence = summarizeConsequence(windowEvents);
449
+ if (consequence !== undefined)
450
+ result.consequence = consequence;
451
+ // Per-step wall time is NOT shipped: it is `window.until - window.since`, computed from two numbers
452
+ // the next line already puts in the same object, under the same emission condition. A step used to
453
+ // carry the subtraction AND both operands, on every step of every replay.
454
+ const cursorAfter = session.elapsed();
455
+ // ONE builder, shared with the live act path, so a driven step and a replayed one describe what
456
+ // happened in identical words rather than in two vocabularies that agree by coincidence.
457
+ Object.assign(result, stepEffect(windowEvents, { since: cursorBefore, until: cursorAfter }));
458
+ /*
459
+ * A prefix step is setup and is not reported -- UNLESS it failed.
460
+ *
461
+ * A failure there means the resume never reached the step it was asked to resume from, and the
462
+ * run did not start where the caller will read it as having started. Swallowing it would turn
463
+ * "I could not get there" into "I got there and it was fine".
464
+ */
465
+ // `tool` is dropped HERE rather than at the ten places that set it, so a new step runner cannot
466
+ // forget the rule and quietly re-introduce the cost. Spelled out only when it is NOT the default.
467
+ if (FlowStepTool.ACT === result.tool)
468
+ delete result.tool;
469
+ if (index >= from || !result.ok || result.drift !== undefined)
470
+ results.push(result);
471
+ // Under `sweep`, a failure whose action still RAN does not stop the run — the page is where the
472
+ // step left it, so the next step is as meaningful as it was going to be. Anything else halts.
473
+ const sweepPast = true === options.sweep &&
474
+ result.drift !== undefined &&
475
+ isConsequenceDrift(result.drift.reasonKind);
476
+ if (!sweepPast && (result.drift !== undefined || !result.ok))
396
477
  break;
397
- const expectDrift = await assertStepExpect(session, expectation, dynamic, waitForSignal, waitFor(step), cursorBefore);
398
- if (expectDrift !== undefined) {
399
- result.ok = false;
400
- result.drift = expectDrift;
401
- }
478
+ index += 1;
402
479
  }
403
- if (page !== undefined)
404
- result.page = page;
405
- const windowEvents = session.eventsSince(cursorBefore).filter((e) => e.t >= cursorBefore);
406
- const consequence = summarizeConsequence(windowEvents);
407
- if (consequence !== undefined)
408
- result.consequence = consequence;
409
- // Per-step wall time is NOT shipped: it is `window.until - window.since`, computed from two numbers
410
- // the next line already puts in the same object, under the same emission condition. A step used to
411
- // carry the subtraction AND both operands, on every step of every replay.
412
- const cursorAfter = session.elapsed();
413
- // ONE builder, shared with the live act path, so a driven step and a replayed one describe what
414
- // happened in identical words rather than in two vocabularies that agree by coincidence.
415
- Object.assign(result, stepEffect(windowEvents, { since: cursorBefore, until: cursorAfter }));
416
- /*
417
- * A prefix step is setup and is not reported -- UNLESS it failed.
418
- *
419
- * A failure there means the resume never reached the step it was asked to resume from, and the
420
- * run did not start where the caller will read it as having started. Swallowing it would turn
421
- * "I could not get there" into "I got there and it was fine".
422
- */
423
- // `tool` is dropped HERE rather than at the ten places that set it, so a new step runner cannot
424
- // forget the rule and quietly re-introduce the cost. Spelled out only when it is NOT the default.
425
- if (FlowStepTool.ACT === result.tool)
426
- delete result.tool;
427
- if (index >= from || !result.ok || result.drift !== undefined)
428
- results.push(result);
429
- // Under `sweep`, a failure whose action still RAN does not stop the run — the page is where the
430
- // step left it, so the next step is as meaningful as it was going to be. Anything else halts.
431
- const sweepPast = true === options.sweep &&
432
- result.drift !== undefined &&
433
- isConsequenceDrift(result.drift.reasonKind);
434
- if (!sweepPast && (result.drift !== undefined || !result.ok))
435
- break;
436
- index += 1;
480
+ }
481
+ catch (error) {
482
+ if (!isDocumentGoneError(error))
483
+ throw error;
484
+ throw new DocumentLostDuringReplay(results, index, error);
437
485
  }
438
486
  return results;
439
487
  }