@reticlehq/server 3.3.0 → 3.4.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/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +4 -0
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +21 -3
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +5 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.js +21 -3
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +13 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +62 -10
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +39 -2
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +80 -44
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +26 -1
- package/dist/command/setup/run-setup.js +96 -13
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.js +3 -3
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +6 -0
- package/dist/features/harness/fill-values.js +3 -0
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.js +4 -7
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +40 -11
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +68 -3
- package/dist/memory/cloud/sync-cycle.js +298 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +8 -0
- package/dist/portal/bridge/bridge.js +43 -2
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +15 -19
- package/dist/surface/http-server.d.ts +2 -1
- package/dist/surface/http-server.js +4 -2
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +4 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +26 -50
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/error-recovery.js +4 -4
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +7 -6
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +0 -4
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +14 -4
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +9 -0
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +3 -3
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +2 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +2 -2
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -6
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* would make the daemon wait. `assert` / `wait_for` / `act_and_wait` all spend the caller's
|
|
18
18
|
* `timeout_ms` (resolve-within.ts); this is the same budget applied to the same wait.
|
|
19
19
|
*/
|
|
20
|
+
import { awaitDocumentSuccessor } from '../../../../portal/session/session-successor.js';
|
|
20
21
|
/**
|
|
21
22
|
* The default when the caller gives no `timeout_ms`: long enough for a dev server to serve a page
|
|
22
23
|
* and the SDK to dial back; short enough to not hang. A caller who knows the app is slower says so.
|
|
@@ -69,19 +70,27 @@ export function idsAtTarget(sessions, target) {
|
|
|
69
70
|
}
|
|
70
71
|
return ids;
|
|
71
72
|
}
|
|
72
|
-
function findArrival(sessions, target, scope) {
|
|
73
|
+
function findArrival(sessions, target, scope, currentSession) {
|
|
73
74
|
let arrived = null;
|
|
74
75
|
for (const s of sessions.all()) {
|
|
75
|
-
|
|
76
|
+
// The currently tracked document gets priority because it is the strongest
|
|
77
|
+
// evidence of where this navigation went.
|
|
78
|
+
if (s.id === currentSession.id) {
|
|
79
|
+
if (samePage(s.url, target)) {
|
|
80
|
+
return { sessionId: s.id };
|
|
81
|
+
}
|
|
82
|
+
// Intermediate routes are not necessarily redirects. Keep polling so
|
|
83
|
+
// the document has a chance to reach the requested target.
|
|
76
84
|
continue;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
}
|
|
86
|
+
// A different session that was already at the target cannot prove this
|
|
87
|
+
// navigation arrived.
|
|
80
88
|
if (scope.priorIds.has(s.id))
|
|
81
89
|
continue;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
if (samePage(s.url, target)) {
|
|
91
|
+
if (null === arrived)
|
|
92
|
+
arrived = { sessionId: s.id };
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
95
|
return arrived;
|
|
87
96
|
}
|
|
@@ -96,11 +105,39 @@ const REAL_CLOCK = {
|
|
|
96
105
|
export async function awaitArrival(sessions, target, scope, timeoutMs = ARRIVAL_TIMEOUT_MS, clock = REAL_CLOCK) {
|
|
97
106
|
const deadline = clock.now() + timeoutMs;
|
|
98
107
|
for (;;) {
|
|
99
|
-
|
|
108
|
+
// Keep checking the live session registry for the entire caller-provided
|
|
109
|
+
// budget. This is important because a document can disconnect and reconnect
|
|
110
|
+
// under another id, and the new session may be at the requested target,
|
|
111
|
+
// including on a different origin.
|
|
112
|
+
const found = findArrival(sessions, target, scope, scope.navigatedSession);
|
|
100
113
|
if (found !== null)
|
|
101
114
|
return found;
|
|
102
115
|
if (clock.now() >= deadline)
|
|
103
|
-
|
|
116
|
+
break;
|
|
104
117
|
await clock.sleep(POLL_MS);
|
|
105
118
|
}
|
|
119
|
+
// Only after the navigation wait has expired do we use successor detection.
|
|
120
|
+
// At this point it is no longer being used to decide whether the requested
|
|
121
|
+
// navigation arrived. It only helps explain where the replaced document
|
|
122
|
+
// landed when the target was never observed.
|
|
123
|
+
const current = sessions.get(scope.navigatedSession.id);
|
|
124
|
+
if (current !== undefined) {
|
|
125
|
+
if (!samePage(current.url, scope.navigatedFrom) && !samePage(current.url, target)) {
|
|
126
|
+
return {
|
|
127
|
+
sessionId: current.id,
|
|
128
|
+
landedOn: current.url,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const successor = await awaitDocumentSuccessor(sessions, scope.navigatedSession, 0, clock);
|
|
134
|
+
if (successor !== null &&
|
|
135
|
+
!samePage(successor.url, target) &&
|
|
136
|
+
!samePage(successor.url, scope.navigatedFrom)) {
|
|
137
|
+
return {
|
|
138
|
+
sessionId: successor.id,
|
|
139
|
+
landedOn: successor.url,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
106
143
|
}
|
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The honest envelope for a navigation nobody can confirm.
|
|
3
|
-
*
|
|
4
|
-
* `window.location.assign(url)` returns before the page moves, and the SDK that would report on the
|
|
5
|
-
* new document is destroyed by the navigation itself. So a successful NAVIGATE means the browser
|
|
6
|
-
* accepted the instruction — not that anything arrived. Driven against a dead URL it returned
|
|
7
|
-
* `{"ok":true}` while the session died, which is the worst combination available: the agent believes
|
|
8
|
-
* it navigated, is looking at nothing, and has lost Reticle.
|
|
9
|
-
*
|
|
10
|
-
* `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
|
|
11
|
-
* This gives navigate the same honesty without changing what it does.
|
|
12
|
-
*/
|
|
13
1
|
/** The session the SDK reconnected as after the page moved, when one was observed in the window. */
|
|
14
2
|
export interface NavigateArrival {
|
|
15
3
|
sessionId: string;
|
|
4
|
+
landedOn?: string;
|
|
16
5
|
}
|
|
17
6
|
export declare function navigateResult(result: {
|
|
18
7
|
ok?: unknown;
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The honest envelope for a navigation nobody can confirm.
|
|
3
|
+
*
|
|
4
|
+
* `window.location.assign(url)` returns before the page moves, and the SDK that would report on the
|
|
5
|
+
* new document is destroyed by the navigation itself. So a successful NAVIGATE means the browser
|
|
6
|
+
* accepted the instruction — not that anything arrived. Driven against a dead URL it returned
|
|
7
|
+
* `{"ok":true}` while the session died, which is the worst combination available: the agent believes
|
|
8
|
+
* it navigated, is looking at nothing, and has lost Reticle.
|
|
9
|
+
*
|
|
10
|
+
* `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
|
|
11
|
+
* This gives navigate the same honesty without changing what it does.
|
|
12
|
+
*/
|
|
13
|
+
import { NAVIGATION_REDIRECT_NOTE } from '@reticlehq/core';
|
|
1
14
|
export function navigateResult(result,
|
|
2
15
|
/**
|
|
3
16
|
* Arrival, if the daemon saw the SDK come back. `null` keeps the original honest envelope.
|
|
@@ -26,12 +39,17 @@ waitedMs) {
|
|
|
26
39
|
if (!ok)
|
|
27
40
|
return base;
|
|
28
41
|
if (arrival !== null) {
|
|
42
|
+
const redirected = 'string' === typeof arrival.landedOn;
|
|
29
43
|
return {
|
|
30
44
|
...base,
|
|
31
|
-
confirmed:
|
|
32
|
-
// The SDK that reconnects after a navigation is a NEW session with a new id. Returning it here
|
|
33
|
-
// is the difference between a confirmation and a confirmation the agent can act on.
|
|
45
|
+
confirmed: !redirected,
|
|
34
46
|
sessionId: arrival.sessionId,
|
|
47
|
+
...(redirected
|
|
48
|
+
? {
|
|
49
|
+
landedOn: arrival.landedOn,
|
|
50
|
+
note: `${NAVIGATION_REDIRECT_NOTE} Landed on: ${arrival.landedOn}`,
|
|
51
|
+
}
|
|
52
|
+
: {}),
|
|
35
53
|
};
|
|
36
54
|
}
|
|
37
55
|
return {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remember the control an act_and_wait PROVED, for the coverage ledger's `proved` level.
|
|
3
|
+
*
|
|
4
|
+
* Structural rather than `Session`, and optional, so a test stub that never proves anything need not
|
|
5
|
+
* implement it — the same reason `beginAction` is optional on the crawl session.
|
|
6
|
+
*/
|
|
7
|
+
export declare function noteProved(session: {
|
|
8
|
+
recordProvedFrom?: (actPayload: unknown) => void;
|
|
9
|
+
}, verified: string, actPayload: unknown): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Verified } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* Remember the control an act_and_wait PROVED, for the coverage ledger's `proved` level.
|
|
4
|
+
*
|
|
5
|
+
* Structural rather than `Session`, and optional, so a test stub that never proves anything need not
|
|
6
|
+
* implement it — the same reason `beginAction` is optional on the crawl session.
|
|
7
|
+
*/
|
|
8
|
+
export function noteProved(session, verified, actPayload) {
|
|
9
|
+
if (Verified.YES === verified)
|
|
10
|
+
session.recordProvedFrom?.(actPayload);
|
|
11
|
+
}
|
|
@@ -35,6 +35,11 @@ export interface StepExpectation {
|
|
|
35
35
|
/** What was seen instead, on a miss — the half of the answer a repair actually needs. */
|
|
36
36
|
observed?: string;
|
|
37
37
|
expected?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Contradiction kinds positively observed in this step's window — deciding ones only, the same
|
|
40
|
+
* filter act_and_wait applies. A held step with one is a UI that advanced over a failed write.
|
|
41
|
+
*/
|
|
42
|
+
contradicted?: readonly string[];
|
|
38
43
|
}
|
|
39
44
|
export interface SequenceGrade {
|
|
40
45
|
verified: Verified;
|
|
@@ -27,7 +27,9 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
|
|
|
27
27
|
const total = Math.max(plan.planned, steps.length);
|
|
28
28
|
const declaring = steps.filter((step) => step.declared);
|
|
29
29
|
const declared = declaring.length;
|
|
30
|
-
|
|
30
|
+
// `held: false` on an undeclared step means it could not be performed at all — the journey broke
|
|
31
|
+
// there whether or not anything was declared about it.
|
|
32
|
+
const missed = steps.find((step) => (step.declared ? true !== step.held : false === step.held));
|
|
31
33
|
// A miss decides the grade before coverage does. How much of the plan declared nothing is
|
|
32
34
|
// context on a red, never a softening of it.
|
|
33
35
|
if (missed !== undefined) {
|
|
@@ -39,6 +41,17 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
|
|
|
39
41
|
because: `a declared consequence did not hold — observed ${observed}`,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
44
|
+
// Same rule as a single action: an observed contradiction outranks a consequence that held.
|
|
45
|
+
const contradictedAt = steps.findIndex((step) => (step.contradicted ?? []).length > 0);
|
|
46
|
+
if (contradictedAt >= 0) {
|
|
47
|
+
const kinds = (steps[contradictedAt]?.contradicted ?? []).join(', ');
|
|
48
|
+
return {
|
|
49
|
+
verified: Verified.NO,
|
|
50
|
+
declared,
|
|
51
|
+
total,
|
|
52
|
+
because: `step ${String(contradictedAt + 1)}: channels disagree about this action (${kinds}) even though it went on`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
42
55
|
/*
|
|
43
56
|
* `unknown` rather than `no-fault`, and the distinction is load-bearing.
|
|
44
57
|
*
|
|
@@ -68,6 +81,24 @@ export function gradeSequence(steps, plan = { planned: steps.length, dispatched:
|
|
|
68
81
|
: `nothing was dispatched, so none of the ${String(total)} step(s) ran — read the per-step \`error\` for the one that was refused, and fix that before adding an \`expect\``,
|
|
69
82
|
};
|
|
70
83
|
}
|
|
84
|
+
// A journey is proved at its END. Declaring only early steps checks that it started, and a yes
|
|
85
|
+
// there was the one a single act_and_wait on the last step would never have given.
|
|
86
|
+
if (steps.length < total) {
|
|
87
|
+
return {
|
|
88
|
+
verified: Verified.UNKNOWN,
|
|
89
|
+
declared,
|
|
90
|
+
total,
|
|
91
|
+
because: `only ${String(steps.length)} of ${String(total)} steps ran, so the journey never reached its end`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (true !== steps.at(-1)?.declared) {
|
|
95
|
+
return {
|
|
96
|
+
verified: Verified.UNKNOWN,
|
|
97
|
+
declared,
|
|
98
|
+
total,
|
|
99
|
+
because: `the last step, where the journey ends, declared nothing — give it an \`expect\` naming the end state (${String(declared)} of ${String(total)} steps declared one)`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
71
102
|
return {
|
|
72
103
|
verified: Verified.YES,
|
|
73
104
|
declared,
|
|
@@ -28,6 +28,13 @@ interface CapsuleSaveInputs {
|
|
|
28
28
|
*/
|
|
29
29
|
root?: string | undefined;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* What the act claimed would happen, in words: `route /settings`, `POST /v1/pay → 200`,
|
|
33
|
+
* `element "Issues"`. Read off the predicate the agent declared, because the causal links only name
|
|
34
|
+
* signals and requests: a failed route, element or text claim used to be filed as "declared
|
|
35
|
+
* consequence", which tells whoever reads the capsule nothing about what was expected.
|
|
36
|
+
*/
|
|
37
|
+
export declare function describeExpected(predicate: unknown): string | undefined;
|
|
31
38
|
/** Returns the capsule id when one was written, or undefined when there was nothing to save. */
|
|
32
39
|
export declare function saveFailedAssertCapsule(inputs: CapsuleSaveInputs): Promise<string | undefined>;
|
|
33
40
|
export {};
|
|
@@ -14,6 +14,50 @@ import { CapsuleStore, capsuleFingerprint, capsuleId, CAPSULE_VERSION, } from '.
|
|
|
14
14
|
import { ReticleTool } from '@reticlehq/core';
|
|
15
15
|
import { asRecord, asString } from '@reticlehq/core';
|
|
16
16
|
import { sessionRoot } from '../../memory/project/session-root.js';
|
|
17
|
+
/**
|
|
18
|
+
* What the act claimed would happen, in words: `route /settings`, `POST /v1/pay → 200`,
|
|
19
|
+
* `element "Issues"`. Read off the predicate the agent declared, because the causal links only name
|
|
20
|
+
* signals and requests: a failed route, element or text claim used to be filed as "declared
|
|
21
|
+
* consequence", which tells whoever reads the capsule nothing about what was expected.
|
|
22
|
+
*/
|
|
23
|
+
export function describeExpected(predicate) {
|
|
24
|
+
const p = asRecord(predicate);
|
|
25
|
+
const kind = asString(p['kind']);
|
|
26
|
+
if (kind === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const all = (key, joiner) => {
|
|
29
|
+
const parts = (Array.isArray(p[key]) ? p[key] : [])
|
|
30
|
+
.map(describeExpected)
|
|
31
|
+
.filter((d) => d !== undefined);
|
|
32
|
+
return 0 === parts.length ? undefined : parts.join(joiner);
|
|
33
|
+
};
|
|
34
|
+
const q = asRecord(p['query']);
|
|
35
|
+
const named = asString(p['name']) ?? asString(q['name']) ?? asString(q['value']) ?? asString(q['testid']);
|
|
36
|
+
switch (kind) {
|
|
37
|
+
case 'route':
|
|
38
|
+
return `route ${asString(p['pathname']) ?? asString(p['contains']) ?? '?'}`;
|
|
39
|
+
case 'net': {
|
|
40
|
+
const status = 'number' === typeof p['status'] ? ` → ${String(p['status'])}` : '';
|
|
41
|
+
return `${asString(p['method']) ?? 'a'} ${asString(p['urlContains']) ?? 'request'}${status}`;
|
|
42
|
+
}
|
|
43
|
+
case 'signal':
|
|
44
|
+
return `signal ${asString(p['name']) ?? '?'}`;
|
|
45
|
+
case 'element':
|
|
46
|
+
return `${true === p['absent'] ? 'no ' : ''}element ${named === undefined ? '?' : `"${named}"`}`;
|
|
47
|
+
case 'text':
|
|
48
|
+
return `${true === p['absent'] ? 'no ' : ''}text "${asString(p['contains']) ?? '?'}"`;
|
|
49
|
+
case 'allOf':
|
|
50
|
+
return all('predicates', ' AND ');
|
|
51
|
+
case 'anyOf':
|
|
52
|
+
return all('predicates', ' OR ');
|
|
53
|
+
case 'not': {
|
|
54
|
+
const inner = describeExpected(p['predicate']);
|
|
55
|
+
return inner === undefined ? undefined : `not ${inner}`;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
return kind;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
17
61
|
/** Returns the capsule id when one was written, or undefined when there was nothing to save. */
|
|
18
62
|
export async function saveFailedAssertCapsule(inputs) {
|
|
19
63
|
const { deps, verdict, capsule, links, args, actResult, actedSource, root: given } = inputs;
|
|
@@ -47,7 +91,8 @@ export async function saveFailedAssertCapsule(inputs) {
|
|
|
47
91
|
const body = {
|
|
48
92
|
version: CAPSULE_VERSION,
|
|
49
93
|
origin: 'failed-assert',
|
|
50
|
-
expected:
|
|
94
|
+
expected: describeExpected(args['until'] ?? args['predicate']) ??
|
|
95
|
+
(expectedText.length > 0 ? expectedText : 'declared consequence'),
|
|
51
96
|
observed: capsule.firstDivergence?.observed ?? verdict.failureReason ?? 'not observed',
|
|
52
97
|
steps: [
|
|
53
98
|
{
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { timeoutMsSchema } from './args/numeric-bounds.js';
|
|
14
|
-
import { compileSequenceStep } from '../../language/flows/replay.js';
|
|
14
|
+
import { compileSequenceStep, pathOf } from '../../language/flows/replay.js';
|
|
15
15
|
import { sequenceStepArgs } from './act/act-preflight.js';
|
|
16
16
|
import { ReticleTool } from '@reticlehq/core';
|
|
17
17
|
import { healthEnvelope } from '../../portal/session/session-health.js';
|
|
18
18
|
import { pausedShortCircuit, pausedOutputShape, withControl, } from '../../portal/session/control-envelope.js';
|
|
19
|
-
import { asRecord } from '@reticlehq/core';
|
|
19
|
+
import { asRecord, isAbsenceDerived, isAdvisory } from '@reticlehq/core';
|
|
20
20
|
import { sessionIdFromArgs } from './tools-helpers.js';
|
|
21
21
|
import { describeStepResult, runStepWithStaleRetry } from './act/act-sequence-retry.js';
|
|
22
22
|
import { assertSequenceSteps } from './act/act-preflight.js';
|
|
@@ -59,10 +59,9 @@ function effectOf(session, since) {
|
|
|
59
59
|
const BURST_MAX_GAP_MS = 500;
|
|
60
60
|
export const ACT_SEQUENCE_TOOL = {
|
|
61
61
|
name: ReticleTool.ACT_SEQUENCE,
|
|
62
|
-
// The example is required for a core tool, and this one carries weight: the
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// exact shape an agent otherwise spends three round trips on.
|
|
62
|
+
// The example is required for a core tool, and this one carries weight: the loop it replaces is
|
|
63
|
+
// a login form driven as three separate reticle_act calls. Showing fill -> fill -> click is showing
|
|
64
|
+
// the exact shape an agent otherwise spends three round trips on.
|
|
66
65
|
example: {
|
|
67
66
|
steps: [
|
|
68
67
|
{ ref: 'e12', action: 'fill', args: { value: 'a@b.com' } },
|
|
@@ -133,6 +132,7 @@ export const ACT_SEQUENCE_TOOL = {
|
|
|
133
132
|
if (paused !== undefined)
|
|
134
133
|
return paused;
|
|
135
134
|
const since = session.elapsed();
|
|
135
|
+
const pageBefore = pathOf(session.url);
|
|
136
136
|
session.beginAction(ReticleTool.ACT_SEQUENCE, asRecord(args));
|
|
137
137
|
try {
|
|
138
138
|
const inputSteps = Array.isArray(args['steps']) ? args['steps'] : [];
|
|
@@ -230,8 +230,12 @@ export const ACT_SEQUENCE_TOOL = {
|
|
|
230
230
|
* satisfy step four's assertion, which is a false green built out of correct parts.
|
|
231
231
|
*/
|
|
232
232
|
const parsed = PredicateSchema.safeParse(step['expect']);
|
|
233
|
+
const contradicted = (described.contradictions ?? [])
|
|
234
|
+
.map((c) => c.kind)
|
|
235
|
+
.filter((kind) => !isAdvisory(kind) && !isAbsenceDerived(kind));
|
|
236
|
+
const disagreement = 0 === contradicted.length ? {} : { contradicted };
|
|
233
237
|
if (!parsed.success) {
|
|
234
|
-
expectations.push({ declared: false });
|
|
238
|
+
expectations.push({ declared: false, ...disagreement });
|
|
235
239
|
stepResults.push(described);
|
|
236
240
|
}
|
|
237
241
|
else {
|
|
@@ -240,6 +244,7 @@ export const ACT_SEQUENCE_TOOL = {
|
|
|
240
244
|
expectations.push({
|
|
241
245
|
declared: true,
|
|
242
246
|
held,
|
|
247
|
+
...disagreement,
|
|
243
248
|
...(verdict.observed === undefined ? {} : { observed: verdict.observed }),
|
|
244
249
|
...(verdict.expected === undefined ? {} : { expected: verdict.expected }),
|
|
245
250
|
});
|
|
@@ -287,7 +292,19 @@ export const ACT_SEQUENCE_TOOL = {
|
|
|
287
292
|
// Same as captureAct: no `active()` gate, so a sequence driven with nothing open still lands
|
|
288
293
|
// in the ambient tape. A stalled plan is still excluded — those steps never ran.
|
|
289
294
|
if (stalledAt === undefined) {
|
|
290
|
-
|
|
295
|
+
// A sub-step keeps its `expect` only if it held — same rule as act_and_wait, so a flow
|
|
296
|
+
// never replays an expectation that was never once observed to hold.
|
|
297
|
+
const proved = inputSteps.map((raw, i) => {
|
|
298
|
+
const step = asRecord(raw);
|
|
299
|
+
const { expect: _expect, ...rest } = step;
|
|
300
|
+
return true === expectations[i]?.held ? step : rest;
|
|
301
|
+
});
|
|
302
|
+
const recorded = compileSequenceStep({ ...args, steps: proved }, {
|
|
303
|
+
count: inputSteps.length,
|
|
304
|
+
steps: stepResults,
|
|
305
|
+
});
|
|
306
|
+
deps.recordings.capture(pageBefore === undefined ? recorded : { ...recorded, page: pageBefore });
|
|
307
|
+
deps.recordings.markEnded(pathOf(session.url));
|
|
291
308
|
}
|
|
292
309
|
/*
|
|
293
310
|
* The grade, and the coverage it is never allowed to hide.
|
|
@@ -3,23 +3,8 @@ import { aliasParam } from './args/alias-args.js';
|
|
|
3
3
|
import { resolveSessionWithin } from '../../portal/session/timing/resolve-within.js';
|
|
4
4
|
import { WALL_CLOCK } from '../../portal/session/timing/wall-clock.js';
|
|
5
5
|
import { timeoutMsSchema } from './args/numeric-bounds.js';
|
|
6
|
-
import { captureAct } from '../../language/flows/replay.js';
|
|
7
|
-
|
|
8
|
-
* The pathname of a session's live url, or undefined when it has none yet.
|
|
9
|
-
*
|
|
10
|
-
* Pathname rather than the whole url, because that is what `startPath` is compared against and what
|
|
11
|
-
* a hash-routed app makes meaningless in the document location — see routeOfEvent.
|
|
12
|
-
*/
|
|
13
|
-
function pathOf(url) {
|
|
14
|
-
if (url === undefined)
|
|
15
|
-
return undefined;
|
|
16
|
-
try {
|
|
17
|
-
return new URL(url).pathname;
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
6
|
+
import { captureAct, captureVerdictedAct, pathOf } from '../../language/flows/replay.js';
|
|
7
|
+
import { durableArg, durableOutput, reloadAndRecheck, withDurability } from './act/durable.js';
|
|
23
8
|
import { ActionType, ActionWarning, ReticleCommand, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, Verified, VerifiedReason, PredicateKind, } from '@reticlehq/core';
|
|
24
9
|
import { leanActResult, mutatedWithin } from './act/act-view.js';
|
|
25
10
|
import { ReticleTool } from '@reticlehq/core';
|
|
@@ -37,8 +22,9 @@ import { intentDebt, openSessionIntents } from '../../memory/intent/open-intents
|
|
|
37
22
|
import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../../memory/intent/inline-intent.js';
|
|
38
23
|
import { declaresState } from '@reticlehq/engine/question/predicate/predicate-asks.js';
|
|
39
24
|
import { isStateUnwatched } from '@reticlehq/engine/evidence/blind-spots.js';
|
|
40
|
-
import { inFlightRequestLabels, repeatedRequestLabels
|
|
41
|
-
import {
|
|
25
|
+
import { inFlightRequestLabels, repeatedRequestLabels } from './act/settle-in-flight.js';
|
|
26
|
+
import { finishAfterMatch } from './act/after-match.js';
|
|
27
|
+
import { noteProved } from './act/proved-controls.js';
|
|
42
28
|
import { decideVerified } from '@reticlehq/engine/evidence/verified.js';
|
|
43
29
|
import { honestyForVerdict } from '@reticlehq/engine/evidence/honesty.js';
|
|
44
30
|
import { declaredExpectations, declaresBodyIndependentChannel, } from '@reticlehq/engine/question/declared.js';
|
|
@@ -254,6 +240,7 @@ export const ACT_TOOLS = [
|
|
|
254
240
|
});
|
|
255
241
|
}
|
|
256
242
|
finally {
|
|
243
|
+
deps.recordings.markEnded(pathOf(session.url));
|
|
257
244
|
// Close the window on every exit (settle or throw), recording the action + settle outcome.
|
|
258
245
|
session.finishAction(undefined, settledOutcome, true === settledOutcome ? session.elapsed() - since : undefined);
|
|
259
246
|
}
|
|
@@ -286,7 +273,7 @@ export const ACT_TOOLS = [
|
|
|
286
273
|
.record(z.unknown())
|
|
287
274
|
.optional()
|
|
288
275
|
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
|
|
289
|
-
predicate: PredicateSchema.optional().describe('Alias for `until
|
|
276
|
+
predicate: PredicateSchema.optional().describe('Alias for `until`.'),
|
|
290
277
|
until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
|
|
291
278
|
timeout_ms: timeoutMsSchema
|
|
292
279
|
.optional()
|
|
@@ -296,9 +283,11 @@ export const ACT_TOOLS = [
|
|
|
296
283
|
.optional()
|
|
297
284
|
.describe('Throw if the tab is throttled. Default: false.'),
|
|
298
285
|
intent: intentArg,
|
|
286
|
+
durable: durableArg,
|
|
299
287
|
...sessionIdShape,
|
|
300
288
|
},
|
|
301
289
|
outputSchema: {
|
|
290
|
+
durable: durableOutput,
|
|
302
291
|
effect: z
|
|
303
292
|
.unknown()
|
|
304
293
|
.describe('The reticle_act result (dispatched, settled, inputMode, etc.).'),
|
|
@@ -433,6 +422,9 @@ export const ACT_TOOLS = [
|
|
|
433
422
|
const { alreadyTrue, alreadyTrueHiddenMatch, alreadyTrueEvidence, baselines } = await readAlreadyTrue(session, until, since);
|
|
434
423
|
// Same as the ACT handler: the route this step RAN on, before the action can move the app.
|
|
435
424
|
const routeBeforeWait = pathOf(session.url);
|
|
425
|
+
// Recorded in `finally`, once the verdict is known — see captureVerdictedAct.
|
|
426
|
+
let dispatched;
|
|
427
|
+
let recordedVerdict;
|
|
436
428
|
try {
|
|
437
429
|
// actCommand is the single interception point for upload+path rewrite.
|
|
438
430
|
//
|
|
@@ -450,7 +442,7 @@ export const ACT_TOOLS = [
|
|
|
450
442
|
if (actResult !== null) {
|
|
451
443
|
if (!actResult.ok)
|
|
452
444
|
throw new Error(actResult.error ?? 'act failed');
|
|
453
|
-
|
|
445
|
+
dispatched = { result: actResult.result };
|
|
454
446
|
// Dispatched — now this act owns the cursor and the effect. Marking its OWN measurement
|
|
455
447
|
// also stops the spread below from inheriting an earlier reticle_act's action and
|
|
456
448
|
// mutation count.
|
|
@@ -476,34 +468,12 @@ export const ACT_TOOLS = [
|
|
|
476
468
|
since = 0;
|
|
477
469
|
session = followed.session;
|
|
478
470
|
verdict = followed.verdict;
|
|
479
|
-
//
|
|
480
|
-
// while the write is still in flight — so the verdict was taken over a window the app had not
|
|
481
|
-
// finished, and came back `unknown / unsettled` asking the CALLER to re-check. A login that
|
|
482
|
-
// genuinely succeeded returned at 492ms of an 8000ms budget with every corroborating channel
|
|
483
|
-
// agreeing. Spend the rest of the budget the caller already granted.
|
|
484
|
-
//
|
|
485
|
-
// This decides nothing: it waits for the answer instead of guessing at it. A response that
|
|
486
|
-
// arrives as a FAILURE still contradicts — and now arrives INSIDE the window, where the
|
|
487
|
-
// detector can see it at all — while a request still open when the budget genuinely runs out
|
|
488
|
-
// reports unsettled exactly as before, an honest limit rather than an early exit. Costs
|
|
489
|
-
// nothing on the common path, where no request is in flight.
|
|
471
|
+
// After a match, let the app finish before the window closes — see act/after-match.ts.
|
|
490
472
|
if (verdict.pass && timeout > 0) {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
await waitForInFlight(session, since, spent(), sleep);
|
|
496
|
-
// Waiting for the response moved the hazard rather than removing it: the response now lands
|
|
497
|
-
// inside the window BY DESIGN, and the app's re-render happens a task or two later. Close
|
|
498
|
-
// the window in that gap and every channel agrees the app took a successful write and did
|
|
499
|
-
// nothing — `response-ignored`, i.e. `verified:"no"` on a correct app, produced entirely by
|
|
500
|
-
// where we stopped looking. A false accusation is the more damaging direction of error for
|
|
501
|
-
// a verification tool: it sends someone to fix code that is not broken.
|
|
502
|
-
//
|
|
503
|
-
// So the response is not the end of the window; the app's REACTION to it is. Paid only in
|
|
504
|
-
// the shape that would otherwise be accused — a successful mutating write with nothing
|
|
505
|
-
// moved after it — and short enough that a genuinely dropped response is still reported.
|
|
506
|
-
await waitForReaction(session, since, spent(), sleep);
|
|
473
|
+
const left = () => timeout - (session.elapsed() - predicateStarted);
|
|
474
|
+
const reverted = await finishAfterMatch(session, until, since, baselines, left);
|
|
475
|
+
if (reverted !== undefined)
|
|
476
|
+
verdict = { pass: false, failureReason: reverted, observed: reverted };
|
|
507
477
|
}
|
|
508
478
|
const r = asRecord(actResult?.result);
|
|
509
479
|
if ('boolean' === typeof r['settled'])
|
|
@@ -650,7 +620,7 @@ export const ACT_TOOLS = [
|
|
|
650
620
|
const outcomePending = acceptedWriteLabels(windowEvents);
|
|
651
621
|
const outcomeUnread = unreadWriteLabels(windowEvents);
|
|
652
622
|
const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
|
|
653
|
-
const decision = decideVerified({
|
|
623
|
+
const { decision, durable } = await withDurability(args['durable'], until, decideVerified({
|
|
654
624
|
pass: verdict.pass,
|
|
655
625
|
// Threaded rather than looked up: decideVerified is pure and has no session.
|
|
656
626
|
...sessionVerdictFacts(session),
|
|
@@ -690,7 +660,9 @@ export const ACT_TOOLS = [
|
|
|
690
660
|
repeated: repeatedRequestLabels(windowEvents),
|
|
691
661
|
},
|
|
692
662
|
...(namedNetIsInFlight(until, stillInFlight) ? { namedRequestInFlight: true } : {}),
|
|
693
|
-
});
|
|
663
|
+
}), (part) => reloadAndRecheck(deps, session, part, timeout));
|
|
664
|
+
recordedVerdict = String(decision.verified);
|
|
665
|
+
noteProved(session, recordedVerdict, actResult?.result);
|
|
694
666
|
// Computed once: the verdict block reports it, and the instrumentation gaps are a second
|
|
695
667
|
// reading of the same evidence rather than a new observation.
|
|
696
668
|
const actionSummary = causalSummary(windowEvents, {
|
|
@@ -818,6 +790,7 @@ export const ACT_TOOLS = [
|
|
|
818
790
|
// agent cannot miss it by not asking. Omitted entirely when clean, so a healthy action
|
|
819
791
|
// pays nothing.
|
|
820
792
|
...(contradictions.length > 0 ? { contradictions } : {}),
|
|
793
|
+
...(durable === undefined ? {} : { durable }),
|
|
821
794
|
honesty: honestyForVerdict(String(decision.verified), honesty),
|
|
822
795
|
// Without its summary: it is byte-identical to `summary` two fields up. See wireCapsule.
|
|
823
796
|
...(capsule === undefined ? {} : { capsule: wireCapsule(capsule) }),
|
|
@@ -828,6 +801,9 @@ export const ACT_TOOLS = [
|
|
|
828
801
|
});
|
|
829
802
|
}
|
|
830
803
|
finally {
|
|
804
|
+
if (dispatched !== undefined)
|
|
805
|
+
captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, recordedVerdict);
|
|
806
|
+
deps.recordings.markEnded(pathOf(currentOf(deps.sessions, session).url));
|
|
831
807
|
acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
|
|
832
808
|
}
|
|
833
809
|
},
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Pure: no IO, no clock.
|
|
12
12
|
*/
|
|
13
|
-
import { isConsequenceKind, isPresenceKind, PredicateKind } from '@reticlehq/core';
|
|
13
|
+
import { compareSourceClauses, isConsequenceKind, isPresenceKind, PredicateKind, } from '@reticlehq/core';
|
|
14
14
|
import { HonestyGrade, strongerGrade, weakerGrade } from '@reticlehq/engine/evidence/honesty.js';
|
|
15
15
|
export const PRESENCE_ONLY_ADVICE = 'This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion (or allOf it with one) so green means the feature actually worked.';
|
|
16
16
|
function walk(predicate, negated = false) {
|
|
@@ -27,6 +27,14 @@ function walk(predicate, negated = false) {
|
|
|
27
27
|
// never matched anything in the first place. Negation makes a presence check weaker, not stronger.
|
|
28
28
|
if (PredicateKind.NOT === predicate.kind)
|
|
29
29
|
return walk(predicate.predicate, true);
|
|
30
|
+
// A comparison grades as what its sides read: with a net/signal/state side it relates the page to
|
|
31
|
+
// the app's own record, which a wrong locator cannot fake; text against text is two readings of
|
|
32
|
+
// the same DOM. Its text side is always a content check, so it never counts as bare presence
|
|
33
|
+
// unless negated.
|
|
34
|
+
if (PredicateKind.COMPARE === predicate.kind) {
|
|
35
|
+
const consequence = compareSourceClauses(predicate).some((c) => isConsequenceKind(c.kind));
|
|
36
|
+
return { consequence, presence: !consequence && negated };
|
|
37
|
+
}
|
|
30
38
|
// Leaf: classify against the single source of truth in core. Kinds that are neither consequence
|
|
31
39
|
// nor presence (route/console/settled/animation) correctly return false for both.
|
|
32
40
|
return {
|
|
@@ -109,6 +117,9 @@ export function gradeOfPredicate(predicate) {
|
|
|
109
117
|
// verdict that proved only presence report `signal`, and a `minGrade: net` gate would trust it.
|
|
110
118
|
case PredicateKind.ANY_OF:
|
|
111
119
|
return combine(predicate.predicates, weakerGrade);
|
|
120
|
+
// Both sides had to be read and agree, so it proves as much as its stronger side.
|
|
121
|
+
case PredicateKind.COMPARE:
|
|
122
|
+
return combine(compareSourceClauses(predicate), strongerGrade);
|
|
112
123
|
// Everything else, `not` included: an absence claim is satisfied trivially by a locator that
|
|
113
124
|
// never matched anything, which is the argument `walk` already makes about negation.
|
|
114
125
|
default:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CaptureLoss,
|
|
1
|
+
import { CaptureLoss, channelsReadBy, PredicateKind } from '@reticlehq/core';
|
|
2
2
|
import { sessionVerdictFacts } from '../../../portal/session/session-verdict-facts.js';
|
|
3
3
|
import { gapsForAction } from '@reticlehq/engine/evidence/instrumentation-gaps.js';
|
|
4
4
|
import { noteSessionGaps } from '@reticlehq/engine/evidence/gap-ledger.js';
|
|
@@ -148,7 +148,7 @@ changeUndeclared) {
|
|
|
148
148
|
// first clause, and one this implementation could not run until the page started declaring.
|
|
149
149
|
// Both halves are conditional on purpose: an SDK too old to declare sends nothing, and
|
|
150
150
|
// treating that silence as an empty set would refuse every claim from every older page.
|
|
151
|
-
channelsRead:
|
|
151
|
+
channelsRead: channelsReadBy(predicate),
|
|
152
152
|
...(session.channels === undefined ? {} : { channelsObservable: session.channels }),
|
|
153
153
|
// Threaded rather than looked up: decideVerified is pure and has no session.
|
|
154
154
|
...sessionVerdictFacts(session),
|