@reticlehq/server 2.12.0 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +67 -109
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +247 -164
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +239 -22
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +30 -12
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +3 -1
- package/dist/init/dev-script.js +8 -5
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +22 -1
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +61 -3
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +68 -0
- package/dist/setup/dev-server-wait.js +67 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +203 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +56 -8
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +18 -0
- package/dist/update/update-nudge.js +20 -0
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
|
@@ -22,6 +22,21 @@ export interface PooledPage {
|
|
|
22
22
|
* "the page said nothing", never as "the page dialled correctly".
|
|
23
23
|
*/
|
|
24
24
|
onConsole?(handler: (text: string) => void): void;
|
|
25
|
+
/**
|
|
26
|
+
* Capture the page as a PNG. OPTIONAL, like `onConsole`: a fake that does not implement it makes
|
|
27
|
+
* the pool report "no provider", which is the correct degradation — an absent screenshotter must
|
|
28
|
+
* read as "this context cannot be captured", never as a blank image that a visual diff would then
|
|
29
|
+
* compare against and pass.
|
|
30
|
+
*/
|
|
31
|
+
screenshot?(opts?: {
|
|
32
|
+
fullPage?: boolean;
|
|
33
|
+
}): Promise<Uint8Array>;
|
|
34
|
+
/**
|
|
35
|
+
* Move the real pointer to (x, y) so CSS `:hover` applies. OPTIONAL, like `screenshot`: a fake
|
|
36
|
+
* that does not implement it makes hover refuse rather than dispatch a synthetic mouseover that
|
|
37
|
+
* reports dispatched/settled while the styles never ran.
|
|
38
|
+
*/
|
|
39
|
+
hover?(x: number, y: number): Promise<void>;
|
|
25
40
|
}
|
|
26
41
|
/** An isolated browsing context (cookies/storage). Real Playwright `BrowserContext` satisfies this. */
|
|
27
42
|
export interface PooledContext {
|
|
@@ -74,6 +89,16 @@ export declare class BrowserPool {
|
|
|
74
89
|
queuedCount(): number;
|
|
75
90
|
/** The sessionIds of every currently leased context — used to report/group leased sessions. */
|
|
76
91
|
leasedSessionIds(): string[];
|
|
92
|
+
/**
|
|
93
|
+
* The public session id of an active lease on this origin, if we already hold one.
|
|
94
|
+
*
|
|
95
|
+
* The lease TOOL reuses that id instead of minting a second context: a second acquire on the same
|
|
96
|
+
* origin used to leave both tabs connected and poison default session resolution (#600). The pool's
|
|
97
|
+
* own `acquire` still mints — the parallel suite needs isolation on purpose.
|
|
98
|
+
*
|
|
99
|
+
* Prefers the alias the agent was given, when the app registered under its own name.
|
|
100
|
+
*/
|
|
101
|
+
leaseIdOnOrigin(origin: string): string | undefined;
|
|
77
102
|
/**
|
|
78
103
|
* Release a lease by its sessionId (closes the context, frees the slot). No-op if unknown.
|
|
79
104
|
*
|
|
@@ -101,16 +126,50 @@ export declare class BrowserPool {
|
|
|
101
126
|
* Reclaim leases untouched for longer than the TTL — a hung/crashed agent never holds a slot
|
|
102
127
|
* forever. Returns the released sessionIds. This is the fault-tolerance backstop for the pool.
|
|
103
128
|
*/
|
|
129
|
+
/**
|
|
130
|
+
* Capture a leased page, or undefined when this session is not a lease or cannot be captured.
|
|
131
|
+
*
|
|
132
|
+
* A lease IS a real browser page, so the pixels were always available — the visual tools simply had
|
|
133
|
+
* no route to them and answered "no provider" for every context an agent can actually get. That
|
|
134
|
+
* made visual regression impossible on exactly the isolated contexts the pool exists to hand out.
|
|
135
|
+
*
|
|
136
|
+
* Touches the lease like any other tool call, so capturing keeps it alive rather than letting the
|
|
137
|
+
* reaper take it out from under a diff.
|
|
138
|
+
*/
|
|
139
|
+
screenshotLease(sessionId: string, opts?: {
|
|
140
|
+
fullPage?: boolean;
|
|
141
|
+
}): Promise<Uint8Array | undefined>;
|
|
142
|
+
/**
|
|
143
|
+
* Move the real pointer on a leased page, or false when this session is not a lease or cannot
|
|
144
|
+
* hover. Same optional-capability contract as screenshotLease: absence must read as "could not
|
|
145
|
+
* hover", never as a successful dispatch of a synthetic mouseover.
|
|
146
|
+
*
|
|
147
|
+
* Touches the lease like any other tool call, so hovering keeps it alive.
|
|
148
|
+
*/
|
|
149
|
+
hoverLease(sessionId: string, x: number, y: number): Promise<boolean>;
|
|
104
150
|
sweepExpired(): Promise<string[]>;
|
|
105
151
|
/**
|
|
106
152
|
* How many leases this pool has reclaimed for going stale.
|
|
107
153
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* because nothing here knows WHICH session the caller just lost.
|
|
154
|
+
* Kept for reporting. NOT a basis for diagnosis: it is a lifetime total that never resets, so
|
|
155
|
+
* `count > 0` latches true forever after the first reap. Ask `wasReapedLease` instead, which
|
|
156
|
+
* answers about a specific session.
|
|
112
157
|
*/
|
|
113
158
|
reapedLeaseCount(): number;
|
|
159
|
+
/**
|
|
160
|
+
* Did THIS session id belong to a lease this pool aged out?
|
|
161
|
+
*
|
|
162
|
+
* The question the no-session diagnosis actually has. It used to ask `reapedLeaseCount() > 0`,
|
|
163
|
+
* which meant that once any lease had ever aged out, every closed human tab for the rest of the
|
|
164
|
+
* daemon's life was reported as an expired lease — and the recovery that followed
|
|
165
|
+
* (`reticle_lease {acquire}`) would open an unauthenticated headless context and lose the app
|
|
166
|
+
* session (#611).
|
|
167
|
+
*
|
|
168
|
+
* Only the last {@link MAX_REMEMBERED_REAPED_LEASES} are remembered, so this answers `false` for
|
|
169
|
+
* a lease reaped long enough ago that thousands have followed. That direction is the safe one:
|
|
170
|
+
* it falls back to the generic "the tab was closed" wording rather than inventing a lease.
|
|
171
|
+
*/
|
|
172
|
+
wasReapedLease(sessionId: string): boolean;
|
|
114
173
|
/**
|
|
115
174
|
* Lease an isolated context, navigate it to `url`, and return a handle. If the pool is at capacity,
|
|
116
175
|
* waits FIFO until a slot frees (or until `signal` aborts, if provided).
|
|
@@ -15,6 +15,11 @@ import { unreachableUrlIn } from '@reticlehq/core';
|
|
|
15
15
|
export const DEFAULT_LEASE_TTL_MS = 5 * 60_000;
|
|
16
16
|
/** Default cap on how long a lease's initial navigation may take before it fails (frees the slot). */
|
|
17
17
|
const DEFAULT_NAV_TIMEOUT_MS = 30_000;
|
|
18
|
+
/**
|
|
19
|
+
* How many reaped lease ids to remember. Only the most recent departure is ever asked about, so
|
|
20
|
+
* this is slack for a burst sweep, not a history.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_REMEMBERED_REAPED_LEASES = 64;
|
|
18
23
|
/**
|
|
19
24
|
* Owns a single browser and leases isolated contexts out of it, capped at `maxContexts`.
|
|
20
25
|
* Not exported as a singleton — the daemon owns one instance.
|
|
@@ -31,6 +36,18 @@ export class BrowserPool {
|
|
|
31
36
|
#closed = false;
|
|
32
37
|
/** Leases reclaimed for going stale — see reapedLeaseCount(). */
|
|
33
38
|
#reapedLeases = 0;
|
|
39
|
+
/**
|
|
40
|
+
* The session ids of recently reaped leases, most recent last.
|
|
41
|
+
*
|
|
42
|
+
* The count above answers "has anything ever aged out", which is not a
|
|
43
|
+
* question any caller actually has: it latches true for the life of the
|
|
44
|
+
* daemon (#611). The ids answer the question the no-session diagnosis is
|
|
45
|
+
* really asking — was THIS session a lease that aged out — so a closed
|
|
46
|
+
* human tab is no longer blamed on an expired lease forever after.
|
|
47
|
+
*
|
|
48
|
+
* Bounded because a long-lived daemon reaps unboundedly many.
|
|
49
|
+
*/
|
|
50
|
+
#reapedLeaseIds = [];
|
|
34
51
|
#active = new Map();
|
|
35
52
|
/** Registered-session-id → lease id, for apps that keep their own session name. See `alias`. */
|
|
36
53
|
#aliases = new Map();
|
|
@@ -73,6 +90,34 @@ export class BrowserPool {
|
|
|
73
90
|
leasedSessionIds() {
|
|
74
91
|
return [...this.#active.keys()];
|
|
75
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The public session id of an active lease on this origin, if we already hold one.
|
|
95
|
+
*
|
|
96
|
+
* The lease TOOL reuses that id instead of minting a second context: a second acquire on the same
|
|
97
|
+
* origin used to leave both tabs connected and poison default session resolution (#600). The pool's
|
|
98
|
+
* own `acquire` still mints — the parallel suite needs isolation on purpose.
|
|
99
|
+
*
|
|
100
|
+
* Prefers the alias the agent was given, when the app registered under its own name.
|
|
101
|
+
*/
|
|
102
|
+
leaseIdOnOrigin(origin) {
|
|
103
|
+
for (const [leaseId, lease] of this.#active) {
|
|
104
|
+
let leaseOrigin;
|
|
105
|
+
try {
|
|
106
|
+
leaseOrigin = new URL(lease.url).origin;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (leaseOrigin !== origin)
|
|
112
|
+
continue;
|
|
113
|
+
for (const [alias, id] of this.#aliases) {
|
|
114
|
+
if (id === leaseId)
|
|
115
|
+
return alias;
|
|
116
|
+
}
|
|
117
|
+
return leaseId;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
76
121
|
/**
|
|
77
122
|
* Release a lease by its sessionId (closes the context, frees the slot). No-op if unknown.
|
|
78
123
|
*
|
|
@@ -116,6 +161,50 @@ export class BrowserPool {
|
|
|
116
161
|
* Reclaim leases untouched for longer than the TTL — a hung/crashed agent never holds a slot
|
|
117
162
|
* forever. Returns the released sessionIds. This is the fault-tolerance backstop for the pool.
|
|
118
163
|
*/
|
|
164
|
+
/**
|
|
165
|
+
* Capture a leased page, or undefined when this session is not a lease or cannot be captured.
|
|
166
|
+
*
|
|
167
|
+
* A lease IS a real browser page, so the pixels were always available — the visual tools simply had
|
|
168
|
+
* no route to them and answered "no provider" for every context an agent can actually get. That
|
|
169
|
+
* made visual regression impossible on exactly the isolated contexts the pool exists to hand out.
|
|
170
|
+
*
|
|
171
|
+
* Touches the lease like any other tool call, so capturing keeps it alive rather than letting the
|
|
172
|
+
* reaper take it out from under a diff.
|
|
173
|
+
*/
|
|
174
|
+
async screenshotLease(sessionId, opts = {}) {
|
|
175
|
+
const lease = this.#active.get(sessionId);
|
|
176
|
+
if (lease === undefined || lease.page.screenshot === undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
this.touch(sessionId);
|
|
179
|
+
try {
|
|
180
|
+
return await lease.page.screenshot(opts);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// A capture that fails is not a broken lease. Report "could not capture" and leave the lease
|
|
184
|
+
// usable — the alternative is losing a working context to one bad frame.
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Move the real pointer on a leased page, or false when this session is not a lease or cannot
|
|
190
|
+
* hover. Same optional-capability contract as screenshotLease: absence must read as "could not
|
|
191
|
+
* hover", never as a successful dispatch of a synthetic mouseover.
|
|
192
|
+
*
|
|
193
|
+
* Touches the lease like any other tool call, so hovering keeps it alive.
|
|
194
|
+
*/
|
|
195
|
+
async hoverLease(sessionId, x, y) {
|
|
196
|
+
const lease = this.#active.get(sessionId);
|
|
197
|
+
if (lease === undefined || lease.page.hover === undefined)
|
|
198
|
+
return false;
|
|
199
|
+
this.touch(sessionId);
|
|
200
|
+
try {
|
|
201
|
+
await lease.page.hover(x, y);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
119
208
|
async sweepExpired() {
|
|
120
209
|
const now = this.#now();
|
|
121
210
|
const expired = [...this.#active.entries()]
|
|
@@ -124,19 +213,39 @@ export class BrowserPool {
|
|
|
124
213
|
for (const sessionId of expired)
|
|
125
214
|
await this.#release(sessionId);
|
|
126
215
|
this.#reapedLeases += expired.length;
|
|
216
|
+
for (const sessionId of expired) {
|
|
217
|
+
this.#reapedLeaseIds.push(sessionId);
|
|
218
|
+
if (this.#reapedLeaseIds.length > MAX_REMEMBERED_REAPED_LEASES)
|
|
219
|
+
this.#reapedLeaseIds.shift();
|
|
220
|
+
}
|
|
127
221
|
return expired;
|
|
128
222
|
}
|
|
129
223
|
/**
|
|
130
224
|
* How many leases this pool has reclaimed for going stale.
|
|
131
225
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* because nothing here knows WHICH session the caller just lost.
|
|
226
|
+
* Kept for reporting. NOT a basis for diagnosis: it is a lifetime total that never resets, so
|
|
227
|
+
* `count > 0` latches true forever after the first reap. Ask `wasReapedLease` instead, which
|
|
228
|
+
* answers about a specific session.
|
|
136
229
|
*/
|
|
137
230
|
reapedLeaseCount() {
|
|
138
231
|
return this.#reapedLeases;
|
|
139
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Did THIS session id belong to a lease this pool aged out?
|
|
235
|
+
*
|
|
236
|
+
* The question the no-session diagnosis actually has. It used to ask `reapedLeaseCount() > 0`,
|
|
237
|
+
* which meant that once any lease had ever aged out, every closed human tab for the rest of the
|
|
238
|
+
* daemon's life was reported as an expired lease — and the recovery that followed
|
|
239
|
+
* (`reticle_lease {acquire}`) would open an unauthenticated headless context and lose the app
|
|
240
|
+
* session (#611).
|
|
241
|
+
*
|
|
242
|
+
* Only the last {@link MAX_REMEMBERED_REAPED_LEASES} are remembered, so this answers `false` for
|
|
243
|
+
* a lease reaped long enough ago that thousands have followed. That direction is the safe one:
|
|
244
|
+
* it falls back to the generic "the tab was closed" wording rather than inventing a lease.
|
|
245
|
+
*/
|
|
246
|
+
wasReapedLease(sessionId) {
|
|
247
|
+
return this.#reapedLeaseIds.includes(sessionId);
|
|
248
|
+
}
|
|
140
249
|
/**
|
|
141
250
|
* Lease an isolated context, navigate it to `url`, and return a handle. If the pool is at capacity,
|
|
142
251
|
* waits FIFO until a slot frees (or until `signal` aborts, if provided).
|
|
@@ -40,6 +40,15 @@ function wrapBrowser(browser) {
|
|
|
40
40
|
return {
|
|
41
41
|
goto: (url, opts) => page.goto(url, gotoOptions(opts?.timeoutMs)),
|
|
42
42
|
close: () => page.close(),
|
|
43
|
+
// Playwright returns a Buffer; Uint8Array is what the visual store and differ take.
|
|
44
|
+
screenshot: async (opts) => new Uint8Array(await page.screenshot({ fullPage: true === opts?.fullPage })),
|
|
45
|
+
// Three moves, same as performGesture: a single move to the center can be a no-op if
|
|
46
|
+
// the pointer was already there, and CSS :hover needs a native hit-test to apply.
|
|
47
|
+
hover: async (x, y) => {
|
|
48
|
+
await page.mouse.move(x, y);
|
|
49
|
+
await page.mouse.move(x + 1, y);
|
|
50
|
+
await page.mouse.move(x, y);
|
|
51
|
+
},
|
|
43
52
|
onCrash: (handler) => page.on('crash', handler),
|
|
44
53
|
onConsole: (handler) => page.on('console', (msg) => handler(msg.text())),
|
|
45
54
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ProjectReadError, RunStatus } from '@reticlehq/core';
|
|
3
3
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
|
+
import { countSchema } from '../tools/numeric-bounds.js';
|
|
4
5
|
import { sessionIdShape } from '../tools/tool-kit.js';
|
|
5
6
|
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
6
7
|
import { cloudFetch, fetchProjectRegressionFromCloud, resolveCloudConfig, } from '../cloud/cloud-sync.js';
|
|
@@ -84,11 +85,10 @@ async function lastTwoRunArtifacts(deps) {
|
|
|
84
85
|
export const PROJECT_TOOLS = [
|
|
85
86
|
{
|
|
86
87
|
name: ReticleTool.PROJECT,
|
|
87
|
-
description: 'Read cross-run history from .reticle/project.json — the memory of how past runs behaved. With { name } it also returns the last run for that flow plus a diff-vs-last summary (status change, regressed flag, consoleErrors/driftSteps deltas) so you can answer "did it behave like last time?". `diff` is the lightweight status/console/drift delta vs the previous run of that NAME; `runDiff` is the per-flow duration/status delta between the two most-recent full verification artifacts ("step 3: 412ms -> 987ms (+140%), +2 requests"). When logged in to Reticle
|
|
88
|
+
description: 'Read cross-run history from .reticle/project.json — the memory of how past runs behaved. With { name } it also returns the last run for that flow plus a diff-vs-last summary (status change, regressed flag, consoleErrors/driftSteps deltas) so you can answer "did it behave like last time?". `diff` is the lightweight status/console/drift delta vs the previous run of that NAME; `runDiff` is the per-flow duration/status delta between the two most-recent full verification artifacts ("step 3: 412ms -> 987ms (+140%), +2 requests"). When logged in to Reticle, also returns `cloud`: the team\'s server-side regression report (broken/changed flows vs before) — the durable memory a fresh or context-lost agent can rely on even when local history is empty. Returns { runs, learned?, lastRun?, diff?, runDiff?, cloud? } or { error, reason, cloud? }.',
|
|
88
89
|
inputSchema: {
|
|
89
90
|
name: z.string().optional().describe('Filter runs by this name. Omit to return all runs.'),
|
|
90
|
-
limit:
|
|
91
|
-
.number()
|
|
91
|
+
limit: countSchema
|
|
92
92
|
.optional()
|
|
93
93
|
.describe('Most-recent N runs to return. Defaults to 25 — the full history is unbounded and grows with every run (measured at ~20KB / ~5,000 tokens on a modest project), which is a large slice of an agent context for data it mostly does not read. `totalRuns` always reports the true count, so a cap is never silent.'),
|
|
94
94
|
...sessionIdShape,
|
|
@@ -55,9 +55,22 @@ export declare function isValidSessionId(sessionId: string): sessionId is Sessio
|
|
|
55
55
|
*/
|
|
56
56
|
export declare function isValidRunId(runId: string): runId is RunId;
|
|
57
57
|
/** The PNG baseline path for `name`.reticle/visual/<name>.png). */
|
|
58
|
-
export declare function visualPath(root: string, name: string): string;
|
|
58
|
+
export declare function visualPath(root: string, name: string, runtime?: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* The directory a visual artifact belongs in, scoped by the runtime that produced it.
|
|
61
|
+
*
|
|
62
|
+
* An Electron window, a Tauri webview and a browser tab render the same url differently — different
|
|
63
|
+
* chrome, different fonts, a different device pixel ratio, a webview that is not the browser at all.
|
|
64
|
+
* One shared baseline makes every cross-runtime diff wrong, and the quiet direction is the dangerous
|
|
65
|
+
* one: a baseline overwritten from another runtime turns a later real regression into a pass.
|
|
66
|
+
*
|
|
67
|
+
* Scoped the way flows already are. A DESKTOP runtime gets a subdir; web, an unknown runtime, and
|
|
68
|
+
* everything captured before this existed stay on the flat path — the SDK has no screenshotter, so
|
|
69
|
+
* every pre-existing baseline came from a driven browser, and moving them would orphan them.
|
|
70
|
+
*/
|
|
71
|
+
export declare function visualDir(root: string, runtime?: string): string;
|
|
59
72
|
/** The overlay-diff PNG path for `name`.reticle/visual/<name>.diff.png). */
|
|
60
|
-
export declare function visualDiffPath(root: string, name: string): string;
|
|
73
|
+
export declare function visualDiffPath(root: string, name: string, runtime?: string): string;
|
|
61
74
|
/**
|
|
62
75
|
* Path to a flow file. With a `projectId`, the flow lives in a per-project subdir
|
|
63
76
|
* (`.reticle/flows/<projectId>/<name>.json`) so a shared daemon serving many apps can't collide two
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
+
import { AppRuntime } from '@reticlehq/core';
|
|
2
3
|
import { CONTRACT_FILE_VERSION, ContractFileSchema, ContractReadError, FLOW_NAME_PATTERN, ReticleDir, } from '@reticlehq/core';
|
|
3
4
|
export function reticleDirPaths(root) {
|
|
4
5
|
return {
|
|
@@ -51,12 +52,28 @@ export function isValidRunId(runId) {
|
|
|
51
52
|
return FLOW_NAME_PATTERN.test(runId) && !runId.includes('..');
|
|
52
53
|
}
|
|
53
54
|
/** The PNG baseline path for `name`.reticle/visual/<name>.png). */
|
|
54
|
-
export function visualPath(root, name) {
|
|
55
|
-
return join(root,
|
|
55
|
+
export function visualPath(root, name, runtime) {
|
|
56
|
+
return join(visualDir(root, runtime), `${name}.png`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The directory a visual artifact belongs in, scoped by the runtime that produced it.
|
|
60
|
+
*
|
|
61
|
+
* An Electron window, a Tauri webview and a browser tab render the same url differently — different
|
|
62
|
+
* chrome, different fonts, a different device pixel ratio, a webview that is not the browser at all.
|
|
63
|
+
* One shared baseline makes every cross-runtime diff wrong, and the quiet direction is the dangerous
|
|
64
|
+
* one: a baseline overwritten from another runtime turns a later real regression into a pass.
|
|
65
|
+
*
|
|
66
|
+
* Scoped the way flows already are. A DESKTOP runtime gets a subdir; web, an unknown runtime, and
|
|
67
|
+
* everything captured before this existed stay on the flat path — the SDK has no screenshotter, so
|
|
68
|
+
* every pre-existing baseline came from a driven browser, and moving them would orphan them.
|
|
69
|
+
*/
|
|
70
|
+
export function visualDir(root, runtime) {
|
|
71
|
+
const visual = join(root, ReticleDir.VISUAL_SUBDIR);
|
|
72
|
+
return runtime === undefined || AppRuntime.WEB === runtime ? visual : join(visual, runtime);
|
|
56
73
|
}
|
|
57
74
|
/** The overlay-diff PNG path for `name`.reticle/visual/<name>.diff.png). */
|
|
58
|
-
export function visualDiffPath(root, name) {
|
|
59
|
-
return join(root,
|
|
75
|
+
export function visualDiffPath(root, name, runtime) {
|
|
76
|
+
return join(visualDir(root, runtime), `${name}.diff.png`);
|
|
60
77
|
}
|
|
61
78
|
/**
|
|
62
79
|
* Path to a flow file. With a `projectId`, the flow lives in a per-project subdir
|
package/dist/runs/run-store.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export type ReadRunResult = {
|
|
|
19
19
|
interface RunStoreOptions {
|
|
20
20
|
retention?: number;
|
|
21
21
|
slack?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Called after a run artifact is safely on disk. Used to wake cloud sync so a finished
|
|
24
|
+
* verification reaches the dashboard promptly instead of waiting for the next timer tick.
|
|
25
|
+
*
|
|
26
|
+
* A callback rather than a dependency on the sync daemon: the store's job is durability, and a
|
|
27
|
+
* store that imported the cloud would make "write a run" fail whenever the cloud did. It is
|
|
28
|
+
* invoked AFTER the rename, so nothing can observe a run that is not yet readable, and it must
|
|
29
|
+
* never throw — see the call site.
|
|
30
|
+
*/
|
|
31
|
+
onWrote?: () => void;
|
|
22
32
|
}
|
|
23
33
|
export declare class RunStore {
|
|
24
34
|
#private;
|
package/dist/runs/run-store.js
CHANGED
|
@@ -15,11 +15,13 @@ export class RunStore {
|
|
|
15
15
|
#root;
|
|
16
16
|
#retention;
|
|
17
17
|
#slack;
|
|
18
|
+
#onWrote;
|
|
18
19
|
constructor(fs, root, opts = {}) {
|
|
19
20
|
this.#fs = fs;
|
|
20
21
|
this.#root = root;
|
|
21
22
|
this.#retention = opts.retention ?? RUN_RETENTION;
|
|
22
23
|
this.#slack = opts.slack ?? RUN_RETENTION_SLACK;
|
|
24
|
+
this.#onWrote = opts.onWrote;
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* Write one run artifact. Creates .reticle/runs/ first; byte-stable (fixed indent + trailing newline).
|
|
@@ -37,6 +39,14 @@ export class RunStore {
|
|
|
37
39
|
const tmp = `${path}${TMP_EXT}`;
|
|
38
40
|
await this.#fs.writeFile(tmp, `${JSON.stringify(run, null, JSON_INDENT)}\n`);
|
|
39
41
|
await this.#fs.rename(tmp, path);
|
|
42
|
+
// AFTER the rename: a listener that reads the run back must find it there. Swallowed, because
|
|
43
|
+
// this is a notification — a sync that cannot be woken must never fail the write it followed.
|
|
44
|
+
try {
|
|
45
|
+
this.#onWrote?.();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Not this store's problem, and not worth failing a durable write over.
|
|
49
|
+
}
|
|
40
50
|
await this.#pruneOld();
|
|
41
51
|
}
|
|
42
52
|
/**
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Turn an MCP verification (a `reticle_flow_verify` suite run) into a first-class ReticleVerificationRun:
|
|
3
|
-
* assemble the artifact from the already-computed replays, persist it to .reticle/runs/ (so
|
|
4
|
-
* `reticle_run_export` and CI can read it), and — when logged in (cloud creds set) — best-effort push it
|
|
5
|
-
* to the cloud's `POST /v1/runs` so the team's dashboard Runs tab shows what the agent just verified.
|
|
6
|
-
*
|
|
7
|
-
* This is the artifact tier that maps to the dashboard's "Runs" (the OEM/CI-consumable verdict); the
|
|
8
|
-
* lighter per-flow RunRecord that feeds regression memory is synced separately in flow-replay-run.ts.
|
|
9
|
-
* Both are best-effort + opt-in: no creds → the artifact still lands on disk, nothing leaves the machine.
|
|
10
|
-
*/
|
|
11
1
|
import { type FlowReplayResult } from '@reticlehq/core';
|
|
12
2
|
import type { ToolDeps } from '../tools/tools.js';
|
|
13
|
-
/** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
|
|
14
3
|
export interface TimedReplay {
|
|
15
4
|
replay: FlowReplayResult;
|
|
16
5
|
durationMs: number;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* lighter per-flow RunRecord that feeds regression memory is synced separately in flow-replay-run.ts.
|
|
9
9
|
* Both are best-effort + opt-in: no creds → the artifact still lands on disk, nothing leaves the machine.
|
|
10
10
|
*/
|
|
11
|
+
import { mcpClientIdentity } from '../mcp/client-identity.js';
|
|
11
12
|
import { RunAgentKind, RunFramework, RunProfile, RunTrigger, } from '@reticlehq/core';
|
|
12
13
|
import { homedir } from 'node:os';
|
|
13
14
|
import { buildVerificationRun } from './build-verification-run.js';
|
|
@@ -17,6 +18,9 @@ import { RunStore } from './run-store.js';
|
|
|
17
18
|
import { cloudFetch, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
18
19
|
import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
19
20
|
import { log } from '../log.js';
|
|
21
|
+
/** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
|
|
22
|
+
/** The author of record when no MCP peer introduced itself — a CLI run, or a client that skipped the handshake. */
|
|
23
|
+
const RETICLE_MCP_AGENT_ID = 'reticle-mcp';
|
|
20
24
|
/** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
|
|
21
25
|
function assembleRun(deps, timed, projectId) {
|
|
22
26
|
const flows = timed.map((t) => mapReplayToFlowResult(t.replay, t.durationMs));
|
|
@@ -25,7 +29,20 @@ function assembleRun(deps, timed, projectId) {
|
|
|
25
29
|
durationMs: flows.reduce((sum, f) => sum + f.durationMs, 0),
|
|
26
30
|
profile: RunProfile.DEV,
|
|
27
31
|
project: { name: projectId ?? 'reticle', framework: RunFramework.OTHER },
|
|
28
|
-
|
|
32
|
+
/*
|
|
33
|
+
* WHICH agent drove this, not merely THAT one did.
|
|
34
|
+
*
|
|
35
|
+
* This was hardcoded to 'reticle-mcp', so every run in every workspace claimed the same author.
|
|
36
|
+
* A dashboard built on it could say "an agent verified this" and never "Claude Code verified
|
|
37
|
+
* this, Cursor broke it" — which is the question a team actually has once more than one agent
|
|
38
|
+
* touches the repo. The name is the client's own claim from the MCP handshake; a plain CLI run
|
|
39
|
+
* with no MCP peer keeps the old id, because inventing a vendor there would be worse than
|
|
40
|
+
* admitting we do not know.
|
|
41
|
+
*/
|
|
42
|
+
agent: {
|
|
43
|
+
id: mcpClientIdentity().name ?? RETICLE_MCP_AGENT_ID,
|
|
44
|
+
kind: RunAgentKind.CODING_AGENT,
|
|
45
|
+
},
|
|
29
46
|
trigger: { kind: RunTrigger.MANUAL },
|
|
30
47
|
changedFiles: [],
|
|
31
48
|
flows,
|
|
@@ -62,5 +79,9 @@ export async function persistAndSyncVerificationRun(deps, timed, projectId) {
|
|
|
62
79
|
if (result.outcome !== SyncOutcome.SYNCED) {
|
|
63
80
|
log('cloud-run-sync-failed', { runId: run.runId, status: result.status, error: result.error });
|
|
64
81
|
}
|
|
82
|
+
// The run is up; its CONTEXT is not. Nudged rather than pushed inline, so the counters and flows
|
|
83
|
+
// ride the ordinary cycle (with its cursor and its overlap guard) instead of growing a second,
|
|
84
|
+
// subtly different upload path here.
|
|
85
|
+
deps.onRunPersisted?.();
|
|
65
86
|
return run.runId;
|
|
66
87
|
}
|
|
@@ -18,7 +18,14 @@ export interface VerifyServerOptions {
|
|
|
18
18
|
}
|
|
19
19
|
/** Build the request listener. Reads + size-caps the body, then hands off to the pure handler. */
|
|
20
20
|
export declare function createVerifyRequestListener(opts: VerifyServerOptions): http.RequestListener;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Start the verify server on localhost. Resolves once listening; returns the bound server + port.
|
|
23
|
+
*
|
|
24
|
+
* A failed bind REJECTS, naming the verify port. It used to emit `'error'` with no listener, so an
|
|
25
|
+
* EADDRINUSE on `--http-port` crashed the daemon with a raw stack, and the parent `serve` reported
|
|
26
|
+
* only that the daemon "did not come up" on the BRIDGE port — the one port that was fine (#687).
|
|
27
|
+
* Rejecting lets the daemon's start-failure path say what actually could not be honoured.
|
|
28
|
+
*/
|
|
22
29
|
export declare function startVerifyServer(opts: VerifyServerOptions, port: number): Promise<{
|
|
23
30
|
server: http.Server;
|
|
24
31
|
port: number;
|
|
@@ -83,13 +83,25 @@ async function writeResponse(req, res, opts, raw) {
|
|
|
83
83
|
res.writeHead(result.status, JSON_HEADERS);
|
|
84
84
|
res.end(JSON.stringify(result.body));
|
|
85
85
|
}
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Start the verify server on localhost. Resolves once listening; returns the bound server + port.
|
|
88
|
+
*
|
|
89
|
+
* A failed bind REJECTS, naming the verify port. It used to emit `'error'` with no listener, so an
|
|
90
|
+
* EADDRINUSE on `--http-port` crashed the daemon with a raw stack, and the parent `serve` reported
|
|
91
|
+
* only that the daemon "did not come up" on the BRIDGE port — the one port that was fine (#687).
|
|
92
|
+
* Rejecting lets the daemon's start-failure path say what actually could not be honoured.
|
|
93
|
+
*/
|
|
87
94
|
export function startVerifyServer(opts, port) {
|
|
88
95
|
const server = http.createServer(createVerifyRequestListener(opts));
|
|
89
96
|
server.requestTimeout = REQUEST_TIMEOUT_MS;
|
|
90
97
|
server.headersTimeout = HEADERS_TIMEOUT_MS;
|
|
91
|
-
return new Promise((resolve) => {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const onError = (err) => {
|
|
100
|
+
reject(new Error(`the verify HTTP endpoint could not bind ${LOCALHOST}:${String(port)} — ${err.message}`));
|
|
101
|
+
};
|
|
102
|
+
server.once('error', onError);
|
|
92
103
|
server.listen(port, LOCALHOST, () => {
|
|
104
|
+
server.removeListener('error', onError);
|
|
93
105
|
const address = server.address();
|
|
94
106
|
const boundPort = 'object' === typeof address && address !== null ? address.port : port;
|
|
95
107
|
resolve({ server, port: boundPort });
|
|
@@ -9,8 +9,11 @@ import { AppRuntime } from '@reticlehq/core';
|
|
|
9
9
|
* developer goes hunting through their own code for a fault that is not there.
|
|
10
10
|
*
|
|
11
11
|
* This advice previously blamed occlusion — "macOS suspends an occluded or off-Space WKWebView" —
|
|
12
|
-
* which is false, and was the kind of wrong steer this function exists to prevent.
|
|
13
|
-
* webview keeps answering while minimized,
|
|
12
|
+
* which is false for those states, and was the kind of wrong steer this function exists to prevent.
|
|
13
|
+
* A LOADED Tauri webview keeps answering while minimized, occluded, and on another Space. `hide()`
|
|
14
|
+
* after load is the remaining headless path: a hidden macOS WKWebView has been observed to go quiet
|
|
15
|
+
* after a pause, but that is not something every machine demonstrates, so this message names it as
|
|
16
|
+
* a candidate rather than a fact.
|
|
14
17
|
*
|
|
15
18
|
* It then over-corrected: it asserted the hidden-before-load ordering as FACT. But the only evidence
|
|
16
19
|
* here is `hidden === true` — the ordering is never observed. Measured on a Tauri shell pointed at an
|
|
@@ -11,11 +11,12 @@ const ONE_WAY_ADVICE = 'The page is ALIVE — the SDK last reported to the bridg
|
|
|
11
11
|
'replaced mid-session, a proxy buffering one way) rather than at your app.';
|
|
12
12
|
const HIDDEN_ADVICE = 'The page last reported itself hidden, and this is a WKWebView (Tauri) window, so the webview is ' +
|
|
13
13
|
'most likely not running the page at all. Two known causes, commonest first: (1) the window was ' +
|
|
14
|
-
'hidden BEFORE its first page load — a webview that has never been presented never loads, so
|
|
14
|
+
'hidden BEFORE its first page load — a webview that has never been presented never loads, so park ' +
|
|
15
15
|
'it from `on_page_load` instead of `setup` (see `reticle_tauri::on_page_load`, which does this for ' +
|
|
16
|
-
'RETICLE_HEADLESS=1)
|
|
17
|
-
'
|
|
18
|
-
'
|
|
16
|
+
'RETICLE_HEADLESS=1). (2) the window was hidden after load and then went quiet — that has been ' +
|
|
17
|
+
'observed on a hidden macOS WKWebView after a pause, even though capture still works; park it ' +
|
|
18
|
+
'off-screen rather than calling hide. If neither fits, the page is reachable but not executing, ' +
|
|
19
|
+
'which is worth reporting.';
|
|
19
20
|
/** True when this session is a WebKit desktop shell — the only runtime that suffers this. */
|
|
20
21
|
function isWebKitDesktop(context) {
|
|
21
22
|
if (AppRuntime.TAURI === context.runtime)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling the human that the agent went somewhere they cannot see.
|
|
3
|
+
*
|
|
4
|
+
* A lease is a separate pooled context, and it is the highest-value path in the product for
|
|
5
|
+
* autonomous work. It also has one failure mode that is entirely about people: the HUD lives in the
|
|
6
|
+
* human's OWN tab, so while an agent drives a lease that tab shows nothing at all. An idle agent and
|
|
7
|
+
* a busy one are then indistinguishable from the only seat a human occupies.
|
|
8
|
+
*
|
|
9
|
+
* That is not hypothetical. An agent followed the daemon's own throttle recommendation into a lease,
|
|
10
|
+
* drove fifteen calls, and the developer watching an empty HUD asked why nothing was running. The
|
|
11
|
+
* product was working perfectly and looked broken, which is the worst combination available.
|
|
12
|
+
*
|
|
13
|
+
* The fix rides on machinery that already exists — `pushNarration` posts `ReticleCommand.NARRATE`
|
|
14
|
+
* and every SDK in the field already renders it — so nothing here moves the wire contract. It is a
|
|
15
|
+
* message to tabs that are ALREADY connected, not a new capability.
|
|
16
|
+
*/
|
|
17
|
+
/** The minimum a session has to expose to be a candidate watcher. Structural, so tests need no SDK. */
|
|
18
|
+
export interface WatcherCandidate {
|
|
19
|
+
id: string;
|
|
20
|
+
projectId: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Shown on a watching human's HUD when an agent takes a lease.
|
|
24
|
+
*
|
|
25
|
+
* States the fact and its consequence, because the fact alone ("agent leased a context") does not
|
|
26
|
+
* tell somebody why their screen is still. The consequence is the whole reason to say anything.
|
|
27
|
+
*/
|
|
28
|
+
export declare const AGENT_DRIVING_ELSEWHERE = "An agent is driving a separate leased context \u2014 its actions will not appear in this tab.";
|
|
29
|
+
/** Shown when the lease is released, so a HUD that went quiet is explained in both directions. */
|
|
30
|
+
export declare const AGENT_DRIVING_HERE_AGAIN = "The agent released its separate context \u2014 this tab is live again.";
|
|
31
|
+
/**
|
|
32
|
+
* Shown to a tab that CONNECTS while a lease is already open.
|
|
33
|
+
*
|
|
34
|
+
* The acquire-time notice only reaches tabs that were already connected, so opening the app — or
|
|
35
|
+
* merely reloading it — during a lease landed somebody on a silent HUD with nothing explaining it.
|
|
36
|
+
* That is the same dark-HUD failure the acquire notice exists to prevent, arrived at from the other
|
|
37
|
+
* direction, and it is the more common one: a person opens the dashboard BECAUSE they want to watch.
|
|
38
|
+
*
|
|
39
|
+
* Worded for arrival rather than for a change of state — nothing just happened from this tab's point
|
|
40
|
+
* of view, so "an agent is driving elsewhere" would describe an event it never saw.
|
|
41
|
+
*/
|
|
42
|
+
export declare const AGENT_ALREADY_DRIVING_ELSEWHERE = "An agent is already driving a separate leased context \u2014 its actions will not appear in this tab.";
|
|
43
|
+
/**
|
|
44
|
+
* Whether a session arriving now should be told a lease is already running.
|
|
45
|
+
*
|
|
46
|
+
* Same rules as `watchersToNotify`, expressed for one session: never tell a leased context about
|
|
47
|
+
* itself, and never tell somebody watching app A about a lease against app B.
|
|
48
|
+
*/
|
|
49
|
+
export declare function shouldGreetWithLeaseNotice(session: WatcherCandidate, leasedSessionIds: readonly string[], leasedProjectIds: readonly (string | undefined)[]): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Which connected sessions belong to a human who should be told.
|
|
52
|
+
*
|
|
53
|
+
* Two exclusions, both deliberate:
|
|
54
|
+
*
|
|
55
|
+
* - Leased sessions. They ARE the agent's context; narrating "an agent is driving elsewhere" into
|
|
56
|
+
* the tab doing the driving is noise, and with several leases open they would talk about each
|
|
57
|
+
* other. Membership comes from the pool rather than an id prefix, because a naming convention is
|
|
58
|
+
* not a fact and `pool.leasedSessionIds()` is.
|
|
59
|
+
*
|
|
60
|
+
* - Other projects. One daemon serves many, and somebody watching app A does not need to hear about
|
|
61
|
+
* a lease taken against app B. A session that declares no project is NOT excluded: an older SDK
|
|
62
|
+
* sends no projectId, and silently dropping those would reintroduce the dark HUD for exactly the
|
|
63
|
+
* people least likely to work out why.
|
|
64
|
+
*
|
|
65
|
+
* Pure: no IO, no clock, no session objects. The caller does the posting.
|
|
66
|
+
*/
|
|
67
|
+
export declare function watchersToNotify(sessions: readonly WatcherCandidate[], leasedSessionIds: readonly string[], projectId: string | undefined): string[];
|