@reticlehq/server 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +702 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +174 -92
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +49 -0
- package/dist/daemon/daemon-resilience.js +191 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +80 -48
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +5 -7
- package/dist/flows/flow-replay.js +114 -47
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +16 -3
- package/dist/honesty/blind-spots.js +24 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +37 -0
- package/dist/init/cra.js +90 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +3 -2
- package/dist/init/detect.js +16 -5
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +52 -9
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +14 -2
- package/dist/session/session.js +69 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +84 -115
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +57 -32
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +88 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures.md +28 -0
- package/docs/flows.md +166 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning "no browser session connected" from a dead end into a next action.
|
|
3
|
+
*
|
|
4
|
+
* This is the most consequential sentence in the product. Measured over a day of telemetry: 74% of
|
|
5
|
+
* sessions never call a Reticle tool, and of the ones that do, half make exactly ONE call — usually
|
|
6
|
+
* `reticle_sessions` — and stop. Ten of those thirteen never touched a browser, and every recorded
|
|
7
|
+
* session error is this one. The agent asks whether anything is connected, is told no, and leaves.
|
|
8
|
+
*
|
|
9
|
+
* The old message asked the agent to check two things it cannot see from where it stands ("is your
|
|
10
|
+
* app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
|
|
11
|
+
* the three cases apart, and each has a different, concrete fix:
|
|
12
|
+
*
|
|
13
|
+
* - a session was here and went away -> the tab closed or reloaded; reopen it
|
|
14
|
+
* - something is listening, never dialled, project not wired -> run `reticle init` in THAT app
|
|
15
|
+
* - something is listening, project wired -> port mismatch or a stale build; restart the dev server
|
|
16
|
+
* - nothing is listening anywhere -> there is no app running; start it
|
|
17
|
+
*
|
|
18
|
+
* Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
|
|
19
|
+
* the hot resolve() path and this stays unit-testable.
|
|
20
|
+
*/
|
|
21
|
+
interface NoSessionFacts {
|
|
22
|
+
/** Whether ANY session has connected to this daemon since it booted. */
|
|
23
|
+
everConnected: boolean;
|
|
24
|
+
/** Whether this project has been through `reticle init` (a .reticle.json / projectId is present). */
|
|
25
|
+
initialized: boolean;
|
|
26
|
+
/** Localhost ports with something listening that looks like a dev server. */
|
|
27
|
+
listening: readonly number[];
|
|
28
|
+
/** The port this daemon is on — half of the mismatch the old message asked about. */
|
|
29
|
+
port: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
|
|
33
|
+
* recovery: a message that already carries its own next action must not be handed a second, more
|
|
34
|
+
* generic one that contradicts it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SELF_RECOVERING_MARKER = "Then call reticle_sessions again \u2014 it will appear within a second of the page loading.";
|
|
37
|
+
export declare function diagnoseNoSession(facts: NoSessionFacts): string;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning "no browser session connected" from a dead end into a next action.
|
|
3
|
+
*
|
|
4
|
+
* This is the most consequential sentence in the product. Measured over a day of telemetry: 74% of
|
|
5
|
+
* sessions never call a Reticle tool, and of the ones that do, half make exactly ONE call — usually
|
|
6
|
+
* `reticle_sessions` — and stop. Ten of those thirteen never touched a browser, and every recorded
|
|
7
|
+
* session error is this one. The agent asks whether anything is connected, is told no, and leaves.
|
|
8
|
+
*
|
|
9
|
+
* The old message asked the agent to check two things it cannot see from where it stands ("is your
|
|
10
|
+
* app running with the SDK enabled?", "does it point at this port?"). The daemon can actually tell
|
|
11
|
+
* the three cases apart, and each has a different, concrete fix:
|
|
12
|
+
*
|
|
13
|
+
* - a session was here and went away -> the tab closed or reloaded; reopen it
|
|
14
|
+
* - something is listening, never dialled, project not wired -> run `reticle init` in THAT app
|
|
15
|
+
* - something is listening, project wired -> port mismatch or a stale build; restart the dev server
|
|
16
|
+
* - nothing is listening anywhere -> there is no app running; start it
|
|
17
|
+
*
|
|
18
|
+
* Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
|
|
19
|
+
* the hot resolve() path and this stays unit-testable.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
|
|
23
|
+
* recovery: a message that already carries its own next action must not be handed a second, more
|
|
24
|
+
* generic one that contradicts it.
|
|
25
|
+
*/
|
|
26
|
+
export const SELF_RECOVERING_MARKER = 'Then call reticle_sessions again — it will appear within a second of the page loading.';
|
|
27
|
+
const RETRY = SELF_RECOVERING_MARKER;
|
|
28
|
+
/**
|
|
29
|
+
* The way out that needs no human at all.
|
|
30
|
+
*
|
|
31
|
+
* `reticle_lease` opens a browser Reticle drives itself, instead of waiting for somebody's tab to
|
|
32
|
+
* dial in. Measured over a day of telemetry it is the single strongest predictor of a session that
|
|
33
|
+
* works: the 5 sessions that used it had a MEDIAN of 30 tool calls and produced 46% of every bug
|
|
34
|
+
* found, against a median of 1 call for the 20 active sessions that did not — and not one
|
|
35
|
+
* single-call bounce used one. It is also advertised on no profile except `full`, so an agent only
|
|
36
|
+
* ever finds it if it already knew it existed. Naming it HERE puts it in front of the agent at the
|
|
37
|
+
* one moment it is the answer, and costs nothing on the turns when it is not.
|
|
38
|
+
*
|
|
39
|
+
* Only offered when the app is known to carry the SDK: leasing an uninstrumented app just burns a
|
|
40
|
+
* browser and comes back `ready:false`.
|
|
41
|
+
*/
|
|
42
|
+
const SELF_SERVE = 'You do not have to wait for the human: reticle_lease {action:"acquire", url} opens a browser ' +
|
|
43
|
+
'Reticle drives itself, and returns a sessionId you can use immediately (reach it with ' +
|
|
44
|
+
'reticle_run {tool:"reticle_lease"} if it is not advertised directly; release it when you finish).';
|
|
45
|
+
export function diagnoseNoSession(facts) {
|
|
46
|
+
const { everConnected, initialized, listening, port } = facts;
|
|
47
|
+
const ports = listening.join(', ');
|
|
48
|
+
if (everConnected) {
|
|
49
|
+
return ('no browser session connected — but one WAS connected to this daemon earlier, so the wiring ' +
|
|
50
|
+
'is correct. The tab was closed, navigated away, or hard-reloaded. Ask the human to reopen ' +
|
|
51
|
+
`the app (or run \`reticle open\`), or reload the tab. ${SELF_SERVE} ${RETRY}`);
|
|
52
|
+
}
|
|
53
|
+
if (0 === listening.length) {
|
|
54
|
+
return ('no browser session connected, and nothing is listening on any of the usual dev-server ports ' +
|
|
55
|
+
'— so the app is almost certainly not running. This is not a Reticle wiring problem: ask the ' +
|
|
56
|
+
`human to start their dev server (\`npm run dev\`), then open the app in a browser. ${RETRY}`);
|
|
57
|
+
}
|
|
58
|
+
if (!initialized) {
|
|
59
|
+
return (`no browser session connected, but something IS listening on port ${ports} — so a server is ` +
|
|
60
|
+
'up and has never dialled this daemon. This project has not been through `reticle init`, ' +
|
|
61
|
+
'which is what installs the SDK and wires it into the build, so the likeliest cause is that ' +
|
|
62
|
+
"the app carries no Reticle SDK. Ask the human to run `reticle init` in the app's directory " +
|
|
63
|
+
`and restart the dev server. ${RETRY}`);
|
|
64
|
+
}
|
|
65
|
+
return (`no browser session connected, but something IS listening on port ${ports} and this project is ` +
|
|
66
|
+
`wired for Reticle — so the app is either serving a build made before the wiring landed, or ` +
|
|
67
|
+
`dialling a different daemon than this one (this daemon is on ${String(port)}). Ask the human ` +
|
|
68
|
+
'to restart the dev server and hard-reload the page; if it still does not appear, check that ' +
|
|
69
|
+
`the app's reticle port matches ${String(port)}. ${SELF_SERVE} ${RETRY}`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the "why is nothing connected" diagnosis fresh, without putting a probe on the hot path.
|
|
3
|
+
*
|
|
4
|
+
* `SessionManager.resolve` is synchronous and runs on every tool call, so it cannot await a port
|
|
5
|
+
* scan. This refreshes the answer in the background and hands the manager a closure that reads the
|
|
6
|
+
* cached result — a hint in an error message is exactly the sort of thing that may be a few seconds
|
|
7
|
+
* stale.
|
|
8
|
+
*
|
|
9
|
+
* It only probes while NOTHING is connected. Once a session is live the question is moot, and a
|
|
10
|
+
* daemon that outlives the agent by hours has no business scanning ports it does not need.
|
|
11
|
+
*/
|
|
12
|
+
import type { SessionManager } from './session-manager.js';
|
|
13
|
+
/**
|
|
14
|
+
* The daemon's whole session-scoping decision in one call: scope auto-selection to the active
|
|
15
|
+
* project, and keep the no-session diagnosis fresh. Both derive from the same one fact — whether
|
|
16
|
+
* this directory has been through `reticle init` — so they belong together rather than as two
|
|
17
|
+
* adjacent blocks in the bootstrap.
|
|
18
|
+
*/
|
|
19
|
+
export declare function wireSessionScope(sessions: SessionManager, activeProjectId: string | undefined, port: number): () => void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the "why is nothing connected" diagnosis fresh, without putting a probe on the hot path.
|
|
3
|
+
*
|
|
4
|
+
* `SessionManager.resolve` is synchronous and runs on every tool call, so it cannot await a port
|
|
5
|
+
* scan. This refreshes the answer in the background and hands the manager a closure that reads the
|
|
6
|
+
* cached result — a hint in an error message is exactly the sort of thing that may be a few seconds
|
|
7
|
+
* stale.
|
|
8
|
+
*
|
|
9
|
+
* It only probes while NOTHING is connected. Once a session is live the question is moot, and a
|
|
10
|
+
* daemon that outlives the agent by hours has no business scanning ports it does not need.
|
|
11
|
+
*/
|
|
12
|
+
import { probeDevServers } from './dev-server-probe.js';
|
|
13
|
+
import { diagnoseNoSession } from './no-session-diagnosis.js';
|
|
14
|
+
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
15
|
+
const REFRESH_MS = 15_000;
|
|
16
|
+
/** Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up. */
|
|
17
|
+
function startNoSessionWatch(options) {
|
|
18
|
+
const probe = options.probe ?? (() => probeDevServers());
|
|
19
|
+
let listening = [];
|
|
20
|
+
let running = false;
|
|
21
|
+
const refresh = () => {
|
|
22
|
+
// Nothing to diagnose while a session is live, and no reason to scan.
|
|
23
|
+
if (running || options.sessions.count() > 0)
|
|
24
|
+
return;
|
|
25
|
+
running = true;
|
|
26
|
+
void probe()
|
|
27
|
+
.then((ports) => {
|
|
28
|
+
listening = ports;
|
|
29
|
+
})
|
|
30
|
+
.catch(() => {
|
|
31
|
+
/* a diagnostic hint must never take the daemon down */
|
|
32
|
+
})
|
|
33
|
+
.finally(() => {
|
|
34
|
+
running = false;
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
refresh();
|
|
38
|
+
const timer = setInterval(refresh, REFRESH_MS);
|
|
39
|
+
timer.unref();
|
|
40
|
+
options.sessions.setNoSessionHint(() => diagnoseNoSession({
|
|
41
|
+
everConnected: options.sessions.everConnected(),
|
|
42
|
+
initialized: options.initialized,
|
|
43
|
+
listening,
|
|
44
|
+
port: options.port,
|
|
45
|
+
}));
|
|
46
|
+
return () => {
|
|
47
|
+
clearInterval(timer);
|
|
48
|
+
options.sessions.setNoSessionHint(undefined);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The daemon's whole session-scoping decision in one call: scope auto-selection to the active
|
|
53
|
+
* project, and keep the no-session diagnosis fresh. Both derive from the same one fact — whether
|
|
54
|
+
* this directory has been through `reticle init` — so they belong together rather than as two
|
|
55
|
+
* adjacent blocks in the bootstrap.
|
|
56
|
+
*/
|
|
57
|
+
export function wireSessionScope(sessions, activeProjectId, port) {
|
|
58
|
+
if (activeProjectId !== undefined)
|
|
59
|
+
sessions.setDefaultScope({ projectId: activeProjectId });
|
|
60
|
+
return startNoSessionWatch({ sessions, port, initialized: activeProjectId !== undefined });
|
|
61
|
+
}
|
|
@@ -42,8 +42,12 @@ export declare class ObservedState {
|
|
|
42
42
|
seedAmbient(counts: AmbientCounts): void;
|
|
43
43
|
/** Record a driven ref. Idempotent. */
|
|
44
44
|
recordActedRef(ref: string): void;
|
|
45
|
+
/** Record a driven control's re-render-surviving label. Idempotent; empty labels are not identities. */
|
|
46
|
+
recordActedLabel(label: string): void;
|
|
45
47
|
/** Every ref driven so far this session. */
|
|
46
48
|
actedRefs(): ReadonlySet<string>;
|
|
49
|
+
/** Every driven control's label, for matching across re-renders. */
|
|
50
|
+
actedLabels(): ReadonlySet<string>;
|
|
47
51
|
/**
|
|
48
52
|
* Record bridge-side sampling as a blind spot.
|
|
49
53
|
*
|
|
@@ -34,7 +34,7 @@ export class ObservedState {
|
|
|
34
34
|
if (event.type === EventType.BLIND_SPOT) {
|
|
35
35
|
const kind = event.data['kind'];
|
|
36
36
|
const count = event.data['count'];
|
|
37
|
-
if (typeof kind
|
|
37
|
+
if ('string' === typeof kind && 'number' === typeof count)
|
|
38
38
|
this.#blindSpots[kind] = count;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -78,15 +78,33 @@ export class ObservedState {
|
|
|
78
78
|
* "recently untouched" — a number that reads as thorough while drifting toward the opposite.
|
|
79
79
|
*/
|
|
80
80
|
#actedRefs = new Set();
|
|
81
|
+
/**
|
|
82
|
+
* The same drives, keyed by something that SURVIVES a re-render.
|
|
83
|
+
*
|
|
84
|
+
* A ref is invalidated whenever the DOM changes, so on a framework that replaces nodes rather than
|
|
85
|
+
* reconciling in place (Next's app router, for one) every driven ref is stale by the time coverage
|
|
86
|
+
* is asked and the number reads 0 forever. The snapshot label — `button "Deploy"` — names the same
|
|
87
|
+
* control on both sides of a re-render.
|
|
88
|
+
*/
|
|
89
|
+
#actedLabels = new Set();
|
|
81
90
|
/** Record a driven ref. Idempotent. */
|
|
82
91
|
recordActedRef(ref) {
|
|
83
92
|
if (ref.length > 0)
|
|
84
93
|
this.#actedRefs.add(ref);
|
|
85
94
|
}
|
|
95
|
+
/** Record a driven control's re-render-surviving label. Idempotent; empty labels are not identities. */
|
|
96
|
+
recordActedLabel(label) {
|
|
97
|
+
if (label.length > 0)
|
|
98
|
+
this.#actedLabels.add(label);
|
|
99
|
+
}
|
|
86
100
|
/** Every ref driven so far this session. */
|
|
87
101
|
actedRefs() {
|
|
88
102
|
return this.#actedRefs;
|
|
89
103
|
}
|
|
104
|
+
/** Every driven control's label, for matching across re-renders. */
|
|
105
|
+
actedLabels() {
|
|
106
|
+
return this.#actedLabels;
|
|
107
|
+
}
|
|
90
108
|
/**
|
|
91
109
|
* Record bridge-side sampling as a blind spot.
|
|
92
110
|
*
|
|
@@ -58,7 +58,7 @@ export function sizeCost(payload) {
|
|
|
58
58
|
* Non-object results (e.g. a thrown-error envelope) pass through unchanged.
|
|
59
59
|
*/
|
|
60
60
|
export function withSizeCost(result) {
|
|
61
|
-
if (typeof result !== 'object' ||
|
|
61
|
+
if (typeof result !== 'object' || null === result)
|
|
62
62
|
return result;
|
|
63
63
|
return { ...result, cost: sizeCost(result) };
|
|
64
64
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BrowserBrand } from '@reticlehq/core';
|
|
1
2
|
import type { Session } from './session.js';
|
|
2
3
|
/**
|
|
3
4
|
* The health block spliced onto act/assert results. Defined here rather than in session.ts: this is
|
|
@@ -45,12 +46,18 @@ export declare function healthEnvelope(session: Session): HealthEnvelope;
|
|
|
45
46
|
*/
|
|
46
47
|
export declare function refuseIfThrottled(session: Session, refuse: unknown): void;
|
|
47
48
|
/** The page's own visibility/runtime report, narrowed out of an untrusted PAGE_HEALTH payload. */
|
|
48
|
-
|
|
49
|
+
interface HealthReport {
|
|
49
50
|
hidden: boolean | undefined;
|
|
50
51
|
focused: boolean | undefined;
|
|
51
52
|
runtime: string | undefined;
|
|
52
53
|
/** Coarse rendering engine (blink/gecko/webkit) — context for a feedback report, not a health input. */
|
|
53
54
|
engine: string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Which browser the page is. Narrowed to `BrowserBrand` HERE rather than trusted: the SDK already
|
|
57
|
+
* normalises, so anything else is a stale or hand-forged payload and is dropped — the point of the
|
|
58
|
+
* closed list is that an unbounded string can never reach telemetry through it.
|
|
59
|
+
*/
|
|
60
|
+
brand: BrowserBrand | undefined;
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
63
|
* Narrow a PAGE_HEALTH payload. Every field is optional on the wire — an older SDK does not report
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BUFFER_EVICTION_WARNING, THROTTLED_WARNING } from '@reticlehq/core';
|
|
1
|
+
import { BrowserBrand, BUFFER_EVICTION_WARNING, THROTTLED_WARNING } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* Buffer-honesty envelope for observe/network/console. When the ring buffer has evicted anything
|
|
4
4
|
* (age/size cap), a "no such event" answer may be a false negative — so we attach the drop count and
|
|
@@ -7,7 +7,7 @@ import { BUFFER_EVICTION_WARNING, THROTTLED_WARNING } from '@reticlehq/core';
|
|
|
7
7
|
*/
|
|
8
8
|
export function bufferEnvelope(session) {
|
|
9
9
|
const { total, dropped } = session.bufferHealth();
|
|
10
|
-
if (
|
|
10
|
+
if (0 === dropped)
|
|
11
11
|
return {};
|
|
12
12
|
return { buffer: { held: total, dropped, note: BUFFER_EVICTION_WARNING } };
|
|
13
13
|
}
|
|
@@ -31,7 +31,7 @@ export function healthEnvelope(session) {
|
|
|
31
31
|
* warn-only so background testing never breaks.
|
|
32
32
|
*/
|
|
33
33
|
export function refuseIfThrottled(session, refuse) {
|
|
34
|
-
if (
|
|
34
|
+
if (true === refuse && session.throttled()) {
|
|
35
35
|
throw new Error(`refusing to act: ${THROTTLED_WARNING}`);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -42,9 +42,10 @@ export function refuseIfThrottled(session, refuse) {
|
|
|
42
42
|
*/
|
|
43
43
|
export function readHealthEvent(data) {
|
|
44
44
|
return {
|
|
45
|
-
hidden: typeof data['hidden']
|
|
46
|
-
focused: typeof data['focused']
|
|
47
|
-
runtime: typeof data['runtime']
|
|
48
|
-
engine: typeof data['engine']
|
|
45
|
+
hidden: 'boolean' === typeof data['hidden'] ? data['hidden'] : undefined,
|
|
46
|
+
focused: 'boolean' === typeof data['focused'] ? data['focused'] : undefined,
|
|
47
|
+
runtime: 'string' === typeof data['runtime'] ? data['runtime'] : undefined,
|
|
48
|
+
engine: 'string' === typeof data['engine'] ? data['engine'] : undefined,
|
|
49
|
+
brand: Object.values(BrowserBrand).find((known) => known === data['brand']),
|
|
49
50
|
};
|
|
50
51
|
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* The shape `reticle_sessions` returns for one connected tab.
|
|
3
|
-
*
|
|
4
|
-
* Split from session.ts when that file crossed the 600-line cap. A pure description of what a
|
|
5
|
-
* session looks like from the outside has no reason to live inside the class that happens to build
|
|
6
|
-
* it, and separating them keeps the wire-facing shape reviewable on its own.
|
|
7
|
-
*/
|
|
1
|
+
import type { SessionHealth } from './session-health.js';
|
|
8
2
|
export interface SessionInfo {
|
|
9
3
|
sessionId: string;
|
|
10
4
|
url: string;
|
|
@@ -13,6 +7,8 @@ export interface SessionInfo {
|
|
|
13
7
|
title: string;
|
|
14
8
|
adapters: string[];
|
|
15
9
|
hasCapabilities: boolean;
|
|
10
|
+
/** Present only when the page's SDK version differs from the daemon's — see version-skew.ts. */
|
|
11
|
+
versionSkew?: string;
|
|
16
12
|
/** ms since the SDK last reported anything (silence ⇒ likely throttled). */
|
|
17
13
|
lastSeenMs: number;
|
|
18
14
|
hidden: boolean;
|
|
@@ -27,3 +23,26 @@ export interface SessionInfo {
|
|
|
27
23
|
/** present with pendingMarks — nudges the agent to drain them with reticle_review. */
|
|
28
24
|
review_suggestion?: string;
|
|
29
25
|
}
|
|
26
|
+
/** The read-only slice of a Session that the projection needs. Keeps this module class-free. */
|
|
27
|
+
interface SessionView {
|
|
28
|
+
id: string;
|
|
29
|
+
url: string;
|
|
30
|
+
projectId: string | undefined;
|
|
31
|
+
title: string;
|
|
32
|
+
adapters: string[];
|
|
33
|
+
hasCapabilities: boolean;
|
|
34
|
+
versionSkew: string | undefined;
|
|
35
|
+
hidden: boolean;
|
|
36
|
+
health: () => SessionHealth;
|
|
37
|
+
staleMs: () => number;
|
|
38
|
+
pendingMarkCount: () => number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Project a live Session into the shape the tools return.
|
|
42
|
+
*
|
|
43
|
+
* Lives beside the SessionInfo type rather than on the class: it is a pure read of already-computed
|
|
44
|
+
* state, it is where every "what does the agent see about this tab" decision belongs, and Session is
|
|
45
|
+
* a stateful class already at its size cap — every field added to a listing was costing a line there.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildSessionInfo(session: SessionView): SessionInfo;
|
|
48
|
+
export {};
|
|
@@ -1 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The shape `reticle_sessions` returns for one connected tab.
|
|
3
|
+
*
|
|
4
|
+
* Split from session.ts when that file crossed the 600-line cap. A pure description of what a
|
|
5
|
+
* session looks like from the outside has no reason to live inside the class that happens to build
|
|
6
|
+
* it, and separating them keeps the wire-facing shape reviewable on its own.
|
|
7
|
+
*/
|
|
8
|
+
import { SESSION_LEASE } from '@reticlehq/core';
|
|
9
|
+
/**
|
|
10
|
+
* Project a live Session into the shape the tools return.
|
|
11
|
+
*
|
|
12
|
+
* Lives beside the SessionInfo type rather than on the class: it is a pure read of already-computed
|
|
13
|
+
* state, it is where every "what does the agent see about this tab" decision belongs, and Session is
|
|
14
|
+
* a stateful class already at its size cap — every field added to a listing was costing a line there.
|
|
15
|
+
*/
|
|
16
|
+
export function buildSessionInfo(session) {
|
|
17
|
+
const base = {
|
|
18
|
+
sessionId: session.id,
|
|
19
|
+
url: session.url,
|
|
20
|
+
...(session.projectId === undefined ? {} : { projectId: session.projectId }),
|
|
21
|
+
title: session.title,
|
|
22
|
+
adapters: session.adapters,
|
|
23
|
+
hasCapabilities: session.hasCapabilities,
|
|
24
|
+
// On every listing, not buried in a log — skew explains failures that read as app bugs.
|
|
25
|
+
...(session.versionSkew === undefined ? {} : { versionSkew: session.versionSkew }),
|
|
26
|
+
hidden: session.hidden,
|
|
27
|
+
...session.health(),
|
|
28
|
+
};
|
|
29
|
+
if (session.staleMs() > SESSION_LEASE.STALE_AFTER_MS) {
|
|
30
|
+
base.stale = true;
|
|
31
|
+
base.cleanup_suggestion =
|
|
32
|
+
'Call reticle_session{action:"end"} to free this session before starting new work.';
|
|
33
|
+
}
|
|
34
|
+
// Surface human bug reports in reticle_sessions (only when > 0, so a clean session adds nothing).
|
|
35
|
+
const marks = session.pendingMarkCount();
|
|
36
|
+
if (marks > 0) {
|
|
37
|
+
base.pendingMarks = marks;
|
|
38
|
+
const s = 1 === marks ? '' : 's';
|
|
39
|
+
base.review_suggestion = `The human flagged ${String(marks)} issue${s} on this tab — call reticle_session{action:"review"} to see and fix ${1 === marks ? 'it' : 'them'}.`;
|
|
40
|
+
}
|
|
41
|
+
return base;
|
|
42
|
+
}
|
|
@@ -11,15 +11,4 @@ export interface SessionLease {
|
|
|
11
11
|
opened_at: number;
|
|
12
12
|
IMPORTANT: string;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Kept SHORT on purpose, and this is the reason to read before editing it.
|
|
16
|
-
*
|
|
17
|
-
* This block is a fixed, identical cost paid by EVERY session. The long-form version was 433 bytes
|
|
18
|
-
* on a `reticle_query` whose actual answer was 259 — 57% of the first call of every session spent
|
|
19
|
-
* re-explaining something that never varies. Measured at 815 → 1058 average observation tokens, and
|
|
20
|
-
* the benchmark's fresh-session-per-scenario shape pays it ten times over.
|
|
21
|
-
*
|
|
22
|
-
* Any edit here is a per-session tax on every user: say it in as few words as still work.
|
|
23
|
-
*/
|
|
24
|
-
export declare const SESSION_LEASE_NOTICE = "When you stop driving, call reticle_session {action:\"yield\", mode:\"waiting\"|\"ask\"} \u2014 the panel reads \"live\" until you do. {action:\"end\"} when the task is done; it revives on your next action.";
|
|
25
14
|
export declare function buildSessionLease(sessionId: string, openedAt: number): SessionLease;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Any edit here is a per-session tax on every user: say it in as few words as still work.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
const SESSION_LEASE_NOTICE = 'When you stop driving, call reticle_session {action:"yield", mode:"waiting"|"ask"} — the panel reads "live" until you do. {action:"end"} when the task is done; it revives on your next action.';
|
|
12
12
|
export function buildSessionLease(sessionId, openedAt) {
|
|
13
13
|
return { sessionId, opened_at: openedAt, IMPORTANT: SESSION_LEASE_NOTICE };
|
|
14
14
|
}
|
|
@@ -21,6 +21,9 @@ export declare class SessionManager {
|
|
|
21
21
|
/** Every connected session — used by the liveness reaper to sweep for idle/disconnected ones. */
|
|
22
22
|
all(): Session[];
|
|
23
23
|
count(): number;
|
|
24
|
+
/** Wire the diagnosis provider (daemon boot). Absent ⇒ the plain, static message. */
|
|
25
|
+
setNoSessionHint(hint: (() => string | undefined) | undefined): void;
|
|
26
|
+
everConnected(): boolean;
|
|
24
27
|
/** Record a bridge-initiated close so `resolve` can explain a session that vanished. */
|
|
25
28
|
noteClosure(reason: string, at: number): void;
|
|
26
29
|
/** The most recent bridge-initiated close, if any. */
|
|
@@ -29,14 +29,29 @@ function scopeSessions(sessions, scope) {
|
|
|
29
29
|
}
|
|
30
30
|
return sessions;
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Honest, scoped error when sessions exist but none match the agent's project.
|
|
34
|
+
*
|
|
35
|
+
* The daemon takes its default scope from the `.reticle.json` of whichever directory started it, so
|
|
36
|
+
* attaching from a second project — or another worktree of the SAME project — makes every tool
|
|
37
|
+
* refuse while `reticle_sessions` shows the tab sitting right there. The old message asked "is that
|
|
38
|
+
* app running with @reticlehq/core enabled?", which is the one thing that is definitely true, so the
|
|
39
|
+
* reader goes looking at their app instead of at the scope. Name what IS connected and how to
|
|
40
|
+
* target it: both facts are already in hand here.
|
|
41
|
+
*/
|
|
42
|
+
function scopeMissError(connected, scope) {
|
|
34
43
|
const who = scope?.projectId !== undefined
|
|
35
44
|
? `project '${scope.projectId}'`
|
|
36
45
|
: scope?.url !== undefined
|
|
37
46
|
? `your app at ${scope.url}`
|
|
38
47
|
: 'the active project';
|
|
39
|
-
|
|
48
|
+
const listed = connected
|
|
49
|
+
.map((s) => `'${s.projectId ?? 'untagged'}' (${s.url}, sessionId '${s.id}')`)
|
|
50
|
+
.join(', ');
|
|
51
|
+
return (`no browser session for ${who}, but ${String(connected.length)} session(s) ARE connected under a ` +
|
|
52
|
+
`different project: ${listed}. The daemon scopes to the .reticle.json of the directory it was ` +
|
|
53
|
+
`started in, so this is a scope mismatch, not a dead app. Pass the sessionId above to target one, ` +
|
|
54
|
+
`or restart the daemon from that app's directory.`);
|
|
40
55
|
}
|
|
41
56
|
/**
|
|
42
57
|
* Owns the set of connected browser sessions and the smart auto-selection that resolves which one a
|
|
@@ -58,6 +73,7 @@ export class SessionManager {
|
|
|
58
73
|
this.#defaultScope = scope;
|
|
59
74
|
}
|
|
60
75
|
add(session) {
|
|
76
|
+
this.#everConnected = true;
|
|
61
77
|
const previous = this.#sessions.get(session.id);
|
|
62
78
|
this.#sessions.set(session.id, session);
|
|
63
79
|
// Publish what this app declared sensitive to the driven-path rule. Here rather than in the
|
|
@@ -112,6 +128,25 @@ export class SessionManager {
|
|
|
112
128
|
* was hung up on. Keeping the last few reasons turns an unexplained disappearance into a fact.
|
|
113
129
|
*/
|
|
114
130
|
#recentClosures = [];
|
|
131
|
+
/**
|
|
132
|
+
* What the daemon knows about WHY nothing is connected, refreshed in the background.
|
|
133
|
+
*
|
|
134
|
+
* Measured: of the sessions that call any tool, half make exactly one call — usually
|
|
135
|
+
* reticle_sessions — and stop, because the answer they get names two things they cannot check and
|
|
136
|
+
* nothing they can do. The daemon can tell "no app is running" from "an app is running and never
|
|
137
|
+
* dialled us" from "one was connected and left", and each has a different fix. Optional so a
|
|
138
|
+
* bridge constructed without a daemon (every unit test) keeps the plain message.
|
|
139
|
+
*/
|
|
140
|
+
#noSessionHint;
|
|
141
|
+
/** Wire the diagnosis provider (daemon boot). Absent ⇒ the plain, static message. */
|
|
142
|
+
setNoSessionHint(hint) {
|
|
143
|
+
this.#noSessionHint = hint;
|
|
144
|
+
}
|
|
145
|
+
/** Whether any session has connected since this daemon booted — half the diagnosis. */
|
|
146
|
+
#everConnected = false;
|
|
147
|
+
everConnected() {
|
|
148
|
+
return this.#everConnected;
|
|
149
|
+
}
|
|
115
150
|
/** Record a bridge-initiated close so `resolve` can explain a session that vanished. */
|
|
116
151
|
noteClosure(reason, at) {
|
|
117
152
|
this.#recentClosures.push({ at, reason });
|
|
@@ -131,8 +166,12 @@ export class SessionManager {
|
|
|
131
166
|
found.markAgentActivity(); // liveness — a targeted tool keeps the session alive / revives it
|
|
132
167
|
return found;
|
|
133
168
|
}
|
|
134
|
-
if (this.#sessions.size
|
|
169
|
+
if (0 === this.#sessions.size) {
|
|
135
170
|
const closure = this.lastClosure();
|
|
171
|
+
// A diagnosis beats a checklist: it names which of the three causes this actually is.
|
|
172
|
+
const hint = this.#noSessionHint?.();
|
|
173
|
+
if (hint !== undefined && closure === undefined)
|
|
174
|
+
throw new Error(hint);
|
|
136
175
|
throw new Error(closure === undefined
|
|
137
176
|
? NO_SESSION_CONNECTED_ERROR
|
|
138
177
|
: `${NO_SESSION_CONNECTED_ERROR} NOTE: the bridge closed a session recently — "${closure.reason}". The app is probably still running; it was disconnected, and the SDK does not retry after a policy close. Reload the page to reconnect.`);
|
|
@@ -142,12 +181,16 @@ export class SessionManager {
|
|
|
142
181
|
// candidate set, no matter how recently it was heard from. This is the anti-cross-talk guard.
|
|
143
182
|
// An explicit per-call scope wins; otherwise the daemon's active-project default applies.
|
|
144
183
|
const effectiveScope = scope ?? this.#defaultScope;
|
|
145
|
-
const
|
|
146
|
-
|
|
184
|
+
const connected = [...this.#sessions.values()];
|
|
185
|
+
const all = scopeSessions(connected, effectiveScope);
|
|
186
|
+
if (0 === all.length) {
|
|
147
187
|
// Sessions exist, but none belong to the scoped project — never fall back to a foreign tab.
|
|
148
|
-
|
|
188
|
+
// ponytail: still a refusal, deliberately. Auto-targeting the only connected tab would be the
|
|
189
|
+
// friendlier 90% case and would also silently defeat the anti-cross-talk guard this scope
|
|
190
|
+
// exists to be. Naming the tab and its sessionId costs the agent one extra argument.
|
|
191
|
+
throw new Error(scopeMissError(connected, effectiveScope));
|
|
149
192
|
}
|
|
150
|
-
if (all.length
|
|
193
|
+
if (1 === all.length) {
|
|
151
194
|
const [only] = all;
|
|
152
195
|
if (only === undefined)
|
|
153
196
|
throw new Error('session lookup failed');
|
|
@@ -173,7 +216,7 @@ export class SessionManager {
|
|
|
173
216
|
// desktop), the gap requirement is dropped: every session is already in "background" mode
|
|
174
217
|
// so we just pick the one with the freshest heartbeat and let the agent proceed. Requiring
|
|
175
218
|
// a gap here only produces spurious "ambiguous" errors while the user works elsewhere.
|
|
176
|
-
const allThrottled =
|
|
219
|
+
const allThrottled = 1 === bestScore;
|
|
177
220
|
const RECENCY_GAP_MS = allThrottled ? 0 : 1_000;
|
|
178
221
|
const clearWinner = runnerUp === undefined || best.ms + RECENCY_GAP_MS < runnerUp.ms;
|
|
179
222
|
if (!clearWinner) {
|
|
@@ -16,7 +16,7 @@ interface WaitForReadyOptions {
|
|
|
16
16
|
now: () => number;
|
|
17
17
|
/** Injected delay between polls. */
|
|
18
18
|
sleep: (ms: number) => Promise<void>;
|
|
19
|
-
/** Poll interval (ms). Default
|
|
19
|
+
/** Poll interval (ms). Default 25 — see DEFAULT_POLL_MS. */
|
|
20
20
|
pollMs?: number;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Same reasoning as session-reconnect: the check is an in-memory lookup and the thing awaited is an
|
|
3
|
+
* event (a session registering), so a coarse grid only adds latency to the first call an agent
|
|
4
|
+
* makes. Four times the checks of something with no I/O costs nothing measurable.
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_POLL_MS = 25;
|
|
2
7
|
/**
|
|
3
8
|
* One-line orientation for a FRESH agent — the Reticle loop, returned by reticle_wait_ready (the first call)
|
|
4
9
|
* so an agent learns how to drive Reticle without reading docs. Named so it is not a free string; kept
|
|
@@ -20,7 +20,7 @@ const IDLE_WAITING_SUMMARY = 'Agent idle — your turn. Continue in your termina
|
|
|
20
20
|
* it rides the end banner so the human can copy it into their terminal. No notes → the base unchanged.
|
|
21
21
|
*/
|
|
22
22
|
export function composeEndedNotice(base, undelivered) {
|
|
23
|
-
if (undelivered.length
|
|
23
|
+
if (0 === undelivered.length)
|
|
24
24
|
return base;
|
|
25
25
|
const notes = undelivered.map((text) => `"${text}"`).join(', ');
|
|
26
26
|
return `${base} · ${UNDELIVERED_NOTES_LABEL} ${notes}`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block until a reloaded page has re-announced itself under the same session id.
|
|
3
|
+
*
|
|
4
|
+
* A reload tears the SDK down. The id survives (see session-continuity), and the daemon replaces the
|
|
5
|
+
* old Session with a new one when the page says HELLO again — so "is it back?" is exactly "does a
|
|
6
|
+
* DIFFERENT object hold this id now?". Identity, not a flag: the returning page always constructs a
|
|
7
|
+
* new Session, and nothing else can claim an id it already owns.
|
|
8
|
+
*
|
|
9
|
+
* Polls a live getter rather than subscribing, because there is no reconnect event to subscribe to
|
|
10
|
+
* and inventing one for this would be a wire change for a two-second wait.
|
|
11
|
+
*/
|
|
12
|
+
interface WaitForReconnectOptions {
|
|
13
|
+
/** The session registered under this id right now, or undefined if none is. */
|
|
14
|
+
current: () => object | undefined;
|
|
15
|
+
/** The session object that was live when the reload was dispatched. */
|
|
16
|
+
previous: object;
|
|
17
|
+
/** Give up after this much elapsed time and let the caller report it honestly. */
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
now: () => number;
|
|
20
|
+
sleep: (ms: number) => Promise<void>;
|
|
21
|
+
pollMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* How long a reload is worth waiting for before answering "not confirmed".
|
|
25
|
+
*
|
|
26
|
+
* A production build is back in well under a second; a cold dev-server route rebuild is the slow
|
|
27
|
+
* case this budget is sized for. It is a CEILING, not a delay — the common path returns on the first
|
|
28
|
+
* or second poll.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RELOAD_RECONNECT_TIMEOUT_MS = 5000;
|
|
31
|
+
export declare function waitForReconnect(opts: WaitForReconnectOptions): Promise<boolean>;
|
|
32
|
+
export {};
|