@reticlehq/server 2.12.0 → 2.13.1
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 +67 -109
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +247 -164
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +239 -22
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +30 -12
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +3 -1
- package/dist/init/dev-script.js +8 -5
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +22 -1
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +61 -3
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +68 -0
- package/dist/setup/dev-server-wait.js +67 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +203 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +56 -8
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +18 -0
- package/dist/update/update-nudge.js +20 -0
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { runTool } from './invoke-tool.js';
|
|
3
3
|
import { buildErrorPayload } from './error-recovery.js';
|
|
4
|
-
import { mergedNameRedirect, mergedNameMessage } from './merged-name-redirect.js';
|
|
4
|
+
import { mergedNameRedirect, mergedNameMessage, retiredToolNames } from './merged-name-redirect.js';
|
|
5
|
+
import { takeVersionSkewOnto } from '../version/version-nudge.js';
|
|
5
6
|
import { ReticleTool } from './tool-names.js';
|
|
6
7
|
import { ADVERTISE_ALL_ENV } from './tool-surface.js';
|
|
7
8
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
@@ -110,11 +111,18 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
110
111
|
name: t.name,
|
|
111
112
|
summary: firstSentence(t.description),
|
|
112
113
|
}));
|
|
114
|
+
// Names that USED to be tools, against the call that replaces each. Instructions written
|
|
115
|
+
// against an earlier release are a permanent fact of the product — `reticle_crawl` is still
|
|
116
|
+
// named by guidance in the wild — and without this the catalogue is silent about them: the
|
|
117
|
+
// old name is simply absent, which reads as "no such capability" rather than "renamed".
|
|
118
|
+
// Reaching the new name then costs an error string, or never happens.
|
|
119
|
+
const retired = retiredToolNames();
|
|
113
120
|
return Promise.resolve({
|
|
114
121
|
total: catalog.length,
|
|
115
122
|
tools: catalog,
|
|
123
|
+
retired,
|
|
116
124
|
...profileBlock,
|
|
117
|
-
next: `All ${catalog.length} tools above are callable, advertised or not. Load full params with reticle_tools { names:[…] }, then call reticle_run { tool, args }.`,
|
|
125
|
+
next: `All ${catalog.length} tools above are callable, advertised or not. Load full params with reticle_tools { names:[…] }, then call reticle_run { tool, args }. \`retired\` maps names that are no longer tools to the call that replaced each.`,
|
|
118
126
|
});
|
|
119
127
|
}
|
|
120
128
|
// The grammar rides ONLY on a `names:[…]` reply that asked for a tool taking a predicate, so
|
|
@@ -124,9 +132,20 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
124
132
|
return Promise.resolve({
|
|
125
133
|
tools: names.map((n) => {
|
|
126
134
|
const t = byName.get(n);
|
|
127
|
-
|
|
135
|
+
if (t !== undefined)
|
|
136
|
+
return { name: n, description: t.description, params: paramInfo(t.inputSchema) };
|
|
137
|
+
// `reticle_run` has answered a merged name with its replacement for a while; the tool
|
|
138
|
+
// asked to DISCOVER tools still said "unknown tool", which is the wrong answer in the one
|
|
139
|
+
// place an agent goes to resolve a name it is unsure of. Same redirect, same derivation.
|
|
140
|
+
const moved = mergedNameRedirect(n);
|
|
141
|
+
return moved === undefined
|
|
128
142
|
? { name: n, error: 'unknown tool' }
|
|
129
|
-
: {
|
|
143
|
+
: {
|
|
144
|
+
name: n,
|
|
145
|
+
error: mergedNameMessage(n, moved),
|
|
146
|
+
tool: moved.tool,
|
|
147
|
+
...(moved.action === undefined ? {} : { action: moved.action }),
|
|
148
|
+
};
|
|
130
149
|
}),
|
|
131
150
|
...(carriesPredicate ? { predicateGrammar: predicateGrammar() } : {}),
|
|
132
151
|
});
|
|
@@ -222,7 +241,7 @@ export function buildDynamicTools(allTools, profile) {
|
|
|
222
241
|
// is reached through here: a stale ref, a paused session, a missing pairing token all came
|
|
223
242
|
// back as the agent's arguments being wrong, which is advice that spends the retry.
|
|
224
243
|
const message = error instanceof Error ? error.message : String(error);
|
|
225
|
-
return { ...buildErrorPayload(message), tool: name };
|
|
244
|
+
return { ...takeVersionSkewOnto(buildErrorPayload(message)), tool: name };
|
|
226
245
|
}
|
|
227
246
|
},
|
|
228
247
|
};
|
|
@@ -49,6 +49,7 @@ export declare const RECOVERY: {
|
|
|
49
49
|
*/
|
|
50
50
|
readonly CONFIRM_DANGEROUS: string;
|
|
51
51
|
readonly UNSUPPORTED_SURFACE: string;
|
|
52
|
+
readonly HOVER_NEEDS_POINTER: string;
|
|
52
53
|
readonly TOKEN_REQUIRED: string;
|
|
53
54
|
readonly SCOPE_MISMATCH: string;
|
|
54
55
|
readonly SESSION_GONE: string;
|
|
@@ -143,6 +143,12 @@ export const RECOVERY = {
|
|
|
143
143
|
'editor keeps its own document model, so writing to the DOM would look right and submit the old ' +
|
|
144
144
|
'content. Drive the same outcome another way (a plain input, a command, a keyboard action) or ' +
|
|
145
145
|
'assert on the result instead of typing it. Known gap, already reported: no need to file it.',
|
|
146
|
+
HOVER_NEEDS_POINTER: 'Hover needs a real pointer: a synthetic mouseover does not apply CSS :hover, and Reticle ' +
|
|
147
|
+
'refuses rather than reporting the styles as applied. Acquire a tab with ' +
|
|
148
|
+
'reticle_run { tool: "reticle_lease", action: "acquire", url } — reticle_lease is not ' +
|
|
149
|
+
'advertised under the default profile, so it is reached through reticle_run, not called ' +
|
|
150
|
+
'directly — or ask the human to drive with `reticle drive` / RETICLE_CDP_URL. This is a ' +
|
|
151
|
+
'deliberate refusal, not a defect: there is nothing to report.',
|
|
146
152
|
TOKEN_REQUIRED: 'The bridge binds beyond localhost and requires a pairing token. Set the same token in the SDK ' +
|
|
147
153
|
'init (@reticlehq/core) and the Reticle server config, then reconnect.',
|
|
148
154
|
SCOPE_MISMATCH: 'A tab IS connected — it just belongs to a different project than the daemon is scoped to, so ' +
|
|
@@ -202,6 +208,7 @@ const REASON_OF = {
|
|
|
202
208
|
TARGET_MISSED: RefusalReason.NO_MATCH,
|
|
203
209
|
FLOW_STEP_MISSING: RefusalReason.NO_MATCH,
|
|
204
210
|
UNSUPPORTED_SURFACE: RefusalReason.UNSUPPORTED,
|
|
211
|
+
HOVER_NEEDS_POINTER: RefusalReason.UNSUPPORTED,
|
|
205
212
|
NOT_EDITABLE: RefusalReason.UNSUPPORTED,
|
|
206
213
|
CONFIRM_DANGEROUS: RefusalReason.UNSUPPORTED,
|
|
207
214
|
WRONG_TARGET: RefusalReason.UNSUPPORTED,
|
|
@@ -247,6 +254,7 @@ const RULES = [
|
|
|
247
254
|
// broken. Order matters: contenteditable and disabled/readonly are `cannot <verb> …` messages too,
|
|
248
255
|
// so they must be tested before the general wrong-target rule.
|
|
249
256
|
{ match: /contenteditable/i, hint: RECOVERY.UNSUPPORTED_SURFACE },
|
|
257
|
+
{ match: /cannot hover without a real pointer/i, hint: RECOVERY.HOVER_NEEDS_POINTER },
|
|
250
258
|
{ match: /cannot \w+ a (disabled|readonly) </i, hint: RECOVERY.NOT_EDITABLE },
|
|
251
259
|
// Three spellings ship — "action", "native action", "WebMCP tool" — and the rule matched one, so
|
|
252
260
|
// two thirds of the same deliberate refusal still read as a possible defect.
|
|
@@ -5,13 +5,14 @@ import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
|
5
5
|
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
6
6
|
import { takeUpdateNudge } from '../update/update-nudge.js';
|
|
7
7
|
import { takeVersionSkew } from '../version/version-nudge.js';
|
|
8
|
+
import { rewriteClosedAsSkew } from '../version/version-skew.js';
|
|
8
9
|
import { noteToolCall } from '../daemon/daemon-usefulness.js';
|
|
9
10
|
import { bugsInResult, routeOf } from '../telemetry/bug-found.js';
|
|
10
11
|
import { noteToolServed, reportToolRefused } from '../telemetry/tool-refused.js';
|
|
11
12
|
import { buildErrorPayload, refusalReasonFor } from './error-recovery.js';
|
|
12
13
|
import { resultIsError } from '../mcp/mcp-is-error.js';
|
|
13
14
|
import { verificationOf } from '../telemetry/verification-of.js';
|
|
14
|
-
import { asString } from './tools-helpers.js';
|
|
15
|
+
import { asString, sessionIdFromArgs, spentRefFromArgs } from './tools-helpers.js';
|
|
15
16
|
import { EnvelopeKey } from './tool-kit.js';
|
|
16
17
|
import { ReticleTool } from './tool-names.js';
|
|
17
18
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
@@ -19,7 +20,7 @@ import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
|
|
|
19
20
|
import { noteRefsMinted, wrongTabRefusal } from '../session/ref-provenance.js';
|
|
20
21
|
import { CAPTURED_TOOLS, noteCapturedCall, noteToolDispatched, } from '../honesty/feature-capture.js';
|
|
21
22
|
import { span } from '../trace.js';
|
|
22
|
-
import { deltaForToolResult, impactSnapshot, initImpact, recordImpact, } from '../impact/impact-recorder.js';
|
|
23
|
+
import { defectForToolResult, deltaForToolResult, impactSnapshot, initImpact, recordImpact, } from '../impact/impact-recorder.js';
|
|
23
24
|
import { frictionOf, inviteFor } from './feedback-invite.js';
|
|
24
25
|
/**
|
|
25
26
|
* The live-session tools whose result MUST carry the
|
|
@@ -37,6 +38,20 @@ const ACTION_TOOLS = new Set([
|
|
|
37
38
|
ReticleTool.ACT_SEQUENCE,
|
|
38
39
|
ReticleTool.ACT_AND_WAIT,
|
|
39
40
|
]);
|
|
41
|
+
/**
|
|
42
|
+
* Tools that go through Playwright CDP rather than the page SDK.
|
|
43
|
+
*
|
|
44
|
+
* Under version skew these fail with "Target page, context or browser has been closed" while every
|
|
45
|
+
* DOM tool against the same tab still works (#688). They are session-EXEMPT (own result contracts),
|
|
46
|
+
* so the bound-tool resolve above never runs for them — resolve again here so we can refuse with
|
|
47
|
+
* the skew sentence before Playwright lies about a closed page.
|
|
48
|
+
*/
|
|
49
|
+
const CDP_TOOLS = new Set([
|
|
50
|
+
ReticleTool.SCREENSHOT,
|
|
51
|
+
ReticleTool.VISUAL_DIFF,
|
|
52
|
+
ReticleTool.NETWORK_MOCK,
|
|
53
|
+
ReticleTool.VIEWPORT,
|
|
54
|
+
]);
|
|
40
55
|
/**
|
|
41
56
|
* Tools whose result hands the agent refs. The tab they were minted in is remembered, so the call
|
|
42
57
|
* that spends one cannot be routed to a different tab without saying so — see ref-provenance.ts.
|
|
@@ -103,6 +118,10 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
103
118
|
ReticleTool.INTENT, // reads/writes .reticle/intent.json; sessionId only picks the project
|
|
104
119
|
ReticleTool.CONTEXT, // folds the journal + intent ledger; must still answer when nothing is connected
|
|
105
120
|
ReticleTool.PROJECT, // reads .reticle/project.json
|
|
121
|
+
// Reads the team's shared memory over HTTP; sessionId only resolves WHICH project's link file to
|
|
122
|
+
// use. It must answer with no tab connected — "what does this project know?" is a question an
|
|
123
|
+
// agent asks before it has opened anything.
|
|
124
|
+
ReticleTool.MEMORY,
|
|
106
125
|
ReticleTool.RUN_EXPORT, // reads .reticle/runs/<id>.json (verification-run artifact)
|
|
107
126
|
ReticleTool.SESSION, // merged lifecycle/human-channel family (tune/yield/end/resume/messages/review/narrate)
|
|
108
127
|
ReticleTool.SCREENSHOT, // own contract; provider-driven, not a live-DOM-health read
|
|
@@ -287,7 +306,7 @@ export async function runTool(tool, deps, args) {
|
|
|
287
306
|
getSessionMetrics().recordAction();
|
|
288
307
|
const settleTiming = getSessionMetrics().startToolCall(tool.name, args);
|
|
289
308
|
const startedAt = Date.now();
|
|
290
|
-
const rawSessionId =
|
|
309
|
+
const rawSessionId = sessionIdFromArgs(args);
|
|
291
310
|
const bound = SESSION_BOUND_TOOLS.has(tool.name);
|
|
292
311
|
// Resolve the session identity ONCE, up front, for a live-session tool. The lease heartbeat must
|
|
293
312
|
// target the session the handler will ACTUALLY drive — which, when the agent omits sessionId, is the
|
|
@@ -296,7 +315,7 @@ export async function runTool(tool, deps, args) {
|
|
|
296
315
|
// before the handler so a long ACT_AND_WAIT is protected for its whole duration; on failure leave it
|
|
297
316
|
// to the handler to throw the canonical no-session error.
|
|
298
317
|
let session;
|
|
299
|
-
if (bound) {
|
|
318
|
+
if (bound || CDP_TOOLS.has(tool.name)) {
|
|
300
319
|
try {
|
|
301
320
|
session = deps.sessions.resolve(rawSessionId);
|
|
302
321
|
}
|
|
@@ -304,6 +323,13 @@ export async function runTool(tool, deps, args) {
|
|
|
304
323
|
session = undefined;
|
|
305
324
|
}
|
|
306
325
|
}
|
|
326
|
+
// CDP tools against a skewed session: refuse with the sentence reticle_sessions already knows,
|
|
327
|
+
// before Playwright invents a closed page. ready:true leases still hit this path.
|
|
328
|
+
if (session?.versionSkew !== undefined && CDP_TOOLS.has(tool.name)) {
|
|
329
|
+
const message = session.versionSkew;
|
|
330
|
+
reportRefusal(tool.name, message);
|
|
331
|
+
throw new Error(message);
|
|
332
|
+
}
|
|
307
333
|
// The read-only calls the journal never keeps, recorded HERE because this is the one dispatch
|
|
308
334
|
// point every call routes through — a second recording site is a second thing to forget, and this
|
|
309
335
|
// instrument exists precisely because nothing was recorded before. Recorded before the handler
|
|
@@ -348,7 +374,7 @@ export async function runTool(tool, deps, args) {
|
|
|
348
374
|
// the guessed tab and return a green about a page nobody asked about.
|
|
349
375
|
if (session !== undefined) {
|
|
350
376
|
const wrongTab = wrongTabRefusal({
|
|
351
|
-
ref:
|
|
377
|
+
ref: spentRefFromArgs(args),
|
|
352
378
|
explicitSessionId: rawSessionId,
|
|
353
379
|
chosenSessionId: session.id,
|
|
354
380
|
connected: () => deps.sessions.list(),
|
|
@@ -375,7 +401,22 @@ export async function runTool(tool, deps, args) {
|
|
|
375
401
|
// to the agent by the MCP boundary, and discarded. Reported here rather than at that boundary
|
|
376
402
|
// because there are two of them (mcp.ts and the reticle_run hatch) and this is the one place both
|
|
377
403
|
// go through — a third would otherwise be invisible from the day it was added.
|
|
378
|
-
|
|
404
|
+
//
|
|
405
|
+
// Under version skew, Playwright's "Target page has been closed" is the wrong cause (#688): the
|
|
406
|
+
// page is still dialled in. Prefer the session's skew sentence when we already know the pair is
|
|
407
|
+
// mismatched — including for tools that are not in CDP_TOOLS (real-input fallbacks that throw).
|
|
408
|
+
let skewText = session?.versionSkew;
|
|
409
|
+
if (skewText === undefined) {
|
|
410
|
+
try {
|
|
411
|
+
skewText = deps.sessions.resolve(rawSessionId).versionSkew;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
skewText = undefined;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const rewritten = rewriteClosedAsSkew(error, skewText);
|
|
418
|
+
const toThrow = rewritten ?? error;
|
|
419
|
+
const message = toThrow instanceof Error ? toThrow.message : String(toThrow);
|
|
379
420
|
reportRefusal(tool.name, message);
|
|
380
421
|
// The invitation on THIS path is the one `buildErrorPayload` attaches when it does not recognise
|
|
381
422
|
// the error, and it was never counted — so the denominator excluded the commonest friction there
|
|
@@ -384,7 +425,7 @@ export async function runTool(tool, deps, args) {
|
|
|
384
425
|
if (tool.name !== ReticleTool.RUN && buildErrorPayload(message).feedback !== undefined) {
|
|
385
426
|
getSessionMetrics().recordFeedbackPrompt();
|
|
386
427
|
}
|
|
387
|
-
throw
|
|
428
|
+
throw toThrow;
|
|
388
429
|
}
|
|
389
430
|
finally {
|
|
390
431
|
// In a `finally` so a THROWN call still settles: otherwise every failing tool would leak a
|
|
@@ -410,11 +451,20 @@ export async function runTool(tool, deps, args) {
|
|
|
410
451
|
// The user's own record of what Reticle did for them. Same chokepoint as everything else that
|
|
411
452
|
// counts, for the same reason: a second recording site is a second thing to forget. Separate
|
|
412
453
|
// store from telemetry - this one never leaves the machine.
|
|
413
|
-
|
|
454
|
+
const finishedAt = Date.now();
|
|
455
|
+
const defect = defectForToolResult(raw, finishedAt);
|
|
456
|
+
// Which project's ledger this call belongs to. Resolved from the SESSION being driven, not from
|
|
457
|
+
// wherever the daemon happens to stand: one daemon serves many projects, and recording every
|
|
458
|
+
// call against its own cwd is how one app's verdicts ended up on another account's dashboard.
|
|
459
|
+
// Falls back to the daemon root when no session can be named, which is the old behaviour.
|
|
460
|
+
const artifactRoot = session?.artifactRoot ?? deps.reticleRoot;
|
|
461
|
+
recordImpact(deltaForToolResult(raw, finishedAt - startedAt, resultIsError(raw)), { ...(defect === undefined ? {} : { defect }) }, artifactRoot);
|
|
414
462
|
// ...and the tab being driven is told, so the report is live rather than a thing you reload.
|
|
415
463
|
// Optional-call, not optional-chain-on-the-object: a test double is a partial Session, and a
|
|
416
464
|
// courtesy push must never be the reason a tool call throws.
|
|
417
|
-
session
|
|
465
|
+
// Bound to THIS session's root: a tab must be shown its own project's numbers, not whichever
|
|
466
|
+
// project the daemon was started in.
|
|
467
|
+
session?.pushImpact?.(() => impactSnapshot(artifactRoot));
|
|
418
468
|
if (resultIsError(raw))
|
|
419
469
|
reportRefusal(tool.name, raw.error);
|
|
420
470
|
else {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lease tools — the agent-facing surface of the BrowserPool.
|
|
3
3
|
*
|
|
4
|
-
* `reticle_lease_acquire` opens
|
|
5
|
-
* the sessionId the app's SDK will register
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `reticle_lease_acquire` opens an isolated headless context navigated to the app URL and returns
|
|
5
|
+
* the sessionId the app's SDK will register. A second acquire on the same origin reuses the live
|
|
6
|
+
* lease rather than minting another tab — that second tab used to poison default session resolution
|
|
7
|
+
* for the rest of the run. The parallel suite still mints via `acquireLeasedSession`, because ten
|
|
8
|
+
* flows on one origin need isolation on purpose.
|
|
8
9
|
*
|
|
9
10
|
* Attach-only: the pool drives a browser against an already-running dev server — it never starts one.
|
|
10
11
|
*/
|
|
11
12
|
import type { ToolDef } from './tool-kit.js';
|
|
13
|
+
/** The origin of a URL, or undefined when it does not parse. */
|
|
14
|
+
export declare function originOf(url: string): string | undefined;
|
|
12
15
|
/**
|
|
13
16
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
14
17
|
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
@@ -40,6 +43,20 @@ export declare function resolveLeasedSessionId(sessions: {
|
|
|
40
43
|
url?: string;
|
|
41
44
|
}[];
|
|
42
45
|
}, leaseId: string): string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Carry a claimed identity across a navigation.
|
|
48
|
+
*
|
|
49
|
+
* A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
|
|
50
|
+
* context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
|
|
51
|
+
* reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
|
|
52
|
+
* first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
|
|
53
|
+
* the lease was orphaned: "no sessions are connected at all", with a browser window still open on
|
|
54
|
+
* screen. A leased browser that cannot survive being navigated is not a usable one.
|
|
55
|
+
*
|
|
56
|
+
* A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
|
|
57
|
+
* a human's own URL.
|
|
58
|
+
*/
|
|
59
|
+
export declare function carryReticleIdentity(fromUrl: string | undefined, toUrl: string): string;
|
|
43
60
|
/**
|
|
44
61
|
* Poll until the leased tab's SDK has actually registered on the bridge, so the sessionId we return
|
|
45
62
|
* is immediately usable (the page navigates first, then connects a moment later — without this wait
|
|
@@ -86,4 +103,13 @@ export declare function waitForLeasedSession(isConnected: () => boolean, sleeper
|
|
|
86
103
|
* any other call instead of being a second, invisible dispatch path.
|
|
87
104
|
*/
|
|
88
105
|
export declare const LEASE_ACQUIRE_TOOL: ToolDef;
|
|
106
|
+
/**
|
|
107
|
+
* What to say when a lease was taken while a real tab was already open.
|
|
108
|
+
*
|
|
109
|
+
* Not a refusal. Leases are the highest-value path for autonomous work and an agent that genuinely
|
|
110
|
+
* needs isolation must still get one — but a human watching their own tab cannot see a lease, so
|
|
111
|
+
* when both exist the visible one is the better default and the agent should hear that here, where
|
|
112
|
+
* it is choosing, rather than discover it when somebody asks why nothing is happening.
|
|
113
|
+
*/
|
|
114
|
+
export declare const PREFER_EXISTING_NOTE = "a non-leased tab for this app was already connected \u2014 that is the one a human can see, and this lease is not. Unless you need an isolated context (a second identity, a clean profile, parallel flows), release this lease and drive that sessionId instead.";
|
|
89
115
|
export declare const LEASE_TOOLS: ToolDef[];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lease tools — the agent-facing surface of the BrowserPool.
|
|
3
3
|
*
|
|
4
|
-
* `reticle_lease_acquire` opens
|
|
5
|
-
* the sessionId the app's SDK will register
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `reticle_lease_acquire` opens an isolated headless context navigated to the app URL and returns
|
|
5
|
+
* the sessionId the app's SDK will register. A second acquire on the same origin reuses the live
|
|
6
|
+
* lease rather than minting another tab — that second tab used to poison default session resolution
|
|
7
|
+
* for the rest of the run. The parallel suite still mints via `acquireLeasedSession`, because ten
|
|
8
|
+
* flows on one origin need isolation on purpose.
|
|
8
9
|
*
|
|
9
10
|
* Attach-only: the pool drives a browser against an already-running dev server — it never starts one.
|
|
10
11
|
*/
|
|
@@ -13,6 +14,7 @@ import { leaseNotConnectedHint } from './lease-hint.js';
|
|
|
13
14
|
import { probeSdkMarker } from './sdk-marker-probe.js';
|
|
14
15
|
import { readProjectFramework, readProjectId } from '../cli/cli-port.js';
|
|
15
16
|
import { hasConnectedBefore } from '../session/connection-memory.js';
|
|
17
|
+
import { AGENT_DRIVING_ELSEWHERE, AGENT_DRIVING_HERE_AGAIN, watchersToNotify, } from '../session/lease-visibility.js';
|
|
16
18
|
import { reticleStateHome } from '../daemon/daemon.js';
|
|
17
19
|
import { RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
18
20
|
import { ReticleTool } from './tool-names.js';
|
|
@@ -47,6 +49,20 @@ async function leaseEvidence(deps, port, url) {
|
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
51
|
const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
|
|
52
|
+
/** The origin of a URL, or undefined when it does not parse. */
|
|
53
|
+
export function originOf(url) {
|
|
54
|
+
try {
|
|
55
|
+
return new URL(url).origin;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const ALREADY_HELD_LEASE = 'you already hold a lease on this origin';
|
|
62
|
+
function alreadyHeldHint(sessionId, origin) {
|
|
63
|
+
return (`${ALREADY_HELD_LEASE} (${origin}): ${sessionId} — reused it rather than opening a second ` +
|
|
64
|
+
'tab, which would make every later call require sessionId. Release first if you want a fresh one.');
|
|
65
|
+
}
|
|
50
66
|
/**
|
|
51
67
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
52
68
|
* adopts them on connect. Pure; falls back to plain concatenation if the URL can't be parsed.
|
|
@@ -115,6 +131,39 @@ export function resolveLeasedSessionId(sessions, leaseId) {
|
|
|
115
131
|
return leaseId;
|
|
116
132
|
return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
|
|
117
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Carry a claimed identity across a navigation.
|
|
136
|
+
*
|
|
137
|
+
* A LEASED tab is addressed by `__reticle_session` in its URL: that param is how the pool finds the
|
|
138
|
+
* context it opened, and — since succession was tightened — it is also the ONLY thing that lets the
|
|
139
|
+
* reconnecting document inherit the lease. `reticle_navigate` sent the caller's raw URL, so the very
|
|
140
|
+
* first navigation stripped the marker, the tab re-announced as an ordinary anonymous session, and
|
|
141
|
+
* the lease was orphaned: "no sessions are connected at all", with a browser window still open on
|
|
142
|
+
* screen. A leased browser that cannot survive being navigated is not a usable one.
|
|
143
|
+
*
|
|
144
|
+
* A tab that claims nothing is left exactly as it was — this must never bolt Reticle's params onto
|
|
145
|
+
* a human's own URL.
|
|
146
|
+
*/
|
|
147
|
+
export function carryReticleIdentity(fromUrl, toUrl) {
|
|
148
|
+
const session = sessionParamOf(fromUrl);
|
|
149
|
+
if (session === undefined)
|
|
150
|
+
return toUrl;
|
|
151
|
+
// Already addressed (a caller passing the stamped URL back) — appending would be a no-op at best.
|
|
152
|
+
if (sessionParamOf(toUrl) !== undefined)
|
|
153
|
+
return toUrl;
|
|
154
|
+
const projectId = projectParamOf(fromUrl);
|
|
155
|
+
return appendReticleParams(toUrl, session, projectId);
|
|
156
|
+
}
|
|
157
|
+
function projectParamOf(url) {
|
|
158
|
+
if (url === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
try {
|
|
161
|
+
return new URL(url).searchParams.get(RETICLE_URL_PARAM.PROJECT) ?? undefined;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
118
167
|
function sessionParamOf(url) {
|
|
119
168
|
if (url === undefined)
|
|
120
169
|
return undefined;
|
|
@@ -173,7 +222,7 @@ export async function waitForLeasedSession(isConnected, sleeper = sleep, attempt
|
|
|
173
222
|
*/
|
|
174
223
|
export const LEASE_ACQUIRE_TOOL = {
|
|
175
224
|
name: ReticleTool.LEASE_ACQUIRE,
|
|
176
|
-
description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running and embed @reticlehq/core). Returns the sessionId the leased tab registers — pass it to other tools. The pool keeps all leases in ONE browser and caps concurrency; if at capacity this waits for a free slot. Release with reticle_lease{action:"release"} when the flow is done.',
|
|
225
|
+
description: 'Lease a fresh isolated headless browser context from the shared pool and navigate it to the app URL (the app must already be running and embed @reticlehq/core). If this origin is already leased and still connected, this returns THAT session rather than minting a second tab — 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 at capacity this waits for a free slot. 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.',
|
|
177
226
|
inputSchema: {
|
|
178
227
|
url: z
|
|
179
228
|
.string()
|
|
@@ -194,7 +243,21 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
194
243
|
.describe('Milliseconds until this lease expires if untouched. Each tool call that targets this session resets the clock. Plan your work to finish or re-acquire before this runs out.'),
|
|
195
244
|
leased: z.number().describe('How many contexts are currently leased from the pool.'),
|
|
196
245
|
queued: z.number().describe('How many acquires are waiting for a free slot.'),
|
|
246
|
+
preferExisting: z
|
|
247
|
+
.object({ sessionId: z.string(), note: z.string() })
|
|
248
|
+
.optional()
|
|
249
|
+
.describe('Present when a live NON-leased tab for this app was already connected. That tab is the one a human can see; this lease is not. Release this lease and drive that sessionId unless you specifically need an isolated context.'),
|
|
250
|
+
reused: z
|
|
251
|
+
.boolean()
|
|
252
|
+
.optional()
|
|
253
|
+
.describe('True when this acquire returned an existing live lease on the same origin rather than minting a second context.'),
|
|
197
254
|
hint: z.string().optional(),
|
|
255
|
+
versionSkew: z
|
|
256
|
+
.string()
|
|
257
|
+
.optional()
|
|
258
|
+
.describe("Present when the leased tab's SDK disagrees with this daemon. ready is still true — the " +
|
|
259
|
+
'page dialled in — but CDP-backed tools (screenshot, viewport, real input, network_mock) ' +
|
|
260
|
+
'will fail until the versions match. Same sentence as reticle_sessions.versionSkew.'),
|
|
198
261
|
},
|
|
199
262
|
handler: async (deps, args) => {
|
|
200
263
|
const pool = deps.pool;
|
|
@@ -215,6 +278,33 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
215
278
|
}
|
|
216
279
|
}
|
|
217
280
|
const projectId = asString(args['projectId']);
|
|
281
|
+
// Sampled BEFORE acquiring: afterwards this lease is itself a session, and the point is to name
|
|
282
|
+
// a tab that already existed. A human's open tab is the one they can watch, so if one is here
|
|
283
|
+
// the agent should be told at the moment it is choosing — not after it has gone dark on them.
|
|
284
|
+
const alreadyOpen = liveTabFor(deps, projectId);
|
|
285
|
+
const origin = originOf(url);
|
|
286
|
+
const existing = origin === undefined ? undefined : pool.leaseIdOnOrigin?.(origin);
|
|
287
|
+
if (existing !== undefined && origin !== undefined) {
|
|
288
|
+
if (deps.sessions.get(existing) !== undefined) {
|
|
289
|
+
pool.touch(existing);
|
|
290
|
+
return {
|
|
291
|
+
sessionId: existing,
|
|
292
|
+
url,
|
|
293
|
+
ready: true,
|
|
294
|
+
reused: true,
|
|
295
|
+
expiresInMs: pool.leaseTtlMs(),
|
|
296
|
+
leased: pool.activeCount(),
|
|
297
|
+
queued: pool.queuedCount(),
|
|
298
|
+
hint: alreadyHeldHint(existing, origin),
|
|
299
|
+
...(alreadyOpen === undefined
|
|
300
|
+
? {}
|
|
301
|
+
: { preferExisting: { sessionId: alreadyOpen, note: PREFER_EXISTING_NOTE } }),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// The lease is still held but the tab has gone — a reload dropped the session. Free the
|
|
305
|
+
// slot and mint, rather than handing back a dead id or leaving both contexts occupied.
|
|
306
|
+
await pool.release(existing);
|
|
307
|
+
}
|
|
218
308
|
const sessionId = newLeaseId();
|
|
219
309
|
const navUrl = appendReticleParams(url, sessionId, projectId);
|
|
220
310
|
let lease;
|
|
@@ -240,6 +330,11 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
240
330
|
// reaper closes the context mid-flow. See BrowserPool.alias and #157.
|
|
241
331
|
if (registeredId !== undefined)
|
|
242
332
|
pool.alias(registeredId, lease.sessionId);
|
|
333
|
+
// The lease now exists, so any HUD a human is watching has just gone dark. Say so.
|
|
334
|
+
tellWatchers(deps, projectId, AGENT_DRIVING_ELSEWHERE);
|
|
335
|
+
// ready means the SDK dialled in — not that contracts match. Carry the skew warning on acquire
|
|
336
|
+
// so the agent does not learn it only after a CDP tool invents a closed page (#688).
|
|
337
|
+
const versionSkew = registeredId === undefined ? undefined : deps.sessions.get(registeredId)?.versionSkew;
|
|
243
338
|
return {
|
|
244
339
|
sessionId: registeredId ?? lease.sessionId,
|
|
245
340
|
url,
|
|
@@ -247,6 +342,15 @@ export const LEASE_ACQUIRE_TOOL = {
|
|
|
247
342
|
expiresInMs: pool.leaseTtlMs(),
|
|
248
343
|
leased: pool.activeCount(),
|
|
249
344
|
queued: pool.queuedCount(),
|
|
345
|
+
...(alreadyOpen === undefined
|
|
346
|
+
? {}
|
|
347
|
+
: {
|
|
348
|
+
preferExisting: {
|
|
349
|
+
sessionId: alreadyOpen,
|
|
350
|
+
note: PREFER_EXISTING_NOTE,
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
...(versionSkew === undefined ? {} : { versionSkew }),
|
|
250
354
|
...(ready
|
|
251
355
|
? {}
|
|
252
356
|
: { hint: await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId)) }),
|
|
@@ -282,8 +386,68 @@ const LEASE_RELEASE_TOOL = {
|
|
|
282
386
|
if (sessionId === undefined || 0 === sessionId.length) {
|
|
283
387
|
throw new Error('reticle_lease{action:"release"} requires a sessionId');
|
|
284
388
|
}
|
|
389
|
+
// Read the project BEFORE releasing: afterwards the session is gone and there is nothing to
|
|
390
|
+
// ask which project it belonged to.
|
|
391
|
+
const projectId = deps.sessions.get(sessionId)?.projectId;
|
|
285
392
|
await pool.release(sessionId);
|
|
393
|
+
// Only once the LAST lease is gone. Announcing "live again" while another lease still drives
|
|
394
|
+
// would be a lie, and a HUD that says the wrong thing is worse than one that says nothing.
|
|
395
|
+
if (0 === pool.activeCount())
|
|
396
|
+
tellWatchers(deps, projectId, AGENT_DRIVING_HERE_AGAIN);
|
|
286
397
|
return { released: true, leased: pool.activeCount() };
|
|
287
398
|
},
|
|
288
399
|
};
|
|
400
|
+
/**
|
|
401
|
+
* What to say when a lease was taken while a real tab was already open.
|
|
402
|
+
*
|
|
403
|
+
* Not a refusal. Leases are the highest-value path for autonomous work and an agent that genuinely
|
|
404
|
+
* needs isolation must still get one — but a human watching their own tab cannot see a lease, so
|
|
405
|
+
* when both exist the visible one is the better default and the agent should hear that here, where
|
|
406
|
+
* it is choosing, rather than discover it when somebody asks why nothing is happening.
|
|
407
|
+
*/
|
|
408
|
+
export const PREFER_EXISTING_NOTE = 'a non-leased tab for this app was already connected — that is the one a human can see, and this lease is not. Unless you need an isolated context (a second identity, a clean profile, parallel flows), release this lease and drive that sessionId instead.';
|
|
409
|
+
/**
|
|
410
|
+
* The first live non-leased session for this project, if any.
|
|
411
|
+
*
|
|
412
|
+
* Reuses the watcher selector rather than re-deriving "which sessions belong to a human": one rule
|
|
413
|
+
* for one question, so the tab we announce to and the tab we recommend can never disagree.
|
|
414
|
+
*/
|
|
415
|
+
function liveTabFor(deps, projectId) {
|
|
416
|
+
const pool = deps.pool;
|
|
417
|
+
if (pool === undefined)
|
|
418
|
+
return undefined;
|
|
419
|
+
try {
|
|
420
|
+
const candidates = deps.sessions
|
|
421
|
+
.all()
|
|
422
|
+
.map((session) => ({ id: session.id, projectId: session.projectId }));
|
|
423
|
+
return watchersToNotify(candidates, pool.leasedSessionIds(), projectId)[0];
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Tell the tabs a human is watching that the agent has gone somewhere they cannot see.
|
|
431
|
+
*
|
|
432
|
+
* Fire-and-forget, and deliberately never able to fail the tool: this is a courtesy to a person, and
|
|
433
|
+
* a lease that succeeded must not be reported as failed because a narration could not be posted to
|
|
434
|
+
* some unrelated tab. `pushNarration` is already fire-and-forget; the try/catch covers a session
|
|
435
|
+
* that disconnects between listing it and posting to it.
|
|
436
|
+
*/
|
|
437
|
+
function tellWatchers(deps, projectId, text) {
|
|
438
|
+
const pool = deps.pool;
|
|
439
|
+
if (pool === undefined)
|
|
440
|
+
return;
|
|
441
|
+
try {
|
|
442
|
+
const candidates = deps.sessions
|
|
443
|
+
.all()
|
|
444
|
+
.map((session) => ({ id: session.id, projectId: session.projectId }));
|
|
445
|
+
for (const id of watchersToNotify(candidates, pool.leasedSessionIds(), projectId)) {
|
|
446
|
+
deps.sessions.get(id)?.pushNarration(text);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// A HUD that missed one line is a smaller problem than a lease reported as broken.
|
|
451
|
+
}
|
|
452
|
+
}
|
|
289
453
|
export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL];
|
|
@@ -8,6 +8,19 @@ interface MergedNameRedirect {
|
|
|
8
8
|
}
|
|
9
9
|
/** Where `name` went, or undefined when it is a live tool or not ours. */
|
|
10
10
|
export declare function mergedNameRedirect(name: string): MergedNameRedirect | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Every retired name against the call that replaces it — the tombstones, as one block.
|
|
13
|
+
*
|
|
14
|
+
* The redirect above answers an agent that already guessed the old name. This answers the one that
|
|
15
|
+
* is reading the surface to find out what exists, and is the case a redirect cannot reach: nothing
|
|
16
|
+
* in the catalogue says `reticle_crawl` became `reticle_verify { action: "crawl" }`, so instructions
|
|
17
|
+
* written against an earlier release lead either to an error string or to nothing at all.
|
|
18
|
+
*
|
|
19
|
+
* Derived from the same two declarations as `BY_OLD_NAME`, for the same reason: a hand-written
|
|
20
|
+
* migration list is one merge away from being wrong, and wrong is worse than absent here — the agent
|
|
21
|
+
* retries into it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function retiredToolNames(): Readonly<Record<string, string>>;
|
|
11
24
|
/** The sentence handed to an agent that called `name`. */
|
|
12
25
|
export declare function mergedNameMessage(name: string, redirect: MergedNameRedirect): string;
|
|
13
26
|
export {};
|
|
@@ -46,6 +46,28 @@ const BY_OLD_NAME = (() => {
|
|
|
46
46
|
export function mergedNameRedirect(name) {
|
|
47
47
|
return BY_OLD_NAME.get(name);
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Every retired name against the call that replaces it — the tombstones, as one block.
|
|
51
|
+
*
|
|
52
|
+
* The redirect above answers an agent that already guessed the old name. This answers the one that
|
|
53
|
+
* is reading the surface to find out what exists, and is the case a redirect cannot reach: nothing
|
|
54
|
+
* in the catalogue says `reticle_crawl` became `reticle_verify { action: "crawl" }`, so instructions
|
|
55
|
+
* written against an earlier release lead either to an error string or to nothing at all.
|
|
56
|
+
*
|
|
57
|
+
* Derived from the same two declarations as `BY_OLD_NAME`, for the same reason: a hand-written
|
|
58
|
+
* migration list is one merge away from being wrong, and wrong is worse than absent here — the agent
|
|
59
|
+
* retries into it.
|
|
60
|
+
*/
|
|
61
|
+
export function retiredToolNames() {
|
|
62
|
+
const tombstones = {};
|
|
63
|
+
for (const [old, redirect] of BY_OLD_NAME) {
|
|
64
|
+
tombstones[old] =
|
|
65
|
+
redirect.action === undefined
|
|
66
|
+
? `retired; ${redirect.note ?? `use ${redirect.tool}`}`
|
|
67
|
+
: `retired; use ${redirect.tool} { action: "${redirect.action}" }`;
|
|
68
|
+
}
|
|
69
|
+
return tombstones;
|
|
70
|
+
}
|
|
49
71
|
/** The sentence handed to an agent that called `name`. */
|
|
50
72
|
export function mergedNameMessage(name, redirect) {
|
|
51
73
|
return redirect.action === undefined
|