@reticlehq/server 2.8.0 → 2.10.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/SKILL.md +130 -813
- 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/capsule-store.d.ts +2 -2
- package/dist/capsule/causal-summary.d.ts +16 -1
- package/dist/capsule/causal-summary.js +2 -1
- package/dist/cli/cli-doctor.js +41 -13
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-launch.d.ts +0 -17
- package/dist/cli/cli-launch.js +0 -28
- package/dist/cli/cli-parse.d.ts +1 -1
- package/dist/cli/cli-parse.js +1 -1
- package/dist/cli/cli-port.d.ts +24 -0
- package/dist/cli/cli-port.js +66 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +45 -1
- 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.js +6 -3
- 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/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +65 -70
- package/dist/crawl/crawl.d.ts +7 -2
- package/dist/crawl/crawl.js +52 -9
- package/dist/ee/audit-log.js +2 -2
- package/dist/events/body-failures.d.ts +2 -2
- package/dist/events/contradiction-folds.d.ts +45 -0
- package/dist/events/contradiction-folds.js +42 -0
- package/dist/events/contradictions.d.ts +51 -1
- package/dist/events/contradictions.js +26 -7
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +9 -3
- 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 -91
- package/dist/events/predicate-eval.js +85 -326
- 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 +127 -5
- package/dist/events/stale-response.d.ts +2 -3
- package/dist/events/stale-response.js +49 -2
- package/dist/events/unit-mismatch.d.ts +2 -2
- 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-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +9 -1
- package/dist/flows/replay.js +3 -0
- package/dist/flows/verify-change-tools.js +1 -1
- 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 +4 -0
- 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 +9 -0
- package/dist/honesty/unsettled.js +7 -3
- package/dist/honesty/verified.d.ts +29 -3
- package/dist/honesty/verified.js +67 -8
- 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 +25 -0
- package/dist/index.js +60 -10
- package/dist/init/agent-rules.d.ts +20 -0
- package/dist/init/agent-rules.js +36 -0
- package/dist/init/astro-patch.js +27 -1
- package/dist/init/confirm.d.ts +21 -3
- package/dist/init/confirm.js +32 -5
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- 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 +16 -2
- package/dist/init/mcp.js +23 -1
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +25 -1
- package/dist/init/plan.d.ts +7 -2
- package/dist/init/plan.js +66 -30
- package/dist/init/run.js +11 -5
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +12 -4
- package/dist/init/snippets.d.ts +1 -1
- package/dist/init/snippets.js +35 -7
- package/dist/init/vite-config.js +24 -2
- package/dist/input/network-mock-tools.js +8 -1
- 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.d.ts +69 -4
- package/dist/license/license.js +84 -15
- package/dist/mcp/mcp-proxy.d.ts +7 -93
- package/dist/mcp/mcp-proxy.js +183 -250
- package/dist/mcp/mcp.d.ts +27 -2
- package/dist/mcp/mcp.js +77 -36
- package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
- package/dist/mcp/proxy-daemon-probe.js +48 -0
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +110 -3
- package/dist/mcp/proxy-lifecycle.d.ts +10 -1
- package/dist/mcp/proxy-lifecycle.js +11 -2
- 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/mcp/sse-frame-parser.d.ts +19 -0
- package/dist/mcp/sse-frame-parser.js +41 -0
- 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 +55 -2
- package/dist/session/no-session-diagnosis.js +199 -22
- 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 +172 -11
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session-readiness.js +1 -1
- 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/bug-found.d.ts +18 -0
- package/dist/telemetry/bug-found.js +14 -0
- package/dist/telemetry/daemon-telemetry.js +29 -0
- package/dist/telemetry/install-source.d.ts +20 -0
- package/dist/telemetry/install-source.js +34 -0
- package/dist/telemetry/instrumentation-stall.d.ts +57 -0
- package/dist/telemetry/instrumentation-stall.js +103 -0
- package/dist/telemetry/license-activation.d.ts +48 -0
- package/dist/telemetry/license-activation.js +57 -0
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +10 -0
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +23 -3
- package/dist/tools/act-tools.js +166 -29
- package/dist/tools/assert-verdict.js +32 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/dynamic-tools.js +17 -8
- package/dist/tools/error-recovery.js +10 -6
- package/dist/tools/feedback-tools.js +31 -6
- package/dist/tools/invoke-tool.d.ts +1 -1
- package/dist/tools/invoke-tool.js +30 -8
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +11 -0
- package/dist/tools/lease-tools.js +141 -96
- package/dist/tools/observe-tools.js +9 -3
- package/dist/tools/react-grace.d.ts +4 -0
- package/dist/tools/react-grace.js +6 -3
- package/dist/tools/read-tools.js +47 -1
- 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 +15 -0
- package/dist/tools/settle-in-flight.js +42 -2
- package/dist/tools/snapshot-delta.d.ts +3 -0
- package/dist/tools/snapshot-delta.js +67 -6
- package/dist/tools/tool-kit.d.ts +38 -3
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +84 -19
- package/dist/tools/tools.js +61 -5
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +1 -2
- package/docs/actions.mdx +41 -8
- package/docs/agent-cheatsheet.md +6 -4
- package/docs/architecture.md +6 -0
- package/docs/benchmarks.md +60 -12
- package/docs/best-practices.mdx +11 -6
- package/docs/capabilities.mdx +204 -57
- package/docs/cli/affected.mdx +6 -4
- package/docs/cli/capsules.mdx +9 -8
- package/docs/cli/cloud.mdx +30 -4
- package/docs/cli/doctor.mdx +41 -8
- package/docs/cli/drive.mdx +34 -15
- package/docs/cli/gate.mdx +6 -6
- package/docs/cli/help.mdx +8 -6
- package/docs/cli/hunt.mdx +20 -4
- package/docs/cli/identify.mdx +16 -2
- package/docs/cli/init.mdx +35 -8
- package/docs/cli/kill.mdx +5 -5
- package/docs/cli/license.mdx +1 -1
- package/docs/cli/open.mdx +6 -0
- package/docs/cli/restart.mdx +6 -6
- package/docs/cli/serve.mdx +10 -3
- package/docs/cli/status.mdx +29 -5
- package/docs/cli/stop.mdx +8 -0
- package/docs/cli/verify.mdx +20 -1
- package/docs/cli/version.mdx +1 -1
- package/docs/cli/watch.mdx +9 -2
- package/docs/cli.mdx +44 -22
- package/docs/debugging.md +3 -1
- package/docs/deploy-checks.md +31 -2
- package/docs/desktop.mdx +276 -80
- package/docs/docs.json +13 -12
- package/docs/enterprise.md +9 -3
- package/docs/faq.mdx +49 -35
- package/docs/fixtures.md +2 -0
- package/docs/flows.md +7 -5
- package/docs/for-agents.md +3 -3
- package/docs/frameworks.mdx +46 -7
- package/docs/gate-plan.md +4 -2
- package/docs/gates.md +8 -6
- package/docs/getting-started.md +53 -12
- package/docs/human-control.md +2 -0
- package/docs/index.mdx +43 -1
- package/docs/install-agentic.mdx +13 -4
- package/docs/install-manual.mdx +6 -4
- package/docs/instrumentation.mdx +7 -1
- package/docs/integration-patterns.md +3 -1
- package/docs/local-registry.md +4 -2
- package/docs/multi-agent-testing.md +3 -1
- package/docs/packages/babel-plugin.mdx +15 -6
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/core.mdx +26 -16
- package/docs/packages/electron.mdx +5 -7
- package/docs/packages/eslint-plugin.mdx +5 -3
- package/docs/packages/next.mdx +3 -5
- package/docs/packages/react.mdx +3 -3
- package/docs/packages/server.mdx +2 -2
- package/docs/packages/tauri.mdx +6 -6
- package/docs/packages/test.mdx +18 -7
- package/docs/packages/vite-plugin.mdx +4 -6
- package/docs/packages.mdx +14 -13
- package/docs/platform-integration.md +2 -0
- package/docs/predicates.mdx +76 -7
- package/docs/quickstart.mdx +51 -5
- package/docs/recipes.mdx +63 -23
- package/docs/skill-file.mdx +3 -1
- package/docs/state-management.mdx +2 -0
- package/docs/system-map.md +3 -1
- package/docs/telemetry-contract.md +33 -5
- package/docs/telemetry-events.mdx +63 -18
- package/docs/telemetry.md +8 -5
- package/docs/testing.md +35 -1
- package/docs/token-efficiency.md +8 -2
- package/docs/tools-act-and-wait.mdx +64 -14
- package/docs/tools-act-sequence.mdx +34 -17
- package/docs/tools-act.mdx +24 -11
- package/docs/tools-assert.mdx +69 -10
- package/docs/tools-console.mdx +11 -7
- package/docs/tools-inspect.mdx +11 -8
- package/docs/tools-navigate.mdx +41 -5
- package/docs/tools-network.mdx +42 -9
- package/docs/tools-observe.mdx +66 -10
- package/docs/tools-overview.mdx +26 -23
- package/docs/tools-query.mdx +52 -8
- package/docs/tools-session-and-feedback.mdx +35 -9
- package/docs/tools-sessions.mdx +39 -10
- package/docs/tools-snapshot.mdx +57 -3
- package/docs/tools-state.mdx +24 -1
- package/docs/tools-tools-and-run.mdx +55 -24
- package/docs/tools-wait-for.mdx +34 -15
- package/docs/troubleshooting.mdx +80 -10
- package/docs/usage.md +46 -24
- package/docs/vs-chrome-devtools-mcp.mdx +41 -0
- package/docs/vs-playwright-mcp.mdx +42 -2
- package/docs/vs-screenshots.mdx +42 -1
- package/docs/what-is-reticle.mdx +1 -1
- package/docs/why-reticle.mdx +3 -1
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -265
|
@@ -7,3 +7,21 @@ import { type BugFound } from '@reticlehq/core';
|
|
|
7
7
|
*/
|
|
8
8
|
export type BugCandidate = Omit<BugFound, 'repeat'>;
|
|
9
9
|
export declare function bugsInResult(toolName: string, result: Record<string, unknown>): BugCandidate[];
|
|
10
|
+
/**
|
|
11
|
+
* Crawl anomaly kinds that are CONTRADICTIONS rather than single-channel faults.
|
|
12
|
+
*
|
|
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.
|
|
16
|
+
*
|
|
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.
|
|
22
|
+
*/
|
|
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;
|
|
@@ -221,4 +221,18 @@ export function bugsInResult(toolName, result) {
|
|
|
221
221
|
* fault, quietly deflating the exact number we intend to publish. A copied enum is a drift hazard
|
|
222
222
|
* wherever it appears; it is a correctness hazard when the thing that drifts is a public claim.
|
|
223
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
|
+
}
|
|
224
238
|
const CONTRADICTION_KINDS = new Set(Object.values(ContradictionKind));
|
|
@@ -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, STALL_AFTER_MS, } 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,22 @@ 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
|
+
const stalled = 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);
|
|
89
|
+
if (stalled) {
|
|
90
|
+
const minutes = Math.round(STALL_AFTER_MS / 60_000);
|
|
91
|
+
process.stderr.write(`[reticle] no app has connected after ${String(minutes)} minutes — ` +
|
|
92
|
+
'the install may be incomplete. Restart the dev server if you added the Reticle plugin.\n');
|
|
93
|
+
}
|
|
74
94
|
if (metrics.empty)
|
|
75
95
|
return; // an idle window is not worth an event
|
|
76
96
|
// NOT daemon_stopped: the daemon is still running. See SESSION_PROGRESS.
|
|
@@ -93,6 +113,15 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
|
|
|
93
113
|
stopped ??= (async () => {
|
|
94
114
|
clearInterval(flush);
|
|
95
115
|
clearTimeout(firstFlush);
|
|
116
|
+
// Last chance to say that no app ever arrived. The periodic path cannot report an
|
|
117
|
+
// UNATTENDED stall at all — that daemon idle-exits at five minutes and the periodic
|
|
118
|
+
// threshold is ten — so without this the event only ever describes daemons with an agent
|
|
119
|
+
// attached, which is not the population it was built to measure. Self-limiting: if the
|
|
120
|
+
// interval already reported, this returns false and emits nothing.
|
|
121
|
+
reportInstrumentationStall({
|
|
122
|
+
initialized: readProjectId(cwd) !== undefined,
|
|
123
|
+
agentAttached: metrics.agentEverAttached,
|
|
124
|
+
}, now, { atShutdown: true });
|
|
96
125
|
// The rich one: the whole session in a single event — duration, the tool histogram, error
|
|
97
126
|
// shapes, verifications, browser launches. This is what replaced the per-tool-call event.
|
|
98
127
|
await getTelemetry().emit(TelemetryEventKind.DAEMON_STOPPED, {
|
|
@@ -34,3 +34,23 @@ export declare const INSTALL_SOURCE_ENV = "RETICLE_INSTALL_SOURCE";
|
|
|
34
34
|
* shell snippets and copy-paste, and `Skill_File` is the same channel as `skill_file`.
|
|
35
35
|
*/
|
|
36
36
|
export declare function resolveInstallSource(env?: NodeJS.ProcessEnv): InstallSource;
|
|
37
|
+
/**
|
|
38
|
+
* The install source declared by the environment, or undefined when nothing declared one.
|
|
39
|
+
*
|
|
40
|
+
* Distinct from `resolveInstallSource`, which answers `unknown` so an event always carries a value.
|
|
41
|
+
* A config file wants the opposite: writing `unknown` into `.reticle.json` is indistinguishable
|
|
42
|
+
* from a config written before this field existed, and those two are different facts.
|
|
43
|
+
*/
|
|
44
|
+
export declare function declaredInstallSource(env?: NodeJS.ProcessEnv): InstallSource | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The install source for a project: what its config recorded, else what the environment declares.
|
|
47
|
+
*
|
|
48
|
+
* The config wins because it is the durable answer. The marker is an environment variable set by
|
|
49
|
+
* whichever channel ran the install, so it exists for exactly one command and is gone by the next
|
|
50
|
+
* one — which is why this field reached a handful of events and nothing else, and why the question
|
|
51
|
+
* it exists to answer could not be asked.
|
|
52
|
+
*
|
|
53
|
+
* Narrowed against the closed list on the way out, the same as the env path: a config file is user
|
|
54
|
+
* data, and a hand-edited one must not be able to put an arbitrary string on the wire.
|
|
55
|
+
*/
|
|
56
|
+
export declare function projectInstallSource(cwd: string, env?: NodeJS.ProcessEnv): InstallSource;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* are indistinguishable once they are in the same column.
|
|
24
24
|
*/
|
|
25
25
|
import { InstallSource } from '@reticlehq/core';
|
|
26
|
+
import { findProjectConfig } from '../cli/cli-port.js';
|
|
26
27
|
/** The one marker. Set by a channel on the process that runs the install. */
|
|
27
28
|
export const INSTALL_SOURCE_ENV = 'RETICLE_INSTALL_SOURCE';
|
|
28
29
|
const KNOWN = new Set(Object.values(InstallSource));
|
|
@@ -38,3 +39,36 @@ export function resolveInstallSource(env = process.env) {
|
|
|
38
39
|
const declared = (env[INSTALL_SOURCE_ENV] ?? '').trim().toLowerCase();
|
|
39
40
|
return KNOWN.has(declared) ? declared : InstallSource.UNKNOWN;
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The install source declared by the environment, or undefined when nothing declared one.
|
|
44
|
+
*
|
|
45
|
+
* Distinct from `resolveInstallSource`, which answers `unknown` so an event always carries a value.
|
|
46
|
+
* A config file wants the opposite: writing `unknown` into `.reticle.json` is indistinguishable
|
|
47
|
+
* from a config written before this field existed, and those two are different facts.
|
|
48
|
+
*/
|
|
49
|
+
export function declaredInstallSource(env = process.env) {
|
|
50
|
+
const declared = (env[INSTALL_SOURCE_ENV] ?? '').trim().toLowerCase();
|
|
51
|
+
return KNOWN.has(declared) && InstallSource.UNKNOWN !== declared
|
|
52
|
+
? declared
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The install source for a project: what its config recorded, else what the environment declares.
|
|
57
|
+
*
|
|
58
|
+
* The config wins because it is the durable answer. The marker is an environment variable set by
|
|
59
|
+
* whichever channel ran the install, so it exists for exactly one command and is gone by the next
|
|
60
|
+
* one — which is why this field reached a handful of events and nothing else, and why the question
|
|
61
|
+
* it exists to answer could not be asked.
|
|
62
|
+
*
|
|
63
|
+
* Narrowed against the closed list on the way out, the same as the env path: a config file is user
|
|
64
|
+
* data, and a hand-edited one must not be able to put an arbitrary string on the wire.
|
|
65
|
+
*/
|
|
66
|
+
export function projectInstallSource(cwd, env = process.env) {
|
|
67
|
+
const recorded = findProjectConfig(cwd)?.['installSource'];
|
|
68
|
+
if ('string' === typeof recorded) {
|
|
69
|
+
const narrowed = recorded.trim().toLowerCase();
|
|
70
|
+
if (KNOWN.has(narrowed))
|
|
71
|
+
return narrowed;
|
|
72
|
+
}
|
|
73
|
+
return resolveInstallSource(env);
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a daemon has waited long enough that no app is coming.
|
|
3
|
+
*
|
|
4
|
+
* `app_instrumented` fires on success and only on success, so the population that matters most —
|
|
5
|
+
* daemons that never get an app wired at all — exists in the data only as an absence. An absence is
|
|
6
|
+
* the one thing telemetry cannot interpret: it cannot separate "nothing was ever wired" from "the
|
|
7
|
+
* process was killed before it could say so" from "we never sent the event". The funnel breaks at
|
|
8
|
+
* exactly this step and nowhere else, which makes it the one silence worth converting into a signal.
|
|
9
|
+
*
|
|
10
|
+
* Once per daemon run, and never after an app has connected. A daemon that takes a while and then
|
|
11
|
+
* works reports nothing at all: the question is "did this run ever get an app", and a run that did
|
|
12
|
+
* is not stalled no matter how long it took.
|
|
13
|
+
*
|
|
14
|
+
* The threshold is a judgement, not a measurement, and it is deliberately generous. Someone running
|
|
15
|
+
* `init` and then going to make coffee has not hit a defect, and calling that a stall would drown
|
|
16
|
+
* the real signal in ordinary human latency.
|
|
17
|
+
*
|
|
18
|
+
* Read the result against `agentAttached`, which is what makes it actionable: a stalled daemon with
|
|
19
|
+
* no agent is somebody who installed and walked away, and is not a defect. A stalled daemon WITH an
|
|
20
|
+
* agent attached is the failure the product actually has — the tools are loaded, something is asking
|
|
21
|
+
* for them, and there is nothing on the other end to drive.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* How long a daemon may sit with no app before it counts as stalled.
|
|
25
|
+
*
|
|
26
|
+
* Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
|
|
27
|
+
* and well short of the half-hour idle windows that dominate the field, so a stall is reported while
|
|
28
|
+
* the session is still alive rather than inferred from its corpse.
|
|
29
|
+
*/
|
|
30
|
+
export declare const STALL_AFTER_MS: number;
|
|
31
|
+
/** Start the clock. Called from the same place the instrumentation clock is marked. */
|
|
32
|
+
export declare function markStallClock(now: number): void;
|
|
33
|
+
export interface StallFacts {
|
|
34
|
+
/** Whether this project has a projectId stamped — i.e. `init` has run here. */
|
|
35
|
+
initialized: boolean;
|
|
36
|
+
/** Whether an MCP client is attached right now. */
|
|
37
|
+
agentAttached: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Report a stalled install, at most once per daemon run.
|
|
41
|
+
*
|
|
42
|
+
* Returns whether it emitted, so the caller's test can assert the once-only rule without reaching
|
|
43
|
+
* into the telemetry client. Best-effort like every other metric here: it must never be the reason
|
|
44
|
+
* a daemon misbehaves.
|
|
45
|
+
*/
|
|
46
|
+
export declare function reportInstrumentationStall(facts: StallFacts, now?: () => number, opts?: {
|
|
47
|
+
atShutdown?: boolean;
|
|
48
|
+
}): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* How long this daemon has waited with no app, or undefined if the question is moot.
|
|
51
|
+
*
|
|
52
|
+
* Moot means: the clock was never started, or an app already connected. Callers use this to
|
|
53
|
+
* surface the stall to user-facing channels without coupling to the telemetry event itself.
|
|
54
|
+
*/
|
|
55
|
+
export declare function stallUptime(now: number): number | undefined;
|
|
56
|
+
/** Tests only. */
|
|
57
|
+
export declare function resetInstrumentationStall(): void;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a daemon has waited long enough that no app is coming.
|
|
3
|
+
*
|
|
4
|
+
* `app_instrumented` fires on success and only on success, so the population that matters most —
|
|
5
|
+
* daemons that never get an app wired at all — exists in the data only as an absence. An absence is
|
|
6
|
+
* the one thing telemetry cannot interpret: it cannot separate "nothing was ever wired" from "the
|
|
7
|
+
* process was killed before it could say so" from "we never sent the event". The funnel breaks at
|
|
8
|
+
* exactly this step and nowhere else, which makes it the one silence worth converting into a signal.
|
|
9
|
+
*
|
|
10
|
+
* Once per daemon run, and never after an app has connected. A daemon that takes a while and then
|
|
11
|
+
* works reports nothing at all: the question is "did this run ever get an app", and a run that did
|
|
12
|
+
* is not stalled no matter how long it took.
|
|
13
|
+
*
|
|
14
|
+
* The threshold is a judgement, not a measurement, and it is deliberately generous. Someone running
|
|
15
|
+
* `init` and then going to make coffee has not hit a defect, and calling that a stall would drown
|
|
16
|
+
* the real signal in ordinary human latency.
|
|
17
|
+
*
|
|
18
|
+
* Read the result against `agentAttached`, which is what makes it actionable: a stalled daemon with
|
|
19
|
+
* no agent is somebody who installed and walked away, and is not a defect. A stalled daemon WITH an
|
|
20
|
+
* agent attached is the failure the product actually has — the tools are loaded, something is asking
|
|
21
|
+
* for them, and there is nothing on the other end to drive.
|
|
22
|
+
*/
|
|
23
|
+
import { TelemetryEventKind } from '@reticlehq/core';
|
|
24
|
+
import { getTelemetry } from './telemetry.js';
|
|
25
|
+
import { appEverConnected } from './app-instrumented.js';
|
|
26
|
+
/**
|
|
27
|
+
* How long a daemon may sit with no app before it counts as stalled.
|
|
28
|
+
*
|
|
29
|
+
* Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
|
|
30
|
+
* and well short of the half-hour idle windows that dominate the field, so a stall is reported while
|
|
31
|
+
* the session is still alive rather than inferred from its corpse.
|
|
32
|
+
*/
|
|
33
|
+
export const STALL_AFTER_MS = 10 * 60 * 1000;
|
|
34
|
+
let reported = false;
|
|
35
|
+
let daemonStartedAt;
|
|
36
|
+
/** Start the clock. Called from the same place the instrumentation clock is marked. */
|
|
37
|
+
export function markStallClock(now) {
|
|
38
|
+
daemonStartedAt = now;
|
|
39
|
+
reported = false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Report a stalled install, at most once per daemon run.
|
|
43
|
+
*
|
|
44
|
+
* Returns whether it emitted, so the caller's test can assert the once-only rule without reaching
|
|
45
|
+
* into the telemetry client. Best-effort like every other metric here: it must never be the reason
|
|
46
|
+
* a daemon misbehaves.
|
|
47
|
+
*/
|
|
48
|
+
export function reportInstrumentationStall(facts, now = () => Date.now(), opts = {}) {
|
|
49
|
+
if (reported)
|
|
50
|
+
return false;
|
|
51
|
+
// An app arrived. Whatever this run was, it was not stalled.
|
|
52
|
+
if (appEverConnected())
|
|
53
|
+
return false;
|
|
54
|
+
if (daemonStartedAt === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
const waited = now() - daemonStartedAt;
|
|
57
|
+
// The threshold is a guard against crying stall seconds after boot. At SHUTDOWN there is nothing
|
|
58
|
+
// left to wait for: this daemon ran, no app ever connected, and it is now gone. That is a fact
|
|
59
|
+
// rather than an inference, and it does not become truer at ten minutes than at five.
|
|
60
|
+
//
|
|
61
|
+
// Skipping it here is what makes the event able to see anything at all. An UNATTENDED daemon
|
|
62
|
+
// idle-exits at DAEMON_IDLE_SHUTDOWN_MS (5 min), well short of STALL_AFTER_MS (10) — only an
|
|
63
|
+
// attached one gets the six-times grace that reaches the threshold. So on the periodic path alone
|
|
64
|
+
// `agentAttached` was forced to true on every row, and the bucket this event exists for —
|
|
65
|
+
// installed it, walked away, never wired the app — could not appear in the data at all.
|
|
66
|
+
//
|
|
67
|
+
// `msWaited` still carries the duration, so a consumer that wants to exclude short runs can do it
|
|
68
|
+
// on the data instead of being unable to see them.
|
|
69
|
+
if (true !== opts.atShutdown && waited < STALL_AFTER_MS)
|
|
70
|
+
return false;
|
|
71
|
+
reported = true;
|
|
72
|
+
try {
|
|
73
|
+
void getTelemetry().emit(TelemetryEventKind.INSTRUMENTATION_STALLED, {
|
|
74
|
+
stall: {
|
|
75
|
+
initialized: facts.initialized,
|
|
76
|
+
agentAttached: facts.agentAttached,
|
|
77
|
+
msWaited: Math.max(0, waited),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* never let a metric interfere with a running daemon */
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* How long this daemon has waited with no app, or undefined if the question is moot.
|
|
88
|
+
*
|
|
89
|
+
* Moot means: the clock was never started, or an app already connected. Callers use this to
|
|
90
|
+
* surface the stall to user-facing channels without coupling to the telemetry event itself.
|
|
91
|
+
*/
|
|
92
|
+
export function stallUptime(now) {
|
|
93
|
+
if (daemonStartedAt === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (appEverConnected())
|
|
96
|
+
return undefined;
|
|
97
|
+
return now - daemonStartedAt;
|
|
98
|
+
}
|
|
99
|
+
/** Tests only. */
|
|
100
|
+
export function resetInstrumentationStall() {
|
|
101
|
+
reported = false;
|
|
102
|
+
daemonStartedAt = undefined;
|
|
103
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise activation, as the three facts telemetry reports on every event.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT IS ON EVERY EVENT, not just an activation one: the questions a licensed customer generates
|
|
5
|
+
* are "how much is this org using it", "what is breaking for them", "did their key lapse" — and every
|
|
6
|
+
* one of those is answered by an event that has nothing to do with licensing. A status that rode only
|
|
7
|
+
* its own event would tell us a key verified once and nothing about the sessions it covered. Same
|
|
8
|
+
* reasoning that moved `installSource` onto every event.
|
|
9
|
+
*
|
|
10
|
+
* WHY STATUS IS SEPARATE FROM IDENTITY: `licenseId` is only present while a key verifies, so on
|
|
11
|
+
* identity alone a lapsed customer and a departed one are the same silence. `licenseStatus` keeps
|
|
12
|
+
* reporting through `expired` and `invalid`, which is the difference between seeing a renewal coming
|
|
13
|
+
* and hearing about it from the customer.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IS DELIBERATELY ABSENT: the organisation NAME, and the PLAN. The name is free text somebody
|
|
16
|
+
* typed at signing time, which rule 3 forbids outright. The plan is merely redundant, which is a
|
|
17
|
+
* quieter reason to leave something out and an easy one to lose sight of: it is keyed by this same
|
|
18
|
+
* `lid` in the issuance ledger, so sending it on every event from every machine forever pays a
|
|
19
|
+
* per-event cost to carry what one local file already holds, and the join that turns a `lid` into a
|
|
20
|
+
* company turns it into a plan at the same moment. `reticle license` still reports it locally, where
|
|
21
|
+
* it costs nothing and answers a question the operator is actually asking.
|
|
22
|
+
*
|
|
23
|
+
* More on the name: It is free text somebody typed at signing time,
|
|
24
|
+
* and rule 3 of the telemetry contract is names-never-values. `licenseId` is an opaque uuid that
|
|
25
|
+
* resolves to a company only against the issuance ledger held locally, so the analytics backend never
|
|
26
|
+
* holds a customer list.
|
|
27
|
+
*/
|
|
28
|
+
import { LicenseActivation } from '@reticlehq/core';
|
|
29
|
+
/** The activation facts that ride the wire. All absent on a build with no issuer key baked. */
|
|
30
|
+
export interface LicenseFacts {
|
|
31
|
+
licenseId?: string;
|
|
32
|
+
licenseStatus?: LicenseActivation;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve activation for one event. The clock is passed in (rule 7) and is the EVENT's clock, not a
|
|
36
|
+
* value captured at startup: sessions here run to eleven hours, so a key that expires mid-session must
|
|
37
|
+
* start reporting `expired` from the event it expired on rather than the whole run inheriting the
|
|
38
|
+
* status it had at boot.
|
|
39
|
+
*
|
|
40
|
+
* `eval` reports NOTHING. It means no issuer key is baked at all, which is every OSS install and every
|
|
41
|
+
* source checkout, so emitting it would add three properties to the dominant population to say "not a
|
|
42
|
+
* licensed build" — which absence already says, more cheaply. A mis-built production release resolves
|
|
43
|
+
* to `invalid` rather than `eval` and IS reported, because that one we need to hear about.
|
|
44
|
+
*
|
|
45
|
+
* Never throws. A telemetry property may not change behaviour (rule 5), and this one sits in front of
|
|
46
|
+
* key parsing, which is the part most likely to be handed something malformed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function licenseFacts(now: number, env?: NodeJS.ProcessEnv): LicenseFacts;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise activation, as the three facts telemetry reports on every event.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT IS ON EVERY EVENT, not just an activation one: the questions a licensed customer generates
|
|
5
|
+
* are "how much is this org using it", "what is breaking for them", "did their key lapse" — and every
|
|
6
|
+
* one of those is answered by an event that has nothing to do with licensing. A status that rode only
|
|
7
|
+
* its own event would tell us a key verified once and nothing about the sessions it covered. Same
|
|
8
|
+
* reasoning that moved `installSource` onto every event.
|
|
9
|
+
*
|
|
10
|
+
* WHY STATUS IS SEPARATE FROM IDENTITY: `licenseId` is only present while a key verifies, so on
|
|
11
|
+
* identity alone a lapsed customer and a departed one are the same silence. `licenseStatus` keeps
|
|
12
|
+
* reporting through `expired` and `invalid`, which is the difference between seeing a renewal coming
|
|
13
|
+
* and hearing about it from the customer.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IS DELIBERATELY ABSENT: the organisation NAME, and the PLAN. The name is free text somebody
|
|
16
|
+
* typed at signing time, which rule 3 forbids outright. The plan is merely redundant, which is a
|
|
17
|
+
* quieter reason to leave something out and an easy one to lose sight of: it is keyed by this same
|
|
18
|
+
* `lid` in the issuance ledger, so sending it on every event from every machine forever pays a
|
|
19
|
+
* per-event cost to carry what one local file already holds, and the join that turns a `lid` into a
|
|
20
|
+
* company turns it into a plan at the same moment. `reticle license` still reports it locally, where
|
|
21
|
+
* it costs nothing and answers a question the operator is actually asking.
|
|
22
|
+
*
|
|
23
|
+
* More on the name: It is free text somebody typed at signing time,
|
|
24
|
+
* and rule 3 of the telemetry contract is names-never-values. `licenseId` is an opaque uuid that
|
|
25
|
+
* resolves to a company only against the issuance ledger held locally, so the analytics backend never
|
|
26
|
+
* holds a customer list.
|
|
27
|
+
*/
|
|
28
|
+
import { LicenseActivation } from '@reticlehq/core';
|
|
29
|
+
import { describeLicense } from '../license/license.js';
|
|
30
|
+
/**
|
|
31
|
+
* Resolve activation for one event. The clock is passed in (rule 7) and is the EVENT's clock, not a
|
|
32
|
+
* value captured at startup: sessions here run to eleven hours, so a key that expires mid-session must
|
|
33
|
+
* start reporting `expired` from the event it expired on rather than the whole run inheriting the
|
|
34
|
+
* status it had at boot.
|
|
35
|
+
*
|
|
36
|
+
* `eval` reports NOTHING. It means no issuer key is baked at all, which is every OSS install and every
|
|
37
|
+
* source checkout, so emitting it would add three properties to the dominant population to say "not a
|
|
38
|
+
* licensed build" — which absence already says, more cheaply. A mis-built production release resolves
|
|
39
|
+
* to `invalid` rather than `eval` and IS reported, because that one we need to hear about.
|
|
40
|
+
*
|
|
41
|
+
* Never throws. A telemetry property may not change behaviour (rule 5), and this one sits in front of
|
|
42
|
+
* key parsing, which is the part most likely to be handed something malformed.
|
|
43
|
+
*/
|
|
44
|
+
export function licenseFacts(now, env = process.env) {
|
|
45
|
+
try {
|
|
46
|
+
const report = describeLicense(now, env);
|
|
47
|
+
if (LicenseActivation.EVAL === report.status)
|
|
48
|
+
return {};
|
|
49
|
+
return {
|
|
50
|
+
licenseStatus: report.status,
|
|
51
|
+
...(report.licenseId !== undefined ? { licenseId: report.licenseId } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -130,6 +130,14 @@ export declare class SessionMetrics {
|
|
|
130
130
|
* false so a session killed before it can exit is not lost entirely (see the flush note in cli.ts).
|
|
131
131
|
*/
|
|
132
132
|
summarize(final: boolean, exit?: SessionSummary['exit']): SessionSummary;
|
|
133
|
+
/**
|
|
134
|
+
* Has an agent ever attached to this daemon?
|
|
135
|
+
*
|
|
136
|
+
* Read by the instrumentation-stall check, which needs to tell two very different silences apart:
|
|
137
|
+
* a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
|
|
138
|
+
* The second is the funnel's failure case and the first is not a problem at all.
|
|
139
|
+
*/
|
|
140
|
+
get agentEverAttached(): boolean;
|
|
133
141
|
/** True when nothing at all happened — a flush of an idle daemon is not worth an event. */
|
|
134
142
|
get empty(): boolean;
|
|
135
143
|
/** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
|
|
@@ -572,6 +572,16 @@ export class SessionMetrics {
|
|
|
572
572
|
return 'verified';
|
|
573
573
|
return 'explored';
|
|
574
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Has an agent ever attached to this daemon?
|
|
577
|
+
*
|
|
578
|
+
* Read by the instrumentation-stall check, which needs to tell two very different silences apart:
|
|
579
|
+
* a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
|
|
580
|
+
* The second is the funnel's failure case and the first is not a problem at all.
|
|
581
|
+
*/
|
|
582
|
+
get agentEverAttached() {
|
|
583
|
+
return this.#clients.size > 0;
|
|
584
|
+
}
|
|
575
585
|
/** True when nothing at all happened — a flush of an idle daemon is not worth an event. */
|
|
576
586
|
get empty() {
|
|
577
587
|
return (0 === this.#toolCalls &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type InstallSource, type McpConnection, type McpOutage, type ToolRefusal, type AppInstrumentation, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
|
|
1
|
+
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type InstallSource, type McpConnection, type McpOutage, type ToolRefusal, type AppInstrumentation, type InstrumentationStall, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* Feedback gets its own, much larger budget — and a retry.
|
|
4
4
|
*
|
|
@@ -78,6 +78,8 @@ export interface TelemetryExtra {
|
|
|
78
78
|
outage?: McpOutage;
|
|
79
79
|
/** `app_instrumented`: an app carrying the SDK reached this daemon for the first time. */
|
|
80
80
|
instrumentation?: AppInstrumentation;
|
|
81
|
+
/** `instrumentation_stalled`: the daemon waited and no app ever arrived. */
|
|
82
|
+
stall?: InstrumentationStall;
|
|
81
83
|
/** `reticle_installed` / `init_completed`: which published route brought this install in. */
|
|
82
84
|
installSource?: InstallSource;
|
|
83
85
|
}
|
|
@@ -70,6 +70,8 @@ const DETACHED_SEND_SCRIPT = "fetch(process.argv[1],{method:'POST',headers:{'con
|
|
|
70
70
|
'.catch(()=>{}).finally(()=>process.exit(0))';
|
|
71
71
|
/** Env var names — mirror cloud-sync's `RETICLE_*` convention. */
|
|
72
72
|
import { isReticleSourceCheckout } from './dev-repo.js';
|
|
73
|
+
import { projectInstallSource } from './install-source.js';
|
|
74
|
+
import { licenseFacts } from './license-activation.js';
|
|
73
75
|
import { gitFacts } from './git-facts.js';
|
|
74
76
|
import { currentAutomationHint } from './automation-hint.js';
|
|
75
77
|
const Env = {
|
|
@@ -274,6 +276,12 @@ export const createTelemetry = (opts) => {
|
|
|
274
276
|
*/
|
|
275
277
|
const sessionId = randomUUID();
|
|
276
278
|
const { projectId, source: projectIdSource } = projectFingerprint(opts.cwd ?? process.cwd());
|
|
279
|
+
// Resolved ONCE, beside projectId, and for the same reason: it is a property of this install, not
|
|
280
|
+
// of any one event. It used to be passed per-call and therefore rode only `reticle_installed` and
|
|
281
|
+
// `init_completed` — two of the rarest events there are — so the channel that brought a user in
|
|
282
|
+
// was unknown for every event that could show whether they got anywhere. Reading it here puts it
|
|
283
|
+
// on all of them, which is the only shape in which the question can be asked.
|
|
284
|
+
const installSource = projectInstallSource(opts.cwd ?? process.cwd());
|
|
277
285
|
showNoticeOnce();
|
|
278
286
|
const spawnDetached = opts.spawnImpl ??
|
|
279
287
|
((command, args) => {
|
|
@@ -295,13 +303,16 @@ export const createTelemetry = (opts) => {
|
|
|
295
303
|
// feedback, and no metric is worth phoning home from a contributor's clone.
|
|
296
304
|
if (feedbackOnly && TelemetryEventKind.FEEDBACK_SUBMITTED !== kind)
|
|
297
305
|
return false;
|
|
306
|
+
// One clock read for the whole event: `ts` and the licence check must agree, and two calls to
|
|
307
|
+
// now() can straddle an expiry.
|
|
308
|
+
const eventTs = now();
|
|
298
309
|
const event = {
|
|
299
310
|
v: TELEMETRY_EVENT_VERSION,
|
|
300
311
|
anonymousId,
|
|
301
312
|
sessionId,
|
|
302
313
|
projectId,
|
|
303
314
|
event: kind,
|
|
304
|
-
ts:
|
|
315
|
+
ts: eventTs,
|
|
305
316
|
version: opts.version,
|
|
306
317
|
ci,
|
|
307
318
|
// A SCALAR, so it needs the event build and the wire schema and no `blocks` entry — the same
|
|
@@ -326,17 +337,25 @@ export const createTelemetry = (opts) => {
|
|
|
326
337
|
...(extra?.refusal !== undefined ? { refusal: extra.refusal } : {}),
|
|
327
338
|
...(extra?.outage !== undefined ? { outage: extra.outage } : {}),
|
|
328
339
|
...(extra?.instrumentation !== undefined ? { instrumentation: extra.instrumentation } : {}),
|
|
340
|
+
...(extra?.stall !== undefined ? { stall: extra.stall } : {}),
|
|
329
341
|
// A SCALAR, so it needs the event build and the wire schema but no entry in `blocks` below —
|
|
330
342
|
// there is nothing to flatten. It still needs both of those, which is the trap `outage` fell
|
|
331
343
|
// into: a field declared on TelemetryExtra and missing from either one is dropped in silence.
|
|
332
|
-
|
|
344
|
+
// Always present now, rather than only when a caller remembered to pass it. A per-call
|
|
345
|
+
// override still wins so a command that genuinely knows better can say so.
|
|
346
|
+
installSource: extra?.installSource ?? installSource,
|
|
347
|
+
// Enterprise activation: three SCALARS, so they need the event build and the wire schema and no
|
|
348
|
+
// `blocks` entry. Resolved per event off the event's own clock rather than once at startup — an
|
|
349
|
+
// eleven-hour session must not report the status it booted with. All three are absent unless a
|
|
350
|
+
// release has an issuer key baked, so an OSS payload is byte-identical to before.
|
|
351
|
+
...licenseFacts(eventTs),
|
|
333
352
|
};
|
|
334
353
|
// Map the core contract onto PostHog's capture shape: id/name/time move up, the rest are properties.
|
|
335
354
|
// The feedback body is FLATTENED into `feedback_*` properties rather than sent as a nested object:
|
|
336
355
|
// PostHog filters and breakdowns operate on top-level properties, so a nested `feedback.stack`
|
|
337
356
|
// would be invisible to the exact "which stacks report the most bugs" question this exists to
|
|
338
357
|
// answer. One prefix keeps them grouped in the UI without a nested path.
|
|
339
|
-
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, refusal, outage, instrumentation, ...rest } = event;
|
|
358
|
+
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, refusal, outage, instrumentation, stall, ...rest } = event;
|
|
340
359
|
// `$session_id` is PostHog's OWN session property, so sending ours under that name lights up
|
|
341
360
|
// its built-in session views and funnels for free rather than requiring custom HogQL everywhere.
|
|
342
361
|
// `sessionId` rides only on events that happened inside a daemon run. A one-shot CLI command
|
|
@@ -367,6 +386,7 @@ export const createTelemetry = (opts) => {
|
|
|
367
386
|
refusal,
|
|
368
387
|
outage,
|
|
369
388
|
instrumentation,
|
|
389
|
+
stall,
|
|
370
390
|
};
|
|
371
391
|
for (const [prefix, block] of Object.entries(blocks)) {
|
|
372
392
|
for (const [key, value] of Object.entries(block ?? {})) {
|