@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
|
@@ -54,3 +54,17 @@ export declare function declaredInstallSource(env?: NodeJS.ProcessEnv): InstallS
|
|
|
54
54
|
* data, and a hand-edited one must not be able to put an arbitrary string on the wire.
|
|
55
55
|
*/
|
|
56
56
|
export declare function projectInstallSource(cwd: string, env?: NodeJS.ProcessEnv): InstallSource;
|
|
57
|
+
/**
|
|
58
|
+
* The same config with `installSource` added, or `undefined` when there is nothing to add.
|
|
59
|
+
*
|
|
60
|
+
* `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
|
|
61
|
+
* `already exists` on every re-run — so everyone who was already here when the marker shipped, and
|
|
62
|
+
* everyone who ran `init` once without one, keeps `unknown` forever with no way to fix it. That is
|
|
63
|
+
* most of the population the field exists to describe.
|
|
64
|
+
*
|
|
65
|
+
* An EXISTING value is never overwritten. The first channel is the one that brought the user in, and
|
|
66
|
+
* a later `init` through a different route must not take credit for an acquisition it did not make.
|
|
67
|
+
* Anything unparseable is left exactly as it is: a config is user data, and `init` rewriting a file
|
|
68
|
+
* it could not read would be a far worse failure than a missing dimension.
|
|
69
|
+
*/
|
|
70
|
+
export declare function configWithInstallSource(source: string | null | undefined, declared: string | undefined): string | undefined;
|
|
@@ -72,3 +72,33 @@ export function projectInstallSource(cwd, env = process.env) {
|
|
|
72
72
|
}
|
|
73
73
|
return resolveInstallSource(env);
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The same config with `installSource` added, or `undefined` when there is nothing to add.
|
|
77
|
+
*
|
|
78
|
+
* `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
|
|
79
|
+
* `already exists` on every re-run — so everyone who was already here when the marker shipped, and
|
|
80
|
+
* everyone who ran `init` once without one, keeps `unknown` forever with no way to fix it. That is
|
|
81
|
+
* most of the population the field exists to describe.
|
|
82
|
+
*
|
|
83
|
+
* An EXISTING value is never overwritten. The first channel is the one that brought the user in, and
|
|
84
|
+
* a later `init` through a different route must not take credit for an acquisition it did not make.
|
|
85
|
+
* Anything unparseable is left exactly as it is: a config is user data, and `init` rewriting a file
|
|
86
|
+
* it could not read would be a far worse failure than a missing dimension.
|
|
87
|
+
*/
|
|
88
|
+
export function configWithInstallSource(source, declared) {
|
|
89
|
+
if (undefined === declared || null === source || undefined === source)
|
|
90
|
+
return undefined;
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(source);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (null === parsed || 'object' !== typeof parsed || Array.isArray(parsed))
|
|
99
|
+
return undefined;
|
|
100
|
+
const fields = parsed;
|
|
101
|
+
if (undefined !== fields['installSource'])
|
|
102
|
+
return undefined;
|
|
103
|
+
return `${JSON.stringify({ ...fields, installSource: declared }, null, 2)}\n`;
|
|
104
|
+
}
|
|
@@ -30,6 +30,12 @@ import { LicenseActivation } from '@reticlehq/core';
|
|
|
30
30
|
export interface LicenseFacts {
|
|
31
31
|
licenseId?: string;
|
|
32
32
|
licenseStatus?: LicenseActivation;
|
|
33
|
+
/**
|
|
34
|
+
* A key was placed, whether or not this build could judge it. See LICENSE_KEY_PRESENT in core.
|
|
35
|
+
* Reported even in evaluation mode, which is the whole point: that is the case where every other
|
|
36
|
+
* licence property is deliberately silent.
|
|
37
|
+
*/
|
|
38
|
+
licenseKeyPresent?: boolean;
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
35
41
|
* Resolve activation for one event. The clock is passed in (rule 7) and is the EVENT's clock, not a
|
|
@@ -45,4 +51,4 @@ export interface LicenseFacts {
|
|
|
45
51
|
* Never throws. A telemetry property may not change behaviour (rule 5), and this one sits in front of
|
|
46
52
|
* key parsing, which is the part most likely to be handed something malformed.
|
|
47
53
|
*/
|
|
48
|
-
export declare function licenseFacts(now: number, env?: NodeJS.ProcessEnv): LicenseFacts;
|
|
54
|
+
export declare function licenseFacts(now: number, env?: NodeJS.ProcessEnv, baked?: string): LicenseFacts;
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* holds a customer list.
|
|
27
27
|
*/
|
|
28
28
|
import { LicenseActivation } from '@reticlehq/core';
|
|
29
|
-
import { describeLicense } from '../license/license.js';
|
|
29
|
+
import { describeLicense, LICENSE_KEY_ENV } from '../license/license.js';
|
|
30
30
|
/**
|
|
31
31
|
* Resolve activation for one event. The clock is passed in (rule 7) and is the EVENT's clock, not a
|
|
32
32
|
* value captured at startup: sessions here run to eleven hours, so a key that expires mid-session must
|
|
@@ -41,13 +41,17 @@ import { describeLicense } from '../license/license.js';
|
|
|
41
41
|
* Never throws. A telemetry property may not change behaviour (rule 5), and this one sits in front of
|
|
42
42
|
* key parsing, which is the part most likely to be handed something malformed.
|
|
43
43
|
*/
|
|
44
|
-
export function licenseFacts(now, env = process.env) {
|
|
44
|
+
export function licenseFacts(now, env = process.env, baked) {
|
|
45
45
|
try {
|
|
46
|
-
const report = describeLicense(now, env);
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const report = baked === undefined ? describeLicense(now, env) : describeLicense(now, env, baked);
|
|
47
|
+
// Asked BEFORE the eval short-circuit, because eval is exactly the case that used to be silent.
|
|
48
|
+
const keyPresent = (env[LICENSE_KEY_ENV] ?? '').length > 0;
|
|
49
|
+
if (LicenseActivation.EVAL === report.status) {
|
|
50
|
+
return keyPresent ? { licenseKeyPresent: true } : {};
|
|
51
|
+
}
|
|
49
52
|
return {
|
|
50
53
|
licenseStatus: report.status,
|
|
54
|
+
...(keyPresent ? { licenseKeyPresent: true } : {}),
|
|
51
55
|
...(report.licenseId !== undefined ? { licenseId: report.licenseId } : {}),
|
|
52
56
|
};
|
|
53
57
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { TelemetryEventKind } from '@reticlehq/core';
|
|
13
13
|
import { getTelemetry } from './telemetry.js';
|
|
14
|
+
import { appEverConnected } from './app-instrumented.js';
|
|
14
15
|
let connectsSeen = 0;
|
|
15
16
|
/** Set when the daemon starts, so the FIRST connect can report how long Reticle sat unused. */
|
|
16
17
|
let daemonStartedAt;
|
|
@@ -30,6 +31,12 @@ export function reportMcpConnected(client, now = () => Date.now()) {
|
|
|
30
31
|
// sat there. On a reconnect it is just how long the daemon has been up.
|
|
31
32
|
daemonAgeMs: daemonStartedAt === undefined ? 0 : Math.max(0, now() - daemonStartedAt),
|
|
32
33
|
...(client !== undefined ? { client } : {}),
|
|
34
|
+
// WHAT THE AGENT HAD TO LOOK AT. Most clients that attach never call a tool, and that cohort
|
|
35
|
+
// emits nothing at all — an agent that reads the instructions, learns nothing is wired and
|
|
36
|
+
// stops has refused nothing, so `tool_refused` cannot see it either. Read off the same flag
|
|
37
|
+
// `app_instrumented` sets, never a second counter, so the two halves of the funnel cannot
|
|
38
|
+
// disagree about one daemon run.
|
|
39
|
+
appConnected: appEverConnected(),
|
|
33
40
|
},
|
|
34
41
|
});
|
|
35
42
|
}
|
|
@@ -32,5 +32,16 @@ type FileReader = (path: string) => string;
|
|
|
32
32
|
* Build the full profile. Best-effort throughout: an unreadable project yields a profile of zeroes
|
|
33
33
|
* rather than an exception, because a telemetry snapshot must never be able to fail a daemon start.
|
|
34
34
|
*/
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Facts about the install that the filesystem cannot answer.
|
|
37
|
+
*
|
|
38
|
+
* Passed in rather than read here for the same reason `previouslyConnected` is passed to the MCP
|
|
39
|
+
* server: it needs the daemon's port and the state home, and a profile that reached for those would
|
|
40
|
+
* stop being a pure function of a directory.
|
|
41
|
+
*/
|
|
42
|
+
export interface InstallFacts {
|
|
43
|
+
/** Has an app for this project EVER connected to Reticle, from durable state. */
|
|
44
|
+
appConnectedBefore: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function profileProject(cwd: string, now: number, install?: InstallFacts): ProjectProfile;
|
|
36
47
|
export {};
|
|
@@ -15,6 +15,7 @@ import { join } from 'node:path';
|
|
|
15
15
|
import { ProjectSize, ReticleDir } from '@reticlehq/core';
|
|
16
16
|
import { gitFacts } from './git-facts.js';
|
|
17
17
|
import { detectStack } from './feedback-context.js';
|
|
18
|
+
import { readProjectId } from '../cli/cli-port.js';
|
|
18
19
|
/**
|
|
19
20
|
* The feature FAMILIES, and the on-disk evidence that a project has adopted each. Named rather than
|
|
20
21
|
* counted so the vocabulary stays stable as tools are added, renamed, or merged into action-dispatched
|
|
@@ -151,11 +152,7 @@ function exists(path) {
|
|
|
151
152
|
return false;
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
|
-
|
|
155
|
-
* Build the full profile. Best-effort throughout: an unreadable project yields a profile of zeroes
|
|
156
|
-
* rather than an exception, because a telemetry snapshot must never be able to fail a daemon start.
|
|
157
|
-
*/
|
|
158
|
-
export function profileProject(cwd, now) {
|
|
155
|
+
export function profileProject(cwd, now, install) {
|
|
159
156
|
const reticleRoot = join(cwd, ReticleDir.ROOT);
|
|
160
157
|
const flowCount = countIn(reticleRoot, ReticleDir.FLOWS_SUBDIR, /\.json$/);
|
|
161
158
|
const baselineCount = countIn(reticleRoot, ReticleDir.BASELINES_SUBDIR);
|
|
@@ -180,7 +177,7 @@ export function profileProject(cwd, now) {
|
|
|
180
177
|
featuresUsed.push(FeatureFamily.BUG_CAPSULES);
|
|
181
178
|
if (hasHistory)
|
|
182
179
|
featuresUsed.push(FeatureFamily.CROSS_RUN_HISTORY);
|
|
183
|
-
const { stack, stackMajor, stackSource } = detectStack(cwd);
|
|
180
|
+
const { stack, stackMajor, stackSource, stackUnknownReason } = detectStack(cwd);
|
|
184
181
|
const ageWeeks = projectAgeWeeks(cwd, now);
|
|
185
182
|
const git = gitFacts(cwd);
|
|
186
183
|
return {
|
|
@@ -189,6 +186,8 @@ export function profileProject(cwd, now) {
|
|
|
189
186
|
...(stack !== undefined ? { stack } : {}),
|
|
190
187
|
...(stackSource !== undefined ? { stackSource } : {}),
|
|
191
188
|
...(stackMajor !== undefined ? { stackMajor } : {}),
|
|
189
|
+
// Present only when `stack` is absent, so the field's presence marks the unknown bucket.
|
|
190
|
+
...(stackUnknownReason !== undefined ? { stackUnknownReason } : {}),
|
|
192
191
|
size: sizeBucket(countSourceFiles(cwd)),
|
|
193
192
|
monorepo: isMonorepo(cwd),
|
|
194
193
|
...(ageWeeks !== undefined ? { ageWeeks } : {}),
|
|
@@ -202,5 +201,11 @@ export function profileProject(cwd, now) {
|
|
|
202
201
|
// Rounded to 2dp: the exact ratio is a function of the counts above, and an unrounded float would
|
|
203
202
|
// just be a higher-cardinality restatement of them.
|
|
204
203
|
featureDepth: Math.round((featuresUsed.length / ALL_FAMILIES.length) * 100) / 100,
|
|
204
|
+
// WHERE the install stopped, for the daemons that never see an app. `daemon_started` minus
|
|
205
|
+
// `app_instrumented` says one happened and cannot say why; these two bits split that silence
|
|
206
|
+
// into "never ran init", "ran it and no page has ever reached us", and "works, just not up
|
|
207
|
+
// right now". See the fields on ProjectProfileSchema.
|
|
208
|
+
initialized: readProjectId(cwd) !== undefined,
|
|
209
|
+
...(install === undefined ? {} : { appConnectedBefore: install.appConnectedBefore }),
|
|
205
210
|
};
|
|
206
211
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP proxy's session rollup.
|
|
3
|
+
*
|
|
4
|
+
* POST socket failures (`ENOBUFS`, `EMFILE`, `ECONNREFUSED` before any bytes were sent) happen in
|
|
5
|
+
* this process, not the daemon's. `tool_refused` never fires (the handler never ran) and
|
|
6
|
+
* `mcp_connection_lost` never fires (the SSE stream is fine). Counts live on SessionMetrics here
|
|
7
|
+
* and leave as one `session_progress` — an existing kind, omitted when nothing happened.
|
|
8
|
+
*
|
|
9
|
+
* The send is awaited. Fire-and-forget microseconds before `process.exit` is how `daemon_stopped`
|
|
10
|
+
* never arrived: the POST was killed every time and nothing threw.
|
|
11
|
+
*/
|
|
12
|
+
import { TelemetryEventKind } from '@reticlehq/core';
|
|
13
|
+
import { getSessionMetrics } from './session-metrics.js';
|
|
14
|
+
import { getTelemetry } from './telemetry.js';
|
|
15
|
+
export async function flushProxySessionMetrics(telemetry = getTelemetry(), metrics = getSessionMetrics()) {
|
|
16
|
+
if (metrics.empty)
|
|
17
|
+
return;
|
|
18
|
+
await telemetry.emit(TelemetryEventKind.SESSION_PROGRESS, {
|
|
19
|
+
session: metrics.summarize(true),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -65,6 +65,18 @@ export declare class SessionMetrics {
|
|
|
65
65
|
* one inside a much larger pile. Same treatment otherwise — fingerprinted, variables stripped.
|
|
66
66
|
*/
|
|
67
67
|
recordSdkFailure(site: string, message: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* The MCP proxy could not POST a tool call because the TCP socket itself failed (`ENOBUFS`,
|
|
70
|
+
* `EMFILE`, `ECONNREFUSED` before any bytes left). The SSE stream is still up, so this is
|
|
71
|
+
* invisible to `mcp_connection_lost`, and the handler never ran, so it is invisible to
|
|
72
|
+
* `tool_refused`. Counted so a dashboard can see it without a new event kind.
|
|
73
|
+
*/
|
|
74
|
+
recordPostSocketFailure(): void;
|
|
75
|
+
/**
|
|
76
|
+
* A bounded retry of an unsent POST then delivered. The pair against `recordPostSocketFailure`:
|
|
77
|
+
* the retry that quietly saved a call is a different fact from the one that never ran.
|
|
78
|
+
*/
|
|
79
|
+
recordPostRetrySaved(): void;
|
|
68
80
|
/** The agent's recent approach run + the call in flight — context for a crash report. */
|
|
69
81
|
get trail(): {
|
|
70
82
|
breadcrumb: string[];
|
|
@@ -123,6 +123,12 @@ export class SessionMetrics {
|
|
|
123
123
|
* call a tool, and of the sessions that made exactly one call, most bounced on this.
|
|
124
124
|
*/
|
|
125
125
|
#noSessionErrors = 0;
|
|
126
|
+
/** Connection-level MCP POST failures the SSE stream cannot see. See SessionSummary. */
|
|
127
|
+
#postSocketFailures = 0;
|
|
128
|
+
#lifetimePostSocketFailures = 0;
|
|
129
|
+
/** Retries of those POSTs that then delivered. */
|
|
130
|
+
#postRetriesSaved = 0;
|
|
131
|
+
#lifetimePostRetriesSaved = 0;
|
|
126
132
|
/** Longest back-to-back run per tool — a retry loop, which `toolCounts` cannot distinguish. */
|
|
127
133
|
#repeatRuns = new Map();
|
|
128
134
|
#lastTool;
|
|
@@ -325,6 +331,24 @@ export class SessionMetrics {
|
|
|
325
331
|
tool: site,
|
|
326
332
|
});
|
|
327
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* The MCP proxy could not POST a tool call because the TCP socket itself failed (`ENOBUFS`,
|
|
336
|
+
* `EMFILE`, `ECONNREFUSED` before any bytes left). The SSE stream is still up, so this is
|
|
337
|
+
* invisible to `mcp_connection_lost`, and the handler never ran, so it is invisible to
|
|
338
|
+
* `tool_refused`. Counted so a dashboard can see it without a new event kind.
|
|
339
|
+
*/
|
|
340
|
+
recordPostSocketFailure() {
|
|
341
|
+
this.#postSocketFailures += 1;
|
|
342
|
+
this.#lifetimePostSocketFailures += 1;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* A bounded retry of an unsent POST then delivered. The pair against `recordPostSocketFailure`:
|
|
346
|
+
* the retry that quietly saved a call is a different fact from the one that never ran.
|
|
347
|
+
*/
|
|
348
|
+
recordPostRetrySaved() {
|
|
349
|
+
this.#postRetriesSaved += 1;
|
|
350
|
+
this.#lifetimePostRetriesSaved += 1;
|
|
351
|
+
}
|
|
328
352
|
/** The agent's recent approach run + the call in flight — context for a crash report. */
|
|
329
353
|
get trail() {
|
|
330
354
|
return { breadcrumb: [...this.#breadcrumb], inFlight: this.#inFlight };
|
|
@@ -469,12 +493,16 @@ export class SessionMetrics {
|
|
|
469
493
|
toolErrors: this.#lifetimeToolErrors,
|
|
470
494
|
verifications: this.#lifetimeVerifications,
|
|
471
495
|
bugsFound: this.#lifetimeBugsFound,
|
|
496
|
+
postSocketFailures: this.#lifetimePostSocketFailures,
|
|
497
|
+
postRetriesSaved: this.#lifetimePostRetriesSaved,
|
|
472
498
|
}
|
|
473
499
|
: {
|
|
474
500
|
toolCalls: this.#toolCalls,
|
|
475
501
|
toolErrors: this.#toolErrors,
|
|
476
502
|
verifications: this.#verifications,
|
|
477
503
|
bugsFound: this.#bugsFound,
|
|
504
|
+
postSocketFailures: this.#postSocketFailures,
|
|
505
|
+
postRetriesSaved: this.#postRetriesSaved,
|
|
478
506
|
};
|
|
479
507
|
return {
|
|
480
508
|
durationMs: Math.max(0, this.#now() - this.#startedAt),
|
|
@@ -488,6 +516,8 @@ export class SessionMetrics {
|
|
|
488
516
|
bugsFound: scalars.bugsFound,
|
|
489
517
|
...(this.#bugKinds.size > 0 ? { bugKinds: Object.fromEntries(this.#bugKinds) } : {}),
|
|
490
518
|
...(this.#noSessionErrors > 0 ? { noSessionErrors: this.#noSessionErrors } : {}),
|
|
519
|
+
...(scalars.postSocketFailures > 0 ? { postSocketFailures: scalars.postSocketFailures } : {}),
|
|
520
|
+
...(scalars.postRetriesSaved > 0 ? { postRetriesSaved: scalars.postRetriesSaved } : {}),
|
|
491
521
|
...(this.#repeatRuns.size > 0
|
|
492
522
|
? { consecutiveRepeats: Object.fromEntries(this.#repeatRuns) }
|
|
493
523
|
: {}),
|
|
@@ -588,7 +618,9 @@ export class SessionMetrics {
|
|
|
588
618
|
0 === this.#verifications &&
|
|
589
619
|
0 === this.#toolErrors &&
|
|
590
620
|
0 === this.#bugsFound &&
|
|
591
|
-
0 === this.#sdkFailures
|
|
621
|
+
0 === this.#sdkFailures &&
|
|
622
|
+
0 === this.#postSocketFailures &&
|
|
623
|
+
0 === this.#postRetriesSaved);
|
|
592
624
|
}
|
|
593
625
|
/** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
|
|
594
626
|
reset() {
|
|
@@ -596,6 +628,8 @@ export class SessionMetrics {
|
|
|
596
628
|
this.#toolErrors = 0;
|
|
597
629
|
this.#verifications = 0;
|
|
598
630
|
this.#noSessionErrors = 0;
|
|
631
|
+
this.#postSocketFailures = 0;
|
|
632
|
+
this.#postRetriesSaved = 0;
|
|
599
633
|
this.#repeatRuns.clear();
|
|
600
634
|
this.#lastTool = undefined;
|
|
601
635
|
this.#currentRun = 0;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - it is best-effort and non-blocking: a send failure NEVER changes what the tool does.
|
|
10
10
|
*
|
|
11
11
|
* Events are `@reticlehq/core`'s `TelemetryEventSchema`, mapped at the wire into PostHog's capture
|
|
12
|
-
* format (https://posthog.com/docs/api/capture) — PostHog is the analytics backend until
|
|
12
|
+
* format (https://posthog.com/docs/api/capture) — PostHog is the analytics backend until the hosted service
|
|
13
13
|
* grows its own; a project API key is WRITE-ONLY by design, so embedding it in an OSS client is safe.
|
|
14
14
|
* Everything here is wrapped so a telemetry bug can never surface to a user — a broken metric must not
|
|
15
15
|
* break a verification.
|
|
@@ -76,8 +76,23 @@ export const FEEDBACK_RETRY_BACKOFF_MS = 750;
|
|
|
76
76
|
* (`reticle version`/`gate`) ~800ms. Long-lived daemon events still send in-process: a spawn per tool
|
|
77
77
|
* call would be far heavier than a fetch inside an already-running server.
|
|
78
78
|
*/
|
|
79
|
+
/**
|
|
80
|
+
* The budget the DISOWNED child gets, and why it is not `SEND_TIMEOUT_MS`.
|
|
81
|
+
*
|
|
82
|
+
* 2s is right for an in-process send: the daemon is waiting on it, so the budget is a latency cost
|
|
83
|
+
* somebody pays. A detached child pays nobody — the parent has already exited — so the only thing a
|
|
84
|
+
* short budget there buys is a lost event.
|
|
85
|
+
*
|
|
86
|
+
* And it loses the worst ones. Every detached send comes from a SHORT-LIVED CLI process, which pays
|
|
87
|
+
* cold DNS and a cold TLS route by definition; measured on the feedback path with a WARM cache it
|
|
88
|
+
* was already 0.694s before any payload moved. `reticle_installed` is the extreme case: it fires
|
|
89
|
+
* once per machine, on the first Reticle command that machine ever runs, which is the single coldest
|
|
90
|
+
* network call it will ever make to the collector — so the top of the funnel is the event most
|
|
91
|
+
* likely to be dropped, and it fails silently like everything else here.
|
|
92
|
+
*/
|
|
93
|
+
const DETACHED_SEND_TIMEOUT_MS = 15_000;
|
|
79
94
|
const DETACHED_SEND_SCRIPT = "fetch(process.argv[1],{method:'POST',headers:{'content-type':'application/json'}," +
|
|
80
|
-
`body:process.argv[2],signal:AbortSignal.timeout(${
|
|
95
|
+
`body:process.argv[2],signal:AbortSignal.timeout(${DETACHED_SEND_TIMEOUT_MS})})` +
|
|
81
96
|
'.catch(()=>{}).finally(()=>process.exit(0))';
|
|
82
97
|
/** Env var names — mirror cloud-sync's `RETICLE_*` convention. */
|
|
83
98
|
import { isReticleSourceCheckout } from './dev-repo.js';
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* Emitted from `runTool`, which is the one place both dispatch paths cross, so a tool added later is
|
|
11
11
|
* covered without anybody remembering to cover it.
|
|
12
12
|
*/
|
|
13
|
-
import { type
|
|
13
|
+
import { RefusalReason, type NoSessionReason } from '@reticlehq/core';
|
|
14
|
+
/** Record the branch code for the no-session error being thrown right now. */
|
|
15
|
+
export declare function notePendingNoSessionReason(reason: NoSessionReason | undefined): void;
|
|
14
16
|
/** A call that was served. Breaks the retry chain: what follows it is not a retry of anything. */
|
|
15
17
|
export declare function noteToolServed(): void;
|
|
16
18
|
/** One refused call, reported with its cause and whether it was itself a retry. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Emitted from `runTool`, which is the one place both dispatch paths cross, so a tool added later is
|
|
11
11
|
* covered without anybody remembering to cover it.
|
|
12
12
|
*/
|
|
13
|
-
import { TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
|
|
13
|
+
import { RefusalReason, TelemetryActor, TelemetryEventKind, } from '@reticlehq/core';
|
|
14
14
|
import { getTelemetry } from './telemetry.js';
|
|
15
15
|
/**
|
|
16
16
|
* Refusal events one daemon run will send.
|
|
@@ -32,6 +32,24 @@ let sent = 0;
|
|
|
32
32
|
* describe — so the flag lands on the RETRY instead of on the first refusal.
|
|
33
33
|
*/
|
|
34
34
|
let lastRefusedTool;
|
|
35
|
+
/**
|
|
36
|
+
* The no-session branch code for the refusal about to be reported, set by whoever threw it.
|
|
37
|
+
*
|
|
38
|
+
* `no_session` is the largest refusal cohort and on its own it is a set difference: nothing
|
|
39
|
+
* connected, and no word on which of several opposite situations that was. The diagnosis that CAN
|
|
40
|
+
* say is computed where the error is thrown, and the refusal is classified downstream from the
|
|
41
|
+
* message, so the two never met (#615).
|
|
42
|
+
*
|
|
43
|
+
* Module-level and consumed once, like `lastRefusedTool` above and for the same reason: the
|
|
44
|
+
* producer and the consumer are one hop apart on the same call, and threading a field through every
|
|
45
|
+
* refusal path to carry it would touch a dozen call sites that have nothing to do with sessions.
|
|
46
|
+
* Cleared on read so a later refusal cannot inherit a stale reason.
|
|
47
|
+
*/
|
|
48
|
+
let pendingNoSessionReason;
|
|
49
|
+
/** Record the branch code for the no-session error being thrown right now. */
|
|
50
|
+
export function notePendingNoSessionReason(reason) {
|
|
51
|
+
pendingNoSessionReason = reason;
|
|
52
|
+
}
|
|
35
53
|
/** A call that was served. Breaks the retry chain: what follows it is not a retry of anything. */
|
|
36
54
|
export function noteToolServed() {
|
|
37
55
|
lastRefusedTool = undefined;
|
|
@@ -40,16 +58,30 @@ export function noteToolServed() {
|
|
|
40
58
|
export function reportToolRefused(tool, reason) {
|
|
41
59
|
const retried = lastRefusedTool === tool;
|
|
42
60
|
lastRefusedTool = tool;
|
|
61
|
+
// Read and clear whatever the throw left, whether or not this refusal can use it: a reason left
|
|
62
|
+
// sitting would attach itself to an unrelated refusal later in the session.
|
|
63
|
+
const noSessionReason = pendingNoSessionReason;
|
|
64
|
+
pendingNoSessionReason = undefined;
|
|
43
65
|
if (sent >= MAX_REFUSALS_PER_SESSION)
|
|
44
66
|
return;
|
|
45
67
|
sent += 1;
|
|
46
68
|
void getTelemetry().emit(TelemetryEventKind.TOOL_REFUSED, {
|
|
47
69
|
actor: TelemetryActor.AGENT,
|
|
48
|
-
refusal: {
|
|
70
|
+
refusal: {
|
|
71
|
+
tool,
|
|
72
|
+
reason,
|
|
73
|
+
retried,
|
|
74
|
+
// Only where it means something. On any other refusal reason this field would be answering
|
|
75
|
+
// a question nobody asked of it.
|
|
76
|
+
...(RefusalReason.NO_SESSION === reason && noSessionReason !== undefined
|
|
77
|
+
? { noSessionReason }
|
|
78
|
+
: {}),
|
|
79
|
+
},
|
|
49
80
|
});
|
|
50
81
|
}
|
|
51
82
|
/** Tests only — the counters are process-lifetime, so each case has to start from zero. */
|
|
52
83
|
export function resetToolRefusals() {
|
|
53
84
|
sent = 0;
|
|
54
85
|
lastRefusedTool = undefined;
|
|
86
|
+
pendingNoSessionReason = undefined;
|
|
55
87
|
}
|
|
@@ -17,6 +17,16 @@ interface CapsuleSaveInputs {
|
|
|
17
17
|
file: string;
|
|
18
18
|
line: number;
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* The `.reticle` this capsule belongs in, from the session the act ACTUALLY drove.
|
|
22
|
+
*
|
|
23
|
+
* Passed in rather than re-resolved here, because re-resolving loses twice: `args.sessionId` is
|
|
24
|
+
* frequently absent, and `sessions.resolve(undefined)` throws whenever more than one tab is
|
|
25
|
+
* connected — which is precisely the multi-project case this routing exists for, so it failed in
|
|
26
|
+
* exactly the situation it was written to fix. The caller also knows about a session the act
|
|
27
|
+
* FOLLOWED through a navigation; this file cannot.
|
|
28
|
+
*/
|
|
29
|
+
root?: string | undefined;
|
|
20
30
|
}
|
|
21
31
|
/** Returns the capsule id when one was written, or undefined when there was nothing to save. */
|
|
22
32
|
export declare function saveFailedAssertCapsule(inputs: CapsuleSaveInputs): Promise<string | undefined>;
|
|
@@ -13,16 +13,22 @@ import { ActionType, AnchorKind } from '@reticlehq/core';
|
|
|
13
13
|
import { CapsuleStore, capsuleId, CAPSULE_VERSION } from '../capsule/capsule-store.js';
|
|
14
14
|
import { ReticleTool } from './tool-names.js';
|
|
15
15
|
import { asRecord, asString } from './tools-helpers.js';
|
|
16
|
+
import { sessionRoot } from '../project/session-root.js';
|
|
16
17
|
/** Returns the capsule id when one was written, or undefined when there was nothing to save. */
|
|
17
18
|
export async function saveFailedAssertCapsule(inputs) {
|
|
18
|
-
const { deps, verdict, capsule, links, args, actResult, actedSource } = inputs;
|
|
19
|
+
const { deps, verdict, capsule, links, args, actResult, actedSource, root: given } = inputs;
|
|
19
20
|
if (verdict.pass || capsule === undefined)
|
|
20
21
|
return undefined;
|
|
21
22
|
const id = capsuleId(deps.now(), asString(args['ref']) ?? 'assert');
|
|
22
23
|
const expectedText = links
|
|
23
24
|
.map((l) => ('name' in l ? `${l.kind} ${String(l.name)}` : l.kind))
|
|
24
25
|
.join(' AND ');
|
|
25
|
-
|
|
26
|
+
// The capsule belongs to the project whose app just failed, not to wherever the daemon stands.
|
|
27
|
+
// Same rule and same resolver as every other artifact: a capsule written into a sibling repo is
|
|
28
|
+
// evidence filed against a codebase that did not produce it, and it outlives the turn.
|
|
29
|
+
// The caller's answer wins; the resolver is the fallback for callers that have no session.
|
|
30
|
+
const root = given ?? sessionRoot(deps, asString(args['sessionId']));
|
|
31
|
+
const saved = await new CapsuleStore(deps.fs, root).save({
|
|
26
32
|
version: CAPSULE_VERSION,
|
|
27
33
|
id,
|
|
28
34
|
createdAt: deps.now(),
|
|
@@ -2,7 +2,8 @@ import { ActionType } from '@reticlehq/core';
|
|
|
2
2
|
export declare function assertNotDestructive(action: ActionType, innerArgs: Record<string, unknown>, inspected: unknown): void;
|
|
3
3
|
/**
|
|
4
4
|
* A drag is judged on BOTH ends. Dropping a harmless row onto "Delete" is destructive, and the
|
|
5
|
-
* source alone never says so.
|
|
5
|
+
* source alone never says so. Each end is classified on its own text and role, so a Payment
|
|
6
|
+
* option dragged onto Save is not a payment.
|
|
6
7
|
*/
|
|
7
8
|
export declare function assertDragNotDestructive(innerArgs: Record<string, unknown>, from: unknown, to: unknown): void;
|
|
8
9
|
/**
|
package/dist/tools/act-danger.js
CHANGED
|
@@ -22,23 +22,31 @@ function descriptorText(value) {
|
|
|
22
22
|
asString(descriptor['formText']) ?? '',
|
|
23
23
|
].join(' ');
|
|
24
24
|
}
|
|
25
|
+
function descriptorRole(value) {
|
|
26
|
+
const role = asString(asRecord(value)['role']);
|
|
27
|
+
return role !== undefined && role.length > 0 ? role : undefined;
|
|
28
|
+
}
|
|
29
|
+
function isDestructiveDescriptor(value) {
|
|
30
|
+
return isDangerousActionText(descriptorText(value), descriptorRole(value));
|
|
31
|
+
}
|
|
25
32
|
export function assertNotDestructive(action, innerArgs, inspected) {
|
|
26
33
|
if (action !== ActionType.CLICK && action !== ActionType.DBLCLICK)
|
|
27
34
|
return;
|
|
28
35
|
if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG])
|
|
29
36
|
return;
|
|
30
|
-
if (!
|
|
37
|
+
if (!isDestructiveDescriptor(inspected))
|
|
31
38
|
return;
|
|
32
39
|
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* A drag is judged on BOTH ends. Dropping a harmless row onto "Delete" is destructive, and the
|
|
36
|
-
* source alone never says so.
|
|
43
|
+
* source alone never says so. Each end is classified on its own text and role, so a Payment
|
|
44
|
+
* option dragged onto Save is not a payment.
|
|
37
45
|
*/
|
|
38
46
|
export function assertDragNotDestructive(innerArgs, from, to) {
|
|
39
47
|
if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG])
|
|
40
48
|
return;
|
|
41
|
-
if (!
|
|
49
|
+
if (!isDestructiveDescriptor(from) && !isDestructiveDescriptor(to))
|
|
42
50
|
return;
|
|
43
51
|
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
44
52
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to do when the observer left in the middle of an act.
|
|
3
|
+
*
|
|
4
|
+
* A full-document navigation — often the very thing the act CAUSED — tears the SDK down. The page
|
|
5
|
+
* re-announces, and whatever this call still had in flight is gone. That is a fact about the
|
|
6
|
+
* observer, never about the application, and the difference matters: every honest verdict this
|
|
7
|
+
* engine can give depends on not converting "I stopped watching" into "your app is broken".
|
|
8
|
+
*
|
|
9
|
+
* Extracted from act-tools.ts, which had reached the file-size cap. It earns its own module because
|
|
10
|
+
* it is one idea — follow the document that took over, and re-ask there — used from the two points
|
|
11
|
+
* where observation can be lost: a write rejected at dispatch, and a wait cut off mid-predicate.
|
|
12
|
+
*/
|
|
13
|
+
import type { Session } from '../session/session.js';
|
|
14
|
+
import { type SuccessorRegistry } from '../session/session-successor.js';
|
|
15
|
+
import type { EvalResult } from '../events/predicate-eval.js';
|
|
16
|
+
export interface FollowSuccessor {
|
|
17
|
+
sessions: SuccessorRegistry;
|
|
18
|
+
session: Session;
|
|
19
|
+
verdict: EvalResult;
|
|
20
|
+
/** Total budget the CALLER granted, in ms. */
|
|
21
|
+
timeout: number;
|
|
22
|
+
/** `session.elapsed()` at the moment the predicate started. */
|
|
23
|
+
predicateStarted: number;
|
|
24
|
+
/** Re-ask the declared consequence on `next`, with whatever budget is left. */
|
|
25
|
+
reevaluate: (next: Session, budgetMs: number) => Promise<EvalResult>;
|
|
26
|
+
}
|
|
27
|
+
export interface FollowedOutcome {
|
|
28
|
+
session: Session;
|
|
29
|
+
verdict: EvalResult;
|
|
30
|
+
/** True when the wait moved to a new document, so the caller must reset its event cursor. */
|
|
31
|
+
followed: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Follow the unique same-origin successor and re-ask there, or leave the verdict untouched.
|
|
35
|
+
*
|
|
36
|
+
* `navigate` already waits for the HELLO and returns the new id; this path used to grade
|
|
37
|
+
* `observation_lost` and leave the agent holding a DEAD id, so the next assert failed even though
|
|
38
|
+
* the new page had loaded perfectly. Two live tabs at that origin is still a guess, and
|
|
39
|
+
* `awaitDocumentSuccessor` refuses to guess — a wrong follow drives somebody else's app.
|
|
40
|
+
*/
|
|
41
|
+
export declare function followLostObservation(input: FollowSuccessor): Promise<FollowedOutcome>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to do when the observer left in the middle of an act.
|
|
3
|
+
*
|
|
4
|
+
* A full-document navigation — often the very thing the act CAUSED — tears the SDK down. The page
|
|
5
|
+
* re-announces, and whatever this call still had in flight is gone. That is a fact about the
|
|
6
|
+
* observer, never about the application, and the difference matters: every honest verdict this
|
|
7
|
+
* engine can give depends on not converting "I stopped watching" into "your app is broken".
|
|
8
|
+
*
|
|
9
|
+
* Extracted from act-tools.ts, which had reached the file-size cap. It earns its own module because
|
|
10
|
+
* it is one idea — follow the document that took over, and re-ask there — used from the two points
|
|
11
|
+
* where observation can be lost: a write rejected at dispatch, and a wait cut off mid-predicate.
|
|
12
|
+
*/
|
|
13
|
+
import { awaitDocumentSuccessor } from '../session/session-successor.js';
|
|
14
|
+
/**
|
|
15
|
+
* Follow the unique same-origin successor and re-ask there, or leave the verdict untouched.
|
|
16
|
+
*
|
|
17
|
+
* `navigate` already waits for the HELLO and returns the new id; this path used to grade
|
|
18
|
+
* `observation_lost` and leave the agent holding a DEAD id, so the next assert failed even though
|
|
19
|
+
* the new page had loaded perfectly. Two live tabs at that origin is still a guess, and
|
|
20
|
+
* `awaitDocumentSuccessor` refuses to guess — a wrong follow drives somebody else's app.
|
|
21
|
+
*/
|
|
22
|
+
export async function followLostObservation(input) {
|
|
23
|
+
const { sessions, session, verdict, timeout, predicateStarted, reevaluate } = input;
|
|
24
|
+
if (true !== verdict.observationLost || 0 >= timeout)
|
|
25
|
+
return { session, verdict, followed: false };
|
|
26
|
+
const remaining = timeout - (session.elapsed() - predicateStarted);
|
|
27
|
+
const next = remaining > 0 ? await awaitDocumentSuccessor(sessions, session, remaining) : null;
|
|
28
|
+
if (null === next)
|
|
29
|
+
return { session, verdict, followed: false };
|
|
30
|
+
// Computed on the DEPARTED session, before the caller points at the successor: the new session's
|
|
31
|
+
// elapsed() starts at 0 and would silently refund the whole budget the caller had already spent.
|
|
32
|
+
const leftover = timeout - (session.elapsed() - predicateStarted);
|
|
33
|
+
if (leftover <= 0)
|
|
34
|
+
return { session: next, verdict, followed: true };
|
|
35
|
+
return { session: next, verdict: await reevaluate(next, leftover), followed: true };
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function assertSequenceSteps(steps: readonly unknown[]): void;
|
|
2
|
+
/**
|
|
3
|
+
* Throws if this call cannot honestly be driven. Call it after the args are parsed and before the
|
|
4
|
+
* first dispatch — anything that depends on what is actually rendered belongs after the action.
|
|
5
|
+
*/
|
|
6
|
+
export declare function preflightAct(actArgs: Record<string, unknown>, until: unknown): void;
|
|
7
|
+
/**
|
|
8
|
+
* Dispatch a write, or report that its outcome went unobserved.
|
|
9
|
+
*
|
|
10
|
+
* Returns null — never throws — when the transport was displaced by a newer connection claiming the
|
|
11
|
+
* same id. That happens on any full-document navigation the act itself causes: the page unloads,
|
|
12
|
+
* re-announces, and the write in flight is rejected by the handle that just died.
|
|
13
|
+
*
|
|
14
|
+
* The write is deliberately NOT re-sent. Re-asking a READ on the successor is free; re-sending a
|
|
15
|
+
* WRITE is not, because nothing can prove the first one did not reach the page — and a duplicated
|
|
16
|
+
* click on a button that charges a card is a far worse outcome than a verdict of "I could not see".
|
|
17
|
+
* Every other failure propagates unchanged: only a replacement is an observation problem, and
|
|
18
|
+
* treating a timeout or a refusal as one would launder a real failure into a shrug.
|
|
19
|
+
*/
|
|
20
|
+
export declare function dispatchAct<T>(send: () => Promise<T>): Promise<T | null>;
|