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