@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.
- package/SKILL.md +17 -18
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +13 -1
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse.d.ts +2 -1
- package/dist/command/cli/cli-parse.js +5 -2
- package/dist/command/cli/cli-verify.js +2 -2
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.js +2 -6
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +10 -2
- package/dist/command/cli/launch/cli-launch.js +32 -39
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +2 -1
- package/dist/command/cli/ports/resolve/cli-port.js +5 -1
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +24 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +48 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.js +2 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +664 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +52 -0
- package/dist/features/harness/platform-config.js +89 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.js +60 -55
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/flow-replay-run.d.ts +25 -2
- package/dist/language/flows/flow-replay-run.js +73 -27
- package/dist/language/flows/flow-replay.d.ts +17 -0
- package/dist/language/flows/flow-replay.js +148 -100
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +8 -7
- package/dist/language/flows/flows.js +7 -3
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/replay.js +1 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +7 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -2
- package/dist/memory/cloud/cloud-sync.js +10 -5
- package/dist/memory/cloud/harness-config.d.ts +10 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-daemon.d.ts +22 -2
- package/dist/memory/cloud/sync-daemon.js +24 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +19 -0
- package/dist/memory/intent/intent-shard-store.js +2 -2
- package/dist/memory/intent/intent-shard.d.ts +12 -12
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +50 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retention.d.ts +87 -2
- package/dist/memory/journal/on-disk/retention.js +177 -8
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +9 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.js +9 -2
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +13 -3
- package/dist/memory/journal/session-end.js +24 -4
- package/dist/memory/journal/session-journal.d.ts +115 -5
- package/dist/memory/journal/session-journal.js +479 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +14 -4
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +33 -5
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +15 -1
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-manager.d.ts +4 -0
- package/dist/portal/session/session-manager.js +19 -1
- package/dist/portal/session/session.d.ts +21 -10
- package/dist/portal/session/session.js +29 -42
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +20 -9
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act-capsule.js +27 -1
- package/dist/surface/tools/assert/assert-verdict.js +44 -17
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +37 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.js +49 -2
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +65 -5
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +55 -9
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/tool-kit.d.ts +18 -3
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +6 -1
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/verification-of.js +31 -3
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +16 -0
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +1 -1
- 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
|
|
79
|
-
const
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
48
|
-
//
|
|
49
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
177
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
398
|
-
if (expectDrift !== undefined) {
|
|
399
|
-
result.ok = false;
|
|
400
|
-
result.drift = expectDrift;
|
|
401
|
-
}
|
|
478
|
+
index += 1;
|
|
402
479
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
}
|