@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
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
/** The short form left in the per-turn schema. Must still make sense with no instructions at all. */
|
|
23
23
|
export const SHARED_PARAM_SHORT = {
|
|
24
24
|
sessionId: 'Target a specific tab. Omit unless you mean one — see instructions.',
|
|
25
|
-
since: 'Cursor from a prior
|
|
25
|
+
since: 'Cursor from a prior act/observe, or a git ref.',
|
|
26
26
|
limit: 'Cap descriptors to the first N.',
|
|
27
27
|
};
|
|
28
28
|
/** The full text, sent once at initialize. Everything trimmed above is restated here. */
|
|
29
29
|
export const SHARED_PARAM_GUIDANCE = 'Arguments that mean the same thing on every tool: `sessionId` — omit unless you mean a specific ' +
|
|
30
30
|
'tab; Reticle scopes to your project, prefers the active one, and refuses rather than guesses when ' +
|
|
31
31
|
'ambiguous. `since` — a cursor from a prior act/observe, bounding an observation to what came ' +
|
|
32
|
-
'after it. `limit` — caps returned descriptors, cutting tokens on broad queries.';
|
|
32
|
+
'after it, or a git ref when verifying a change. `limit` — caps returned descriptors, cutting tokens on broad queries.';
|
|
33
33
|
/** Is this a parameter whose guidance now lives in the instructions block? */
|
|
34
34
|
export function isSharedParam(name) {
|
|
35
35
|
return Object.hasOwn(SHARED_PARAM_SHORT, name);
|
|
@@ -59,6 +59,14 @@ export interface ArrivalScope {
|
|
|
59
59
|
* "the app redirected the newly arrived document somewhere else."
|
|
60
60
|
*/
|
|
61
61
|
navigatedFrom: string;
|
|
62
|
+
/**
|
|
63
|
+
* The DOCUMENT the session was on when the navigation was dispatched.
|
|
64
|
+
*
|
|
65
|
+
* A navigation to the page already shown replaces the document without changing the URL, so the
|
|
66
|
+
* old document is still the connected one until the replacement reports (#1320). Only a different
|
|
67
|
+
* document identity proves the replacement; `undefined` on both sides proves nothing and waits.
|
|
68
|
+
*/
|
|
69
|
+
navigatedFromDocumentId?: string;
|
|
62
70
|
/** Sessions already on the target before dispatch cannot prove this navigation arrived. */
|
|
63
71
|
priorIds: ReadonlySet<string>;
|
|
64
72
|
}
|
|
@@ -42,6 +42,21 @@ function samePage(a, b) {
|
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Same origin and path, different fragment: the browser changes the URL WITHOUT replacing the
|
|
47
|
+
* document. There is no replacement to wait for on this path, so it must not be treated as a
|
|
48
|
+
* same-page navigation (#1320).
|
|
49
|
+
*/
|
|
50
|
+
function sameDocumentFragmentChange(a, b) {
|
|
51
|
+
if (!samePage(a, b))
|
|
52
|
+
return false;
|
|
53
|
+
try {
|
|
54
|
+
return new URL(a).hash !== new URL(b).hash;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
45
60
|
/**
|
|
46
61
|
* The navigation target as an absolute URL, resolved against the tab being navigated — the same
|
|
47
62
|
* resolution the browser applies. An agent passes a path (`/orders?id=4`) as often as a full URL,
|
|
@@ -70,13 +85,39 @@ export function idsAtTarget(sessions, target) {
|
|
|
70
85
|
}
|
|
71
86
|
return ids;
|
|
72
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether `current` is a different document from the one the navigation was dispatched on.
|
|
90
|
+
*
|
|
91
|
+
* A new Session object under the same id is not enough: the SDK also reopens its socket in place,
|
|
92
|
+
* and that replaces the Session while the page stays exactly where it was. What a real unload
|
|
93
|
+
* changes is the document id — the one sampled before dispatch (`navigatedFromDocumentId`) against
|
|
94
|
+
* the one the live session is reporting (`currentDocumentId`). Either side not having reported one
|
|
95
|
+
* yet is "cannot tell", which is never reported as a new document.
|
|
96
|
+
*/
|
|
97
|
+
function documentUnloaded(scope, current) {
|
|
98
|
+
const before = scope.navigatedFromDocumentId;
|
|
99
|
+
const after = current.currentDocumentId;
|
|
100
|
+
return before !== undefined && after !== undefined && before !== after;
|
|
101
|
+
}
|
|
73
102
|
function findArrival(sessions, target, scope, currentSession) {
|
|
74
103
|
let arrived = null;
|
|
75
|
-
for
|
|
104
|
+
// One snapshot for the whole look, so "is the driven session still connected?" and "has a
|
|
105
|
+
// replacement reported?" are answered from the same moment.
|
|
106
|
+
const snapshot = sessions.all();
|
|
107
|
+
const drivenPresent = snapshot.some((s) => s.id === currentSession.id);
|
|
108
|
+
// Navigating to the page already shown replaces its document without moving the URL, so the
|
|
109
|
+
// OLD document is still the connected one at the first looks; its presence there is not
|
|
110
|
+
// arrival until the replacement reports a different document identity (#1320). A fragment-only
|
|
111
|
+
// change keeps the document, so it is an arrival as soon as the URL matches.
|
|
112
|
+
const replacesDocument = samePage(scope.navigatedFrom, target) &&
|
|
113
|
+
!sameDocumentFragmentChange(scope.navigatedFrom, target);
|
|
114
|
+
for (const s of snapshot) {
|
|
76
115
|
// The currently tracked document gets priority because it is the strongest
|
|
77
116
|
// evidence of where this navigation went.
|
|
78
117
|
if (s.id === currentSession.id) {
|
|
79
118
|
if (samePage(s.url, target)) {
|
|
119
|
+
if (replacesDocument && s.currentDocumentId === scope.navigatedFromDocumentId)
|
|
120
|
+
continue;
|
|
80
121
|
return { sessionId: s.id };
|
|
81
122
|
}
|
|
82
123
|
// Intermediate routes are not necessarily redirects. Keep polling so
|
|
@@ -87,6 +128,12 @@ function findArrival(sessions, target, scope, currentSession) {
|
|
|
87
128
|
// navigation arrived.
|
|
88
129
|
if (scope.priorIds.has(s.id))
|
|
89
130
|
continue;
|
|
131
|
+
// While the replaced document is still connected, a DIFFERENT session at the target is an
|
|
132
|
+
// unrelated arrival (a second tab, a neighbour app): the replacement this navigation waits
|
|
133
|
+
// for has not reported yet, and letting the newcomer stand in would confirm too early and
|
|
134
|
+
// name the wrong session (#1320).
|
|
135
|
+
if (replacesDocument && drivenPresent)
|
|
136
|
+
continue;
|
|
90
137
|
if (samePage(s.url, target)) {
|
|
91
138
|
if (null === arrived)
|
|
92
139
|
arrived = { sessionId: s.id };
|
|
@@ -122,7 +169,11 @@ export async function awaitArrival(sessions, target, scope, timeoutMs = ARRIVAL_
|
|
|
122
169
|
// landed when the target was never observed.
|
|
123
170
|
const current = sessions.get(scope.navigatedSession.id);
|
|
124
171
|
if (current !== undefined) {
|
|
125
|
-
|
|
172
|
+
// Sitting on `navigatedFrom` means two different things: a tab that has not moved, or a page
|
|
173
|
+
// that unloaded and came back here (an auth guard bouncing `/checkout` to `/login` while the
|
|
174
|
+
// tab started on `/login`). Only a document that is provably new tells them apart.
|
|
175
|
+
if (!samePage(current.url, target) &&
|
|
176
|
+
(!samePage(current.url, scope.navigatedFrom) || documentUnloaded(scope, current))) {
|
|
126
177
|
return {
|
|
127
178
|
sessionId: current.id,
|
|
128
179
|
landedOn: current.url,
|
|
@@ -131,6 +182,9 @@ export async function awaitArrival(sessions, target, scope, timeoutMs = ARRIVAL_
|
|
|
131
182
|
return null;
|
|
132
183
|
}
|
|
133
184
|
const successor = await awaitDocumentSuccessor(sessions, scope.navigatedSession, 0, clock);
|
|
185
|
+
// A successor under another id is only "unique at the origin", which a tab opened by someone else
|
|
186
|
+
// after the driven one went away satisfies equally. On the starting page that guess cannot be told
|
|
187
|
+
// from a redirect, so it is not reported as one.
|
|
134
188
|
if (successor !== null &&
|
|
135
189
|
!samePage(successor.url, target) &&
|
|
136
190
|
!samePage(successor.url, scope.navigatedFrom)) {
|
|
@@ -27,7 +27,26 @@
|
|
|
27
27
|
* - still in flight when the budget genuinely runs out → `unsettled`, as before, but now an honest
|
|
28
28
|
* limit instead of an early exit.
|
|
29
29
|
*/
|
|
30
|
-
import { EventType, NetInitiator, isDevToolingUrl, isForeignTraffic } from '@reticlehq/core';
|
|
30
|
+
import { EventType, NetInitiator, isDevToolingUrl, isForeignTraffic, urlForMatch, } from '@reticlehq/core';
|
|
31
|
+
/**
|
|
32
|
+
* What a URL is matched against, as opposed to what is shown.
|
|
33
|
+
*
|
|
34
|
+
* Redaction runs at emit time and rewrites a sensitive path segment in place: `/verify/refresh-token`
|
|
35
|
+
* becomes `/verify/[REDACTED]`, and the original survives only in `urlRaw`. A project that DECLARED
|
|
36
|
+
* `/verify/refresh-token` in `.reticle.json` `background` is matched with `String.includes`, so
|
|
37
|
+
* classifying on the displayed URL silently put the endpoint back into the in-flight count and held
|
|
38
|
+
* the settle window open — the exact defect this file exists to prevent, reached through the one
|
|
39
|
+
* field the classifier did not consult.
|
|
40
|
+
*
|
|
41
|
+
* The same division every other URL comparison in the product already uses (`urlContains`, `netCall`):
|
|
42
|
+
* match against `urlForMatch`, DISCLOSE `url`. The transcript keeps the redacted spelling.
|
|
43
|
+
*/
|
|
44
|
+
const matchUrlOf = (data) => {
|
|
45
|
+
// `urlForMatch` answers with the empty string when the event carries no URL at all, and every
|
|
46
|
+
// classifier below takes `undefined` to mean "nothing to judge". Fails OPEN either way.
|
|
47
|
+
const url = urlForMatch(data);
|
|
48
|
+
return 0 === url.length ? undefined : url;
|
|
49
|
+
};
|
|
31
50
|
/**
|
|
32
51
|
* The dev toolchain talking about ITSELF is not the app finishing its work.
|
|
33
52
|
*
|
|
@@ -36,7 +55,7 @@ import { EventType, NetInitiator, isDevToolingUrl, isForeignTraffic } from '@ret
|
|
|
36
55
|
* deliberately disabled control — no state change, no storage change, zero application requests —
|
|
37
56
|
* was graded on the strength of a Next webpack hot-update the same payload printed as ignored.
|
|
38
57
|
*/
|
|
39
|
-
const isDevTooling = (data) => isDevToolingUrl(
|
|
58
|
+
const isDevTooling = (data) => isDevToolingUrl(matchUrlOf(data));
|
|
40
59
|
/**
|
|
41
60
|
* Somebody else's host is not the app finishing its work.
|
|
42
61
|
*
|
|
@@ -55,12 +74,15 @@ const isDevTooling = (data) => isDevToolingUrl('string' === typeof data['url'] ?
|
|
|
55
74
|
* SAME-ORIGIN background traffic is never guessed: nothing in a URL separates the app's telemetry
|
|
56
75
|
* from the app's work. It is excluded only when the project DECLARED it (`background`).
|
|
57
76
|
*/
|
|
58
|
-
const isForeign = (data, appOrigin, background) => isForeignTraffic(
|
|
77
|
+
const isForeign = (data, appOrigin, background) => isForeignTraffic(matchUrlOf(data), appOrigin, background);
|
|
59
78
|
/**
|
|
60
79
|
* A departure records where the browser was SENT, not a request whose result we will see — the
|
|
61
80
|
* response is delivered to a document this session does not live into. It can never be matched by a
|
|
62
81
|
* NET_REQUEST, so counting it as outstanding would mean no page containing an outbound link or a
|
|
63
82
|
* download button ever settles again. See `NetInitiator.NAVIGATION`.
|
|
83
|
+
*
|
|
84
|
+
* The `settled` predicate asks the same question in `evalSettled`; it spells it against this same
|
|
85
|
+
* constant rather than against a string, which is what keeps the two answers one vocabulary.
|
|
64
86
|
*/
|
|
65
87
|
const isDeparture = (data) => data['initiator'] === NetInitiator.NAVIGATION;
|
|
66
88
|
/** How often to re-read the buffer. Short enough to return promptly, long enough not to spin. */
|
|
@@ -303,8 +303,8 @@ export const ACT_SEQUENCE_TOOL = {
|
|
|
303
303
|
count: inputSteps.length,
|
|
304
304
|
steps: stepResults,
|
|
305
305
|
});
|
|
306
|
-
deps.recordings.capture(pageBefore === undefined ? recorded : { ...recorded, page: pageBefore });
|
|
307
|
-
deps.recordings.markEnded(pathOf(session.url));
|
|
306
|
+
deps.recordings.capture(pageBefore === undefined ? recorded : { ...recorded, page: pageBefore }, undefined, session.id);
|
|
307
|
+
deps.recordings.markEnded(pathOf(session.url), session.id);
|
|
308
308
|
}
|
|
309
309
|
/*
|
|
310
310
|
* The grade, and the coverage it is never allowed to hide.
|
|
@@ -22,6 +22,7 @@ import { intentDebt, openSessionIntents } from '../../memory/intent/open-intents
|
|
|
22
22
|
import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../../memory/intent/inline-intent.js';
|
|
23
23
|
import { declaresState } from '@reticlehq/engine/question/predicate/predicate-asks.js';
|
|
24
24
|
import { isStateUnwatched } from '@reticlehq/engine/evidence/blind-spots.js';
|
|
25
|
+
import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
|
|
25
26
|
import { inFlightRequestLabels, repeatedRequestLabels } from './act/settle-in-flight.js';
|
|
26
27
|
import { finishAfterMatch } from './act/after-match.js';
|
|
27
28
|
import { noteProved } from './act/proved-controls.js';
|
|
@@ -62,8 +63,8 @@ import { readAlreadyTrue } from './act/already-true.js';
|
|
|
62
63
|
/**
|
|
63
64
|
* The action vocabulary, derived from ActionType — never retyped.
|
|
64
65
|
*
|
|
65
|
-
* The description used to
|
|
66
|
-
*
|
|
66
|
+
* The description used to omit callable actions such as blur, upload and drag,
|
|
67
|
+
* because a hand-copied list drifts the moment
|
|
67
68
|
* someone adds an arm. Deriving both the schema and the prose from the enum makes that impossible.
|
|
68
69
|
*
|
|
69
70
|
* The handler already refused an unknown action with a good message; the schema now refuses it
|
|
@@ -124,7 +125,7 @@ export const ACT_TOOLS = [
|
|
|
124
125
|
args: z
|
|
125
126
|
.record(z.unknown())
|
|
126
127
|
.optional()
|
|
127
|
-
.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 — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
|
|
128
|
+
.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 — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer or the key DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For press: { keys: ["Control","k"] } holds several keys down TOGETHER (pressed in order, released in reverse) and { code } names the physical key — both stay on the in-page dispatcher, the only path that can express them. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
|
|
128
129
|
refuseWhenThrottled: z
|
|
129
130
|
.boolean()
|
|
130
131
|
.optional()
|
|
@@ -193,7 +194,7 @@ export const ACT_TOOLS = [
|
|
|
193
194
|
// drive native pointer input when a provider is available; otherwise fall back.
|
|
194
195
|
const real = await tryRealInput(deps, session, ref, action, args);
|
|
195
196
|
if (real.result !== undefined) {
|
|
196
|
-
captureAct(deps.recordings, args, real.result, routeBefore);
|
|
197
|
+
captureAct(deps.recordings, args, real.result, routeBefore, session.id);
|
|
197
198
|
settledOutcome = real.settled ?? undefined;
|
|
198
199
|
// Native input reports no synthetic effect block, so nothing measured in-target: undefined
|
|
199
200
|
// (the weaker empty-window test), never a fabricated zero.
|
|
@@ -217,7 +218,7 @@ export const ACT_TOOLS = [
|
|
|
217
218
|
});
|
|
218
219
|
if (!result.ok)
|
|
219
220
|
throw new Error(result.error ?? 'act failed');
|
|
220
|
-
captureAct(deps.recordings, args, result.result, routeBefore);
|
|
221
|
+
captureAct(deps.recordings, args, result.result, routeBefore, session.id);
|
|
221
222
|
// lift dispatch/settle status to the envelope (a settle timeout is NOT a failure).
|
|
222
223
|
const r = asRecord(result.result);
|
|
223
224
|
if ('boolean' === typeof r['settled'])
|
|
@@ -240,7 +241,7 @@ export const ACT_TOOLS = [
|
|
|
240
241
|
});
|
|
241
242
|
}
|
|
242
243
|
finally {
|
|
243
|
-
deps.recordings.markEnded(pathOf(session.url));
|
|
244
|
+
deps.recordings.markEnded(pathOf(session.url), session.id);
|
|
244
245
|
// Close the window on every exit (settle or throw), recording the action + settle outcome.
|
|
245
246
|
session.finishAction(undefined, settledOutcome, true === settledOutcome ? session.elapsed() - since : undefined);
|
|
246
247
|
}
|
|
@@ -524,6 +525,8 @@ export const ACT_TOOLS = [
|
|
|
524
525
|
const spots = blindSpotsFromState(session.blindSpots(), session.runtime);
|
|
525
526
|
const coverage = buildCoverageStatement(spots);
|
|
526
527
|
const absenceBlindSpot = absenceBlindSpotNote(until, spots);
|
|
528
|
+
// A green resting on hidden matches says the node exists, not that it shows (#1408).
|
|
529
|
+
const hiddenMatch = hiddenMatchNote(until, verdict.evidence);
|
|
527
530
|
// Nothing subscribed ⇒ the state channel is dark, and the summary must say so rather than
|
|
528
531
|
// report an empty diff list that reads like a fact about the app. See CausalSummary.
|
|
529
532
|
const stateUnwatched = isStateUnwatched(spots);
|
|
@@ -569,6 +572,10 @@ export const ACT_TOOLS = [
|
|
|
569
572
|
...(0 === impeachingNotes.length ? {} : { blindSpots: impeachingNotes }),
|
|
570
573
|
losses,
|
|
571
574
|
});
|
|
575
|
+
// A refuted expectation must not ride into a regression flow cut from the ambient tape; it is
|
|
576
|
+
// kept as the capsule below instead. See RecordingStore.unassertLast.
|
|
577
|
+
if (!verdict.pass && actResult !== null)
|
|
578
|
+
deps.recordings.unassertLast();
|
|
572
579
|
const capsuleSaved = await saveFailedAssertCapsule({
|
|
573
580
|
deps,
|
|
574
581
|
verdict,
|
|
@@ -606,6 +613,7 @@ export const ACT_TOOLS = [
|
|
|
606
613
|
prior,
|
|
607
614
|
actionSince: since,
|
|
608
615
|
expectedFailures: declared.netFailures,
|
|
616
|
+
namedNetUrls: declared.netUrls,
|
|
609
617
|
// A consequence that was already true before the action proves nothing about it, so it is
|
|
610
618
|
// not evidence the destination rendered either — `alreadyTrue` decides that, once.
|
|
611
619
|
renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
|
|
@@ -617,7 +625,14 @@ export const ACT_TOOLS = [
|
|
|
617
625
|
});
|
|
618
626
|
// The single field an agent reads. Everything below it is the evidence it was derived from;
|
|
619
627
|
// this is the only one that has to be interpreted, and now it interprets itself.
|
|
620
|
-
|
|
628
|
+
// Same routing as the assert path (#1120), fed by the links this verdict PROVED. On red,
|
|
629
|
+
// `gradedLinks` is the declared surface (every anyOf branch), which must not exempt a 202
|
|
630
|
+
// the verdict never rested on — so only a green passes it through.
|
|
631
|
+
const outcomePending = acceptedWriteLabels(windowEvents, {
|
|
632
|
+
appUrl: session.url,
|
|
633
|
+
background: session.background,
|
|
634
|
+
asserted: verdict.pass ? gradedLinks : [],
|
|
635
|
+
});
|
|
621
636
|
const outcomeUnread = unreadWriteLabels(windowEvents);
|
|
622
637
|
const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
|
|
623
638
|
const { decision, durable } = await withDurability(args['durable'], until, decideVerified({
|
|
@@ -643,6 +658,7 @@ export const ACT_TOOLS = [
|
|
|
643
658
|
? { observationLost: true, lastUrl: session.url }
|
|
644
659
|
: {}),
|
|
645
660
|
...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
|
|
661
|
+
...(hiddenMatch === undefined ? {} : { hiddenMatch }),
|
|
646
662
|
honesty,
|
|
647
663
|
contradictions,
|
|
648
664
|
...(0 === outcomePending.length ? {} : { outcomePending }),
|
|
@@ -802,8 +818,8 @@ export const ACT_TOOLS = [
|
|
|
802
818
|
}
|
|
803
819
|
finally {
|
|
804
820
|
if (dispatched !== undefined)
|
|
805
|
-
captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, recordedVerdict);
|
|
806
|
-
deps.recordings.markEnded(pathOf(currentOf(deps.sessions, session).url));
|
|
821
|
+
captureVerdictedAct(deps.recordings, args, dispatched.result, routeBeforeWait, recordedVerdict, session.id);
|
|
822
|
+
deps.recordings.markEnded(pathOf(currentOf(deps.sessions, session).url), session.id);
|
|
807
823
|
acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
|
|
808
824
|
}
|
|
809
825
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InstrumentationGap, JournalVerdictEffect } from '@reticlehq/core/artifacts';
|
|
2
|
-
import type
|
|
2
|
+
import { type Predicate } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
3
3
|
import type { Session } from '../../../portal/session/session.js';
|
|
4
4
|
import { type Contradiction } from '@reticlehq/engine/disagreement/contradictions.js';
|
|
5
5
|
/**
|
|
@@ -4,12 +4,14 @@ import { gapsForAction } from '@reticlehq/engine/evidence/instrumentation-gaps.j
|
|
|
4
4
|
import { noteSessionGaps } from '@reticlehq/engine/evidence/gap-ledger.js';
|
|
5
5
|
import { declaresState } from '@reticlehq/engine/question/predicate/predicate-asks.js';
|
|
6
6
|
import { isStateUnwatched } from '@reticlehq/engine/evidence/blind-spots.js';
|
|
7
|
+
import { hiddenMatchNote } from '@reticlehq/engine/evidence/already-true.js';
|
|
8
|
+
import { provenExpectedLinks, } from '@reticlehq/engine/question/predicate/predicate.js';
|
|
7
9
|
import { findContradictions, } from '@reticlehq/engine/disagreement/contradictions.js';
|
|
8
10
|
import { crashedRuleNotes } from '@reticlehq/engine/disagreement/contradiction-folds.js';
|
|
9
11
|
import { declaredExpectations, declaresBodyIndependentChannel, } from '@reticlehq/engine/question/declared.js';
|
|
10
12
|
import { absenceBlindSpotNote, blindSpotsFromState, buildCoverageStatement, restsOnCompleteWindow, Coverage, impeachesCapture, transportGapNote, } from '@reticlehq/engine/evidence/blind-spots.js';
|
|
11
13
|
import { buildHonestyBlock } from '@reticlehq/engine/evidence/honesty.js';
|
|
12
|
-
import { acceptedWriteLabels } from '@reticlehq/engine/evidence/accepted-write.js';
|
|
14
|
+
import { acceptedWriteLabels, hasAcceptedWrite, } from '@reticlehq/engine/evidence/accepted-write.js';
|
|
13
15
|
import { unreadWriteLabels } from '@reticlehq/engine/evidence/unread-outcome.js';
|
|
14
16
|
import { decideVerified } from '@reticlehq/engine/evidence/verified.js';
|
|
15
17
|
import { describeWaitTarget, namedNetIsInFlight } from '@reticlehq/engine/evidence/unsettled.js';
|
|
@@ -58,6 +60,8 @@ changeUndeclared) {
|
|
|
58
60
|
const spots = blindSpotsFromState(session.blindSpots(), session.runtime);
|
|
59
61
|
const statement = buildCoverageStatement(spots);
|
|
60
62
|
const absenceBlindSpot = absenceBlindSpotNote(predicate, spots);
|
|
63
|
+
// A green resting on hidden matches says the node exists, not that it shows (#1408).
|
|
64
|
+
const hiddenMatch = hiddenMatchNote(predicate, evidence);
|
|
61
65
|
// Omitted entirely when coverage is full, so an intact page pays nothing and the field's PRESENCE
|
|
62
66
|
// is the warning.
|
|
63
67
|
const coverage = Coverage.PARTIAL === statement.coverage
|
|
@@ -115,7 +119,17 @@ changeUndeclared) {
|
|
|
115
119
|
// A consumer rule that crashed belongs here too: the engine ran with fewer rules than it claims,
|
|
116
120
|
// so part of what happened may simply not have been looked for.
|
|
117
121
|
const impeachingNotes = [impeaching.note, gap, ...crashedRuleNotes()].filter((n) => n !== undefined);
|
|
118
|
-
|
|
122
|
+
// Which 202s are still the claim's outcome to wait for (#1120). The proven links decide the
|
|
123
|
+
// exemption, so a 200 branch of an `anyOf` cannot exempt the 202 that also fired; and the walk
|
|
124
|
+
// only runs when a 202 is actually waiting, because that is the only case it can change.
|
|
125
|
+
const assertedAccepts = pass && hasAcceptedWrite(windowEvents)
|
|
126
|
+
? await provenExpectedLinks(session, predicate, since)
|
|
127
|
+
: [];
|
|
128
|
+
const outcomePending = acceptedWriteLabels(windowEvents, {
|
|
129
|
+
appUrl: session.url,
|
|
130
|
+
background: session.background,
|
|
131
|
+
asserted: assertedAccepts,
|
|
132
|
+
});
|
|
119
133
|
const outcomeUnread = unreadWriteLabels(windowEvents);
|
|
120
134
|
const stillInFlight = inFlightRequestLabels(windowEvents, session.url, session.background);
|
|
121
135
|
const effectiveInconclusive = inconclusive ?? (!pass ? session.preconditionFailure?.() : undefined);
|
|
@@ -160,6 +174,7 @@ changeUndeclared) {
|
|
|
160
174
|
...(effectiveInconclusive === undefined ? {} : { inconclusive: effectiveInconclusive }),
|
|
161
175
|
...(true === observationLost ? { observationLost: true, lastUrl: session.url } : {}),
|
|
162
176
|
...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
|
|
177
|
+
...(hiddenMatch === undefined ? {} : { hiddenMatch }),
|
|
163
178
|
...(namedNetIsInFlight(predicate, stillInFlight) ? { namedRequestInFlight: true } : {}),
|
|
164
179
|
honesty: buildHonestyBlock({
|
|
165
180
|
grade: gradeOfPredicate(predicate),
|
|
@@ -96,6 +96,10 @@ export const BROWSER_TOOLS = [
|
|
|
96
96
|
// which reads whatever is at the target afterwards, cannot tell the two apart on its own.
|
|
97
97
|
const target = absoluteTarget(url, session.url);
|
|
98
98
|
const priorIds = idsAtTarget(deps.sessions, target);
|
|
99
|
+
// The document the tab is on NOW. Sampled before dispatch because the replacement can
|
|
100
|
+
// connect while the NAVIGATE reply is still pending, and reading it after the await would
|
|
101
|
+
// record the replacement as the starting document (#1320).
|
|
102
|
+
const navigatedFromDocumentId = session.currentDocumentId;
|
|
99
103
|
session.beginAction(ReticleTool.NAVIGATE, { url });
|
|
100
104
|
// Same floor as the reload path above, for the same reason: going to a new URL replaces the
|
|
101
105
|
// document just as thoroughly. `beginAction` attributes events to this action; it does not move
|
|
@@ -112,9 +116,12 @@ export const BROWSER_TOOLS = [
|
|
|
112
116
|
const arrival = true === result.ok
|
|
113
117
|
? await awaitArrival(deps.sessions, target, {
|
|
114
118
|
// Keep the original Session object and URL so arrival detection can distinguish
|
|
115
|
-
// an in-progress navigation from a document that arrived somewhere else.
|
|
119
|
+
// an in-progress navigation from a document that arrived somewhere else. The
|
|
120
|
+
// document identity lets a same-URL navigation wait for its replacement instead
|
|
121
|
+
// of confirming the document it is about to destroy (#1320).
|
|
116
122
|
navigatedSession: session,
|
|
117
123
|
navigatedFrom: session.url,
|
|
124
|
+
...(navigatedFromDocumentId === undefined ? {} : { navigatedFromDocumentId }),
|
|
118
125
|
priorIds,
|
|
119
126
|
}, timeoutMs)
|
|
120
127
|
: null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SessionInfo } from '../../portal/session/session-info.js';
|
|
2
|
+
import { LinkCapability, type PresenterTone } from '@reticlehq/core';
|
|
3
|
+
import type { ToolDeps } from './tool-kit.js';
|
|
4
|
+
import type { FileSystemPort } from '../../memory/project/fs/fs-port.js';
|
|
5
|
+
/** What this daemon does for the platform's chat. Each one is listed only once it is built. */
|
|
6
|
+
export declare const CHAT_CAPABILITIES: readonly LinkCapability[];
|
|
7
|
+
/** The slice of the session manager this needs. */
|
|
8
|
+
export interface ChatDriveSessions {
|
|
9
|
+
list(): SessionInfo[];
|
|
10
|
+
count(): number;
|
|
11
|
+
get(id: string): {
|
|
12
|
+
autoEnd(text: string, tone: PresenterTone): void;
|
|
13
|
+
pushNarration(text: string): void;
|
|
14
|
+
} | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface ChatDriveCloud {
|
|
17
|
+
/** The platform and key a tab's own project is linked with, or undefined when it is not. */
|
|
18
|
+
sessionCloud: (sessionId: string) => Promise<{
|
|
19
|
+
url: string;
|
|
20
|
+
apiKey: string;
|
|
21
|
+
} | undefined>;
|
|
22
|
+
version: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function startChatDrives(deps: ToolDeps, sessions: ChatDriveSessions, fs: FileSystemPort, cloud: ChatDriveCloud, log: (line: string) => void): {
|
|
25
|
+
stop: () => void;
|
|
26
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon's side of the platform chat: the apps it reports, and the drives it takes.
|
|
3
|
+
*
|
|
4
|
+
* Out of `index.ts`, which only calls this and hands it the cloud credentials (the tool surface does
|
|
5
|
+
* not reach `memory/cloud`; see `withLinkedCredential`): both halves share the same view of which tab is which
|
|
6
|
+
* app, and keeping them together is what stops a drive attached to one app landing in another that
|
|
7
|
+
* happens to be on the same port. See `local-apps.ts` and `remote-drive.ts` for each half.
|
|
8
|
+
*/
|
|
9
|
+
import { basename, join } from 'node:path';
|
|
10
|
+
import { homedir, hostname } from 'node:os';
|
|
11
|
+
import { LinkCapability, ReticleTool, asRecord } from '@reticlehq/core';
|
|
12
|
+
import { prepareDrive } from '../../features/harness/platform/drive-target.js';
|
|
13
|
+
import { reticleToolset } from './harness-toolset.js';
|
|
14
|
+
import { projectDirFor } from '../../memory/project/session-root.js';
|
|
15
|
+
import { driveFrame } from '../../features/visual/visual-tools.js';
|
|
16
|
+
import { LEASE_ACQUIRE_TOOL } from './lease-tools.js';
|
|
17
|
+
import { REMOTE_DRIVE_JPEG_QUALITY, endDrivenTab, pickOwnDriveSession, startRemoteDrives, } from '../../features/harness/platform/remote-drive.js';
|
|
18
|
+
import { appKeyOf, appsByPlatform, machineOf, mayOpen, NOT_AN_APP_ADDRESS, nameFromProjectId, pickAppTab, startAppReports, } from '../../features/harness/platform/local-apps.js';
|
|
19
|
+
import { serverOptionsFromEnv } from '../../features/harness/platform/server-driver.js';
|
|
20
|
+
import { driveForChat } from './explore-tools.js';
|
|
21
|
+
import { withLinkedCredential } from './harness-explore.js';
|
|
22
|
+
import { runTool } from './invoke-tool.js';
|
|
23
|
+
/** Why a platform drive with no tab of its own runs no tool. */
|
|
24
|
+
const NO_DRIVEN_TAB = 'No tab was picked for this drive, so no tool runs.';
|
|
25
|
+
/** What this daemon does for the platform's chat. Each one is listed only once it is built. */
|
|
26
|
+
export const CHAT_CAPABILITIES = [
|
|
27
|
+
LinkCapability.DRIVE_SPEC,
|
|
28
|
+
LinkCapability.TARGET_TAB,
|
|
29
|
+
LinkCapability.TARGET_HEADLESS,
|
|
30
|
+
LinkCapability.TARGET_HEADED,
|
|
31
|
+
LinkCapability.HUD,
|
|
32
|
+
LinkCapability.TOOL_SESSION,
|
|
33
|
+
LinkCapability.FRAMES,
|
|
34
|
+
];
|
|
35
|
+
/** A project's own name: its package's, else its id's, else its folder's. */
|
|
36
|
+
const nameOf = async (fs, dir, projectId) => {
|
|
37
|
+
try {
|
|
38
|
+
const name = JSON.parse(await fs.readFile(join(dir, 'package.json')))
|
|
39
|
+
.name;
|
|
40
|
+
if ('string' === typeof name && 0 < name.length)
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// No package, or not JSON: the folder names it.
|
|
45
|
+
}
|
|
46
|
+
// Not mapped to a checkout (the daemon's shared `unmatched` directory): the id the SDK stamped.
|
|
47
|
+
if (undefined !== projectId)
|
|
48
|
+
return nameFromProjectId(projectId);
|
|
49
|
+
return basename(dir) || dir;
|
|
50
|
+
};
|
|
51
|
+
export function startChatDrives(deps, sessions, fs, cloud, log) {
|
|
52
|
+
const env = process.env;
|
|
53
|
+
const machine = machineOf(hostname(), homedir());
|
|
54
|
+
const sessionCloud = cloud.sessionCloud;
|
|
55
|
+
const sessionKey = async (id) => (await sessionCloud(id))?.apiKey;
|
|
56
|
+
// ponytail: a tab whose project resolves to nothing counts as the daemon's own project here, as it
|
|
57
|
+
// does for flows and runs; a per-tab "unresolved" app is the upgrade if that ever misleads.
|
|
58
|
+
const appOf = (id) => {
|
|
59
|
+
try {
|
|
60
|
+
const projectId = sessions.list().find((tab) => tab.sessionId === id)?.projectId;
|
|
61
|
+
return appKeyOf(machine.id, projectDirFor(deps, id), projectId);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// Opened headless, like `reticle drive <url>`, through runTool so it is counted like any call.
|
|
68
|
+
const openable = (url) => {
|
|
69
|
+
if (!mayOpen(url, sessions.list().map((tab) => tab.url)))
|
|
70
|
+
throw new Error(NOT_AN_APP_ADDRESS);
|
|
71
|
+
};
|
|
72
|
+
const open = async (url) => {
|
|
73
|
+
openable(url);
|
|
74
|
+
await runTool(LEASE_ACQUIRE_TOOL, deps, { url });
|
|
75
|
+
};
|
|
76
|
+
const pick = (goal, key, app) => undefined === app
|
|
77
|
+
? pickOwnDriveSession(sessions.list(), goal, key, sessionKey)
|
|
78
|
+
: pickAppTab(app, goal, () => sessions.list(), appOf, open).then(async (id) =>
|
|
79
|
+
// The app the platform named must be one this credential's project owns.
|
|
80
|
+
'string' === typeof id && (await sessionKey(id)) !== key ? null : id);
|
|
81
|
+
const drives = startRemoteDrives({
|
|
82
|
+
env: () => withLinkedCredential(deps, env),
|
|
83
|
+
connected: () => 0 < sessions.count(),
|
|
84
|
+
pick,
|
|
85
|
+
// The spec's target and HUD, applied through the same counted tools an agent would call.
|
|
86
|
+
prepare: ({ goal, apiKey, app, spec }) => prepareDrive(spec, app?.url ?? undefined, {
|
|
87
|
+
pickTab: () => pick(goal, apiKey, app),
|
|
88
|
+
open: async (url, headed, hud) => {
|
|
89
|
+
openable(url);
|
|
90
|
+
const lease = asRecord(await runTool(LEASE_ACQUIRE_TOOL, deps, {
|
|
91
|
+
url,
|
|
92
|
+
...(headed ? { headed } : {}),
|
|
93
|
+
...(hud === undefined ? {} : { hud }),
|
|
94
|
+
}));
|
|
95
|
+
const id = lease['sessionId'];
|
|
96
|
+
if ('string' !== typeof id)
|
|
97
|
+
throw new Error(`could not open ${url}`);
|
|
98
|
+
return id;
|
|
99
|
+
},
|
|
100
|
+
tuneHud: async (sessionId, hud) => {
|
|
101
|
+
await reticleToolset(deps, { sessionId }).invoke(ReticleTool.SESSION_TUNE, { hud });
|
|
102
|
+
},
|
|
103
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
104
|
+
}),
|
|
105
|
+
// A platform drive: Reticle's own tools on the driven tab, and nothing else. Locked, so a call
|
|
106
|
+
// naming another tab still lands on this one; with no tab picked, nothing runs at all.
|
|
107
|
+
session: (sessionId) => {
|
|
108
|
+
if (sessionId === undefined)
|
|
109
|
+
return { invoke: () => Promise.reject(new Error(NO_DRIVEN_TAB)) };
|
|
110
|
+
const tools = reticleToolset(deps, { sessionId, locked: true });
|
|
111
|
+
return { tools: tools.tools, invoke: (tool, args) => tools.invoke(tool, args) };
|
|
112
|
+
},
|
|
113
|
+
drive: async (goal, sessionId) => {
|
|
114
|
+
const url = sessions.list().find((tab) => tab.sessionId === sessionId)?.url;
|
|
115
|
+
const outcome = await driveForChat(deps, goal, sessionId);
|
|
116
|
+
return url === undefined ? outcome : { ...outcome, url };
|
|
117
|
+
},
|
|
118
|
+
// The leased tab, or a desktop window's own capture. A web tab only the SDK reaches has no camera.
|
|
119
|
+
frame: (sessionId) => sessionId === undefined
|
|
120
|
+
? Promise.resolve(undefined)
|
|
121
|
+
: driveFrame(deps, sessionId, REMOTE_DRIVE_JPEG_QUALITY),
|
|
122
|
+
settle: (id, outcome) => endDrivenTab(id === undefined ? undefined : sessions.get(id), outcome),
|
|
123
|
+
log,
|
|
124
|
+
});
|
|
125
|
+
const reports = startAppReports({
|
|
126
|
+
machine,
|
|
127
|
+
version: cloud.version,
|
|
128
|
+
capabilities: CHAT_CAPABILITIES,
|
|
129
|
+
apps: async () => appsByPlatform(sessions.list(), serverOptionsFromEnv(await withLinkedCredential(deps, env)), machine.id, async (tab) => {
|
|
130
|
+
const dir = projectDirFor(deps, tab.sessionId);
|
|
131
|
+
const platform = await sessionCloud(tab.sessionId);
|
|
132
|
+
return {
|
|
133
|
+
dir,
|
|
134
|
+
name: await nameOf(fs, dir, tab.projectId),
|
|
135
|
+
...(undefined === platform
|
|
136
|
+
? {}
|
|
137
|
+
: { platform: { url: platform.url.replace(/\/+$/, ''), apiKey: platform.apiKey } }),
|
|
138
|
+
};
|
|
139
|
+
}),
|
|
140
|
+
open,
|
|
141
|
+
drivePending: (platform) => void drives.tick(platform),
|
|
142
|
+
// What the platform wants the person told: in the daemon's log, and on the HUD of each open tab
|
|
143
|
+
// of that project, so somebody who never reads the log still sees "update Reticle".
|
|
144
|
+
notify: (notice, platform) => {
|
|
145
|
+
const line = `${notice.text}${notice.url === undefined ? '' : ` ${notice.url}`}`;
|
|
146
|
+
log(`reticle: from the platform: ${line}`);
|
|
147
|
+
void (async () => {
|
|
148
|
+
for (const tab of sessions.list()) {
|
|
149
|
+
const key = await sessionKey(tab.sessionId).catch(() => undefined);
|
|
150
|
+
if (key === platform.apiKey)
|
|
151
|
+
sessions.get(tab.sessionId)?.pushNarration(line);
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
},
|
|
155
|
+
log,
|
|
156
|
+
});
|
|
157
|
+
// Once now, so the chat sees this machine's apps as soon as the daemon is up.
|
|
158
|
+
void reports.tick();
|
|
159
|
+
return {
|
|
160
|
+
stop: () => {
|
|
161
|
+
drives.stop();
|
|
162
|
+
reports.stop();
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|