@reticlehq/server 3.8.1 → 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/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +21 -23
- package/dist/portal/input/real-input.js +153 -194
- 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 +12 -12
- 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/real-input-attempt.js +70 -31
- 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/react.mdx +14 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +1 -0
- 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 +14 -10
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -14,8 +14,8 @@ export declare function renderRunReport(report: RunReport): string;
|
|
|
14
14
|
export declare function endsRun(tool: string, args: Record<string, unknown>): boolean;
|
|
15
15
|
/** What an answer says the run saved or read. An answer that refused counts for nothing. */
|
|
16
16
|
export declare function noteToolResult(ledger: RunLedger, tool: string, args: Record<string, unknown>, result: unknown): void;
|
|
17
|
-
/** The report as an answer field, or no field when the
|
|
18
|
-
export declare function runReportField(ledger: RunLedger, root: string | undefined, env: Record<string, string | undefined>, now: number): Partial<Record<typeof RUN_REPORT_FIELD, {
|
|
17
|
+
/** The owner's report as an answer field, or no field when the owner has no run. Ends that run. */
|
|
18
|
+
export declare function runReportField(ledger: RunLedger, root: string | undefined, env: Record<string, string | undefined>, now: number, owner?: string): Partial<Record<typeof RUN_REPORT_FIELD, {
|
|
19
19
|
report: RunReport;
|
|
20
20
|
markdown: string;
|
|
21
21
|
instruction: string;
|
|
@@ -34,12 +34,15 @@ function headline(report) {
|
|
|
34
34
|
const count = (v) => all.filter((each) => v === each).length;
|
|
35
35
|
const total = all.length;
|
|
36
36
|
const word = `${HEAD}${WORD[report.verified]}**${SEP}`;
|
|
37
|
-
if (0 === total)
|
|
37
|
+
if (0 === total) {
|
|
38
38
|
return `${word}${Said.NOTHING_CHECKED}`;
|
|
39
|
-
|
|
39
|
+
}
|
|
40
|
+
if (Verified.NO === report.verified) {
|
|
40
41
|
return `${word}${String(count(Verified.NO))} of ${plural(total, 'check')} did not hold`;
|
|
41
|
-
|
|
42
|
+
}
|
|
43
|
+
if (Verified.YES === report.verified) {
|
|
42
44
|
return `${word}${String(total)} of ${plural(total, 'check')} held`;
|
|
45
|
+
}
|
|
43
46
|
return `${word}${String(total - count(Verified.YES) - count(Verified.NO))} of ${plural(total, 'check')} undecided`;
|
|
44
47
|
}
|
|
45
48
|
/** The report as markdown: the verdict, each claim with its evidence, then what the run left. */
|
|
@@ -95,39 +98,48 @@ const INTENT_READS = new Set(['list', 'index', 'subject', 'get']);
|
|
|
95
98
|
const isRecord = (value) => null !== value && 'object' === typeof value && !Array.isArray(value);
|
|
96
99
|
/** What an answer says the run saved or read. An answer that refused counts for nothing. */
|
|
97
100
|
export function noteToolResult(ledger, tool, args, result) {
|
|
98
|
-
if (!isRecord(result) || result['error'] !== undefined)
|
|
101
|
+
if (!isRecord(result) || result['error'] !== undefined) {
|
|
99
102
|
return;
|
|
103
|
+
}
|
|
100
104
|
if (ReticleTool.FLOW_SAVE === tool || ReticleTool.FLOW_SAVE_RECORDED === tool) {
|
|
101
105
|
const name = result['name'];
|
|
102
|
-
if ('string' === typeof name)
|
|
106
|
+
if ('string' === typeof name) {
|
|
103
107
|
ledger.flowSaved(name);
|
|
108
|
+
}
|
|
104
109
|
return;
|
|
105
110
|
}
|
|
106
111
|
const action = asString(args['action']);
|
|
107
112
|
if (ReticleTool.INTENT === tool) {
|
|
108
113
|
// The intent ledger in `.reticle/intent/` is the memory an unlinked project has.
|
|
109
|
-
if (IntentWrite.RECORD === action)
|
|
114
|
+
if (IntentWrite.RECORD === action) {
|
|
110
115
|
ledger.memoryWritten();
|
|
116
|
+
}
|
|
111
117
|
const declared = args['intents'];
|
|
112
|
-
if (IntentWrite.DECLARE === action && Array.isArray(declared))
|
|
118
|
+
if (IntentWrite.DECLARE === action && Array.isArray(declared)) {
|
|
113
119
|
ledger.memoryWritten(declared.length);
|
|
114
|
-
|
|
120
|
+
}
|
|
121
|
+
if (action !== undefined && INTENT_READS.has(action)) {
|
|
115
122
|
ledger.memoryRead([JSON.stringify(result)]);
|
|
123
|
+
}
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
118
|
-
if (ReticleTool.MEMORY !== tool)
|
|
126
|
+
if (ReticleTool.MEMORY !== tool) {
|
|
119
127
|
return;
|
|
120
|
-
|
|
128
|
+
}
|
|
129
|
+
if ('add' === action && result['added'] !== undefined) {
|
|
121
130
|
ledger.memoryWritten();
|
|
131
|
+
}
|
|
122
132
|
const known = result['known'];
|
|
123
|
-
if (Array.isArray(known))
|
|
133
|
+
if (Array.isArray(known)) {
|
|
124
134
|
ledger.memoryRead(known.map((entry) => JSON.stringify(entry)));
|
|
135
|
+
}
|
|
125
136
|
}
|
|
126
|
-
/** The report as an answer field, or no field when the
|
|
127
|
-
export function runReportField(ledger, root, env, now) {
|
|
128
|
-
if (ledger.isEmpty())
|
|
137
|
+
/** The owner's report as an answer field, or no field when the owner has no run. Ends that run. */
|
|
138
|
+
export function runReportField(ledger, root, env, now, owner) {
|
|
139
|
+
if (ledger.isEmpty(owner)) {
|
|
129
140
|
return {};
|
|
130
|
-
|
|
141
|
+
}
|
|
142
|
+
const taken = ledger.take(now, owner);
|
|
131
143
|
const silenced = env[UPGRADE_HINT_SILENCE_ENV] !== undefined;
|
|
132
144
|
const linked = root === undefined || linkedToPlatform(root, env);
|
|
133
145
|
const report = silenced || linked ? taken : { ...taken, cloud: CLOUD_VALUE_LINE };
|
|
@@ -96,6 +96,11 @@ export interface ToolDeps<Ext = unknown> {
|
|
|
96
96
|
} | undefined>;
|
|
97
97
|
/** The one dev server running in the caller's project folder, for a lease that names no url. */
|
|
98
98
|
projectAppUrl?: () => Promise<string | undefined>;
|
|
99
|
+
/**
|
|
100
|
+
* The first-session offer for this attach id's first call, when no app is attached and the
|
|
101
|
+
* project is not wired (see zero-install-first-call.ts). Absent: no offer.
|
|
102
|
+
*/
|
|
103
|
+
firstSessionOffer?: (attachId: string) => string | undefined;
|
|
99
104
|
/** cross-run outcome memory (.reticle/project.json). */
|
|
100
105
|
project: ProjectStore;
|
|
101
106
|
/** optional native-input provider. undefined ⇒ everything stays synthetic. */
|
|
@@ -303,10 +308,17 @@ export declare const EnvelopeKey: {
|
|
|
303
308
|
* what it cannot see, and the command that wires the app for good. See zero-install-first-call.ts.
|
|
304
309
|
*/
|
|
305
310
|
readonly ZERO_INSTALL: "zero_install";
|
|
311
|
+
/**
|
|
312
|
+
* Once per agent connection, on its first call: this unwired web app can be checked now with the
|
|
313
|
+
* lease, and `init` needs the user's consent. See zero-install-first-call.ts.
|
|
314
|
+
*/
|
|
315
|
+
readonly FIRST_SESSION_OFFER: "first_session_offer";
|
|
306
316
|
/** Once-per-session advisory lines an agent's own call carries. See agent/notices.ts. */
|
|
307
317
|
readonly NOTICES: "notices";
|
|
308
318
|
/** The end-of-run report, on the call that ends an agent's run. See run-report.ts. */
|
|
309
319
|
readonly RUN_REPORT: "runReport";
|
|
320
|
+
/** On the third identical call with the same result: what to try instead. See repeat-hint.ts. */
|
|
321
|
+
readonly REPEAT_HINT: "repeatHint";
|
|
310
322
|
};
|
|
311
323
|
export type EnvelopeKey = (typeof EnvelopeKey)[keyof typeof EnvelopeKey];
|
|
312
324
|
/**
|
|
@@ -99,10 +99,17 @@ export const EnvelopeKey = {
|
|
|
99
99
|
* what it cannot see, and the command that wires the app for good. See zero-install-first-call.ts.
|
|
100
100
|
*/
|
|
101
101
|
ZERO_INSTALL: 'zero_install',
|
|
102
|
+
/**
|
|
103
|
+
* Once per agent connection, on its first call: this unwired web app can be checked now with the
|
|
104
|
+
* lease, and `init` needs the user's consent. See zero-install-first-call.ts.
|
|
105
|
+
*/
|
|
106
|
+
FIRST_SESSION_OFFER: 'first_session_offer',
|
|
102
107
|
/** Once-per-session advisory lines an agent's own call carries. See agent/notices.ts. */
|
|
103
108
|
NOTICES: 'notices',
|
|
104
109
|
/** The end-of-run report, on the call that ends an agent's run. See run-report.ts. */
|
|
105
110
|
RUN_REPORT: RUN_REPORT_FIELD,
|
|
111
|
+
/** On the third identical call with the same result: what to try instead. See repeat-hint.ts. */
|
|
112
|
+
REPEAT_HINT: 'repeatHint',
|
|
106
113
|
};
|
|
107
114
|
/**
|
|
108
115
|
* The sentence every session-bound result carries while its leased tab runs on a stubbed shell.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TESTID_FOUND_UNDER_SCHEMA } from './query-hint-schema.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { EMPTY_SESSION_LIST_OUTPUT, emptySessionList, nextStepBesideLiveSessions, } from './empty-session-list.js';
|
|
4
|
-
import { AttrNamesSchema, QueryBy, ReticleCommand, SnapshotMode } from '@reticlehq/core';
|
|
3
|
+
import { EMPTY_SESSION_LIST_OUTPUT, emptySessionList, NEXT_STEP_FIELD, nextStepBesideLiveSessions, } from './empty-session-list.js';
|
|
4
|
+
import { AttrNamesSchema, NearestTargetSchema, QueryBy, ReticleCommand, SnapshotMode, } from '@reticlehq/core';
|
|
5
5
|
import { ReticleTool } from '@reticlehq/core';
|
|
6
6
|
import { withSizeCost } from '../../portal/session/output-budget.js';
|
|
7
7
|
import { applySnapshotDelta, SnapshotCache } from './read/snapshot-delta.js';
|
|
@@ -145,11 +145,12 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
145
145
|
})));
|
|
146
146
|
const sessions = await list();
|
|
147
147
|
// An empty list is a dead end on its own; see empty-session-list.ts for what it carries.
|
|
148
|
-
if (0 === sessions.length)
|
|
148
|
+
if (0 === sessions.length) {
|
|
149
149
|
return emptySessionList(deps, sessions);
|
|
150
|
+
}
|
|
150
151
|
// Another project's tab is up. This caller's project may still never have been wired.
|
|
151
152
|
const nextStep = nextStepBesideLiveSessions(deps, sessions);
|
|
152
|
-
return nextStep === undefined ? { sessions } : { sessions,
|
|
153
|
+
return nextStep === undefined ? { sessions } : { sessions, [NEXT_STEP_FIELD]: nextStep };
|
|
153
154
|
},
|
|
154
155
|
},
|
|
155
156
|
{
|
|
@@ -284,7 +285,7 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
284
285
|
name: z
|
|
285
286
|
.string()
|
|
286
287
|
.optional()
|
|
287
|
-
.describe('
|
|
288
|
+
.describe('Exact accessible-name filter - narrows results when `by` is role and the page has many elements of that role. Matches the whole name; use `text` for a partial match.'),
|
|
288
289
|
scope: z
|
|
289
290
|
.string()
|
|
290
291
|
.optional()
|
|
@@ -398,6 +399,10 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
398
399
|
.optional()
|
|
399
400
|
.describe('Present when a ROLE+NAME search missed and that role DOES carry a nearly-matching name — role+name is exact, so "Mesh" does not find "2 Mesh". Retry with one of these spellings; no snapshot needed.'),
|
|
400
401
|
testidFoundUnder: TESTID_FOUND_UNDER_SCHEMA,
|
|
402
|
+
nearest: z
|
|
403
|
+
.array(NearestTargetSchema)
|
|
404
|
+
.optional()
|
|
405
|
+
.describe('Present when a search missed and the page has visible targets like the one asked for: up to 3, the asked role first, then by name similarity. Each has a ref to retry with. nameTruncated is true when the name was cut. Advice only, never a match.'),
|
|
401
406
|
})
|
|
402
407
|
.optional()
|
|
403
408
|
.describe('Present only on zero matches — tells you what IS on the page so you can diagnose the miss.'),
|
|
@@ -422,8 +427,9 @@ export const RAW_TOOLS = withCompactEnvelope([
|
|
|
422
427
|
// Accept the predicate's named-field spelling too — see query-shape.ts.
|
|
423
428
|
const args = normalizeQueryArgs(rawArgs);
|
|
424
429
|
const empty = emptyQueryRefusal(args);
|
|
425
|
-
if (empty !== undefined)
|
|
430
|
+
if (empty !== undefined) {
|
|
426
431
|
return Promise.reject(empty);
|
|
432
|
+
}
|
|
427
433
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
428
434
|
return commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.QUERY, {
|
|
429
435
|
by: args['by'],
|
|
@@ -603,14 +609,17 @@ export const RETIRED_FROM_SURFACE = [
|
|
|
603
609
|
* The browser counts what leanness passed over, so the tool can say which of the two it is.
|
|
604
610
|
*/
|
|
605
611
|
function noteEmptyLeanTree(result, mode) {
|
|
606
|
-
if (SnapshotMode.INTERACTIVE !== mode)
|
|
612
|
+
if (SnapshotMode.INTERACTIVE !== mode) {
|
|
607
613
|
return result;
|
|
614
|
+
}
|
|
608
615
|
const row = asRecord(result);
|
|
609
|
-
if (0 !== asNumber(row['nodes']))
|
|
616
|
+
if (0 !== asNumber(row['nodes'])) {
|
|
610
617
|
return result;
|
|
618
|
+
}
|
|
611
619
|
const skipped = asNumber(row['leanSkipped']) ?? 0;
|
|
612
|
-
if (0 === skipped)
|
|
620
|
+
if (0 === skipped) {
|
|
613
621
|
return result;
|
|
622
|
+
}
|
|
614
623
|
return {
|
|
615
624
|
...row,
|
|
616
625
|
note: `no element on this page carries an interactive ARIA role, so this mode found nothing — ` +
|
|
@@ -635,18 +644,22 @@ function noteEmptyLeanTree(result, mode) {
|
|
|
635
644
|
* empty tree it replaces.
|
|
636
645
|
*/
|
|
637
646
|
function noteHiddenPage(result, mode) {
|
|
638
|
-
if (SnapshotMode.STATUS === mode)
|
|
647
|
+
if (SnapshotMode.STATUS === mode) {
|
|
639
648
|
return result;
|
|
649
|
+
}
|
|
640
650
|
const row = asRecord(result);
|
|
641
|
-
if (0 !== asNumber(row['nodes']))
|
|
651
|
+
if (0 !== asNumber(row['nodes'])) {
|
|
642
652
|
return result;
|
|
653
|
+
}
|
|
643
654
|
// A note already there is the more specific one (leanness), and two explanations for one empty
|
|
644
655
|
// tree is worse than the better of them alone.
|
|
645
|
-
if (row['note'] !== undefined)
|
|
656
|
+
if (row['note'] !== undefined) {
|
|
646
657
|
return result;
|
|
658
|
+
}
|
|
647
659
|
const skipped = asNumber(row['hiddenSkipped']) ?? 0;
|
|
648
|
-
if (0 === skipped)
|
|
660
|
+
if (0 === skipped) {
|
|
649
661
|
return result;
|
|
662
|
+
}
|
|
650
663
|
return {
|
|
651
664
|
...row,
|
|
652
665
|
note: `this is NOT an empty page: ${String(skipped)} subtree(s) were skipped because their root ` +
|
|
@@ -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
|
/**
|