@reticlehq/server 3.8.2 → 3.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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input.d.ts +15 -0
- package/dist/portal/input/real-input.js +59 -3
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +10 -10
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +13 -9
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* exact values allowed — anything outside the list is reported as `other`, so a schema change can
|
|
19
19
|
* never quietly start forwarding free text.
|
|
20
20
|
*/
|
|
21
|
-
import { ActionType, FeedbackKind, QueryBy, SnapshotMode } from '@reticlehq/core';
|
|
21
|
+
import { ActionType, FeedbackKind, QueryBy, ReticleTool, SnapshotMode } from '@reticlehq/core';
|
|
22
22
|
/**
|
|
23
23
|
* Parameters whose values are enums WE define, and the exact values allowed on the wire. An
|
|
24
24
|
* unrecognized value reports `other` rather than itself; that fallback is what makes this safe
|
|
@@ -35,6 +35,9 @@ const SAFE_ENUM_PARAMS = new Map([
|
|
|
35
35
|
['by', new Set(Object.values(QueryBy))],
|
|
36
36
|
['mode', new Set(Object.values(SnapshotMode))],
|
|
37
37
|
['kind', new Set(Object.values(FeedbackKind))],
|
|
38
|
+
// `reticle_run { tool }` names the tool it calls. Our own tool names, so the session summary can
|
|
39
|
+
// say which tools agents reach through it. A name that is not a Reticle tool reports `other`.
|
|
40
|
+
['tool', new Set(Object.values(ReticleTool))],
|
|
38
41
|
// `level` has no core enum — console levels come from the DOM console API, not from us. Listed
|
|
39
42
|
// literally BECAUSE there is nothing to derive from, which is a different situation from the four
|
|
40
43
|
// above and the reason it is called out rather than left looking like an oversight.
|
|
@@ -49,10 +52,12 @@ export const OTHER_VALUE = 'other';
|
|
|
49
52
|
*/
|
|
50
53
|
export function allowParamValues(name, values) {
|
|
51
54
|
const allowed = SAFE_ENUM_PARAMS.get(name);
|
|
52
|
-
if (allowed === undefined)
|
|
55
|
+
if (allowed === undefined) {
|
|
53
56
|
return;
|
|
54
|
-
|
|
57
|
+
}
|
|
58
|
+
for (const value of values) {
|
|
55
59
|
allowed.add(value);
|
|
60
|
+
}
|
|
56
61
|
}
|
|
57
62
|
/**
|
|
58
63
|
* The reportable form of one parameter: `name` on its own, or `name:value` when the parameter is a
|
|
@@ -60,10 +65,12 @@ export function allowParamValues(name, values) {
|
|
|
60
65
|
*/
|
|
61
66
|
export function describeParam(name, value) {
|
|
62
67
|
const allowed = SAFE_ENUM_PARAMS.get(name);
|
|
63
|
-
if (allowed === undefined)
|
|
68
|
+
if (allowed === undefined) {
|
|
64
69
|
return name;
|
|
65
|
-
|
|
70
|
+
}
|
|
71
|
+
if (typeof value !== 'string') {
|
|
66
72
|
return name;
|
|
73
|
+
}
|
|
67
74
|
return `${name}:${allowed.has(value) ? value : OTHER_VALUE}`;
|
|
68
75
|
}
|
|
69
76
|
/**
|
|
@@ -96,10 +103,12 @@ export function describeCliFlags(argv) {
|
|
|
96
103
|
const flags = new Set();
|
|
97
104
|
for (const arg of argv) {
|
|
98
105
|
// A bare `--` ends the options: everything after it is passed through to something else.
|
|
99
|
-
if (END_OF_OPTIONS === arg)
|
|
106
|
+
if (END_OF_OPTIONS === arg) {
|
|
100
107
|
break;
|
|
101
|
-
|
|
108
|
+
}
|
|
109
|
+
if (!arg.startsWith('--')) {
|
|
102
110
|
continue;
|
|
111
|
+
}
|
|
103
112
|
// `--port=9000` — split so the NAME is reported and the value is discarded with everything else.
|
|
104
113
|
const name = arg.split('=')[0] ?? arg;
|
|
105
114
|
flags.add(KNOWN_CLI_FLAGS.has(name) ? name : OTHER_VALUE);
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Lives in its own module so `cli.ts` stays a dispatcher, and so the `_daemon` exclusion below sits
|
|
5
5
|
* next to the explanation of why it exists rather than buried in a startup function.
|
|
6
6
|
*/
|
|
7
|
-
import { TelemetryActor } from '@reticlehq/core/telemetry';
|
|
7
|
+
import { TelemetryActor, type GateCompleted } from '@reticlehq/core/telemetry';
|
|
8
|
+
import { type Telemetry } from './telemetry.js';
|
|
8
9
|
/** True when this command represents a person typing something. Exported for the gate. */
|
|
9
10
|
export declare function isHumanCliCommand(command: string): boolean;
|
|
10
11
|
/**
|
|
@@ -23,3 +24,11 @@ export declare function cliActorOf(env: NodeJS.ProcessEnv): TelemetryActor;
|
|
|
23
24
|
*/
|
|
24
25
|
export declare function claimInstallReport(command: string, firstRun: boolean, stateDir: string): boolean;
|
|
25
26
|
export declare function reportCliRun(argv: readonly string[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* `gate_completed`: how one `reticle gate` run ended.
|
|
29
|
+
*
|
|
30
|
+
* `cli_command_run` says that the gate ran. Only this event says whether it passed. The gate is a
|
|
31
|
+
* one-shot command that exits immediately, so the send is DETACHED, like `init_completed`.
|
|
32
|
+
* Best-effort: a metric never changes what the gate decides.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reportGateCompleted(gate: GateCompleted, telemetry?: Pick<Telemetry, 'emit'>): void;
|
|
@@ -161,3 +161,18 @@ export function reportCliRun(argv) {
|
|
|
161
161
|
flags: describeCliFlags(argv),
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* `gate_completed`: how one `reticle gate` run ended.
|
|
166
|
+
*
|
|
167
|
+
* `cli_command_run` says that the gate ran. Only this event says whether it passed. The gate is a
|
|
168
|
+
* one-shot command that exits immediately, so the send is DETACHED, like `init_completed`.
|
|
169
|
+
* Best-effort: a metric never changes what the gate decides.
|
|
170
|
+
*/
|
|
171
|
+
export function reportGateCompleted(gate, telemetry = getTelemetry()) {
|
|
172
|
+
try {
|
|
173
|
+
void telemetry.emit(TelemetryEventKind.GATE_COMPLETED, { gate, detach: true });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
/* a metric never fails the gate */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* event loop alive and tax the command by most of a second.
|
|
12
12
|
*/
|
|
13
13
|
import { type InitOutcome } from '@reticlehq/core/telemetry';
|
|
14
|
-
|
|
14
|
+
import { InitFailure } from '@reticlehq/init';
|
|
15
|
+
export { InitFailure };
|
|
15
16
|
/** Report one init outcome. Best-effort; setup must never fail because a metric did. */
|
|
16
17
|
export declare function reportInitOutcome(init: InitOutcome): void;
|
|
@@ -10,12 +10,56 @@
|
|
|
10
10
|
* same treatment `cli_command_run` gets, for the same reason: an in-process fetch would keep the
|
|
11
11
|
* event loop alive and tax the command by most of a second.
|
|
12
12
|
*/
|
|
13
|
-
import { TelemetryEventKind } from '@reticlehq/core/telemetry';
|
|
13
|
+
import { OnboardingPhase, OnboardingStop, TelemetryEventKind, } from '@reticlehq/core/telemetry';
|
|
14
|
+
import { InitFailure } from '@reticlehq/init';
|
|
14
15
|
import { getTelemetry } from './telemetry.js';
|
|
15
16
|
import { INSTALL_STATE_DIR, resolveInstallSource } from './install-source.js';
|
|
17
|
+
import { reportOnboardingStop } from './onboarding-funnel.js';
|
|
16
18
|
// The failure vocabulary lives in `@reticlehq/init`, which is what classifies a failed run.
|
|
17
19
|
// Re-exported so every consumer here is unchanged.
|
|
18
|
-
export { InitFailure }
|
|
20
|
+
export { InitFailure };
|
|
21
|
+
const PROJECT_DETECTED = 'project_detected';
|
|
22
|
+
const INSTRUMENTED = 'instrumented';
|
|
23
|
+
const APP_CONNECTED = 'app_connected';
|
|
24
|
+
/**
|
|
25
|
+
* Each init failure as one stop, with the first-run step it blocked. A `Record` over the whole
|
|
26
|
+
* enum, so a new `InitFailure` does not compile until it names its stop.
|
|
27
|
+
*/
|
|
28
|
+
const INIT_STOP = {
|
|
29
|
+
[InitFailure.NO_PACKAGE_JSON]: { step: PROJECT_DETECTED, stop: OnboardingStop.NO_WEB_APP },
|
|
30
|
+
[InitFailure.MALFORMED_PACKAGE_JSON]: {
|
|
31
|
+
step: PROJECT_DETECTED,
|
|
32
|
+
stop: OnboardingStop.SETUP_FAILED,
|
|
33
|
+
},
|
|
34
|
+
[InitFailure.PREFLIGHT]: { step: PROJECT_DETECTED, stop: OnboardingStop.SETUP_FAILED },
|
|
35
|
+
[InitFailure.DEPENDENCY_INSTALL]: { step: INSTRUMENTED, stop: OnboardingStop.SETUP_FAILED },
|
|
36
|
+
[InitFailure.MCP_REGISTRATION]: { step: INSTRUMENTED, stop: OnboardingStop.MCP_NOT_REGISTERED },
|
|
37
|
+
[InitFailure.OTHER]: { step: INSTRUMENTED, stop: OnboardingStop.UNCLASSIFIED },
|
|
38
|
+
[InitFailure.BRIDGE_OCCUPIED]: { step: APP_CONNECTED, stop: OnboardingStop.SETUP_FAILED },
|
|
39
|
+
[InitFailure.DEV_SERVER]: { step: APP_CONNECTED, stop: OnboardingStop.DEV_SERVER_FAILED },
|
|
40
|
+
[InitFailure.APP_CONNECTION]: { step: APP_CONNECTED, stop: OnboardingStop.APP_NOT_CONNECTED },
|
|
41
|
+
[InitFailure.RUNTIME_ERROR]: { step: APP_CONNECTED, stop: OnboardingStop.UNCLASSIFIED },
|
|
42
|
+
[InitFailure.INTERRUPTED]: { step: APP_CONNECTED, stop: OnboardingStop.INTERRUPTED },
|
|
43
|
+
};
|
|
44
|
+
const INIT_STOP_UNKNOWN = INIT_STOP[InitFailure.OTHER];
|
|
45
|
+
function initStop(reason) {
|
|
46
|
+
if (reason !== undefined && Object.hasOwn(INIT_STOP, reason)) {
|
|
47
|
+
return INIT_STOP[reason];
|
|
48
|
+
}
|
|
49
|
+
return INIT_STOP_UNKNOWN;
|
|
50
|
+
}
|
|
51
|
+
/** The stop row of a failed init, or nothing for a run that worked. */
|
|
52
|
+
function reportInitStop(init) {
|
|
53
|
+
if (init.ok) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
reportOnboardingStop({
|
|
57
|
+
phase: OnboardingPhase.FIRST_RUN,
|
|
58
|
+
...initStop(init.reason),
|
|
59
|
+
...(init.reason === undefined ? {} : { reason: init.reason }),
|
|
60
|
+
...(init.stack === undefined ? {} : { stack: init.stack }),
|
|
61
|
+
}, { detach: true });
|
|
62
|
+
}
|
|
19
63
|
/** Report one init outcome. Best-effort; setup must never fail because a metric did. */
|
|
20
64
|
export function reportInitOutcome(init) {
|
|
21
65
|
try {
|
|
@@ -27,6 +71,7 @@ export function reportInitOutcome(init) {
|
|
|
27
71
|
// marker, same refusal to infer — see install-source.ts.
|
|
28
72
|
installSource: resolveInstallSource(process.env, INSTALL_STATE_DIR),
|
|
29
73
|
});
|
|
74
|
+
reportInitStop(init);
|
|
30
75
|
}
|
|
31
76
|
catch {
|
|
32
77
|
/* a metric must never break `reticle init` */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { OnboardingStepSchema } from '@reticlehq/core/telemetry';
|
|
3
|
+
import { OnboardingStepSchema, OnboardingStop } from '@reticlehq/core/telemetry';
|
|
4
4
|
import { reportOnboardingStep } from './onboarding-funnel.js';
|
|
5
5
|
/**
|
|
6
6
|
* What the one-line installer could not report itself.
|
|
@@ -27,6 +27,22 @@ const TRACE_FILE = 'install-trace.jsonl';
|
|
|
27
27
|
* file must not turn into thousands of events. Far above any real install, which writes three.
|
|
28
28
|
*/
|
|
29
29
|
const MAX_LINES = 32;
|
|
30
|
+
/**
|
|
31
|
+
* The stop that each installer failure reason names. The keys are the reasons that `install.sh`
|
|
32
|
+
* and `install.ps1` write. A reason not in this table gets no stop.
|
|
33
|
+
*/
|
|
34
|
+
const INSTALLER_STOP = {
|
|
35
|
+
no_node: OnboardingStop.NO_NODE,
|
|
36
|
+
node_too_old: OnboardingStop.NODE_TOO_OLD,
|
|
37
|
+
no_npm: OnboardingStop.CLI_INSTALL_FAILED,
|
|
38
|
+
npm_install: OnboardingStop.CLI_INSTALL_FAILED,
|
|
39
|
+
not_on_path: OnboardingStop.CLI_INSTALL_FAILED,
|
|
40
|
+
};
|
|
41
|
+
function installerStop(reason) {
|
|
42
|
+
return reason !== undefined && Object.hasOwn(INSTALLER_STOP, reason)
|
|
43
|
+
? INSTALLER_STOP[reason]
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
30
46
|
/**
|
|
31
47
|
* Drain the installer's breadcrumbs, once.
|
|
32
48
|
*
|
|
@@ -36,8 +52,9 @@ const MAX_LINES = 32;
|
|
|
36
52
|
*/
|
|
37
53
|
export function drainInstallTrace(stateDir) {
|
|
38
54
|
const path = join(stateDir, TRACE_FILE);
|
|
39
|
-
if (!existsSync(path))
|
|
55
|
+
if (!existsSync(path)) {
|
|
40
56
|
return 0;
|
|
57
|
+
}
|
|
41
58
|
let drained = 0;
|
|
42
59
|
try {
|
|
43
60
|
const lines = readFileSync(path, 'utf8')
|
|
@@ -55,9 +72,11 @@ export function drainInstallTrace(stateDir) {
|
|
|
55
72
|
// deliberate belt-and-braces: this is the one path where the payload is a file a person can
|
|
56
73
|
// edit, and rule 3 is that unvalidated text never reaches the wire.
|
|
57
74
|
const step = OnboardingStepSchema.safeParse(parsed);
|
|
58
|
-
if (!step.success)
|
|
75
|
+
if (!step.success) {
|
|
59
76
|
continue;
|
|
60
|
-
|
|
77
|
+
}
|
|
78
|
+
const stop = installerStop(step.data.reason);
|
|
79
|
+
void reportOnboardingStep({ ...step.data, ...(stop === undefined ? {} : { stop }) });
|
|
61
80
|
drained += 1;
|
|
62
81
|
}
|
|
63
82
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OnboardingPhase, type OnboardingStep } from '@reticlehq/core/telemetry';
|
|
1
|
+
import { OnboardingPhase, type OnboardingStep, type OnboardingStop } from '@reticlehq/core/telemetry';
|
|
2
2
|
/**
|
|
3
3
|
* The one place an onboarding step is reported.
|
|
4
4
|
*
|
|
@@ -11,7 +11,24 @@ import { OnboardingPhase, type OnboardingStep } from '@reticlehq/core/telemetry'
|
|
|
11
11
|
* arriving as a step name would be a rule-3 leak that no reader would ever notice, because a funnel
|
|
12
12
|
* with one weird row still renders.
|
|
13
13
|
*/
|
|
14
|
-
export declare function reportOnboardingStep(step: OnboardingStep
|
|
14
|
+
export declare function reportOnboardingStep(step: OnboardingStep, opts?: {
|
|
15
|
+
detach?: boolean;
|
|
16
|
+
}): Promise<boolean>;
|
|
17
|
+
/** One stop: the rung it blocked and its name. The status is always `failed`. */
|
|
18
|
+
export type StopRow = Omit<OnboardingStep, 'status' | 'stop'> & {
|
|
19
|
+
stop: OnboardingStop;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Report one stop on the path to a first verdict, once for each process and stop name.
|
|
23
|
+
*
|
|
24
|
+
* A daemon can fail to launch Chromium on every lease. One row says the stop happened here; more
|
|
25
|
+
* rows count retries, not people. Use `detach` from a CLI that exits next, or the row is lost.
|
|
26
|
+
*/
|
|
27
|
+
export declare function reportOnboardingStop(row: StopRow, opts?: {
|
|
28
|
+
detach?: boolean;
|
|
29
|
+
}): void;
|
|
30
|
+
/** Tests only: forget the stops this process sent. */
|
|
31
|
+
export declare function resetOnboardingStops(): void;
|
|
15
32
|
/**
|
|
16
33
|
* Time a step and report it, whatever happens to it.
|
|
17
34
|
*
|
|
@@ -12,13 +12,35 @@ import { getTelemetry } from './telemetry.js';
|
|
|
12
12
|
* arriving as a step name would be a rule-3 leak that no reader would ever notice, because a funnel
|
|
13
13
|
* with one weird row still renders.
|
|
14
14
|
*/
|
|
15
|
-
export async function reportOnboardingStep(step) {
|
|
15
|
+
export async function reportOnboardingStep(step, opts = {}) {
|
|
16
16
|
const parsed = OnboardingStepSchema.safeParse(step);
|
|
17
17
|
// A malformed step is DROPPED rather than sent or thrown. Sending it would put unvalidated text on
|
|
18
18
|
// the wire; throwing would let a metric change what the user's install does, which rule 5 forbids.
|
|
19
|
-
if (!parsed.success)
|
|
19
|
+
if (!parsed.success) {
|
|
20
20
|
return false;
|
|
21
|
-
|
|
21
|
+
}
|
|
22
|
+
return getTelemetry().emit(TelemetryEventKind.ONBOARDING_STEP, {
|
|
23
|
+
onboarding: parsed.data,
|
|
24
|
+
...(true === opts.detach ? { detach: true } : {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const stopsSent = new Set();
|
|
28
|
+
/**
|
|
29
|
+
* Report one stop on the path to a first verdict, once for each process and stop name.
|
|
30
|
+
*
|
|
31
|
+
* A daemon can fail to launch Chromium on every lease. One row says the stop happened here; more
|
|
32
|
+
* rows count retries, not people. Use `detach` from a CLI that exits next, or the row is lost.
|
|
33
|
+
*/
|
|
34
|
+
export function reportOnboardingStop(row, opts = {}) {
|
|
35
|
+
if (stopsSent.has(row.stop)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
stopsSent.add(row.stop);
|
|
39
|
+
void reportOnboardingStep({ ...row, status: OnboardingStepStatus.FAILED }, opts);
|
|
40
|
+
}
|
|
41
|
+
/** Tests only: forget the stops this process sent. */
|
|
42
|
+
export function resetOnboardingStops() {
|
|
43
|
+
stopsSent.clear();
|
|
22
44
|
}
|
|
23
45
|
/**
|
|
24
46
|
* Time a step and report it, whatever happens to it.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* never arrived: the POST was killed every time and nothing threw.
|
|
11
11
|
*/
|
|
12
12
|
import { type ProxyExitReason, type WakeRefusalPresence } from '@reticlehq/core/telemetry';
|
|
13
|
+
import type { DaemonDeath } from '@reticlehq/core';
|
|
13
14
|
import { type SessionMetrics } from './session-metrics.js';
|
|
14
15
|
import { type Telemetry } from './telemetry.js';
|
|
15
16
|
export declare function flushProxySessionMetrics(telemetry?: Telemetry, metrics?: SessionMetrics): Promise<void>;
|
|
@@ -37,7 +38,7 @@ export declare function reportProxyExited(reason: ProxyExitReason, telemetry?: T
|
|
|
37
38
|
/** Everything the proxy sends on its way out. Awaited, then the caller exits. */
|
|
38
39
|
export declare function flushProxyExit(reason: ProxyExitReason): Promise<void>;
|
|
39
40
|
/** Send `daemon_died_unclean`. Fire-and-forget is fine for callers: the process lives on. */
|
|
40
|
-
export declare function reportDaemonDiedUnclean(ageMs: number, telemetry?: Telemetry): Promise<void>;
|
|
41
|
+
export declare function reportDaemonDiedUnclean(ageMs: number, cause?: Omit<DaemonDeath, 'ageMs'>, telemetry?: Telemetry): Promise<void>;
|
|
41
42
|
/**
|
|
42
43
|
* Send `wake_refused`, once per process. A dormant proxy refuses again on every client request, and
|
|
43
44
|
* one event per request would count the client's retries, not the refusal.
|
|
@@ -73,9 +73,9 @@ export async function flushProxyExit(reason) {
|
|
|
73
73
|
await Promise.allSettled([flushProxySessionMetrics(), reportProxyExited(reason)]);
|
|
74
74
|
}
|
|
75
75
|
/** Send `daemon_died_unclean`. Fire-and-forget is fine for callers: the process lives on. */
|
|
76
|
-
export async function reportDaemonDiedUnclean(ageMs, telemetry = getTelemetry()) {
|
|
76
|
+
export async function reportDaemonDiedUnclean(ageMs, cause, telemetry = getTelemetry()) {
|
|
77
77
|
await telemetry.emit(TelemetryEventKind.DAEMON_DIED_UNCLEAN, {
|
|
78
|
-
daemonDeath: { ageMs: Math.max(0, Math.round(ageMs)) },
|
|
78
|
+
daemonDeath: { ...cause, ageMs: Math.max(0, Math.round(ageMs)) },
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* URLs, no error messages — the accumulator has no way to hold them, which is a stronger guarantee
|
|
16
16
|
* than a rule saying it shouldn't.
|
|
17
17
|
*/
|
|
18
|
-
import { BrowserLaunchKind,
|
|
18
|
+
import { BrowserLaunchKind, ConnectFailure, type AgentNudgeKind, type SessionSummary } from '@reticlehq/core/telemetry';
|
|
19
19
|
import type { HudUseData } from '@reticlehq/core';
|
|
20
20
|
import { type HudSignals } from './hud-metrics.js';
|
|
21
21
|
/**
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
* URLs, no error messages — the accumulator has no way to hold them, which is a stronger guarantee
|
|
16
16
|
* than a rule saying it shouldn't.
|
|
17
17
|
*/
|
|
18
|
-
import { BrowserLaunchKind, } from '@reticlehq/core/telemetry';
|
|
18
|
+
import { BrowserLaunchKind, ConnectFailure, OnboardingPhase, OnboardingStop, } from '@reticlehq/core/telemetry';
|
|
19
19
|
import { errorSkeleton, fingerprintError } from './error-fingerprint.js';
|
|
20
20
|
import { describeToolParams } from './argument-shape.js';
|
|
21
21
|
import { machineSnapshot } from './machine-snapshot.js';
|
|
22
22
|
import { classifyError } from './error-class.js';
|
|
23
23
|
import { HudMetrics } from './hud-metrics.js';
|
|
24
|
+
import { reportOnboardingStop } from './onboarding-funnel.js';
|
|
24
25
|
import { releaseVersionOf } from '../command/update/version-seen.js';
|
|
25
26
|
/** Cap the distinct error shapes held in memory — a pathological loop must not grow unbounded. */
|
|
26
27
|
const MAX_ERROR_KINDS = 40;
|
|
@@ -58,6 +59,12 @@ const BREADCRUMB_LENGTH = 12;
|
|
|
58
59
|
* never fires for the sessions that need it most.
|
|
59
60
|
*/
|
|
60
61
|
const UNVERIFIED_ACTION_NUDGE_AT = 3;
|
|
62
|
+
/** The launch failures that leave no browser to drive: a stop on the path to a first verdict. */
|
|
63
|
+
const CHROMIUM_UNAVAILABLE = new Set([
|
|
64
|
+
ConnectFailure.CHROMIUM_MISSING,
|
|
65
|
+
ConnectFailure.PLAYWRIGHT_MISSING,
|
|
66
|
+
]);
|
|
67
|
+
const DRIVEN_STEP = 'driven';
|
|
61
68
|
/**
|
|
62
69
|
* Mutable counters for one daemon lifetime. A plain class, not a store: it is process-local, it never
|
|
63
70
|
* persists, and it dies with the daemon.
|
|
@@ -187,10 +194,12 @@ export class SessionMetrics {
|
|
|
187
194
|
}
|
|
188
195
|
/** A tab went into the background (true) or came back (false): its HUD time pauses meanwhile. */
|
|
189
196
|
setHudTabHidden(tab, hidden) {
|
|
190
|
-
if (hidden)
|
|
197
|
+
if (hidden) {
|
|
191
198
|
this.#hud.hide(tab);
|
|
192
|
-
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
193
201
|
this.#hud.show(tab);
|
|
202
|
+
}
|
|
194
203
|
}
|
|
195
204
|
/** A tab went away: its HUD stops accruing time. */
|
|
196
205
|
closeHudTab(tab) {
|
|
@@ -204,12 +213,14 @@ export class SessionMetrics {
|
|
|
204
213
|
startToolCall(tool, args) {
|
|
205
214
|
this.recordToolCall(tool, args);
|
|
206
215
|
this.#concurrent += 1;
|
|
207
|
-
if (this.#concurrent > this.#peakConcurrent)
|
|
216
|
+
if (this.#concurrent > this.#peakConcurrent) {
|
|
208
217
|
this.#peakConcurrent = this.#concurrent;
|
|
218
|
+
}
|
|
209
219
|
let settled = false;
|
|
210
220
|
return (durationMs) => {
|
|
211
|
-
if (settled)
|
|
212
|
-
return;
|
|
221
|
+
if (settled) {
|
|
222
|
+
return;
|
|
223
|
+
} // a double-settle would corrupt the concurrency count
|
|
213
224
|
settled = true;
|
|
214
225
|
this.#concurrent = Math.max(0, this.#concurrent - 1);
|
|
215
226
|
this.#busyMs += durationMs;
|
|
@@ -243,8 +254,9 @@ export class SessionMetrics {
|
|
|
243
254
|
*/
|
|
244
255
|
recordUnknownTool(name) {
|
|
245
256
|
this.#unknownToolCalls += 1;
|
|
246
|
-
if (name === undefined || 0 === name.length)
|
|
257
|
+
if (name === undefined || 0 === name.length) {
|
|
247
258
|
return;
|
|
259
|
+
}
|
|
248
260
|
const key = name.slice(0, 64);
|
|
249
261
|
const seen = this.#unknownTools.get(key);
|
|
250
262
|
if (seen !== undefined) {
|
|
@@ -253,8 +265,9 @@ export class SessionMetrics {
|
|
|
253
265
|
}
|
|
254
266
|
// Stop growing once full: a pathological loop must not balloon the payload. The head is what
|
|
255
267
|
// gets built; the tail is noise.
|
|
256
|
-
if (this.#unknownTools.size >= MAX_ERROR_KINDS)
|
|
268
|
+
if (this.#unknownTools.size >= MAX_ERROR_KINDS) {
|
|
257
269
|
return;
|
|
270
|
+
}
|
|
258
271
|
this.#unknownTools.set(key, 1);
|
|
259
272
|
}
|
|
260
273
|
recordToolCall(tool, args) {
|
|
@@ -269,8 +282,9 @@ export class SessionMetrics {
|
|
|
269
282
|
if (this.#currentRun > 1) {
|
|
270
283
|
const best = this.#repeatRuns.get(tool) ?? 0;
|
|
271
284
|
if (this.#currentRun > best &&
|
|
272
|
-
(this.#repeatRuns.has(tool) || this.#repeatRuns.size < MAX_REPEAT_TOOLS))
|
|
285
|
+
(this.#repeatRuns.has(tool) || this.#repeatRuns.size < MAX_REPEAT_TOOLS)) {
|
|
273
286
|
this.#repeatRuns.set(tool, this.#currentRun);
|
|
287
|
+
}
|
|
274
288
|
}
|
|
275
289
|
this.#toolCalls += 1;
|
|
276
290
|
this.#lifetimeToolCalls += 1;
|
|
@@ -280,23 +294,27 @@ export class SessionMetrics {
|
|
|
280
294
|
bump(this.#toolCounts, tool);
|
|
281
295
|
this.#inFlight = tool;
|
|
282
296
|
this.#breadcrumb.push(tool);
|
|
283
|
-
if (this.#breadcrumb.length > BREADCRUMB_LENGTH)
|
|
297
|
+
if (this.#breadcrumb.length > BREADCRUMB_LENGTH) {
|
|
284
298
|
this.#breadcrumb.shift();
|
|
285
|
-
|
|
299
|
+
}
|
|
300
|
+
if (args === undefined) {
|
|
286
301
|
return;
|
|
302
|
+
}
|
|
287
303
|
// Which PARAMETERS get used, per tool — names only (plus a short allowlist of our own enums).
|
|
288
304
|
// Bounded by our own schemas: a tool has a fixed handful of declared parameters, so this map
|
|
289
305
|
// cannot grow with traffic the way a value-carrying one would.
|
|
290
306
|
let perTool = this.#toolParams.get(tool);
|
|
291
307
|
if (perTool === undefined) {
|
|
292
|
-
if (this.#toolParams.size >= MAX_PARAM_TOOLS)
|
|
308
|
+
if (this.#toolParams.size >= MAX_PARAM_TOOLS) {
|
|
293
309
|
return;
|
|
310
|
+
}
|
|
294
311
|
perTool = new Map();
|
|
295
312
|
this.#toolParams.set(tool, perTool);
|
|
296
313
|
}
|
|
297
314
|
for (const param of describeToolParams(args)) {
|
|
298
|
-
if (perTool.size < MAX_PARAMS_PER_TOOL || perTool.has(param))
|
|
315
|
+
if (perTool.size < MAX_PARAMS_PER_TOOL || perTool.has(param)) {
|
|
299
316
|
bump(perTool, param);
|
|
317
|
+
}
|
|
300
318
|
}
|
|
301
319
|
}
|
|
302
320
|
/**
|
|
@@ -310,8 +328,9 @@ export class SessionMetrics {
|
|
|
310
328
|
// indistinguishable from the rest inside one `toolErrors` count.
|
|
311
329
|
bump(this.#errorClasses, classifyError(message));
|
|
312
330
|
this.#currentCallErrored = true;
|
|
313
|
-
if (NO_SESSION_ERROR.test(message))
|
|
331
|
+
if (NO_SESSION_ERROR.test(message)) {
|
|
314
332
|
this.#noSessionErrors += 1;
|
|
333
|
+
}
|
|
315
334
|
const fingerprint = fingerprintError(message);
|
|
316
335
|
const existing = this.#errorKinds.get(fingerprint);
|
|
317
336
|
if (existing !== undefined) {
|
|
@@ -319,8 +338,9 @@ export class SessionMetrics {
|
|
|
319
338
|
return;
|
|
320
339
|
}
|
|
321
340
|
// Stop growing once the cap is hit; the tail is noise and the head is what gets fixed.
|
|
322
|
-
if (this.#errorKinds.size >= MAX_ERROR_KINDS)
|
|
341
|
+
if (this.#errorKinds.size >= MAX_ERROR_KINDS) {
|
|
323
342
|
return;
|
|
343
|
+
}
|
|
324
344
|
this.#errorKinds.set(fingerprint, {
|
|
325
345
|
fingerprint,
|
|
326
346
|
count: 1,
|
|
@@ -343,8 +363,9 @@ export class SessionMetrics {
|
|
|
343
363
|
existing.count += 1;
|
|
344
364
|
return;
|
|
345
365
|
}
|
|
346
|
-
if (this.#sdkFailureKinds.size >= MAX_ERROR_KINDS)
|
|
366
|
+
if (this.#sdkFailureKinds.size >= MAX_ERROR_KINDS) {
|
|
347
367
|
return;
|
|
368
|
+
}
|
|
348
369
|
this.#sdkFailureKinds.set(fingerprint, {
|
|
349
370
|
fingerprint,
|
|
350
371
|
count: 1,
|
|
@@ -428,8 +449,9 @@ export class SessionMetrics {
|
|
|
428
449
|
/** The 1-based occurrence of this finding in the session, or undefined past the cap. */
|
|
429
450
|
recordBugOccurrence(fingerprint) {
|
|
430
451
|
const seen = this.#bugOccurrences.get(fingerprint);
|
|
431
|
-
if (seen === undefined && this.#bugOccurrences.size >= MAX_BUG_FINGERPRINTS)
|
|
452
|
+
if (seen === undefined && this.#bugOccurrences.size >= MAX_BUG_FINGERPRINTS) {
|
|
432
453
|
return undefined;
|
|
454
|
+
}
|
|
433
455
|
const occurrence = (seen ?? 0) + 1;
|
|
434
456
|
this.#bugOccurrences.set(fingerprint, occurrence);
|
|
435
457
|
return occurrence;
|
|
@@ -437,8 +459,9 @@ export class SessionMetrics {
|
|
|
437
459
|
recordBug(kind) {
|
|
438
460
|
this.#bugsFound += 1;
|
|
439
461
|
this.#lifetimeBugsFound += 1;
|
|
440
|
-
if (this.#bugKinds.size < MAX_ERROR_KINDS || this.#bugKinds.has(kind))
|
|
462
|
+
if (this.#bugKinds.size < MAX_ERROR_KINDS || this.#bugKinds.has(kind)) {
|
|
441
463
|
bump(this.#bugKinds, kind);
|
|
464
|
+
}
|
|
442
465
|
const first = !this.#seenBugKinds.has(kind);
|
|
443
466
|
this.#seenBugKinds.add(kind);
|
|
444
467
|
return first;
|
|
@@ -457,8 +480,9 @@ export class SessionMetrics {
|
|
|
457
480
|
this.#connections.set(kind, stats);
|
|
458
481
|
let settled = false;
|
|
459
482
|
return (failure) => {
|
|
460
|
-
if (settled)
|
|
483
|
+
if (settled) {
|
|
461
484
|
return;
|
|
485
|
+
}
|
|
462
486
|
settled = true;
|
|
463
487
|
if (failure === undefined) {
|
|
464
488
|
stats.successes += 1;
|
|
@@ -466,12 +490,21 @@ export class SessionMetrics {
|
|
|
466
490
|
}
|
|
467
491
|
stats.failures ??= {};
|
|
468
492
|
stats.failures[failure] = (stats.failures[failure] ?? 0) + 1;
|
|
493
|
+
if (CHROMIUM_UNAVAILABLE.has(failure)) {
|
|
494
|
+
reportOnboardingStop({
|
|
495
|
+
phase: OnboardingPhase.FIRST_RUN,
|
|
496
|
+
step: DRIVEN_STEP,
|
|
497
|
+
reason: failure,
|
|
498
|
+
stop: OnboardingStop.CHROMIUM_UNAVAILABLE,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
469
501
|
};
|
|
470
502
|
}
|
|
471
503
|
recordClient(name, version) {
|
|
472
504
|
const key = name.slice(0, 64);
|
|
473
|
-
if (this.#clients.size < MAX_CLIENTS)
|
|
505
|
+
if (this.#clients.size < MAX_CLIENTS) {
|
|
474
506
|
this.#clients.add(key);
|
|
507
|
+
}
|
|
475
508
|
// The version was already in the handshake and was being dropped. A regression in agent
|
|
476
509
|
// behaviour is usually a client version, and without this it is unattributable.
|
|
477
510
|
if (version !== undefined && '' !== version && this.#clientVersions.size < MAX_CLIENTS) {
|
|
@@ -511,8 +544,9 @@ export class SessionMetrics {
|
|
|
511
544
|
summarize(final, exit) {
|
|
512
545
|
// The last call has no successor to resolve it, so resolve it here. Only on a FINAL summary:
|
|
513
546
|
// a periodic flush is not the end of anything and would resolve a call still in flight.
|
|
514
|
-
if (final)
|
|
547
|
+
if (final) {
|
|
515
548
|
this.#resolveRecovery();
|
|
549
|
+
}
|
|
516
550
|
const machine = machineSnapshot();
|
|
517
551
|
// EVERY event reports its own window, the final one included: summing a session's
|
|
518
552
|
// `session_progress` + `daemon_stopped` rows is then its total. The final event used to swap in
|
|
@@ -612,10 +646,12 @@ export class SessionMetrics {
|
|
|
612
646
|
*/
|
|
613
647
|
#resolveRecovery() {
|
|
614
648
|
if (this.#priorCallErrored) {
|
|
615
|
-
if (this.#currentCallErrored)
|
|
649
|
+
if (this.#currentCallErrored) {
|
|
616
650
|
this.#errorsRepeated += 1;
|
|
617
|
-
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
618
653
|
this.#errorsRecovered += 1;
|
|
654
|
+
}
|
|
619
655
|
}
|
|
620
656
|
this.#priorCallErrored = this.#currentCallErrored;
|
|
621
657
|
this.#currentCallErrored = false;
|
|
@@ -625,14 +661,18 @@ export class SessionMetrics {
|
|
|
625
661
|
* that left explains everything after it, and a verdict settles whatever preceded it.
|
|
626
662
|
*/
|
|
627
663
|
#endReason() {
|
|
628
|
-
if (0 === this.#lifetimeToolCalls)
|
|
664
|
+
if (0 === this.#lifetimeToolCalls) {
|
|
629
665
|
return 'never_used';
|
|
630
|
-
|
|
666
|
+
}
|
|
667
|
+
if (this.#clientLeft) {
|
|
631
668
|
return 'client_left';
|
|
632
|
-
|
|
669
|
+
}
|
|
670
|
+
if (this.#unsettledActions > 0) {
|
|
633
671
|
return 'abandoned';
|
|
634
|
-
|
|
672
|
+
}
|
|
673
|
+
if (this.#lifetimeVerifications > 0) {
|
|
635
674
|
return 'verified';
|
|
675
|
+
}
|
|
636
676
|
return 'explored';
|
|
637
677
|
}
|
|
638
678
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type OnboardingStep, type InstallSource, type McpConnection, type McpOutage, type DaemonDeath, type ProxyExit, type WakeRefusal, type DaemonSpawnMethod, type ToolRefusal, type AppInstrumentation, type HarnessRun, type HudEvent, type PlatformLink, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core/telemetry';
|
|
1
|
+
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type OnboardingStep, type InstallSource, type McpConnection, type McpOutage, type DaemonDeath, type GateCompleted, type ProxyExit, type WakeRefusal, type DaemonSpawnMethod, type ToolRefusal, type AppInstrumentation, type HarnessRun, type HudEvent, type PlatformLink, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core/telemetry';
|
|
2
2
|
/**
|
|
3
3
|
* Presence of this file marks the MACHINE as the Reticle team's own: every event carries
|
|
4
4
|
* `internal: true`. The persistent twin of `RETICLE_INTERNAL=1`, for a dev machine where setting a
|
|
@@ -113,6 +113,8 @@ export interface TelemetryExtra {
|
|
|
113
113
|
daemonDeath?: DaemonDeath;
|
|
114
114
|
/** `wake_refused`: what held the port. */
|
|
115
115
|
wakeRefusal?: WakeRefusal;
|
|
116
|
+
/** `gate_completed`: how `reticle gate` ended. */
|
|
117
|
+
gate?: GateCompleted;
|
|
116
118
|
/** `daemon_started`: how the parent started the daemon. */
|
|
117
119
|
spawnMethod?: DaemonSpawnMethod;
|
|
118
120
|
/**
|