@reticlehq/server 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/SKILL.md +130 -778
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +36 -1
- package/dist/capsule/causal-summary.js +33 -7
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +65 -34
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +32 -0
- package/dist/cli/cli-port.js +82 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +47 -3
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +69 -0
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +119 -39
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +46 -0
- package/dist/events/contradictions.js +35 -22
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -72
- package/dist/events/predicate-eval.js +117 -236
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +296 -22
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +21 -5
- package/dist/flows/heal-run.js +1 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +10 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +49 -0
- package/dist/honesty/unsettled.js +82 -0
- package/dist/honesty/verified.d.ts +36 -3
- package/dist/honesty/verified.js +54 -11
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +47 -10
- package/dist/init/agent-rules.d.ts +36 -0
- package/dist/init/agent-rules.js +132 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +38 -6
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +114 -0
- package/dist/init/confirm.js +165 -0
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +78 -16
- package/dist/init/plan.d.ts +47 -6
- package/dist/init/plan.js +228 -69
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +157 -45
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +54 -6
- package/dist/init/snippets.d.ts +45 -5
- package/dist/init/snippets.js +194 -42
- package/dist/init/vite-config.js +53 -5
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +37 -5
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +35 -30
- package/dist/mcp/mcp-proxy.js +306 -104
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +70 -34
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +11 -2
- package/dist/mcp/proxy-lifecycle.js +12 -3
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +67 -2
- package/dist/session/no-session-diagnosis.js +236 -27
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +174 -8
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +15 -17
- package/dist/telemetry/bug-found.js +52 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +56 -0
- package/dist/telemetry/install-source.js +74 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +15 -0
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +26 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +209 -28
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +49 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +73 -9
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +159 -26
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +40 -1
- package/dist/tools/lease-tools.js +181 -83
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +10 -4
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +52 -0
- package/dist/tools/react-grace.js +90 -0
- package/dist/tools/read-tools.js +67 -5
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +94 -0
- package/dist/tools/settle-in-flight.js +146 -0
- package/dist/tools/snapshot-delta.d.ts +10 -0
- package/dist/tools/snapshot-delta.js +194 -9
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +86 -1
- package/dist/tools/tools.js +61 -8
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +55 -11
- package/docs/actions.mdx +120 -0
- package/docs/agent-cheatsheet.md +95 -29
- package/docs/architecture.md +52 -35
- package/docs/benchmarks.md +127 -75
- package/docs/best-practices.mdx +216 -0
- package/docs/capabilities.mdx +534 -0
- package/docs/cli/affected.mdx +53 -0
- package/docs/cli/capsules.mdx +70 -0
- package/docs/cli/cloud.mdx +153 -0
- package/docs/cli/doctor.mdx +109 -0
- package/docs/cli/drive.mdx +74 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +94 -0
- package/docs/cli/hunt.mdx +73 -0
- package/docs/cli/identify.mdx +76 -0
- package/docs/cli/init.mdx +121 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +74 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +69 -0
- package/docs/cli/status.mdx +88 -0
- package/docs/cli/stop.mdx +64 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +76 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +62 -0
- package/docs/cli.mdx +241 -0
- package/docs/debugging.md +16 -10
- package/docs/deploy-checks.md +45 -12
- package/docs/desktop.mdx +384 -0
- package/docs/docs.json +364 -0
- package/docs/enterprise.md +22 -16
- package/docs/faq.mdx +102 -0
- package/docs/fixtures.md +13 -7
- package/docs/flows.md +29 -23
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +238 -0
- package/docs/gate-plan.md +48 -42
- package/docs/gates.md +28 -22
- package/docs/getting-started.md +129 -77
- package/docs/human-control.md +24 -18
- package/docs/index.mdx +143 -0
- package/docs/install-agentic.mdx +224 -0
- package/docs/install-manual.mdx +196 -0
- package/docs/instrumentation.mdx +238 -0
- package/docs/integration-patterns.md +41 -35
- package/docs/local-registry.md +19 -13
- package/docs/multi-agent-testing.md +19 -13
- package/docs/packages/babel-plugin.mdx +79 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +116 -0
- package/docs/packages/electron.mdx +85 -0
- package/docs/packages/eslint-plugin.mdx +86 -0
- package/docs/packages/next.mdx +89 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +119 -0
- package/docs/packages/vite-plugin.mdx +94 -0
- package/docs/packages.mdx +218 -0
- package/docs/platform-integration.md +35 -26
- package/docs/predicates.mdx +248 -0
- package/docs/quickstart.mdx +257 -0
- package/docs/recipes.mdx +282 -0
- package/docs/skill-file.mdx +107 -0
- package/docs/state-management.mdx +199 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +50 -33
- package/docs/telemetry-contract.md +106 -52
- package/docs/telemetry-events.mdx +475 -0
- package/docs/telemetry.md +63 -55
- package/docs/testing.md +44 -6
- package/docs/token-efficiency.md +37 -27
- package/docs/tools-act-and-wait.mdx +173 -0
- package/docs/tools-act-sequence.mdx +137 -0
- package/docs/tools-act.mdx +95 -0
- package/docs/tools-assert.mdx +129 -0
- package/docs/tools-console.mdx +63 -0
- package/docs/tools-inspect.mdx +88 -0
- package/docs/tools-navigate.mdx +99 -0
- package/docs/tools-network.mdx +119 -0
- package/docs/tools-observe.mdx +145 -0
- package/docs/tools-overview.mdx +160 -0
- package/docs/tools-query.mdx +126 -0
- package/docs/tools-session-and-feedback.mdx +112 -0
- package/docs/tools-sessions.mdx +94 -0
- package/docs/tools-snapshot.mdx +129 -0
- package/docs/tools-state.mdx +104 -0
- package/docs/tools-tools-and-run.mdx +156 -0
- package/docs/tools-wait-for.mdx +85 -0
- package/docs/troubleshooting.mdx +263 -0
- package/docs/usage.md +209 -165
- package/docs/vs-chrome-devtools-mcp.mdx +123 -0
- package/docs/vs-playwright-mcp.mdx +125 -0
- package/docs/vs-screenshots.mdx +137 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +109 -0
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -256
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
package/dist/session/session.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { recordImpact } from '../impact/impact-recorder.js';
|
|
1
2
|
import { LastAct } from './last-act.js';
|
|
2
3
|
import { commandTimeoutMessage } from './command-timeout.js';
|
|
3
4
|
import { readHealthEvent } from './session-health.js';
|
|
4
|
-
|
|
5
|
+
/** The HUD does not need 200 frames to watch a counter climb; the last state always lands. */
|
|
6
|
+
const IMPACT_PUSH_DEBOUNCE_MS = 700;
|
|
7
|
+
import { PendingCommands, CommandTimeoutError } from './pending-commands.js';
|
|
5
8
|
import { span } from '../trace.js';
|
|
6
9
|
import { AppRuntime, EventType, HumanControlDataSchema, HumanControlKind, HumanMarkDataSchema, ReticleCommand, MessageKind, parseEventPayload, PresenterTone, SESSION_HEALTH, SESSION_LEASE, SESSION_LIFECYCLE, SessionState, } from '@reticlehq/core';
|
|
7
10
|
import { RingBuffer } from '../events/ring-buffer.js';
|
|
@@ -18,6 +21,33 @@ import { buildSessionInfo } from './session-info.js';
|
|
|
18
21
|
const DEFAULT_COMMAND_TIMEOUT_MS = 8000;
|
|
19
22
|
/** Prefix on correlated command ids (c1, c2, …) — distinguishes them from mark ids. */
|
|
20
23
|
const COMMAND_ID_PREFIX = 'c';
|
|
24
|
+
/**
|
|
25
|
+
* Commands that may be re-issued against the connection that replaced this session.
|
|
26
|
+
*
|
|
27
|
+
* Reads only, and the boundary is the point. Re-reading a page costs nothing and answers the same
|
|
28
|
+
* question the caller asked. An act cannot be replayed: it may already have been dispatched in the
|
|
29
|
+
* page that went away, and performing it a second time behind the caller's back is a double submit
|
|
30
|
+
* nobody asked for. A replaced act still errors, and the caller's own retry — with the same id,
|
|
31
|
+
* which is still the right one — is the safe path.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* How far to walk a chain of replacements before giving up.
|
|
35
|
+
*
|
|
36
|
+
* Generous for the real case (a page reloading a handful of times while a call is in flight) and
|
|
37
|
+
* small enough that a cycle costs nothing. The number is a backstop, not a policy: a chain longer
|
|
38
|
+
* than this means something is re-dialling in a loop, and that is a different problem.
|
|
39
|
+
*/
|
|
40
|
+
const MAX_SUCCESSOR_HOPS = 32;
|
|
41
|
+
const REBINDABLE_COMMANDS = new Set([
|
|
42
|
+
ReticleCommand.SNAPSHOT,
|
|
43
|
+
ReticleCommand.QUERY,
|
|
44
|
+
ReticleCommand.MATCH,
|
|
45
|
+
ReticleCommand.INSPECT,
|
|
46
|
+
ReticleCommand.STATE_READ,
|
|
47
|
+
ReticleCommand.STORAGE_READ,
|
|
48
|
+
ReticleCommand.CAPABILITIES,
|
|
49
|
+
ReticleCommand.ANIMATIONS,
|
|
50
|
+
]);
|
|
21
51
|
/** Prefix on minted action ids (a1, a2, …) — the journal's action identity, independent of commands. */
|
|
22
52
|
const ACTION_ID_PREFIX = 'a';
|
|
23
53
|
/** ws readyState for an OPEN socket — guard fire-and-forget pushes against a closing tab. */
|
|
@@ -192,8 +222,12 @@ export class Session {
|
|
|
192
222
|
if (event.type === EventType.HUMAN_MARK) {
|
|
193
223
|
// A human pinned a mistake to an element. Narrow at the boundary; an invalid mark is ignored.
|
|
194
224
|
const parsed = HumanMarkDataSchema.safeParse(event.data);
|
|
195
|
-
if (parsed.success)
|
|
225
|
+
if (parsed.success) {
|
|
196
226
|
this.#review.add(parsed.data, this.elapsed());
|
|
227
|
+
// The impact record's `marks` field existed and nothing ever wrote it: the report would
|
|
228
|
+
// have shown a permanent zero next to a page covered in pins.
|
|
229
|
+
recordImpact({ marks: 1 });
|
|
230
|
+
}
|
|
197
231
|
}
|
|
198
232
|
if (event.type === EventType.ROUTE_CHANGE) {
|
|
199
233
|
// Keep the reported URL live across SPA navigation. The SDK already emits route.change on
|
|
@@ -215,8 +249,18 @@ export class Session {
|
|
|
215
249
|
: seen;
|
|
216
250
|
this.#observed.observe(attributed);
|
|
217
251
|
this.#buffer.push(attributed, t, byteSize);
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
// Each subscriber is independent, and this runs inside the websocket's `message` handler — a
|
|
253
|
+
// NON-async callback, so a sync throw here escapes every try/catch the tool call is wrapped in
|
|
254
|
+
// and becomes an uncaughtException, which the daemon answers by exiting. One observer's bug must
|
|
255
|
+
// not cost the fleet its daemon. Same rule the session-ready fan-out already follows.
|
|
256
|
+
for (const listener of this.#listeners) {
|
|
257
|
+
try {
|
|
258
|
+
listener(attributed);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* an event observer must never break the session it observes */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
220
264
|
}
|
|
221
265
|
// Passthroughs to ObservedState, which owns the per-session facts that must SURVIVE buffer
|
|
222
266
|
// eviction: refs the agent drove, and what the layer could not see. See observed-state.ts.
|
|
@@ -397,6 +441,12 @@ export class Session {
|
|
|
397
441
|
if ('string' === typeof ref)
|
|
398
442
|
this.recordActedRef(ref);
|
|
399
443
|
}
|
|
444
|
+
// The page already re-dialled under this same id. Nothing has been sent yet, so handing the
|
|
445
|
+
// command to the live connection cannot perform anything twice — it is the call the agent would
|
|
446
|
+
// have made itself after a `reticle_sessions` round trip, minus the round trip.
|
|
447
|
+
const successor = this.#liveSuccessor();
|
|
448
|
+
if (successor !== undefined)
|
|
449
|
+
return successor.command(name, args, timeoutMs);
|
|
400
450
|
const id = this.#pending.nextId(COMMAND_ID_PREFIX);
|
|
401
451
|
const payload = JSON.stringify({
|
|
402
452
|
kind: MessageKind.COMMAND,
|
|
@@ -425,7 +475,27 @@ export class Session {
|
|
|
425
475
|
this.recordActedLabelFrom(result);
|
|
426
476
|
return result;
|
|
427
477
|
})
|
|
428
|
-
.finally(() => recordBrowserLatency(Date.now() - sentAt))
|
|
478
|
+
.finally(() => recordBrowserLatency(Date.now() - sentAt))
|
|
479
|
+
.catch((error) => {
|
|
480
|
+
// The replacement landed while this command was on the wire, which is the reported case:
|
|
481
|
+
// `reticle_navigate` reloads the page, the reload sends a fresh HELLO carrying the same id,
|
|
482
|
+
// and the `reticle_snapshot` already in flight is rejected by the displaced session.
|
|
483
|
+
// A TIMEOUT is not a replacement. The catch fired on any rejection, so a command that had
|
|
484
|
+
// simply not been answered in budget was read as "you were replaced, ask again" — and
|
|
485
|
+
// against a page re-dialling in a loop that turned one read into hundreds of commands on the
|
|
486
|
+
// wire, minutes of wall clock, and an answer still quoting the budget it had blown.
|
|
487
|
+
const next = error instanceof CommandTimeoutError ? undefined : this.#liveSuccessor();
|
|
488
|
+
if (next === undefined || !REBINDABLE_COMMANDS.has(name))
|
|
489
|
+
throw error;
|
|
490
|
+
// What is LEFT of the caller's budget, not a fresh copy of it. Re-issuing with the original
|
|
491
|
+
// timeout means a caller who granted 8s can wait 16, and across a chain of replacements the
|
|
492
|
+
// total is unbounded — a wait nobody asked for, on the path that exists to save a round trip.
|
|
493
|
+
// A budget already spent means there is nothing left to retry into, so the error stands.
|
|
494
|
+
const remaining = timeoutMs - (Date.now() - sentAt);
|
|
495
|
+
if (remaining <= 0)
|
|
496
|
+
throw error;
|
|
497
|
+
return next.command(name, args, remaining);
|
|
498
|
+
});
|
|
429
499
|
}
|
|
430
500
|
handleResult(result) {
|
|
431
501
|
this.#pending.settle(result);
|
|
@@ -437,8 +507,52 @@ export class Session {
|
|
|
437
507
|
listener();
|
|
438
508
|
this.#disconnectListeners.clear();
|
|
439
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* The connection that took this session's id over, once one has.
|
|
512
|
+
*
|
|
513
|
+
* Set by the bridge at the moment it displaces this session, so the two facts a replaced handle
|
|
514
|
+
* needs — that it was replaced, and by whom — arrive together. Without it, every tool call holding
|
|
515
|
+
* this handle could only report "session replaced by a newer connection claiming the same id", and
|
|
516
|
+
* the caller's only recovery was `reticle_sessions` plus a retry: two round trips to rediscover an
|
|
517
|
+
* id the daemon was already holding, and which had not even changed.
|
|
518
|
+
*/
|
|
519
|
+
#successor;
|
|
520
|
+
succeededBy(next) {
|
|
521
|
+
// A session is never its own replacement; guarding here keeps `command` from recursing forever
|
|
522
|
+
// if a registry ever re-adds the same object.
|
|
523
|
+
if (next !== this)
|
|
524
|
+
this.#successor = next;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The replacement, but only while it can actually answer.
|
|
528
|
+
*
|
|
529
|
+
* A successor is recorded once and never cleared, so without this a session that was replaced an
|
|
530
|
+
* hour ago keeps handing commands to a socket that has since closed — and the caller pays the full
|
|
531
|
+
* timeout to learn it, turning an instant "session replaced" into a wait. The point of delegating
|
|
532
|
+
* was to save the caller a round trip; delegating to a dead socket costs them more than the error
|
|
533
|
+
* did.
|
|
534
|
+
*/
|
|
535
|
+
#liveSuccessor() {
|
|
536
|
+
// Walked, not just read one hop. Two replacements in quick succession — a page that reloads
|
|
537
|
+
// twice — leave a CHAIN, and the handle the caller holds points at a session that is itself
|
|
538
|
+
// already displaced. Stopping at the first dead link fails safe and also fails: the open page is
|
|
539
|
+
// one hop further on, and the caller gets back exactly the error this exists to remove.
|
|
540
|
+
//
|
|
541
|
+
// Bounded, because `succeededBy` refuses self-succession but nothing prevents a cycle, and an
|
|
542
|
+
// unbounded walk over one is a hang — which is worse than the error it was trying to avoid.
|
|
543
|
+
let next = this.#successor;
|
|
544
|
+
for (let hops = 0; next !== undefined && hops < MAX_SUCCESSOR_HOPS; hops += 1) {
|
|
545
|
+
if (next.#socket.readyState === WS_OPEN)
|
|
546
|
+
return next;
|
|
547
|
+
next = next.#successor;
|
|
548
|
+
}
|
|
549
|
+
return undefined;
|
|
550
|
+
}
|
|
440
551
|
/** End this transport without letting a stale socket remove its replacement session. */
|
|
441
552
|
disconnect(reason) {
|
|
553
|
+
// "Longest run" means the longest SESSION, and it used to be fed a single tool call's duration
|
|
554
|
+
// - so the report's superlative was a number in milliseconds that never grew past a click.
|
|
555
|
+
recordImpact({}, { runMs: this.elapsed() });
|
|
442
556
|
this.rejectAll(reason);
|
|
443
557
|
try {
|
|
444
558
|
this.#socket.close(1008, reason);
|
|
@@ -515,10 +629,37 @@ export class Session {
|
|
|
515
629
|
* Push a lifecycle state to the panel with optional human-facing `text`. State changes still flow
|
|
516
630
|
* through `setState`; an auto-ended session rides a `warn` tone so the panel can shout "agent stopped".
|
|
517
631
|
*/
|
|
632
|
+
/**
|
|
633
|
+
* Push the impact record to this tab's HUD.
|
|
634
|
+
*
|
|
635
|
+
* Debounced: the record changes on every tool call, and the panel does not need 200 frames to
|
|
636
|
+
* show a counter going up. The LAST state always lands, because the timer re-reads the store
|
|
637
|
+
* when it fires rather than capturing a snapshot when it was scheduled.
|
|
638
|
+
*/
|
|
639
|
+
pushImpact(read, immediate = false) {
|
|
640
|
+
if (immediate) {
|
|
641
|
+
// A tab that just connected has nothing to show yet, so its first record goes out at once -
|
|
642
|
+
// waiting a debounce here is a report that reads "nothing recorded" over a month of history.
|
|
643
|
+
const first = read();
|
|
644
|
+
if (first !== undefined)
|
|
645
|
+
this.#post(ReticleCommand.IMPACT, { snapshot: first });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (this.#impactTimer !== undefined)
|
|
649
|
+
return;
|
|
650
|
+
this.#impactTimer = setTimeout(() => {
|
|
651
|
+
this.#impactTimer = undefined;
|
|
652
|
+
const snapshot = read();
|
|
653
|
+
if (snapshot !== undefined)
|
|
654
|
+
this.#post(ReticleCommand.IMPACT, { snapshot });
|
|
655
|
+
}, IMPACT_PUSH_DEBOUNCE_MS);
|
|
656
|
+
this.#impactTimer.unref?.();
|
|
657
|
+
}
|
|
518
658
|
pushPresenter(state, text, tone) {
|
|
519
659
|
this.#post(ReticleCommand.PRESENTER, buildPresenterArgs(state, text, tone));
|
|
520
660
|
}
|
|
521
661
|
/** Fire-and-forget a narration row to the live panel (so a resolved mark shows "✓ fixed"). */
|
|
662
|
+
#impactTimer;
|
|
522
663
|
pushNarration(text) {
|
|
523
664
|
this.#post(ReticleCommand.NARRATE, { text, level: 'info' });
|
|
524
665
|
}
|
|
@@ -33,5 +33,13 @@ export interface InstrumentationFacts {
|
|
|
33
33
|
* never be the reason a session fails to register.
|
|
34
34
|
*/
|
|
35
35
|
export declare function reportAppInstrumented(facts: InstrumentationFacts, now?: () => number): void;
|
|
36
|
+
/**
|
|
37
|
+
* Has an app connected to this daemon run at all?
|
|
38
|
+
*
|
|
39
|
+
* The same flag `reportAppInstrumented` uses for idempotency, read from outside. The stall check
|
|
40
|
+
* needs exactly this question and must not answer it from a window counter, which resets on every
|
|
41
|
+
* flush and would then report a long-instrumented daemon as freshly stalled.
|
|
42
|
+
*/
|
|
43
|
+
export declare function appEverConnected(): boolean;
|
|
36
44
|
/** Tests only. */
|
|
37
45
|
export declare function resetAppInstrumented(): void;
|
|
@@ -47,6 +47,16 @@ export function reportAppInstrumented(facts, now = () => Date.now()) {
|
|
|
47
47
|
/* never let a metric interfere with a page connecting */
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Has an app connected to this daemon run at all?
|
|
52
|
+
*
|
|
53
|
+
* The same flag `reportAppInstrumented` uses for idempotency, read from outside. The stall check
|
|
54
|
+
* needs exactly this question and must not answer it from a window counter, which resets on every
|
|
55
|
+
* flush and would then report a long-instrumented daemon as freshly stalled.
|
|
56
|
+
*/
|
|
57
|
+
export function appEverConnected() {
|
|
58
|
+
return reported;
|
|
59
|
+
}
|
|
50
60
|
/** Tests only. */
|
|
51
61
|
export function resetAppInstrumented() {
|
|
52
62
|
reported = false;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AutomationHint } from '@reticlehq/core';
|
|
2
|
+
export interface AutomationInput {
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
fileExists: (path: string) => boolean;
|
|
5
|
+
hasTty: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The most specific signal that fires, or undefined.
|
|
9
|
+
*
|
|
10
|
+
* Ordered most specific first: a container inside a Codespace with no terminal is one fact about the
|
|
11
|
+
* run, and sending the weakest of the three would waste the field. Omitted rather than sent as a
|
|
12
|
+
* "none" value, so its presence is itself the signal — the convention every other advisory field
|
|
13
|
+
* here follows.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveAutomationHint(input: AutomationInput): AutomationHint | undefined;
|
|
16
|
+
/** The real reading, taken once per process. */
|
|
17
|
+
export declare function currentAutomationHint(env: NodeJS.ProcessEnv): AutomationHint | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ADVISORY marker that a run looks automated, for the runs `ci` cannot see.
|
|
3
|
+
*
|
|
4
|
+
* `ci` is a single environment variable, which is exactly right for a GitHub Actions runner and
|
|
5
|
+
* blind to everything else: our own gate has run from a datacenter and landed in the user data
|
|
6
|
+
* indistinguishable from somebody at a laptop.
|
|
7
|
+
*
|
|
8
|
+
* ADVISORY, and never a filter. A real person works inside a container, inside a Codespace, and over
|
|
9
|
+
* an ssh session with no terminal attached, and every one of them is a user. Nothing may drop a row
|
|
10
|
+
* because this is set; it exists so a surprising number can be looked at from a second angle, and a
|
|
11
|
+
* hint that starts being used as an exclusion has stopped being a hint. Absent means "nothing here
|
|
12
|
+
* looked automated", never "a human was present".
|
|
13
|
+
*
|
|
14
|
+
* WHAT WAS REJECTED, so nobody re-proposes it:
|
|
15
|
+
* - IP address, ASN or datacenter geolocation. Not knowable client-side, and it is a stronger
|
|
16
|
+
* identifier than anything else we collect. The one signal that would actually separate a cloud
|
|
17
|
+
* sandbox from a laptop is the one we will not collect.
|
|
18
|
+
* - Hostname patterns (`runner-*`, `ip-10-*`). A hostname is user data, and matching names is
|
|
19
|
+
* guesswork dressed as a measurement — a guessed dimension and a measured one are
|
|
20
|
+
* indistinguishable once they are in the same column.
|
|
21
|
+
* - Process ancestry. Costs a spawn on a path that must never tax a command, and reads the name of
|
|
22
|
+
* somebody else's program to classify our own user.
|
|
23
|
+
* - `npm_config_user_agent`. Says which package manager ran us, which every route shares.
|
|
24
|
+
* - An absent `$DISPLAY`. Every Linux server, every ssh session, and a large share of genuine
|
|
25
|
+
* developer machines trip it; it separates nothing.
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync } from 'node:fs';
|
|
28
|
+
import { AutomationHint } from '@reticlehq/core';
|
|
29
|
+
/** Container runtimes drop one of these at the filesystem root. */
|
|
30
|
+
const CONTAINER_MARKERS = ['/.dockerenv', '/run/.containerenv'];
|
|
31
|
+
/** Hosted dev environments that declare themselves. Self-declared, never inferred. */
|
|
32
|
+
const HOSTED_WORKSPACE_ENV = [
|
|
33
|
+
'CODESPACES',
|
|
34
|
+
'GITPOD_WORKSPACE_ID',
|
|
35
|
+
'CLOUD_SHELL',
|
|
36
|
+
'REMOTE_CONTAINERS',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* The most specific signal that fires, or undefined.
|
|
40
|
+
*
|
|
41
|
+
* Ordered most specific first: a container inside a Codespace with no terminal is one fact about the
|
|
42
|
+
* run, and sending the weakest of the three would waste the field. Omitted rather than sent as a
|
|
43
|
+
* "none" value, so its presence is itself the signal — the convention every other advisory field
|
|
44
|
+
* here follows.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveAutomationHint(input) {
|
|
47
|
+
if (CONTAINER_MARKERS.some((m) => input.fileExists(m)))
|
|
48
|
+
return AutomationHint.CONTAINER;
|
|
49
|
+
if (HOSTED_WORKSPACE_ENV.some((k) => (input.env[k] ?? '') !== ''))
|
|
50
|
+
return AutomationHint.HOSTED_WORKSPACE;
|
|
51
|
+
if (!input.hasTty)
|
|
52
|
+
return AutomationHint.NO_TTY;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** The real reading, taken once per process. */
|
|
56
|
+
export function currentAutomationHint(env) {
|
|
57
|
+
return resolveAutomationHint({
|
|
58
|
+
env,
|
|
59
|
+
fileExists: (p) => {
|
|
60
|
+
try {
|
|
61
|
+
return existsSync(p);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false; // an unreadable root is not evidence of anything
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
// Both ends, because a piped stdout alone is how `reticle status | jq` looks, and that is a
|
|
68
|
+
// person typing.
|
|
69
|
+
hasTty: true === process.stdout.isTTY || true === process.stdin.isTTY,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -6,24 +6,22 @@ import { type BugFound } from '@reticlehq/core';
|
|
|
6
6
|
* nothing about sessions and should stay that way. The emission site owns the dedup flag.
|
|
7
7
|
*/
|
|
8
8
|
export type BugCandidate = Omit<BugFound, 'repeat'>;
|
|
9
|
+
export declare function bugsInResult(toolName: string, result: Record<string, unknown>): BugCandidate[];
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* It shipped twice and was wrong both times. First it stamped `app` on every contradiction, including
|
|
13
|
-
* `request-never-settled` — which Reticle raises from the ABSENCE of a settle event, so a dev-overlay
|
|
14
|
-
* poll manufactured it with the app doing nothing wrong. That was fixed, and a real drive then found
|
|
15
|
-
* it still inverted: across two full runs, EVERY `attribution: 'app'` was a misattribution, while the
|
|
16
|
-
* one defect that genuinely was a bad agent predicate carried none. One session would have published
|
|
17
|
-
* "2 defects in the app" against a true count of 0.
|
|
11
|
+
* Crawl anomaly kinds that are CONTRADICTIONS rather than single-channel faults.
|
|
18
12
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
13
|
+
* `reticle_crawl` returns both classes in one `anomalies` array — a console error is one channel
|
|
14
|
+
* complaining, while a UI that advanced over a failed write is two channels disagreeing. Only the
|
|
15
|
+
* second presents as success, and collapsing them would inflate the one claim that has to be exact.
|
|
22
16
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
17
|
+
* DERIVED from core's `ContradictionKind`, never hand-listed. The first version copied the twelve
|
|
18
|
+
* strings into a literal here, which matched on the day it was written and would have silently
|
|
19
|
+
* misclassified the thirteenth: a new contradiction kind would have been counted as an ordinary
|
|
20
|
+
* fault, quietly deflating the exact number we intend to publish. A copied enum is a drift hazard
|
|
21
|
+
* wherever it appears; it is a correctness hazard when the thing that drifts is a public claim.
|
|
28
22
|
*/
|
|
29
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The route the app was on when this result was produced — extracted from the tool result so the
|
|
25
|
+
* fingerprint can identify the same defect across sessions without sending the route itself.
|
|
26
|
+
*/
|
|
27
|
+
export declare function routeOf(result: Record<string, unknown>): string | undefined;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* anyone's app: that a class of defect was found, never what it was found in.
|
|
17
17
|
*/
|
|
18
18
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
19
|
-
import { BugSource, ContradictionKind } from '@reticlehq/core';
|
|
19
|
+
import { BugAttribution, BugSource, ContradictionKind, isAbsenceDerived, } from '@reticlehq/core';
|
|
20
20
|
/** Cap per tool call — a pathological crawl must not turn one call into a thousand events. */
|
|
21
21
|
const MAX_BUGS_PER_CALL = 25;
|
|
22
22
|
function kindsOf(value) {
|
|
@@ -90,25 +90,35 @@ function couldNotRun(failure) {
|
|
|
90
90
|
true === failure.couldNotRun);
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* WHOSE fault a contradiction was.
|
|
94
94
|
*
|
|
95
|
-
*
|
|
96
|
-
* `request-never-settled` — which Reticle raises from the ABSENCE of a settle event, so a
|
|
97
|
-
* poll manufactured it with the app doing nothing wrong.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
95
|
+
* `attribution` shipped twice and was wrong both times. It stamped `app` on every contradiction,
|
|
96
|
+
* including `request-never-settled` — which Reticle raises from the ABSENCE of a settle event, so a
|
|
97
|
+
* dev-overlay poll manufactured it with the app doing nothing wrong. Across two full real drives
|
|
98
|
+
* EVERY `attribution: 'app'` was a misattribution, while the one defect that genuinely was a bad
|
|
99
|
+
* agent predicate carried none. A metric confidently wrong about whose fault a defect is, is worse
|
|
100
|
+
* than no metric: it is the number a founder steers on, and it points at the customer.
|
|
101
101
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* The evidence that was missing then exists now, and it is already drawn in core for the verdict:
|
|
103
|
+
* `ABSENCE_DERIVED_CONTRADICTIONS` separates the kinds inferred from something NOT having happened
|
|
104
|
+
* inside a window Reticle chose the end of, from the kinds positively OBSERVED — a request that came
|
|
105
|
+
* back failed, a signal the app fired that disagrees with its own screen, a written field echoed
|
|
106
|
+
* back changed. Only the second class is evidence against the app, and it is the same line that
|
|
107
|
+
* decides whether a verdict may say `no`. Reusing it rather than inventing a second judgement beside
|
|
108
|
+
* it is the whole point: every historical misattribution was an absence-derived kind, so this rule
|
|
109
|
+
* produces zero of them on the data that broke the last two versions.
|
|
105
110
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
+
* Everything else — a failed assertion, a replay regression, a crawl anomaly that is not a
|
|
112
|
+
* contradiction — is `unclassified`, which is a VALUE and not a gap. A failed `element.present`
|
|
113
|
+
* covers "the button is missing", "the API is down" and "the agent mistyped a testid" identically,
|
|
114
|
+
* and a console error can be the app, a browser extension or a framework dev overlay. Guessing there
|
|
115
|
+
* puts a guess into the one number we intend to publish. See issue #122.
|
|
111
116
|
*/
|
|
117
|
+
function attributionOf(kind) {
|
|
118
|
+
return CONTRADICTION_KINDS.has(kind) && !isAbsenceDerived(kind)
|
|
119
|
+
? BugAttribution.APP
|
|
120
|
+
: BugAttribution.UNCLASSIFIED;
|
|
121
|
+
}
|
|
112
122
|
export function bugsInResult(toolName, result) {
|
|
113
123
|
// `reticle_run` is a WRAPPER: its handler calls runTool on the real tool, which already reported
|
|
114
124
|
// that result's defects under the real tool's name — then the outer chokepoint reports the very
|
|
@@ -133,6 +143,7 @@ export function bugsInResult(toolName, result) {
|
|
|
133
143
|
kind,
|
|
134
144
|
falseGreen: passed,
|
|
135
145
|
tool: toolName,
|
|
146
|
+
attribution: attributionOf(kind),
|
|
136
147
|
});
|
|
137
148
|
}
|
|
138
149
|
// 2. Crawl anomalies — found autonomously, with nobody writing a test. Crawl reports its own
|
|
@@ -148,8 +159,9 @@ export function bugsInResult(toolName, result) {
|
|
|
148
159
|
// — even though no assertion was made. See the definition on `falseGreen`.
|
|
149
160
|
falseGreen: CONTRADICTION_KINDS.has(kind),
|
|
150
161
|
tool: toolName,
|
|
151
|
-
//
|
|
152
|
-
// fault
|
|
162
|
+
// Same kind, same owner, wherever it arrives. A `request-never-settled` found by the crawler
|
|
163
|
+
// is no better evidence about whose fault it was than one found by an assertion.
|
|
164
|
+
attribution: attributionOf(kind),
|
|
153
165
|
});
|
|
154
166
|
}
|
|
155
167
|
// 3. A failed assertion — the agent declared a consequence and it did not hold.
|
|
@@ -167,7 +179,9 @@ export function bugsInResult(toolName, result) {
|
|
|
167
179
|
kind,
|
|
168
180
|
falseGreen: false,
|
|
169
181
|
tool: toolName,
|
|
170
|
-
//
|
|
182
|
+
// The assertion label cannot say whose fault it was: `element.present` covers a missing
|
|
183
|
+
// button, a downed API and a mistyped testid identically.
|
|
184
|
+
attribution: BugAttribution.UNCLASSIFIED,
|
|
171
185
|
});
|
|
172
186
|
}
|
|
173
187
|
// 4. Replay failures — a saved flow that used to pass no longer does. That is a regression caught,
|
|
@@ -184,8 +198,11 @@ export function bugsInResult(toolName, result) {
|
|
|
184
198
|
kind: 'flow-regression',
|
|
185
199
|
falseGreen: false,
|
|
186
200
|
tool: toolName,
|
|
187
|
-
// A
|
|
188
|
-
//
|
|
201
|
+
// A flow that used to pass and no longer does is a regression SOMEWHERE, and the row does
|
|
202
|
+
// not say where: the app changed, or a selector strategy of ours did. Rows that never RAN
|
|
203
|
+
// are skipped above, which keeps Reticle's outright failures out of the count entirely, but
|
|
204
|
+
// that is a weaker fact than the positive evidence `app` requires.
|
|
205
|
+
attribution: BugAttribution.UNCLASSIFIED,
|
|
189
206
|
});
|
|
190
207
|
}
|
|
191
208
|
}
|
|
@@ -204,4 +221,18 @@ export function bugsInResult(toolName, result) {
|
|
|
204
221
|
* fault, quietly deflating the exact number we intend to publish. A copied enum is a drift hazard
|
|
205
222
|
* wherever it appears; it is a correctness hazard when the thing that drifts is a public claim.
|
|
206
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* The route the app was on when this result was produced — extracted from the tool result so the
|
|
226
|
+
* fingerprint can identify the same defect across sessions without sending the route itself.
|
|
227
|
+
*/
|
|
228
|
+
export function routeOf(result) {
|
|
229
|
+
const status = result['status'];
|
|
230
|
+
if ('object' === typeof status && status !== null) {
|
|
231
|
+
const r = status['route'];
|
|
232
|
+
if ('string' === typeof r && r.length > 0)
|
|
233
|
+
return r;
|
|
234
|
+
}
|
|
235
|
+
const r = result['route'];
|
|
236
|
+
return 'string' === typeof r && r.length > 0 ? r : undefined;
|
|
237
|
+
}
|
|
207
238
|
const CONTRADICTION_KINDS = new Set(Object.values(ContradictionKind));
|
|
@@ -8,6 +8,7 @@ import { TelemetryEventKind } from '@reticlehq/core';
|
|
|
8
8
|
import { DAEMON_INNER_COMMAND, knownCommand } from '../cli/cli-parse.js';
|
|
9
9
|
import { getTelemetry } from './telemetry.js';
|
|
10
10
|
import { describeCliFlags } from './argument-shape.js';
|
|
11
|
+
import { resolveInstallSource } from './install-source.js';
|
|
11
12
|
/**
|
|
12
13
|
* Report that a person ran a `reticle` subcommand.
|
|
13
14
|
*
|
|
@@ -45,7 +46,12 @@ export function reportCliRun(argv) {
|
|
|
45
46
|
// installed the SDK into nine apps — the top of every funnel, missing.
|
|
46
47
|
// `detach` so a quick command (`version`/`gate`) exits immediately instead of waiting out the POST.
|
|
47
48
|
if (telemetry.firstRun)
|
|
48
|
-
void telemetry.emit(TelemetryEventKind.RETICLE_INSTALLED, {
|
|
49
|
+
void telemetry.emit(TelemetryEventKind.RETICLE_INSTALLED, {
|
|
50
|
+
detach: true,
|
|
51
|
+
// WHICH published route brought this machine in. Self-declared by the channel, never inferred
|
|
52
|
+
// — see install-source.ts for which of the four routes can actually carry the marker.
|
|
53
|
+
installSource: resolveInstallSource(),
|
|
54
|
+
});
|
|
49
55
|
if (!isHumanCliCommand(command))
|
|
50
56
|
return;
|
|
51
57
|
void telemetry.emit(TelemetryEventKind.CLI_COMMAND_RUN, {
|
|
@@ -13,6 +13,8 @@ import { profileProject } from './project-profile.js';
|
|
|
13
13
|
import { startUpdateCheck } from '../update/update-nudge.js';
|
|
14
14
|
import { markDaemonStart } from './mcp-connection.js';
|
|
15
15
|
import { markInstrumentationClock } from './app-instrumented.js';
|
|
16
|
+
import { markStallClock, reportInstrumentationStall } from './instrumentation-stall.js';
|
|
17
|
+
import { readProjectId } from '../cli/cli-port.js';
|
|
16
18
|
/** Let the daemon finish coming up before walking the source tree for a profile. */
|
|
17
19
|
const PROJECT_PROFILE_DELAY_MS = 5_000;
|
|
18
20
|
/**
|
|
@@ -57,6 +59,8 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
57
59
|
// Same clock, different half of the install: one measures how long before an AGENT attached, this
|
|
58
60
|
// one how long before an APP did. The gap between them is the funnel.
|
|
59
61
|
markInstrumentationClock(now());
|
|
62
|
+
// The other half of the same question: how long before an app arrives, and a report if none does.
|
|
63
|
+
markStallClock(now());
|
|
60
64
|
// One profile per daemon start: what kind of project this is and how deeply Reticle is used in it.
|
|
61
65
|
// Deferred off the boot path — it walks the source tree, and nothing about a daemon coming up
|
|
62
66
|
// should wait on a filesystem scan.
|
|
@@ -71,6 +75,17 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
71
75
|
}
|
|
72
76
|
}, PROJECT_PROFILE_DELAY_MS).unref();
|
|
73
77
|
const flushWindow = () => {
|
|
78
|
+
// BEFORE the idle return, and that ordering is the whole point: a stalled install produces an
|
|
79
|
+
// empty window by definition — no app, so no tool calls, so nothing to roll up. Checking it
|
|
80
|
+
// after the early return would make the one case this event exists for the one case it never
|
|
81
|
+
// sees. It is self-limiting (once per run, never after an app connects), so running it on every
|
|
82
|
+
// tick costs a comparison.
|
|
83
|
+
reportInstrumentationStall({
|
|
84
|
+
// Same test `app_instrumented` uses for the same field, so the success and failure events
|
|
85
|
+
// stay directly comparable: a stamped projectId means `init` has run here.
|
|
86
|
+
initialized: readProjectId(cwd) !== undefined,
|
|
87
|
+
agentAttached: metrics.agentEverAttached,
|
|
88
|
+
}, now);
|
|
74
89
|
if (metrics.empty)
|
|
75
90
|
return; // an idle window is not worth an event
|
|
76
91
|
// NOT daemon_stopped: the daemon is still running. See SESSION_PROGRESS.
|
|
@@ -93,6 +108,15 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
93
108
|
stopped ??= (async () => {
|
|
94
109
|
clearInterval(flush);
|
|
95
110
|
clearTimeout(firstFlush);
|
|
111
|
+
// Last chance to say that no app ever arrived. The periodic path cannot report an
|
|
112
|
+
// UNATTENDED stall at all — that daemon idle-exits at five minutes and the periodic
|
|
113
|
+
// threshold is ten — so without this the event only ever describes daemons with an agent
|
|
114
|
+
// attached, which is not the population it was built to measure. Self-limiting: if the
|
|
115
|
+
// interval already reported, this returns false and emits nothing.
|
|
116
|
+
reportInstrumentationStall({
|
|
117
|
+
initialized: readProjectId(cwd) !== undefined,
|
|
118
|
+
agentAttached: metrics.agentEverAttached,
|
|
119
|
+
}, now, { atShutdown: true });
|
|
96
120
|
// The rich one: the whole session in a single event — duration, the tool histogram, error
|
|
97
121
|
// shapes, verifications, browser launches. This is what replaced the per-tool-call event.
|
|
98
122
|
await getTelemetry().emit(TelemetryEventKind.DAEMON_STOPPED, {
|
|
@@ -147,7 +147,7 @@ export async function submitFeedback(input, opts = {}) {
|
|
|
147
147
|
// Reticle SOURCE CHECKOUT disables telemetry by cwd, and that is exactly where release runs and
|
|
148
148
|
// contributor sessions happen — so the reports most worth having were the ones silently lost.
|
|
149
149
|
reason: isReticleSourceCheckout(opts.cwd ?? process.cwd())
|
|
150
|
-
? 'this is a Reticle source checkout, where telemetry is disabled by design
|
|
150
|
+
? 'this is a Reticle source checkout, where telemetry is disabled by design, so feedback has nowhere to go. Open an issue at https://github.com/reticlehq/reticle/issues instead, or run from the app you are verifying.'
|
|
151
151
|
: `telemetry is disabled on this machine, so feedback has nowhere to go. Re-enable with \`reticle telemetry enable\`, or open an issue at https://github.com/reticlehq/reticle/issues.`,
|
|
152
152
|
redacted,
|
|
153
153
|
context,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { TelemetryEventKind } from '@reticlehq/core';
|
|
14
14
|
import { getTelemetry } from './telemetry.js';
|
|
15
|
+
import { resolveInstallSource } from './install-source.js';
|
|
15
16
|
/** Classified init failures — our own vocabulary, never a raw error or a path. */
|
|
16
17
|
export const InitFailure = {
|
|
17
18
|
/** Run outside a project root. The single most common first-run mistake. */
|
|
@@ -25,7 +26,14 @@ export const InitFailure = {
|
|
|
25
26
|
/** Report one init outcome. Best-effort; setup must never fail because a metric did. */
|
|
26
27
|
export function reportInitOutcome(init) {
|
|
27
28
|
try {
|
|
28
|
-
void getTelemetry().emit(TelemetryEventKind.INIT_COMPLETED, {
|
|
29
|
+
void getTelemetry().emit(TelemetryEventKind.INIT_COMPLETED, {
|
|
30
|
+
init,
|
|
31
|
+
detach: true,
|
|
32
|
+
// The other half of attribution: `reticle_installed` says a machine arrived, this says the
|
|
33
|
+
// setup on it finished, and only together do they say which route converts. Same self-declared
|
|
34
|
+
// marker, same refusal to infer — see install-source.ts.
|
|
35
|
+
installSource: resolveInstallSource(),
|
|
36
|
+
});
|
|
29
37
|
}
|
|
30
38
|
catch {
|
|
31
39
|
/* a metric must never break `reticle init` */
|