@reticlehq/server 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +13 -12
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -187
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +46 -41
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-runtime.js +6 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +44 -0
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +28 -9
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +1 -1
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +2 -2
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +13 -13
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as production from '../../portal/production/production-activation.js';
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
-
import { leaseNotConnectedHint, leaseReaderFailedHint, leaseStillLoadingHint, } from './lease-hint.js';
|
|
15
|
-
import { LEASE_READY_TIMEOUT_MS, awaitLeaseAnswering, connectOrInject, connectWaitUntil, moveReusedLease, notReadyReason, probeLeaseAlive, } from './lease-readiness.js';
|
|
14
|
+
import { keptLeaseAnswer, notLoadedCause, leaseNotConnectedHint, leaseReaderFailedHint, leaseStillLoadingHint, } from './lease-hint.js';
|
|
15
|
+
import { LEASE_READY_TIMEOUT_MS, awaitLeaseAnswering, connectOrInject, connectWaitUntil, moveReusedLease, notReadyReason, probeLeaseAlive, reconnectHeldLease, HeldLeaseReload, } from './lease-readiness.js';
|
|
16
16
|
import { probeSdkMarker } from './gaps/sdk-marker-probe.js';
|
|
17
17
|
import { observeWebDocument } from '../../portal/session/dev-server/served-document.js';
|
|
18
18
|
import { diagnoseObservedWebCsp } from '@reticlehq/init';
|
|
@@ -21,7 +21,7 @@ import { hasAnyAppConnectedBefore } from '../../memory/recall/prior/connection-m
|
|
|
21
21
|
import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../../portal/session/lease-visibility.js';
|
|
22
22
|
import { reticleStateHome } from '../../command/daemon/daemon.js';
|
|
23
23
|
import { claimArtifactRoot, dropArtifactRootClaim } from '../../memory/project/root-claims.js';
|
|
24
|
-
import { LeaseNotReadyReason,
|
|
24
|
+
import { LeaseNotReadyReason, RETICLE_DEFAULT_PORT, SeedStorageSchema, originOf, } from '@reticlehq/core';
|
|
25
25
|
import { HudVisibility, ReticleTool, SESSION_HEALTH } from '@reticlehq/core';
|
|
26
26
|
import { HOST_STUB_NOTE } from './tool-kit.js';
|
|
27
27
|
import { PoolAtCapacityError } from '../../portal/pool/browser-pool.js';
|
|
@@ -30,6 +30,8 @@ import { timeoutMsSchema } from './args/numeric-bounds.js';
|
|
|
30
30
|
import { chromiumPreflightRefusal } from '../../command/cli/doctor/browser/chromium-hint.js';
|
|
31
31
|
import { LEASE_PERMISSIONS_ARG, hintOf, notificationReadBack, parseLeasePermissions, permissionRefusal, refuseRegrant, } from './lease-permissions.js';
|
|
32
32
|
import { LEASE_EMULATE_ARG, leaseEmulationOf } from '../../portal/input/viewport-tools.js';
|
|
33
|
+
import { LEASE_DETERMINISTIC_ARG, leaseDeterminismOf } from '../../portal/pool/determinism.js';
|
|
34
|
+
import { LEASE_COVERAGE_TOOL, LEASE_JS_COVERAGE_ARG } from './lease-coverage-tools.js';
|
|
33
35
|
export { sessionMutationPort, sessionPerturbationPort, suiteFixturePort, suiteFixtureSeed, } from './session-ports.js';
|
|
34
36
|
/**
|
|
35
37
|
* Everything the daemon already knows about why a leased tab might not have dialled in.
|
|
@@ -87,38 +89,17 @@ async function leaseEvidence(deps, port, url) {
|
|
|
87
89
|
}
|
|
88
90
|
const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
|
|
89
91
|
const ALREADY_HELD_LEASE = 'you already hold a lease on this origin';
|
|
92
|
+
const DETERMINISTIC_NOTE = 'Time stands still on this tab. Step it with reticle_clock { stepFrames: N }, then read or capture.';
|
|
90
93
|
function alreadyHeldHint(sessionId, origin) {
|
|
91
94
|
return (`${ALREADY_HELD_LEASE} (${origin}): ${sessionId} — reused it rather than opening a second ` +
|
|
92
95
|
'tab, which would make every later call require sessionId. It is still signed in as whoever ' +
|
|
93
96
|
'used it last (cookies and storage come with it): release it first for a fresh context or ' +
|
|
94
97
|
'another identity, or pass fresh: true to open a separate tab and keep this one.');
|
|
95
98
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
99
|
-
*/
|
|
100
|
-
export function appendReticleParams(url, session, projectId, hud) {
|
|
101
|
-
try {
|
|
102
|
-
const u = new URL(url);
|
|
103
|
-
u.searchParams.set(RETICLE_URL_PARAM.SESSION, session);
|
|
104
|
-
if (projectId !== undefined && projectId.length > 0) {
|
|
105
|
-
u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
|
|
106
|
-
}
|
|
107
|
-
if (hud !== undefined) {
|
|
108
|
-
u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
|
|
109
|
-
}
|
|
110
|
-
return u.toString();
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
const sep = url.includes('?') ? '&' : '?';
|
|
114
|
-
const proj = projectId !== undefined && projectId.length > 0
|
|
115
|
-
? `&${RETICLE_URL_PARAM.PROJECT}=${encodeURIComponent(projectId)}`
|
|
116
|
-
: '';
|
|
117
|
-
return `${url}${sep}${RETICLE_URL_PARAM.SESSION}=${encodeURIComponent(session)}${proj}`;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
99
|
+
export { appendReticleParams, carryReticleIdentity, resolveLeasedSessionId, } from './lease-url-identity.js';
|
|
100
|
+
import { appendReticleParams, carryReticleIdentity, resolveLeasedSessionId, } from './lease-url-identity.js';
|
|
120
101
|
export { cleanNavError, evaluateSeedPrecondition, scrubSeedFromError } from './lease-seed.js';
|
|
121
|
-
import { evaluateSeedPrecondition, looksLikeStorageStateExport, navFailureMessage, scrubSeedFromError, } from './lease-seed.js';
|
|
102
|
+
import { evaluateSeedPrecondition, looksLikeStorageStateExport, navFailureMessage, scrubSeedFromError, serverAcceptedAfterTimeout, } from './lease-seed.js';
|
|
122
103
|
/** A fresh, collision-resistant lease id. Uses crypto at the I/O boundary (not pure logic). */
|
|
123
104
|
function newLeaseId() {
|
|
124
105
|
const uuid = 'function' === typeof globalThis.crypto?.randomUUID
|
|
@@ -126,84 +107,19 @@ function newLeaseId() {
|
|
|
126
107
|
: `${String(Date.now())}-${String(performance.now())}`;
|
|
127
108
|
return `lease-${uuid}`;
|
|
128
109
|
}
|
|
129
|
-
/**
|
|
130
|
-
* The session a lease's tab actually registered as, or undefined while nothing has.
|
|
131
|
-
*
|
|
132
|
-
* Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
|
|
133
|
-
* SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
|
|
134
|
-
* which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
|
|
135
|
-
* known id. Matching on the lease id alone could never see those tabs, so the lease reported
|
|
136
|
-
* `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
|
|
137
|
-
*
|
|
138
|
-
* The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
|
|
139
|
-
* carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
|
|
140
|
-
* so `lease-abc` cannot claim `lease-abcdef`.
|
|
141
|
-
*/
|
|
142
|
-
export function resolveLeasedSessionId(sessions, leaseId) {
|
|
143
|
-
if (sessions.get(leaseId) !== undefined) {
|
|
144
|
-
return leaseId;
|
|
145
|
-
}
|
|
146
|
-
return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Carry a claimed identity across a navigation.
|
|
150
|
-
*
|
|
151
|
-
* A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
|
|
152
|
-
* context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
|
|
153
|
-
* reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
|
|
154
|
-
* first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
|
|
155
|
-
* the lease was orphaned: "no sessions are connected at all", with a browser window still open on
|
|
156
|
-
* screen. A leased browser that cannot survive being navigated is not a usable one.
|
|
157
|
-
*
|
|
158
|
-
* A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
|
|
159
|
-
* a human's own URL.
|
|
160
|
-
*/
|
|
161
|
-
export function carryReticleIdentity(fromUrl, toUrl) {
|
|
162
|
-
const session = sessionParamOf(fromUrl);
|
|
163
|
-
if (session === undefined) {
|
|
164
|
-
return toUrl;
|
|
165
|
-
}
|
|
166
|
-
// Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
|
|
167
|
-
if (sessionParamOf(toUrl) !== undefined) {
|
|
168
|
-
return toUrl;
|
|
169
|
-
}
|
|
170
|
-
const projectId = projectParamOf(fromUrl);
|
|
171
|
-
return appendReticleParams(toUrl, session, projectId);
|
|
172
|
-
}
|
|
173
|
-
function projectParamOf(url) {
|
|
174
|
-
if (url === undefined) {
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return undefined;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
function sessionParamOf(url) {
|
|
185
|
-
if (url === undefined) {
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
return undefined;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
110
|
/**
|
|
196
111
|
* Acquire a leased, SDK-connected context for a caller that is not the lease TOOL (the parallel suite).
|
|
197
112
|
* Returns the session id plus its release; the caller must release in a finally so a crashed flow never
|
|
198
113
|
* leaks a pool slot and starves the rest of the suite.
|
|
199
114
|
*/
|
|
200
|
-
export async function acquireLeasedSession(pool, sessions, url, projectId, seedStorage) {
|
|
115
|
+
export async function acquireLeasedSession(pool, sessions, url, projectId, seedStorage, ignoreHTTPSErrors) {
|
|
201
116
|
const sessionId = newLeaseId();
|
|
202
117
|
let lease;
|
|
203
118
|
try {
|
|
204
119
|
lease = await pool.acquire(appendReticleParams(url, sessionId, projectId), {
|
|
205
120
|
sessionId,
|
|
206
121
|
...(seedStorage !== undefined ? { seedStorage } : {}),
|
|
122
|
+
...(true === ignoreHTTPSErrors ? { ignoreHTTPSErrors: true } : {}),
|
|
207
123
|
});
|
|
208
124
|
}
|
|
209
125
|
catch (err) {
|
|
@@ -249,7 +165,7 @@ export function hasOriginLock(origin) {
|
|
|
249
165
|
}
|
|
250
166
|
export const LEASE_ACQUIRE_TOOL = {
|
|
251
167
|
name: ReticleTool.LEASE_ACQUIRE,
|
|
252
|
-
description: 'Lease a fresh isolated browser context from the shared pool (shown or hidden as the daemon was started; `headed: true` forces a window) and navigate it to the app URL (the app must already be running; one with no Reticle SDK gets one supplied, reported as zeroInstall). If this origin is already leased
|
|
168
|
+
description: 'Lease a fresh isolated browser context from the shared pool (shown or hidden as the daemon was started; `headed: true` forces a window) and navigate it to the app URL (the app must already be running; one with no Reticle SDK gets one supplied, reported as zeroInstall). If this origin is already leased, this returns THAT session rather than minting a second tab (a tab that has not connected is loaded again in its own context, so its cookies and sign-in are kept) — a second acquire on the same origin poisons default session resolution. Returns the sessionId the leased tab registers — pass it to other tools. The pool keeps all leases in ONE browser and caps concurrency; if every slot is taken this is refused at once, naming the cap, rather than queued. Release with reticle_lease{action:"release"} when the flow is done. PREFER AN ALREADY-OPEN TAB: if reticle_sessions lists a non-leased session for this app, drive THAT instead — a lease is invisible to the person watching the app, whose HUD lives in their own tab, and a tab flagged hidden/throttled is often still driveable. Lease for isolation you actually need (a second identity, a clean context, parallel flows) or when driving the open tab has failed — this call answers with `preferExisting` when a live tab was available.',
|
|
253
169
|
inputSchema: {
|
|
254
170
|
url: z
|
|
255
171
|
.string()
|
|
@@ -282,6 +198,8 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
282
198
|
.describe('How the HUD starts on the page: shown (default), hidden, or removed.'),
|
|
283
199
|
permissions: LEASE_PERMISSIONS_ARG,
|
|
284
200
|
emulate: LEASE_EMULATE_ARG,
|
|
201
|
+
deterministic: LEASE_DETERMINISTIC_ARG,
|
|
202
|
+
jsCoverage: LEASE_JS_COVERAGE_ARG,
|
|
285
203
|
production: production.PRODUCTION_LEASE_ARG,
|
|
286
204
|
timeout_ms: timeoutMsSchema
|
|
287
205
|
.optional()
|
|
@@ -289,7 +207,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
289
207
|
root: z
|
|
290
208
|
.string()
|
|
291
209
|
.optional()
|
|
292
|
-
.describe("Absolute path of the .reticle folder this lease's runs belong in — the caller's project.
|
|
210
|
+
.describe("Absolute path of the .reticle folder this lease's runs belong in — the caller's project. An existing project folder is accepted even when it has no .reticle yet (the first run writes it). Omitted, the page's project id decides, and a page with none lands in ~/.reticle/unmatched, which nothing syncs."),
|
|
293
211
|
},
|
|
294
212
|
outputSchema: {
|
|
295
213
|
sessionId: z.string(),
|
|
@@ -301,6 +219,10 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
301
219
|
.boolean()
|
|
302
220
|
.optional()
|
|
303
221
|
.describe('Present when this app ships no Reticle SDK and the lease supplied one. Verdicts work on the DOM, network, console and routes; there is no framework adapter, so no component state and no source file:line. Install with `npx @reticlehq/server init` for those.'),
|
|
222
|
+
deterministic: z
|
|
223
|
+
.string()
|
|
224
|
+
.optional()
|
|
225
|
+
.describe('Present when this tab runs a seeded random source and a stepped clock. Its time moves only on reticle_clock { stepFrames }, so a wait for a timer or an animation never ends until you step.'),
|
|
304
226
|
hostStub: z
|
|
305
227
|
.string()
|
|
306
228
|
.optional()
|
|
@@ -371,7 +293,11 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
371
293
|
const headed = true === args['headed'];
|
|
372
294
|
const hud = Object.values(HudVisibility).find((v) => v === args['hud']);
|
|
373
295
|
const permissions = production.leasePermissions(parseLeasePermissions(args['permissions']), args['production']);
|
|
374
|
-
|
|
296
|
+
// Determinism is set on the context, like the emulation, so it travels with it.
|
|
297
|
+
const deterministic = leaseDeterminismOf(args['deterministic']);
|
|
298
|
+
const emulated = leaseEmulationOf(args['emulate']);
|
|
299
|
+
const emulate = deterministic === undefined ? emulated : { ...(emulated ?? {}), deterministic };
|
|
300
|
+
const jsCoverage = true === args['jsCoverage'];
|
|
375
301
|
// The stub of the shell bindings, when the caller supplied one. Installed by the pool before
|
|
376
302
|
// the first navigation, so it is already defined when the app's first module runs.
|
|
377
303
|
const initScript = asString(args['initScript']);
|
|
@@ -443,6 +369,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
443
369
|
if ((validatedSeed !== undefined ||
|
|
444
370
|
certMismatch ||
|
|
445
371
|
emulate !== undefined ||
|
|
372
|
+
jsCoverage ||
|
|
446
373
|
activated !== undefined) &&
|
|
447
374
|
existing !== undefined) {
|
|
448
375
|
// Seeding or a different certificate setting asked for: release its own context so the new
|
|
@@ -457,6 +384,9 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
457
384
|
// context — the tab-poisoning this branch exists to prevent, reached through the branch
|
|
458
385
|
// itself. It bites when the first acquire returned `ready: false`, since the mint path only
|
|
459
386
|
// aliases once its wait has resolved, so nothing recorded the app's own name (#692).
|
|
387
|
+
// A tab that never registered is loaded again in its OWN context, and kept while it is open:
|
|
388
|
+
// releasing it and minting threw away the cookies, so a signed-in tab came back signed out (#1924).
|
|
389
|
+
const reload = await reconnectHeldLease((ms) => pool.gotoLease(existing, carryReticleIdentity(pool.leaseUrl(existing), url), ms), async () => (await pool.viewportOfLease(existing)) !== undefined, () => pool.leaseLoading(existing), () => resolveLeasedSessionId(deps.sessions, existing) !== undefined, deps.now() + (asNumber(args['timeout_ms']) ?? LEASE_READY_TIMEOUT_MS), deps.now);
|
|
460
390
|
const resolved = resolveLeasedSessionId(deps.sessions, existing);
|
|
461
391
|
if (resolved !== undefined) {
|
|
462
392
|
// The other name this lease answers to, told to the pool exactly as the mint path tells it.
|
|
@@ -496,9 +426,14 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
496
426
|
: { preferExisting: { sessionId: alreadyOpen, note: PREFER_EXISTING_NOTE } }),
|
|
497
427
|
};
|
|
498
428
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
429
|
+
if (reload === HeldLeaseReload.RELOADED || reload === HeldLeaseReload.NOT_LOADED) {
|
|
430
|
+
const cause = reload === HeldLeaseReload.RELOADED
|
|
431
|
+
? await notConnectedHint(deps, url, pool.dialFailureUrl?.(existing))
|
|
432
|
+
: await notLoadedCause(url, deps.portAccepts);
|
|
433
|
+
return keptLeaseAnswer({ sessionId: existing, origin, url, reload }, pool, cause);
|
|
434
|
+
}
|
|
435
|
+
// The tab is gone: its page or its context was closed. Free the slot and mint, rather than
|
|
436
|
+
// handing back a dead id or leaving both contexts occupied.
|
|
502
437
|
await pool.release(existing);
|
|
503
438
|
}
|
|
504
439
|
const sessionId = newLeaseId();
|
|
@@ -521,6 +456,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
521
456
|
...(permissions !== undefined ? { permissions } : {}),
|
|
522
457
|
...(true === args['ignoreHTTPSErrors'] ? { ignoreHTTPSErrors: true } : {}),
|
|
523
458
|
...(emulate === undefined ? {} : { emulate }),
|
|
459
|
+
...(jsCoverage ? { jsCoverage } : {}),
|
|
524
460
|
});
|
|
525
461
|
}
|
|
526
462
|
catch (err) {
|
|
@@ -535,8 +471,10 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
535
471
|
throw new Error(scrubSeedFromError(err.message, validatedSeed));
|
|
536
472
|
}
|
|
537
473
|
// A raw page.goto failure is noisy and leaks the internal URL params — surface a clean,
|
|
538
|
-
// actionable message instead, one that tells a slow first compile from a missing app.
|
|
539
|
-
|
|
474
|
+
// actionable message instead, one that tells a slow first compile from a missing app. A
|
|
475
|
+
// timeout on a port that accepted the connection is a server still compiling (#2005).
|
|
476
|
+
const accepted = await serverAcceptedAfterTimeout(url, err, validatedSeed, deps.portAccepts);
|
|
477
|
+
throw new Error(navFailureMessage(url, err, validatedSeed, accepted));
|
|
540
478
|
}
|
|
541
479
|
// Wait for the leased tab's SDK to connect so the returned sessionId is usable right away.
|
|
542
480
|
// Resolved rather than assumed: an app that names its own session registers under that name,
|
|
@@ -618,6 +556,7 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
618
556
|
...(versionSkew === undefined ? {} : { versionSkew }),
|
|
619
557
|
...production.summary(activated),
|
|
620
558
|
...(initScript === undefined ? {} : { hostStub: HOST_STUB_NOTE }),
|
|
559
|
+
...(deterministic === undefined ? {} : { deterministic: DETERMINISTIC_NOTE }),
|
|
621
560
|
...cspWidenedOf(pool, lease.sessionId),
|
|
622
561
|
...hintOf(notReadyHint, ready ? await notificationReadBack(pool, lease.sessionId, permissions) : undefined),
|
|
623
562
|
};
|
|
@@ -646,11 +585,12 @@ async function notConnectedHint(deps, url, dialledUrl) {
|
|
|
646
585
|
}
|
|
647
586
|
const LEASE_RELEASE_TOOL = {
|
|
648
587
|
name: ReticleTool.LEASE_RELEASE,
|
|
649
|
-
description: 'Release a leased browser context by sessionId, closing it and freeing the pool slot for a queued acquire. Call this when a flow finishes so the pool stays within its concurrency cap.',
|
|
588
|
+
description: 'Release a leased browser context by sessionId, closing it and freeing the pool slot for a queued acquire. Call this when a flow finishes so the pool stays within its concurrency cap. With no sessionId, it releases the one lease this agent holds.',
|
|
650
589
|
inputSchema: {
|
|
651
590
|
sessionId: z
|
|
652
591
|
.string()
|
|
653
|
-
.
|
|
592
|
+
.optional()
|
|
593
|
+
.describe('The leased sessionId returned by reticle_lease{action:"acquire"}. Omit it when this agent holds one lease.'),
|
|
654
594
|
},
|
|
655
595
|
outputSchema: {
|
|
656
596
|
released: z.boolean(),
|
|
@@ -661,10 +601,7 @@ const LEASE_RELEASE_TOOL = {
|
|
|
661
601
|
if (pool === undefined) {
|
|
662
602
|
throw new Error(POOL_UNAVAILABLE);
|
|
663
603
|
}
|
|
664
|
-
const sessionId = asString(args['sessionId']);
|
|
665
|
-
if (sessionId === undefined || 0 === sessionId.length) {
|
|
666
|
-
throw new Error('reticle_lease{action:"release"} requires a sessionId');
|
|
667
|
-
}
|
|
604
|
+
const sessionId = asString(args['sessionId']) ?? ownOnlyLease(deps, pool);
|
|
668
605
|
// An id that holds no lease (a typo, or one that expired) answered `released: true`.
|
|
669
606
|
if (pool.leaseUrl(sessionId) === undefined) {
|
|
670
607
|
const held = pool.leasedSessionIds();
|
|
@@ -674,6 +611,21 @@ const LEASE_RELEASE_TOOL = {
|
|
|
674
611
|
return { released: true, leased: await releaseLease(deps, pool, sessionId) };
|
|
675
612
|
},
|
|
676
613
|
};
|
|
614
|
+
/**
|
|
615
|
+
* The one lease this caller holds, for a release with no sessionId. Another agent's lease is never
|
|
616
|
+
* this caller's to release (#1226), so none or several is a refusal that names what is held.
|
|
617
|
+
*/
|
|
618
|
+
function ownOnlyLease(deps, pool) {
|
|
619
|
+
const own = pool.leasedSessionIds().filter((id) => pool.heldBy(id, deps.attachId));
|
|
620
|
+
const [only] = own;
|
|
621
|
+
if (only !== undefined && 1 === own.length) {
|
|
622
|
+
return only;
|
|
623
|
+
}
|
|
624
|
+
const held = 0 === own.length
|
|
625
|
+
? 'this agent holds no lease'
|
|
626
|
+
: `this agent holds ${String(own.length)} leases: ${own.join(', ')}`;
|
|
627
|
+
throw new Error(`reticle_lease{action:"release"} needs a sessionId: ${held}.`);
|
|
628
|
+
}
|
|
677
629
|
/**
|
|
678
630
|
* Release one lease and everything that hangs off it. Returns how many leases are still held.
|
|
679
631
|
*
|
|
@@ -764,4 +716,4 @@ function tellWatchers(deps, projectId, text) {
|
|
|
764
716
|
// A HUD that missed one line is a smaller problem than a lease reported as broken.
|
|
765
717
|
}
|
|
766
718
|
}
|
|
767
|
-
export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL];
|
|
719
|
+
export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL, LEASE_COVERAGE_TOOL];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reticle's identity params on a lease URL: stamp them, read them back, carry them across a
|
|
3
|
+
* navigation, and match a registered session to the lease that stamped it.
|
|
4
|
+
*
|
|
5
|
+
* Split out of lease-tools.ts at the 1000-line cap. Pure: no I/O.
|
|
6
|
+
*/
|
|
7
|
+
import { type HudVisibility } from '@reticlehq/core';
|
|
8
|
+
/**
|
|
9
|
+
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
10
|
+
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function appendReticleParams(url: string, session: string, projectId?: string, hud?: HudVisibility): string;
|
|
13
|
+
/**
|
|
14
|
+
* The session a lease's tab actually registered as, or undefined while nothing has.
|
|
15
|
+
*
|
|
16
|
+
* Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
|
|
17
|
+
* SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
|
|
18
|
+
* which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
|
|
19
|
+
* known id. Matching on the lease id alone could never see those tabs, so the lease reported
|
|
20
|
+
* `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
|
|
21
|
+
*
|
|
22
|
+
* The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
|
|
23
|
+
* carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
|
|
24
|
+
* so `lease-abc` cannot claim `lease-abcdef`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveLeasedSessionId(sessions: {
|
|
27
|
+
get: (id: string) => unknown;
|
|
28
|
+
all: () => {
|
|
29
|
+
id: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
}[];
|
|
32
|
+
}, leaseId: string): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Carry a claimed identity across a navigation.
|
|
35
|
+
*
|
|
36
|
+
* A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
|
|
37
|
+
* context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
|
|
38
|
+
* reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
|
|
39
|
+
* first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
|
|
40
|
+
* the lease was orphaned: "no sessions are connected at all", with a browser window still open on
|
|
41
|
+
* screen. A leased browser that cannot survive being navigated is not a usable one.
|
|
42
|
+
*
|
|
43
|
+
* A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
|
|
44
|
+
* a human's own URL.
|
|
45
|
+
*/
|
|
46
|
+
export declare function carryReticleIdentity(fromUrl: string | undefined, toUrl: string): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reticle's identity params on a lease URL: stamp them, read them back, carry them across a
|
|
3
|
+
* navigation, and match a registered session to the lease that stamped it.
|
|
4
|
+
*
|
|
5
|
+
* Split out of lease-tools.ts at the 1000-line cap. Pure: no I/O.
|
|
6
|
+
*/
|
|
7
|
+
import { RETICLE_URL_PARAM } from '@reticlehq/core';
|
|
8
|
+
/**
|
|
9
|
+
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
10
|
+
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
11
|
+
*/
|
|
12
|
+
export function appendReticleParams(url, session, projectId, hud) {
|
|
13
|
+
try {
|
|
14
|
+
const u = new URL(url);
|
|
15
|
+
u.searchParams.set(RETICLE_URL_PARAM.SESSION, session);
|
|
16
|
+
if (projectId !== undefined && projectId.length > 0) {
|
|
17
|
+
u.searchParams.set(RETICLE_URL_PARAM.PROJECT, projectId);
|
|
18
|
+
}
|
|
19
|
+
if (hud !== undefined) {
|
|
20
|
+
u.searchParams.set(RETICLE_URL_PARAM.HUD, hud);
|
|
21
|
+
}
|
|
22
|
+
return u.toString();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
26
|
+
const proj = projectId !== undefined && projectId.length > 0
|
|
27
|
+
? `&${RETICLE_URL_PARAM.PROJECT}=${encodeURIComponent(projectId)}`
|
|
28
|
+
: '';
|
|
29
|
+
return `${url}${sep}${RETICLE_URL_PARAM.SESSION}=${encodeURIComponent(session)}${proj}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The session a lease's tab actually registered as, or undefined while nothing has.
|
|
34
|
+
*
|
|
35
|
+
* Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
|
|
36
|
+
* SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
|
|
37
|
+
* which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
|
|
38
|
+
* known id. Matching on the lease id alone could never see those tabs, so the lease reported
|
|
39
|
+
* `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
|
|
40
|
+
*
|
|
41
|
+
* The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
|
|
42
|
+
* carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
|
|
43
|
+
* so `lease-abc` cannot claim `lease-abcdef`.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveLeasedSessionId(sessions, leaseId) {
|
|
46
|
+
if (sessions.get(leaseId) !== undefined) {
|
|
47
|
+
return leaseId;
|
|
48
|
+
}
|
|
49
|
+
return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Carry a claimed identity across a navigation.
|
|
53
|
+
*
|
|
54
|
+
* A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
|
|
55
|
+
* context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
|
|
56
|
+
* reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
|
|
57
|
+
* first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
|
|
58
|
+
* the lease was orphaned: "no sessions are connected at all", with a browser window still open on
|
|
59
|
+
* screen. A leased browser that cannot survive being navigated is not a usable one.
|
|
60
|
+
*
|
|
61
|
+
* A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
|
|
62
|
+
* a human's own URL.
|
|
63
|
+
*/
|
|
64
|
+
export function carryReticleIdentity(fromUrl, toUrl) {
|
|
65
|
+
const session = sessionParamOf(fromUrl);
|
|
66
|
+
if (session === undefined) {
|
|
67
|
+
return toUrl;
|
|
68
|
+
}
|
|
69
|
+
// Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
|
|
70
|
+
if (sessionParamOf(toUrl) !== undefined) {
|
|
71
|
+
return toUrl;
|
|
72
|
+
}
|
|
73
|
+
const projectId = projectParamOf(fromUrl);
|
|
74
|
+
return appendReticleParams(toUrl, session, projectId);
|
|
75
|
+
}
|
|
76
|
+
function projectParamOf(url) {
|
|
77
|
+
if (url === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function sessionParamOf(url) {
|
|
88
|
+
if (url === undefined) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -36,14 +36,104 @@ function strayKeysRefusal(tool, action, stray, actions) {
|
|
|
36
36
|
/** The union of member input shapes, every field optional so one schema serves every action. */
|
|
37
37
|
function unionShape(actions) {
|
|
38
38
|
const shape = {};
|
|
39
|
+
const contested = contestedKeys(actions);
|
|
39
40
|
for (const tool of Object.values(actions)) {
|
|
40
41
|
for (const [key, schema] of Object.entries(tool.inputSchema)) {
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
if (key in shape) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// First definition wins, unless members accept different values for it: see contestedKeys.
|
|
46
|
+
if (contested.has(key)) {
|
|
47
|
+
shape[key] = eitherBound(key, actions);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
shape[key] = schema.isOptional() ? schema : schema.optional();
|
|
51
|
+
}
|
|
43
52
|
}
|
|
44
53
|
}
|
|
45
54
|
return shape;
|
|
46
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* What a field accepts, without its description: two members that word one field differently
|
|
58
|
+
* still accept the same values.
|
|
59
|
+
*/
|
|
60
|
+
function boundOf(schema) {
|
|
61
|
+
try {
|
|
62
|
+
const json = { ...z.toJSONSchema(schema, { unrepresentable: 'any' }) };
|
|
63
|
+
delete json['description'];
|
|
64
|
+
return JSON.stringify(json);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The fields two members accept different values for (#2144).
|
|
72
|
+
*
|
|
73
|
+
* `reticle_assert { action: "now" }` caps `timeout_ms` at one request, and `action: "wait"` takes up
|
|
74
|
+
* to 600000 and answers `resume_ms` for the rest. With the first definition winning, the merged tool
|
|
75
|
+
* kept the cap of "now", and refused every wait longer than one request: `reticle verify --expect
|
|
76
|
+
* --timeout 120000` printed `verified: unknown`.
|
|
77
|
+
*/
|
|
78
|
+
function contestedKeys(actions) {
|
|
79
|
+
const seen = new Map();
|
|
80
|
+
const contested = new Set();
|
|
81
|
+
for (const tool of Object.values(actions)) {
|
|
82
|
+
for (const [key, schema] of Object.entries(tool.inputSchema)) {
|
|
83
|
+
const bound = boundOf(schema);
|
|
84
|
+
if (!seen.has(key)) {
|
|
85
|
+
seen.set(key, bound);
|
|
86
|
+
}
|
|
87
|
+
else if (seen.get(key) !== bound) {
|
|
88
|
+
contested.add(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return contested;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A contested field accepts what any member accepts. The dispatch then checks the value against the
|
|
96
|
+
* chosen action's own schema (`contestedRefusal`), so no action gets a value it would refuse.
|
|
97
|
+
*/
|
|
98
|
+
function eitherBound(key, actions) {
|
|
99
|
+
const owners = Object.entries(actions).filter(([, tool]) => key in tool.inputSchema);
|
|
100
|
+
const schemas = owners.flatMap(([, tool]) => {
|
|
101
|
+
const schema = tool.inputSchema[key];
|
|
102
|
+
return schema === undefined ? [] : [schema];
|
|
103
|
+
});
|
|
104
|
+
const described = owners
|
|
105
|
+
.map(([action, tool]) => {
|
|
106
|
+
const said = tool.inputSchema[key]?.description;
|
|
107
|
+
return said === undefined ? undefined : `For action "${action}": ${said}`;
|
|
108
|
+
})
|
|
109
|
+
.filter((said) => said !== undefined)
|
|
110
|
+
.join(' ');
|
|
111
|
+
const [first, second, ...rest] = schemas;
|
|
112
|
+
if (first === undefined || second === undefined) {
|
|
113
|
+
throw new Error(`mergeTools: contested key '${key}' has fewer than two schemas`);
|
|
114
|
+
}
|
|
115
|
+
const either = z.union([first, second, ...rest]).optional();
|
|
116
|
+
return 0 === described.length ? either : either.describe(described);
|
|
117
|
+
}
|
|
118
|
+
/** The refusal for a contested field whose value the chosen action does not accept. */
|
|
119
|
+
function contestedRefusal(tool, action, args, chosen, contested) {
|
|
120
|
+
const named = [];
|
|
121
|
+
for (const key of contested) {
|
|
122
|
+
const schema = chosen.inputSchema[key];
|
|
123
|
+
if (schema === undefined || args[key] === undefined) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const checked = schema.safeParse(args[key]);
|
|
127
|
+
if (!checked.success) {
|
|
128
|
+
named.push(`${key} (${checked.error.issues.map((issue) => issue.message).join('; ')})`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (0 === named.length) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return (`invalid ${1 === named.length ? 'parameter' : 'parameters'} for ${tool} { action: "${action}" }: ` +
|
|
135
|
+
`${named.join('; ')} — NOT applied, so any result would be an answer to a different question`);
|
|
136
|
+
}
|
|
47
137
|
/**
|
|
48
138
|
* A member that declares `action` itself cannot be merged, and the failure was SILENT.
|
|
49
139
|
*
|
|
@@ -120,6 +210,7 @@ export function mergeTools(spec) {
|
|
|
120
210
|
if (spec.defaultAction !== undefined && !(spec.defaultAction in spec.actions)) {
|
|
121
211
|
throw new Error(`mergeTools(${spec.name}): defaultAction '${spec.defaultAction}' is not one of its actions`);
|
|
122
212
|
}
|
|
213
|
+
const contested = contestedKeys(spec.actions);
|
|
123
214
|
return {
|
|
124
215
|
name: spec.name,
|
|
125
216
|
description: spec.description,
|
|
@@ -148,6 +239,12 @@ export function mergeTools(spec) {
|
|
|
148
239
|
if (0 < stray.length && action !== undefined) {
|
|
149
240
|
return Promise.reject(new Error(strayKeysRefusal(spec.name, action, stray, spec.actions)));
|
|
150
241
|
}
|
|
242
|
+
const outOfBounds = action === undefined
|
|
243
|
+
? undefined
|
|
244
|
+
: contestedRefusal(spec.name, action, args, chosen, contested);
|
|
245
|
+
if (outOfBounds !== undefined) {
|
|
246
|
+
return Promise.reject(new Error(outOfBounds));
|
|
247
|
+
}
|
|
151
248
|
return chosen.handler(deps, args, call);
|
|
152
249
|
},
|
|
153
250
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where a merged or retired tool's old name went.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Many names exported from `ReticleTool` are no longer tools: `reticle_record_start` became
|
|
5
5
|
* `reticle_record { action: "start" }`, `reticle_diff` became `reticle_baseline { action: "diff" }`,
|
|
6
6
|
* and three were retired outright. Confirmed live in a field sweep, they all answered `unknown tool`
|
|
7
7
|
* — which is the wrong answer, because the capability exists and only moved. An agent trained on an
|
|
@@ -23,6 +23,10 @@ const RETIRED_NOTE = {
|
|
|
23
23
|
tool: ReticleTool.PROJECT,
|
|
24
24
|
note: 'run outcomes are persisted automatically by reticle_flow_replay; read them with reticle_project',
|
|
25
25
|
},
|
|
26
|
+
[ReticleTool.EXPLORE]: {
|
|
27
|
+
tool: ReticleTool.LOOK,
|
|
28
|
+
note: 'list the controls with reticle_look { action: "page", mode: "interactive" } (reticle_snapshot { mode: "interactive" }), and read console errors with reticle_observe { action: "console" } (reticle_console)',
|
|
29
|
+
},
|
|
26
30
|
[ReticleTool.WAIT_READY]: {
|
|
27
31
|
tool: ReticleTool.SESSIONS,
|
|
28
32
|
note: 'waiting is implicit — the first live call already blocks until the session is ready',
|