@reticlehq/server 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/SKILL.md +130 -778
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +36 -1
- package/dist/capsule/causal-summary.js +33 -7
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +65 -34
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +32 -0
- package/dist/cli/cli-port.js +82 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +47 -3
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +69 -0
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +119 -39
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +46 -0
- package/dist/events/contradictions.js +35 -22
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -72
- package/dist/events/predicate-eval.js +117 -236
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +296 -22
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +21 -5
- package/dist/flows/heal-run.js +1 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +10 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +49 -0
- package/dist/honesty/unsettled.js +82 -0
- package/dist/honesty/verified.d.ts +36 -3
- package/dist/honesty/verified.js +54 -11
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +47 -10
- package/dist/init/agent-rules.d.ts +36 -0
- package/dist/init/agent-rules.js +132 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +38 -6
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +114 -0
- package/dist/init/confirm.js +165 -0
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +78 -16
- package/dist/init/plan.d.ts +47 -6
- package/dist/init/plan.js +228 -69
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +157 -45
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +54 -6
- package/dist/init/snippets.d.ts +45 -5
- package/dist/init/snippets.js +194 -42
- package/dist/init/vite-config.js +53 -5
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +37 -5
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +35 -30
- package/dist/mcp/mcp-proxy.js +306 -104
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +70 -34
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +11 -2
- package/dist/mcp/proxy-lifecycle.js +12 -3
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +67 -2
- package/dist/session/no-session-diagnosis.js +236 -27
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +174 -8
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +15 -17
- package/dist/telemetry/bug-found.js +52 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +56 -0
- package/dist/telemetry/install-source.js +74 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +15 -0
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +26 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +209 -28
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +49 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +73 -9
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +159 -26
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +40 -1
- package/dist/tools/lease-tools.js +181 -83
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +10 -4
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +52 -0
- package/dist/tools/react-grace.js +90 -0
- package/dist/tools/read-tools.js +67 -5
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +94 -0
- package/dist/tools/settle-in-flight.js +146 -0
- package/dist/tools/snapshot-delta.d.ts +10 -0
- package/dist/tools/snapshot-delta.js +194 -9
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +86 -1
- package/dist/tools/tools.js +61 -8
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +55 -11
- package/docs/actions.mdx +120 -0
- package/docs/agent-cheatsheet.md +95 -29
- package/docs/architecture.md +52 -35
- package/docs/benchmarks.md +127 -75
- package/docs/best-practices.mdx +216 -0
- package/docs/capabilities.mdx +534 -0
- package/docs/cli/affected.mdx +53 -0
- package/docs/cli/capsules.mdx +70 -0
- package/docs/cli/cloud.mdx +153 -0
- package/docs/cli/doctor.mdx +109 -0
- package/docs/cli/drive.mdx +74 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +94 -0
- package/docs/cli/hunt.mdx +73 -0
- package/docs/cli/identify.mdx +76 -0
- package/docs/cli/init.mdx +121 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +74 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +69 -0
- package/docs/cli/status.mdx +88 -0
- package/docs/cli/stop.mdx +64 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +76 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +62 -0
- package/docs/cli.mdx +241 -0
- package/docs/debugging.md +16 -10
- package/docs/deploy-checks.md +45 -12
- package/docs/desktop.mdx +384 -0
- package/docs/docs.json +364 -0
- package/docs/enterprise.md +22 -16
- package/docs/faq.mdx +102 -0
- package/docs/fixtures.md +13 -7
- package/docs/flows.md +29 -23
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +238 -0
- package/docs/gate-plan.md +48 -42
- package/docs/gates.md +28 -22
- package/docs/getting-started.md +129 -77
- package/docs/human-control.md +24 -18
- package/docs/index.mdx +143 -0
- package/docs/install-agentic.mdx +224 -0
- package/docs/install-manual.mdx +196 -0
- package/docs/instrumentation.mdx +238 -0
- package/docs/integration-patterns.md +41 -35
- package/docs/local-registry.md +19 -13
- package/docs/multi-agent-testing.md +19 -13
- package/docs/packages/babel-plugin.mdx +79 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +116 -0
- package/docs/packages/electron.mdx +85 -0
- package/docs/packages/eslint-plugin.mdx +86 -0
- package/docs/packages/next.mdx +89 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +119 -0
- package/docs/packages/vite-plugin.mdx +94 -0
- package/docs/packages.mdx +218 -0
- package/docs/platform-integration.md +35 -26
- package/docs/predicates.mdx +248 -0
- package/docs/quickstart.mdx +257 -0
- package/docs/recipes.mdx +282 -0
- package/docs/skill-file.mdx +107 -0
- package/docs/state-management.mdx +199 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +50 -33
- package/docs/telemetry-contract.md +106 -52
- package/docs/telemetry-events.mdx +475 -0
- package/docs/telemetry.md +63 -55
- package/docs/testing.md +44 -6
- package/docs/token-efficiency.md +37 -27
- package/docs/tools-act-and-wait.mdx +173 -0
- package/docs/tools-act-sequence.mdx +137 -0
- package/docs/tools-act.mdx +95 -0
- package/docs/tools-assert.mdx +129 -0
- package/docs/tools-console.mdx +63 -0
- package/docs/tools-inspect.mdx +88 -0
- package/docs/tools-navigate.mdx +99 -0
- package/docs/tools-network.mdx +119 -0
- package/docs/tools-observe.mdx +145 -0
- package/docs/tools-overview.mdx +160 -0
- package/docs/tools-query.mdx +126 -0
- package/docs/tools-session-and-feedback.mdx +112 -0
- package/docs/tools-sessions.mdx +94 -0
- package/docs/tools-snapshot.mdx +129 -0
- package/docs/tools-state.mdx +104 -0
- package/docs/tools-tools-and-run.mdx +156 -0
- package/docs/tools-wait-for.mdx +85 -0
- package/docs/troubleshooting.mdx +263 -0
- package/docs/usage.md +209 -165
- package/docs/vs-chrome-devtools-mcp.mdx +123 -0
- package/docs/vs-playwright-mcp.mdx +125 -0
- package/docs/vs-screenshots.mdx +137 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +109 -0
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -256
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
package/dist/tools/lease-hint.js
CHANGED
|
@@ -5,20 +5,71 @@
|
|
|
5
5
|
* — it does not wait for the human's tab. Measured over a day: lease sessions had a median of 30
|
|
6
6
|
* tool calls and produced 46% of all bugs found, against a median of 1 call for everything else.
|
|
7
7
|
*
|
|
8
|
-
* When it fails it is almost always a PORT MISMATCH: the tab loads, and the app's SDK dials the port
|
|
9
|
-
* it was BUILT with, which need not be the daemon that issued the lease. Verified by driving it —
|
|
10
|
-
* against a matching daemon `acquire` returns ready:true and the session is driveable immediately;
|
|
11
|
-
* against a different one the tab connects to the OTHER daemon instead.
|
|
12
|
-
*
|
|
13
8
|
* The old hint asked "is <url> running with @reticlehq/core enabled?", which is the one thing that
|
|
14
|
-
* cannot be in doubt: the pool just navigated a real browser to it.
|
|
15
|
-
* the
|
|
9
|
+
* cannot be in doubt: the pool just navigated a real browser to it. Its replacement then swung to
|
|
10
|
+
* the other confident-but-unevidenced claim — "the usual cause is a PORT MISMATCH" — and the daemon
|
|
11
|
+
* ended up contradicting ITSELF inside one response, saying in `reticle_sessions` that a session had
|
|
12
|
+
* connected earlier "so the wiring is correct" and here that the port was probably wrong. Across a
|
|
13
|
+
* batch of field reports on four apps the port was correct every single time, and `reticle init`
|
|
14
|
+
* would have been the wrong action every single time.
|
|
15
|
+
*
|
|
16
|
+
* So: rank by what the daemon actually knows, and fall back to the differential only when it knows
|
|
17
|
+
* nothing. A cause we hold positive evidence against is not printed at all.
|
|
16
18
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const NUXT = 'nuxt';
|
|
20
|
+
const PORT_DIFFERENTIAL = "The usual cause is a PORT MISMATCH: the app's SDK connects to the port it was BUILT with, so a " +
|
|
21
|
+
"daemon on a different port never hears from it — check the app's reticle port (`.reticle.json`, " +
|
|
22
|
+
"RETICLE_PORT, or the build plugin's `port`), or start the daemon on the port the app expects.";
|
|
23
|
+
/**
|
|
24
|
+
* The causes that actually occur on a wired app, in the order they occur. Mirrors the no-session
|
|
25
|
+
* diagnosis deliberately: two surfaces answering the same question must not rank it differently.
|
|
26
|
+
*/
|
|
27
|
+
const REAL_CAUSES = 'Causes that produce exactly this and are NOT a port problem: (a) the SDK is in the bundle but ' +
|
|
28
|
+
'`connect()` is never reached — a dev-mode guard or a missing runtime-config value returning ' +
|
|
29
|
+
'early, which emits nothing at all; (b) the dev server was started BEFORE the Reticle plugin was ' +
|
|
30
|
+
'added, so it is not in the bundle — restart it, HMR is not enough; (c) a peer dependency is ' +
|
|
31
|
+
'missing (typically `@reticlehq/react` in a non-React app) so the dynamic import fails silently; ' +
|
|
32
|
+
'(d) the page is not on localhost, where the SDK needs BOTH `allowNonLocalhost: true` and a ' +
|
|
33
|
+
'pairing token — the flag alone is not sufficient.';
|
|
34
|
+
const NUXT_FIRST = 'This project is Nuxt: the most likely cause by a distance is a dev server that predates the ' +
|
|
35
|
+
'Reticle plugin, because Nuxt does NOT register a newly added plugin on HMR. Restart `nuxt dev` ' +
|
|
36
|
+
'and re-acquire before investigating anything else.';
|
|
37
|
+
const RELEASE = 'The tab stays leased either way — release it with reticle_lease{action:"release"}.';
|
|
38
|
+
function markerClause(sdkMarker) {
|
|
39
|
+
if (sdkMarker === undefined)
|
|
40
|
+
return '';
|
|
41
|
+
return sdkMarker
|
|
42
|
+
? ' A Reticle SDK marker WAS found in the page that loaded, so the app does ship the SDK — the ' +
|
|
43
|
+
'question is only why it did not reach this daemon.'
|
|
44
|
+
: ' No Reticle SDK marker was found in the page that loaded. That is a check of the SERVED ' +
|
|
45
|
+
'document, so a lazily-imported SDK can still be missed — but combined with the rest it ' +
|
|
46
|
+
'points at a bundle that carries no Reticle at all, i.e. wiring that never took effect.';
|
|
47
|
+
}
|
|
48
|
+
export function leaseNotConnectedHint(url, port, evidence = {}) {
|
|
49
|
+
const opening = `the leased tab loaded ${url} but never dialled this daemon (port ${String(port)}).`;
|
|
50
|
+
const marker = markerClause(evidence.sdkMarker);
|
|
51
|
+
// 1. A recorded refusal. The only cause with proof behind it, so nothing outranks it.
|
|
52
|
+
if (evidence.refusal !== undefined) {
|
|
53
|
+
return `${opening} A dial DID arrive and was refused: ${evidence.refusal}${marker} ${RELEASE}`;
|
|
54
|
+
}
|
|
55
|
+
// 2. Nuxt, whose most likely cause differs from every other framework's and which `init` warns
|
|
56
|
+
// about at install time — while this hint never mentioned it.
|
|
57
|
+
const nuxt = evidence.framework?.toLowerCase() === NUXT ? ` ${NUXT_FIRST}` : '';
|
|
58
|
+
// 3. Proven wiring. Blaming the port here is the self-contradiction this rewrite exists to end.
|
|
59
|
+
if (true === evidence.previouslyConnected) {
|
|
60
|
+
return (`${opening} An app for this project HAS connected on this port before, so the port and the ` +
|
|
61
|
+
`wiring are both correct — what is failing is the SDK reaching initialise on the page.${nuxt}` +
|
|
62
|
+
`${marker} ${REAL_CAUSES} ${RELEASE}`);
|
|
63
|
+
}
|
|
64
|
+
// 4. Wired, but never seen connect. Rank the real causes; keep the port differential last, where
|
|
65
|
+
// the evidence for it actually sits.
|
|
66
|
+
if (true === evidence.initialized) {
|
|
67
|
+
return `${opening}${nuxt}${marker} ${REAL_CAUSES} If none of those, it may be dialling a different daemon than this one: check the app's reticle port matches ${String(port)}. ${RELEASE}`;
|
|
68
|
+
}
|
|
69
|
+
// 5. Nothing known. The differential, plus the possibility this app carries no SDK at all —
|
|
70
|
+
// except when the marker check already ruled that out, which is the whole reason for the bit.
|
|
71
|
+
const noSdk = true === evidence.sdkMarker
|
|
72
|
+
? ''
|
|
73
|
+
: ' If the app carries no Reticle SDK at all, run `reticle init` in it first.';
|
|
74
|
+
return `${opening}${nuxt}${marker} ${PORT_DIFFERENTIAL}${noSdk} ${REAL_CAUSES} ${RELEASE}`;
|
|
24
75
|
}
|
|
@@ -20,6 +20,26 @@ export declare function appendReticleParams(url: string, session: string, projec
|
|
|
20
20
|
* of an internals-leaking wall of text.
|
|
21
21
|
*/
|
|
22
22
|
export declare function cleanNavError(err: unknown): string;
|
|
23
|
+
/**
|
|
24
|
+
* The session a lease's tab actually registered as, or undefined while nothing has.
|
|
25
|
+
*
|
|
26
|
+
* Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
|
|
27
|
+
* SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
|
|
28
|
+
* which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
|
|
29
|
+
* known id. Matching on the lease id alone could never see those tabs, so the lease reported
|
|
30
|
+
* `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
|
|
31
|
+
*
|
|
32
|
+
* The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
|
|
33
|
+
* carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
|
|
34
|
+
* so `lease-abc` cannot claim `lease-abcdef`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveLeasedSessionId(sessions: {
|
|
37
|
+
get: (id: string) => unknown;
|
|
38
|
+
all: () => {
|
|
39
|
+
id: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
}[];
|
|
42
|
+
}, leaseId: string): string | undefined;
|
|
23
43
|
/**
|
|
24
44
|
* Poll until the leased tab's SDK has actually registered on the bridge, so the sessionId we return
|
|
25
45
|
* is immediately usable (the page navigates first, then connects a moment later — without this wait
|
|
@@ -38,9 +58,28 @@ export declare function acquireLeasedSession(pool: {
|
|
|
38
58
|
sessionId: string;
|
|
39
59
|
release: () => Promise<void>;
|
|
40
60
|
}>;
|
|
41
|
-
|
|
61
|
+
/** Optional so a test double need not implement it; the real pool always does. */
|
|
62
|
+
alias?: (registeredId: string, leaseId: string) => void;
|
|
63
|
+
}, sessions: {
|
|
64
|
+
get: (id: string) => unknown;
|
|
65
|
+
all: () => {
|
|
66
|
+
id: string;
|
|
67
|
+
url?: string;
|
|
68
|
+
}[];
|
|
69
|
+
}, url: string, projectId?: string): Promise<{
|
|
42
70
|
sessionId: string;
|
|
43
71
|
release: () => Promise<void>;
|
|
44
72
|
}>;
|
|
45
73
|
export declare function waitForLeasedSession(isConnected: () => boolean, sleeper?: (ms: number) => Promise<void>, attempts?: number, pollMs?: number): Promise<boolean>;
|
|
74
|
+
/**
|
|
75
|
+
* Named on its own because `reticle drive <url>` runs it too.
|
|
76
|
+
*
|
|
77
|
+
* When a daemon already owns the bridge port, `drive` asks that daemon for a browser instead of
|
|
78
|
+
* competing for the port (see cli/drive-attach.ts), and the browser it should get is exactly the one
|
|
79
|
+
* an agent gets: same pool, same isolation, same reporting. Sharing the ToolDef rather than the
|
|
80
|
+
* handler keeps that literal — the drive route dispatches through `runTool`, so it is counted like
|
|
81
|
+
* any other call instead of being a second, invisible dispatch path.
|
|
82
|
+
*/
|
|
83
|
+
export declare const LEASE_ACQUIRE_TOOL: ToolDef;
|
|
84
|
+
export declare const LEASE_RELEASE_TOOL: ToolDef;
|
|
46
85
|
export declare const LEASE_TOOLS: ToolDef[];
|
|
@@ -10,9 +10,32 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { leaseNotConnectedHint } from './lease-hint.js';
|
|
13
|
+
import { readProjectFramework, readProjectId } from '../cli/cli-port.js';
|
|
14
|
+
import { hasConnectedBefore } from '../session/connection-memory.js';
|
|
15
|
+
import { reticleStateHome } from '../daemon/daemon.js';
|
|
13
16
|
import { RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
14
17
|
import { ReticleTool } from './tool-names.js';
|
|
15
18
|
import { asString } from './tools-helpers.js';
|
|
19
|
+
import { chromiumHint } from '../cli/chromium-hint.js';
|
|
20
|
+
/**
|
|
21
|
+
* Everything the daemon already knows about why a leased tab might not have dialled in.
|
|
22
|
+
*
|
|
23
|
+
* Gathered here rather than guessed in the hint, because the daemon HAD this evidence all along and
|
|
24
|
+
* printed a static differential over the top of it — telling one agent in `reticle_sessions` that
|
|
25
|
+
* the wiring was proven correct and telling it seconds later that the port was probably wrong.
|
|
26
|
+
*/
|
|
27
|
+
function leaseEvidence(deps, port) {
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const projectId = readProjectId(cwd);
|
|
30
|
+
const refusal = deps.sessions.lastClosure()?.reason;
|
|
31
|
+
const framework = readProjectFramework(cwd);
|
|
32
|
+
return {
|
|
33
|
+
...(refusal === undefined ? {} : { refusal }),
|
|
34
|
+
...(framework === undefined ? {} : { framework }),
|
|
35
|
+
previouslyConnected: hasConnectedBefore(reticleStateHome(), port, projectId),
|
|
36
|
+
initialized: projectId !== undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
16
39
|
const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
|
|
17
40
|
/**
|
|
18
41
|
* Append Reticle identity params (__reticle_session, optional __reticle_project) to a URL so the app's own SDK
|
|
@@ -64,6 +87,34 @@ function newLeaseId() {
|
|
|
64
87
|
: `${String(Date.now())}-${String(performance.now())}`;
|
|
65
88
|
return `lease-${uuid}`;
|
|
66
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The session a lease's tab actually registered as, or undefined while nothing has.
|
|
92
|
+
*
|
|
93
|
+
* Normally that is the lease id: `appendReticleParams` stamps `__reticle_session` on the URL and the
|
|
94
|
+
* SDK adopts it. An app that passes an explicit `session` to `connect()` keeps its own name instead,
|
|
95
|
+
* which is legitimate — a single-app fixture does it deliberately so a battery can address it by a
|
|
96
|
+
* known id. Matching on the lease id alone could never see those tabs, so the lease reported
|
|
97
|
+
* `ready: false` and a hint blaming a port mismatch while the session was connected and driveable.
|
|
98
|
+
*
|
|
99
|
+
* The URL marker is the evidence, and it is what makes this safe: two concurrent leases on one origin
|
|
100
|
+
* carry different markers, so neither can adopt the other's tab. Parsed rather than substring-matched,
|
|
101
|
+
* so `lease-abc` cannot claim `lease-abcdef`.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveLeasedSessionId(sessions, leaseId) {
|
|
104
|
+
if (sessions.get(leaseId) !== undefined)
|
|
105
|
+
return leaseId;
|
|
106
|
+
return sessions.all().find((s) => sessionParamOf(s.url) === leaseId)?.id;
|
|
107
|
+
}
|
|
108
|
+
function sessionParamOf(url) {
|
|
109
|
+
if (url === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
return new URL(url).searchParams.get(RETICLE_URL_PARAM.SESSION) ?? undefined;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
67
118
|
const LEASE_READY_ATTEMPTS = 100;
|
|
68
119
|
const LEASE_READY_POLL_MS = 100;
|
|
69
120
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -78,11 +129,20 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
78
129
|
* Returns the session id plus its release; the caller must release in a finally so a crashed flow never
|
|
79
130
|
* leaks a pool slot and starves the rest of the suite.
|
|
80
131
|
*/
|
|
81
|
-
export async function acquireLeasedSession(pool,
|
|
132
|
+
export async function acquireLeasedSession(pool, sessions, url, projectId) {
|
|
82
133
|
const sessionId = newLeaseId();
|
|
83
134
|
const lease = await pool.acquire(appendReticleParams(url, sessionId, projectId), { sessionId });
|
|
84
|
-
|
|
85
|
-
|
|
135
|
+
// Resolved, not assumed — see resolveLeasedSessionId. The parallel suite replays against whatever
|
|
136
|
+
// id comes back, so handing it the lease id for an app that named its own session sent every flow
|
|
137
|
+
// in the run at a session that does not exist.
|
|
138
|
+
let registeredId;
|
|
139
|
+
await waitForLeasedSession(() => {
|
|
140
|
+
registeredId = resolveLeasedSessionId(sessions, lease.sessionId);
|
|
141
|
+
return registeredId !== undefined;
|
|
142
|
+
});
|
|
143
|
+
if (registeredId !== undefined)
|
|
144
|
+
pool.alias?.(registeredId, lease.sessionId);
|
|
145
|
+
return { sessionId: registeredId ?? lease.sessionId, release: () => lease.release() };
|
|
86
146
|
}
|
|
87
147
|
export async function waitForLeasedSession(isConnected, sleeper = sleep, attempts = LEASE_READY_ATTEMPTS, pollMs = LEASE_READY_POLL_MS) {
|
|
88
148
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -92,86 +152,124 @@ export async function waitForLeasedSession(isConnected, sleeper = sleep, attempt
|
|
|
92
152
|
}
|
|
93
153
|
return isConnected();
|
|
94
154
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
let lease;
|
|
129
|
-
try {
|
|
130
|
-
lease = await pool.acquire(navUrl, { sessionId });
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
// A raw page.goto failure is noisy and leaks the internal URL params — surface a clean,
|
|
134
|
-
// actionable message instead.
|
|
135
|
-
throw new Error(`could not open ${url} — is the app running there? (${cleanNavError(err)})`);
|
|
136
|
-
}
|
|
137
|
-
// Wait for the leased tab's SDK to connect so the returned sessionId is usable right away.
|
|
138
|
-
const ready = await waitForLeasedSession(() => deps.sessions.get(lease.sessionId) !== undefined);
|
|
139
|
-
return {
|
|
140
|
-
sessionId: lease.sessionId,
|
|
141
|
-
url,
|
|
142
|
-
ready,
|
|
143
|
-
leased: pool.activeCount(),
|
|
144
|
-
queued: pool.queuedCount(),
|
|
145
|
-
...(ready
|
|
146
|
-
? {}
|
|
147
|
-
: {
|
|
148
|
-
hint: leaseNotConnectedHint(url, deps.bridgePort ?? RETICLE_DEFAULT_PORT),
|
|
149
|
-
}),
|
|
150
|
-
};
|
|
151
|
-
},
|
|
155
|
+
/**
|
|
156
|
+
* Named on its own because `reticle drive <url>` runs it too.
|
|
157
|
+
*
|
|
158
|
+
* When a daemon already owns the bridge port, `drive` asks that daemon for a browser instead of
|
|
159
|
+
* competing for the port (see cli/drive-attach.ts), and the browser it should get is exactly the one
|
|
160
|
+
* an agent gets: same pool, same isolation, same reporting. Sharing the ToolDef rather than the
|
|
161
|
+
* handler keeps that literal — the drive route dispatches through `runTool`, so it is counted like
|
|
162
|
+
* any other call instead of being a second, invisible dispatch path.
|
|
163
|
+
*/
|
|
164
|
+
export const LEASE_ACQUIRE_TOOL = {
|
|
165
|
+
name: ReticleTool.LEASE_ACQUIRE,
|
|
166
|
+
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.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
url: z
|
|
169
|
+
.string()
|
|
170
|
+
.describe('URL of the already-running app to drive (e.g. http://localhost:3000/dashboard).'),
|
|
171
|
+
projectId: z
|
|
172
|
+
.string()
|
|
173
|
+
.optional()
|
|
174
|
+
.describe('Stable project id to stamp on the leased tab so the agent can scope to it.'),
|
|
175
|
+
},
|
|
176
|
+
outputSchema: {
|
|
177
|
+
sessionId: z.string(),
|
|
178
|
+
url: z.string(),
|
|
179
|
+
ready: z
|
|
180
|
+
.boolean()
|
|
181
|
+
.describe('Whether the leased tab connected — false ⇒ the app may not embed @reticlehq/core.'),
|
|
182
|
+
expiresInMs: z
|
|
183
|
+
.number()
|
|
184
|
+
.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.'),
|
|
185
|
+
leased: z.number().describe('How many contexts are currently leased from the pool.'),
|
|
186
|
+
queued: z.number().describe('How many acquires are waiting for a free slot.'),
|
|
187
|
+
hint: z.string().optional(),
|
|
152
188
|
},
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
168
|
-
throw new Error(
|
|
169
|
-
const sessionId = asString(args['sessionId']);
|
|
170
|
-
if (sessionId === undefined || 0 === sessionId.length) {
|
|
171
|
-
throw new Error('reticle_lease{action:"release"} requires a sessionId');
|
|
189
|
+
handler: async (deps, args) => {
|
|
190
|
+
const pool = deps.pool;
|
|
191
|
+
if (pool === undefined)
|
|
192
|
+
throw new Error(POOL_UNAVAILABLE);
|
|
193
|
+
const url = asString(args['url']);
|
|
194
|
+
if (url === undefined || 0 === url.length)
|
|
195
|
+
throw new Error('reticle_lease{action:"acquire"} requires a url');
|
|
196
|
+
// Preflight the browser before spending the round trip. Without it a missing Playwright Chromium
|
|
197
|
+
// only surfaces inside pool.acquire, where the launch failure is caught and reported as
|
|
198
|
+
// "could not open <url> — is the app running?" — sending the caller to debug an app that is
|
|
199
|
+
// fine. Say the real thing at the first refusal instead. The phrasing carries "Chromium is not
|
|
200
|
+
// installed" so error-recovery routes it to the NO_POOL fix (install + drive a human tab).
|
|
201
|
+
if (deps.browserProbe !== undefined) {
|
|
202
|
+
const probe = await deps.browserProbe();
|
|
203
|
+
if (!probe.exists) {
|
|
204
|
+
throw new Error(`Chromium is not installed for Playwright — ${chromiumHint(probe)}`);
|
|
172
205
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
206
|
+
}
|
|
207
|
+
const projectId = asString(args['projectId']);
|
|
208
|
+
const sessionId = newLeaseId();
|
|
209
|
+
const navUrl = appendReticleParams(url, sessionId, projectId);
|
|
210
|
+
let lease;
|
|
211
|
+
try {
|
|
212
|
+
lease = await pool.acquire(navUrl, { sessionId });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
// A raw page.goto failure is noisy and leaks the internal URL params — surface a clean,
|
|
216
|
+
// actionable message instead.
|
|
217
|
+
throw new Error(`could not open ${url} — is the app running there? (${cleanNavError(err)})`);
|
|
218
|
+
}
|
|
219
|
+
// Wait for the leased tab's SDK to connect so the returned sessionId is usable right away.
|
|
220
|
+
// Resolved rather than assumed: an app that names its own session registers under that name,
|
|
221
|
+
// and the id we hand back has to be the one the agent can actually drive.
|
|
222
|
+
let registeredId;
|
|
223
|
+
const ready = await waitForLeasedSession(() => {
|
|
224
|
+
registeredId = resolveLeasedSessionId(deps.sessions, lease.sessionId);
|
|
225
|
+
return registeredId !== undefined;
|
|
226
|
+
});
|
|
227
|
+
// Tell the pool the other name this lease answers to. Every later touch and release arrives
|
|
228
|
+
// under the id we are about to hand back, and the pool is keyed by the id it navigated with;
|
|
229
|
+
// without this the touches miss, the lease ages out despite continuous activity, and the
|
|
230
|
+
// reaper closes the context mid-flow. See BrowserPool.alias and #157.
|
|
231
|
+
if (registeredId !== undefined)
|
|
232
|
+
pool.alias(registeredId, lease.sessionId);
|
|
233
|
+
return {
|
|
234
|
+
sessionId: registeredId ?? lease.sessionId,
|
|
235
|
+
url,
|
|
236
|
+
ready,
|
|
237
|
+
expiresInMs: pool.leaseTtlMs(),
|
|
238
|
+
leased: pool.activeCount(),
|
|
239
|
+
queued: pool.queuedCount(),
|
|
240
|
+
...(ready
|
|
241
|
+
? {}
|
|
242
|
+
: (() => {
|
|
243
|
+
const bridgePort = deps.bridgePort ?? RETICLE_DEFAULT_PORT;
|
|
244
|
+
return {
|
|
245
|
+
hint: leaseNotConnectedHint(url, bridgePort, leaseEvidence(deps, bridgePort)),
|
|
246
|
+
};
|
|
247
|
+
})()),
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
export const LEASE_RELEASE_TOOL = {
|
|
252
|
+
name: ReticleTool.LEASE_RELEASE,
|
|
253
|
+
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.',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
sessionId: z
|
|
256
|
+
.string()
|
|
257
|
+
.describe('The leased sessionId returned by reticle_lease{action:"acquire"}.'),
|
|
258
|
+
},
|
|
259
|
+
outputSchema: {
|
|
260
|
+
released: z.boolean(),
|
|
261
|
+
leased: z.number(),
|
|
262
|
+
},
|
|
263
|
+
handler: async (deps, args) => {
|
|
264
|
+
const pool = deps.pool;
|
|
265
|
+
if (pool === undefined)
|
|
266
|
+
throw new Error(POOL_UNAVAILABLE);
|
|
267
|
+
const sessionId = asString(args['sessionId']);
|
|
268
|
+
if (sessionId === undefined || 0 === sessionId.length) {
|
|
269
|
+
throw new Error('reticle_lease{action:"release"} requires a sessionId');
|
|
270
|
+
}
|
|
271
|
+
await pool.release(sessionId);
|
|
272
|
+
return { released: true, leased: pool.activeCount() };
|
|
176
273
|
},
|
|
177
|
-
|
|
274
|
+
};
|
|
275
|
+
export const LEASE_TOOLS = [LEASE_ACQUIRE_TOOL, LEASE_RELEASE_TOOL];
|
|
@@ -30,7 +30,7 @@ arrival = null) {
|
|
|
30
30
|
return {
|
|
31
31
|
...base,
|
|
32
32
|
confirmed: false,
|
|
33
|
-
note: 'ok means the navigation was DISPATCHED, not that the page arrived
|
|
33
|
+
note: 'ok means the navigation was DISPATCHED, not that the page arrived. The SDK is torn down by ' +
|
|
34
34
|
'the navigation itself, so nothing here can see the new document. Call reticle_sessions to ' +
|
|
35
35
|
'confirm a session reconnected at the new URL before acting; if none appears, the page did ' +
|
|
36
36
|
'not load or is not instrumented.',
|
|
@@ -179,7 +179,7 @@ export const OBSERVE_TOOLS = [
|
|
|
179
179
|
// never asked about.
|
|
180
180
|
const actCursor = session.lastAct.cursor();
|
|
181
181
|
const judgingTheAct = actCursor !== undefined && actCursor >= since;
|
|
182
|
-
const contradictions = findContradictions(filtered, judgingTheAct ? session.lastAct.effect() : {});
|
|
182
|
+
const contradictions = findContradictions(filtered, judgingTheAct ? { ...session.lastAct.effect(), actionSince: actCursor } : {});
|
|
183
183
|
// carry session health — a throttled tab means the observed timeline may be incomplete.
|
|
184
184
|
return withControl(session, {
|
|
185
185
|
...report,
|
|
@@ -262,7 +262,7 @@ export const OBSERVE_TOOLS = [
|
|
|
262
262
|
// undocumented here, including `route` — and "did submitting the login form navigate away"
|
|
263
263
|
// is the most common thing an agent wants to assert. A field report reached us from an agent
|
|
264
264
|
// that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
|
|
265
|
-
'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count } ' +
|
|
265
|
+
'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count|bodyContains } ' +
|
|
266
266
|
'{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
|
|
267
267
|
'{ element, testid|role|text } { text } { console, level|absent } { animation, name } ' +
|
|
268
268
|
'{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
|
|
@@ -386,6 +386,10 @@ export const OBSERVE_TOOLS = [
|
|
|
386
386
|
.nonnegative()
|
|
387
387
|
.optional()
|
|
388
388
|
.describe('Keep only the most recent N matching calls (older are dropped and counted in droppedOldest) — cuts tokens on a wide window. Defaults to 200 when omitted; pass a higher number for more, or scope with since/until.'),
|
|
389
|
+
bodies: z
|
|
390
|
+
.boolean()
|
|
391
|
+
.optional()
|
|
392
|
+
.describe('Include request/response bodies (default true). Pass false for a body-free listing — method, url, status, timing only — for the common "did POST /x return 200?" read. Bodies dominate the payload, so this cuts the cheap case by a large factor.'),
|
|
389
393
|
...sessionIdShape,
|
|
390
394
|
},
|
|
391
395
|
outputSchema: {
|
|
@@ -411,6 +415,8 @@ export const OBSERVE_TOOLS = [
|
|
|
411
415
|
const status = asNumber(args['status']);
|
|
412
416
|
const ok = 'boolean' === typeof args['ok'] ? args['ok'] : undefined;
|
|
413
417
|
const limit = asNumber(args['limit']);
|
|
418
|
+
// Default true keeps the current shape; `bodies: false` returns the body-free listing (#401).
|
|
419
|
+
const bodies = 'boolean' === typeof args['bodies'] ? args['bodies'] : true;
|
|
414
420
|
const buffer = bufferEnvelope(session);
|
|
415
421
|
// Completed calls + unresolved in-flight requests (a hung request shows as pending).
|
|
416
422
|
const allNet = reconcileNet(await session.queryEvents({
|
|
@@ -425,14 +431,14 @@ export const OBSERVE_TOOLS = [
|
|
|
425
431
|
}
|
|
426
432
|
// Default the cap so an omitted `limit` can't dump a whole flooded session (since defaults to 0).
|
|
427
433
|
const { events: budgeted, droppedOldest } = applyEventBudget(matched, limit ?? DEFAULT_QUERY_LIMIT);
|
|
428
|
-
const calls = budgeted.map(projectNetCall);
|
|
434
|
+
const calls = budgeted.map((e) => projectNetCall(e, bodies));
|
|
429
435
|
// A zero-match FILTER already reports what did fire (netEmptyHint above). Zero calls at all
|
|
430
436
|
// fell through as a bare `[]`, which is indistinguishable from an observer that is not
|
|
431
437
|
// recording — and those need opposite responses. Say the look happened.
|
|
432
438
|
return withSizeCost(noteEmptyRead({
|
|
433
439
|
calls,
|
|
434
440
|
...(droppedOldest > 0 ? { total: matched.length, droppedOldest } : {}),
|
|
435
|
-
...bodiesNotCaptured(calls),
|
|
441
|
+
...(bodies ? bodiesNotCaptured(calls) : {}),
|
|
436
442
|
...buffer,
|
|
437
443
|
}, 'calls', { noun: 'network calls' }));
|
|
438
444
|
},
|
|
@@ -28,6 +28,6 @@ export function noteEmptyRead(result, key, context) {
|
|
|
28
28
|
...result,
|
|
29
29
|
// The distinction the field sweep could not make: this is a RESULT, not a failure to look.
|
|
30
30
|
observed: true,
|
|
31
|
-
note: `no ${context.noun}${window}
|
|
31
|
+
note: `no ${context.noun}${window}; the observation ran and found none, which is a result, not a missing reading`,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "The app ignored this response" is only sayable once the app has had a chance to read it.
|
|
3
|
+
*
|
|
4
|
+
* `response-ignored` fires when a write succeeded on the server and nothing on the client moved. It
|
|
5
|
+
* is a real and valuable finding — a lost write, a response parsed into the void, a render that never
|
|
6
|
+
* happened. It is also an ACCUSATION, and it is decided by where a window happens to end.
|
|
7
|
+
*
|
|
8
|
+
* Waiting for in-flight requests before taking the verdict made that boundary dangerous in a way it
|
|
9
|
+
* was not before. Previously the write was still pending when the window closed, so it never reached
|
|
10
|
+
* the settled list and this rule could not fire at all. Now the response lands inside the window BY
|
|
11
|
+
* DESIGN — and the app's re-render happens in a later task, a few milliseconds after it. Close the
|
|
12
|
+
* window in that gap and every channel agrees that the app took a successful write and did nothing.
|
|
13
|
+
*
|
|
14
|
+
* The verdict is then `verified:"no"` on a completely correct application, produced entirely by where
|
|
15
|
+
* we chose to stop looking. For a verification tool that is the more damaging direction of error: a
|
|
16
|
+
* false green is a missed catch, but a false accusation sends someone to fix code that is not broken,
|
|
17
|
+
* and it is how the instrument stops being believed.
|
|
18
|
+
*
|
|
19
|
+
* So the response is not the end of the window — the app's REACTION to it is. The grace is paid only
|
|
20
|
+
* in the exact case that would otherwise be accused: a successful mutating write with nothing moved
|
|
21
|
+
* after it. Every other action returns without a single poll.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately short. This covers a task-boundary re-render, not a slow app: if nothing has moved
|
|
24
|
+
* within the grace, "ignored" has become an honest description and the finding should stand.
|
|
25
|
+
*/
|
|
26
|
+
import type { SettleSource } from './settle-in-flight.js';
|
|
27
|
+
/**
|
|
28
|
+
* Is this a window that would be wrongly accused if it ended right now?
|
|
29
|
+
*
|
|
30
|
+
* True only for the accusable shape: a SUCCESSFUL MUTATING request settled, and nothing has moved. A
|
|
31
|
+
* read that changed nothing is a prefetch, a failed write is a different finding entirely, and an
|
|
32
|
+
* action that already moved the UI has nothing to wait for — none of them buy a grace period.
|
|
33
|
+
*
|
|
34
|
+
* "Mutating" is read from the same constant the accuser uses rather than restated here. Restating it
|
|
35
|
+
* is how `IPC` came to be absent from this list and present in that one, so on a desktop app every
|
|
36
|
+
* successful write skipped the grace and a correct app was told it had ignored its own response.
|
|
37
|
+
*/
|
|
38
|
+
export declare function awaitsReaction(events: readonly {
|
|
39
|
+
type: string;
|
|
40
|
+
data: Record<string, unknown>;
|
|
41
|
+
}[]): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Wait, bounded by BOTH the grace and the caller's remaining budget, for the app to react to a write
|
|
44
|
+
* it has just been told succeeded.
|
|
45
|
+
*
|
|
46
|
+
* Returns whether the app moved. A `false` is not a verdict — it hands the question back to the
|
|
47
|
+
* contradiction engine, which will raise `response-ignored` exactly as it does today. The point is
|
|
48
|
+
* only that by then the app has been given its chance, so the finding means what it says.
|
|
49
|
+
*/
|
|
50
|
+
export declare function waitForReaction(session: SettleSource, since: number, budgetMs: number, opts: {
|
|
51
|
+
sleep(ms: number): Promise<void>;
|
|
52
|
+
}): Promise<boolean>;
|