@reticlehq/server 2.10.0 → 2.11.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 +47 -9
- package/dist/dev/stale-issue-guard.d.ts +43 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- package/dist/events/predicate-asks.d.ts +17 -0
- package/dist/events/predicate-asks.js +26 -0
- package/dist/events/predicate-eval.js +211 -24
- package/dist/events/predicate-parse.js +9 -3
- package/dist/events/predicate-schema.d.ts +18 -0
- package/dist/events/predicate-schema.js +55 -0
- package/dist/flows/flow-tools.js +33 -4
- package/dist/flows/predicate-to-expect.js +8 -0
- package/dist/flows/verify-change-tools.js +5 -1
- package/dist/honesty/blind-spots.d.ts +9 -0
- package/dist/honesty/blind-spots.js +17 -4
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +55 -0
- package/dist/honesty/instrumentation-gaps.js +21 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +41 -0
- package/dist/init/node-io.js +3 -0
- package/dist/init/run.d.ts +18 -0
- package/dist/init/run.js +34 -3
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- package/dist/journal/envelope.d.ts +24 -24
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- package/dist/project/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +88 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -0
- package/dist/runs/run-tools.js +2 -1
- package/dist/session/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/no-session-diagnosis.d.ts +10 -0
- package/dist/session/no-session-diagnosis.js +19 -7
- package/dist/session/no-session-watch.js +1 -1
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.js +9 -0
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/daemon-telemetry.js +12 -23
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/tools/act-tools.js +34 -4
- package/dist/tools/assert-verdict.d.ts +2 -0
- package/dist/tools/assert-verdict.js +24 -1
- package/dist/tools/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.js +33 -0
- package/dist/tools/observe-tools.js +8 -2
- package/dist/tools/tool-kit.d.ts +16 -1
- package/docs/README.md +1 -0
- package/docs/docs.json +2 -2
- package/docs/enterprise.md +3 -1
- package/docs/license-activation.mdx +145 -0
- package/docs/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- package/package.json +2 -2
- package/dist/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Activating a license key'
|
|
3
|
+
description: 'Set your key, confirm it worked, and fix it when it did not. Everything an activation needs, with no account and no call.'
|
|
4
|
+
icon: key
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
If your organisation has a Reticle license key, this page is everything you need to put it in place. There is no account to create, no login, and no call with us required.
|
|
8
|
+
|
|
9
|
+
Do not have a key? Write to [hey@reticle.sh](mailto:hey@reticle.sh).
|
|
10
|
+
|
|
11
|
+
## One key per organisation
|
|
12
|
+
|
|
13
|
+
A key covers your whole organisation. Every developer and every machine uses the same one, so share it the way you would share any other build-time credential: your secrets manager, your onboarding doc, your CI configuration.
|
|
14
|
+
|
|
15
|
+
## Set it
|
|
16
|
+
|
|
17
|
+
Reticle reads one environment variable, `RETICLE_LICENSE_KEY`. Set it everywhere Reticle runs, which is usually more than one place.
|
|
18
|
+
|
|
19
|
+
<CodeGroup>
|
|
20
|
+
|
|
21
|
+
```bash macOS / Linux
|
|
22
|
+
# In ~/.zshrc, ~/.bashrc, or your team's equivalent.
|
|
23
|
+
export RETICLE_LICENSE_KEY="your-key-here"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```powershell Windows
|
|
27
|
+
[Environment]::SetEnvironmentVariable('RETICLE_LICENSE_KEY', 'your-key-here', 'User')
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```yaml GitHub Actions
|
|
31
|
+
env:
|
|
32
|
+
RETICLE_LICENSE_KEY: ${{ secrets.RETICLE_LICENSE_KEY }}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash Docker
|
|
36
|
+
docker run -e RETICLE_LICENSE_KEY="your-key-here" your-image
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
</CodeGroup>
|
|
40
|
+
|
|
41
|
+
An export only affects shells started after it, so open a new terminal or `source` your profile.
|
|
42
|
+
|
|
43
|
+
### Editors and agents that launch Reticle themselves
|
|
44
|
+
|
|
45
|
+
<Warning>
|
|
46
|
+
This is the step teams most often miss. Claude Desktop, Cursor and similar tools launch from the
|
|
47
|
+
desktop rather than from a terminal, so they never read your shell profile. An `export` that
|
|
48
|
+
plainly works in your terminal is invisible to them, and Reticle reports itself unlicensed.
|
|
49
|
+
</Warning>
|
|
50
|
+
|
|
51
|
+
Put the key in the MCP server's own `env` block instead, then restart the editor. Configuration is read at startup.
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"reticle": {
|
|
57
|
+
"command": "npx",
|
|
58
|
+
"args": ["@reticlehq/server", "mcp"],
|
|
59
|
+
"env": {
|
|
60
|
+
"RETICLE_LICENSE_KEY": "your-key-here"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Confirm it worked
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx @reticlehq/server license
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You do not need `NODE_ENV` set, a project, a dev server, or a running app. This reads the key and nothing else.
|
|
74
|
+
|
|
75
|
+
| `status` | What it means | What to do |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `active` | Valid and in date. | Nothing. You are done. |
|
|
78
|
+
| `missing` | Reticle found no key in its environment. | The variable is not visible to this process. See below. |
|
|
79
|
+
| `expired` | The key was valid and its term has ended. | Contact us to renew. |
|
|
80
|
+
| `invalid` | The key was not accepted. | Usually a copy or paste problem. See below. |
|
|
81
|
+
| `eval` | This build has no licensing configured. | You are on a build older than licensing. Update. |
|
|
82
|
+
|
|
83
|
+
## Over time
|
|
84
|
+
|
|
85
|
+
Your key carries an expiry, shown in the `license` output. A renewal is a new key for the same organisation: replace the value everywhere you set it, and nothing else changes.
|
|
86
|
+
|
|
87
|
+
Nothing breaks without warning. An expired key reports `expired` rather than failing silently, and you can check at any time with the command above.
|
|
88
|
+
|
|
89
|
+
Activation is **offline**. Reticle verifies your key cryptographically on your own machine, so there is no license server to reach. It works behind a firewall, on an air-gapped machine, and during any outage on our side.
|
|
90
|
+
|
|
91
|
+
## When it does not work
|
|
92
|
+
|
|
93
|
+
<AccordionGroup>
|
|
94
|
+
|
|
95
|
+
<Accordion title="It says missing, but the variable is definitely set">
|
|
96
|
+
The commonest cause by a distance is that Reticle is being launched by something that does not
|
|
97
|
+
inherit your shell environment: an editor, a desktop agent, a systemd unit, a container. Run
|
|
98
|
+
`echo $RETICLE_LICENSE_KEY` in the same context Reticle runs in, not just in your own terminal. If
|
|
99
|
+
an editor launches it, use the MCP `env` block above.
|
|
100
|
+
|
|
101
|
+
Second commonest: the variable was set in a shell that was already open. Environment variables are read when a process starts, so open a new terminal and restart anything long-running.
|
|
102
|
+
|
|
103
|
+
</Accordion>
|
|
104
|
+
|
|
105
|
+
<Accordion title="It says invalid">
|
|
106
|
+
The key changed between us sending it and Reticle reading it. Almost always one of:
|
|
107
|
+
|
|
108
|
+
- truncated, most often by a line wrap when pasted into a config file or a chat client
|
|
109
|
+
- a character dropped or added while copying by hand
|
|
110
|
+
- quoted in a way that swallowed a character
|
|
111
|
+
|
|
112
|
+
Surrounding whitespace and a trailing newline are tolerated, so neither is the cause. Copy the key again in one piece. If it still reports `invalid`, write to us and we will re-issue it.
|
|
113
|
+
|
|
114
|
+
</Accordion>
|
|
115
|
+
|
|
116
|
+
<Accordion title="command not found: reticle">
|
|
117
|
+
Use the `npx @reticlehq/server ...` form shown on this page. It runs without a global install, and
|
|
118
|
+
it is the form we support.
|
|
119
|
+
</Accordion>
|
|
120
|
+
|
|
121
|
+
</AccordionGroup>
|
|
122
|
+
|
|
123
|
+
## What a licensed deployment reports
|
|
124
|
+
|
|
125
|
+
Stated plainly, because a security review will ask.
|
|
126
|
+
|
|
127
|
+
Activation itself makes **no network request**. Your key is verified on your machine with public key cryptography.
|
|
128
|
+
|
|
129
|
+
Separately, Reticle sends anonymous usage metrics, as it does for every user. On a licensed deployment these carry your **license id**, an opaque identifier that means nothing without our own records, so we can tell your organisation's usage apart from everyone else's and know when to talk to you about renewal.
|
|
130
|
+
|
|
131
|
+
Never sent, licensed or not: your source code, your application's data, your users' data, URLs, selectors, page content, file contents, or your organisation's name.
|
|
132
|
+
|
|
133
|
+
The kill switch has no exception for licensed installations:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
export RETICLE_TELEMETRY=0
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`DO_NOT_TRACK=1` is honoured identically. If your policy forbids outbound telemetry, set it. Your activation is unaffected, because activation never needed the network. To keep your own usage data without sending it, `RETICLE_TELEMETRY_FILE=/path/to/file.jsonl` records the same stream locally and sends nothing.
|
|
140
|
+
|
|
141
|
+
## Getting help
|
|
142
|
+
|
|
143
|
+
[hey@reticle.sh](mailto:hey@reticle.sh) for activation problems, renewals and re-issues.
|
|
144
|
+
|
|
145
|
+
Include the output of `npx @reticlehq/server license`. It carries your license id and no secrets, and it usually tells us the answer immediately.
|
|
@@ -103,7 +103,6 @@ The single exception is `daemon_stopped`, which is **awaited**, because the proc
|
|
|
103
103
|
| `identified` | `reticle identify` | joins anonymous machine ids to a person who volunteered one |
|
|
104
104
|
| `mcp_client_connected` | an MCP client attached | how many sessions are agent-driven at all |
|
|
105
105
|
| `app_instrumented` | the first app carrying the SDK reached this daemon | **the funnel step everything turns on**; see below |
|
|
106
|
-
| `instrumentation_stalled` | the daemon waited and no app ever arrived | the same funnel step, seen from the failure side; see below |
|
|
107
106
|
| `mcp_connection_lost` | the proxy lost its daemon | **the transport-stability metric.** The disconnect that makes a user reopen `/mcp` is invisible without it |
|
|
108
107
|
| `init_completed` | `reticle init` finished | does install actually work, outside the fixtures gate |
|
|
109
108
|
| `bug_found` | a defect was detected in the app under test | the value delivered, as opposed to the work done |
|
|
@@ -117,17 +116,7 @@ Nothing measured the second. `daemon_started` and `mcp_client_connected` describ
|
|
|
117
116
|
|
|
118
117
|
`app_instrumented` fires **once per daemon run**, on the first session-ready only, so `daemon_started` → `app_instrumented` is a rate rather than an inference and a reloading page cannot inflate it. It carries `initialized` (had `init` run here), `agentAttached` (was an agent already waiting), and `msToFirstApp` (how long the daemon sat with nothing wired). It deliberately carries no stack and no framework: `project_profiled` already reports both for the same run, and the two join on `sessionId`.
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`app_instrumented` fires on success and only on success, so the larger population, runs where no app ever arrives, exists in the data as an **absence**. An absence is the one thing telemetry cannot interpret: it cannot separate "nothing was ever wired" from "the process was killed before it could say so" from "we never sent the event". Since the funnel breaks at this step and nowhere else, this is the one silence worth converting into a signal.
|
|
123
|
-
|
|
124
|
-
`instrumentation_stalled` fires **once per daemon run**, only after the daemon has been up past a generous threshold, and **never once an app has connected**. A run that starts slowly and then works reports nothing at all. It carries the same three facts as its success counterpart so the two are directly comparable, with `msWaited` in place of `msToFirstApp`.
|
|
125
|
-
|
|
126
|
-
Read it against `agentAttached`, which is what makes it actionable rather than merely sad. A stalled daemon with no agent attached is somebody who installed and walked away, and is not a defect. A stalled daemon **with** an agent attached is the failure the product actually has: the tools are loaded, something is asking for them, and there is nothing on the other end to drive.
|
|
127
|
-
|
|
128
|
-
It deliberately carries no diagnosis. Whether a dev server is listening is a live probe, and running one on a timer to enrich a metric would be collecting for the metric's sake. The diagnosis belongs where a human or an agent asks for it.
|
|
129
|
-
|
|
130
|
-
What neither event can see is **why** an app never connected. Every cause for that is page-side (the non-localhost gate, a port mismatch, a stale build, a dev server never restarted), where the daemon has no visibility. That needs the SDK to report its own refusals, and it is the next thing to build.
|
|
119
|
+
The failure side is a **set difference**, not an event: `daemon_started` minus `app_instrumented`, joined on `sessionId`. There used to be an `instrumentation_stalled` event for it, and it was removed because the derivation is strictly more complete. The event could only fire on a flush tick or a graceful shutdown, so a killed daemon never produced one and landed in the absence anyway, while the set difference catches it. Two ways to compute one number, disagreeing with each other, is worse than one way.
|
|
131
120
|
|
|
132
121
|
## Sessions: `daemon_stopped` vs `session_progress`
|
|
133
122
|
|
|
@@ -55,7 +55,7 @@ Plus `installSource`, which is not a block. It is resolved once per process and
|
|
|
55
55
|
|
|
56
56
|
`isSessionScoped(kind)` decides whether `sessionId` is attached. Twelve kinds are session-scoped:
|
|
57
57
|
|
|
58
|
-
`daemon_started`, `daemon_stopped`, `session_progress`, `mcp_client_connected`, `app_instrumented`, `
|
|
58
|
+
`daemon_started`, `daemon_stopped`, `session_progress`, `mcp_client_connected`, `app_instrumented`, `project_profiled`, `verification_completed`, `bug_found`, `tool_refused`, `runtime_crashed`, `feedback_submitted`.
|
|
59
59
|
|
|
60
60
|
`mcp_connection_lost` is deliberately **not** on that list even though it feels session-shaped. It fires from the proxy process rather than the daemon, so it carries no `sessionId` and cannot be joined to a session at all.
|
|
61
61
|
|
|
@@ -341,39 +341,6 @@ Fired **once per daemon run**, on the first connect only, so `daemon_started` to
|
|
|
341
341
|
|
|
342
342
|
---
|
|
343
343
|
|
|
344
|
-
## instrumentation_stalled
|
|
345
|
-
|
|
346
|
-
**Wire name:** `instrumentation_stalled`. **Emitted from:** `packages/server/src/telemetry/instrumentation-stall.ts:73`, called from `daemon-telemetry.ts:105`. **Session-scoped:** yes. **Payload block:** `stall`.
|
|
347
|
-
|
|
348
|
-
The failure side of `app_instrumented`, and the reason the funnel has a denominator at all. A daemon that runs for ten minutes without a single SDK-carrying page is an install that half worked, and until this event existed that outcome was pure silence: indistinguishable from a machine nobody touched.
|
|
349
|
-
|
|
350
|
-
| Field | Type | Meaning |
|
|
351
|
-
| --- | --- | --- |
|
|
352
|
-
| `initialized` | boolean | Has `init` run in this working directory (`readProjectId(cwd)` resolves). False means the daemon is running somewhere `init` never wrote a project id, which is usually a wrong-directory problem rather than a wiring one |
|
|
353
|
-
| `agentAttached` | boolean | Was an agent connected at the moment the check ran. **This is the field that decides whether the row is a defect.** False means somebody installed and walked away, which is not a bug. True means an agent was sitting there with tools it could not use, which is the failure the product actually has |
|
|
354
|
-
| `msWaited` | int | Milliseconds since the daemon started, from the clock marked at `daemon-telemetry.ts:84` |
|
|
355
|
-
|
|
356
|
-
Deliberately the same three facts as `app_instrumented`, with `msWaited` standing in for `msToFirstApp`, so the success and failure rows are directly comparable without reshaping either.
|
|
357
|
-
|
|
358
|
-
**When it fires:** on every flush tick, but only once all four guards at `instrumentation-stall.ts:64` pass: it has not already been reported this run, no app has ever connected, the clock was marked, and at least `STALL_AFTER_MS` (10 minutes) has elapsed. It is checked **before** the empty-window early return in `flushWindow`, which is not an accident: a stalled install produces an empty window by definition, so checking after the return would blind the event to the exact condition it exists to report.
|
|
359
|
-
|
|
360
|
-
**The question it answers:** of the installs that never reach a verdict, how many had an agent present and waiting? That is the difference between a distribution problem and a wiring problem, and they need opposite responses.
|
|
361
|
-
|
|
362
|
-
**What it is blind to:**
|
|
363
|
-
|
|
364
|
-
- **Any diagnosis.** It never says whether a dev server is listening, because that is a live probe, and probing a user's machine to enrich a metric is collecting for the metric's sake.
|
|
365
|
-
- **Slow starts.** A run that connects at minute eleven reports nothing at all, and neither does the recovery. The event answers "did this run stall", not "how long do installs take".
|
|
366
|
-
- **The page side.** Every real cause (a non-localhost gate, a port mismatch, a stale build, a dev server never restarted) lives where the daemon cannot see. Attributing a stall needs the SDK to report its own refusals, and that is not built.
|
|
367
|
-
- **Repeats.** Once per daemon run, so a user stuck for a week contributes one row per daemon start, not one per hour.
|
|
368
|
-
|
|
369
|
-
<Warning>
|
|
370
|
-
This kind has no live check in `apps/e2e/specs/telemetry-events-test.mjs`, and neither do
|
|
371
|
-
`session_progress` and `app_instrumented`. Those three are the funnel, and the funnel is the part
|
|
372
|
-
of the wire nothing fires end to end. Adding one is the highest-value thing left in this file.
|
|
373
|
-
</Warning>
|
|
374
|
-
|
|
375
|
-
---
|
|
376
|
-
|
|
377
344
|
## mcp_connection_lost
|
|
378
345
|
|
|
379
346
|
**Wire name:** `mcp_connection_lost`. **Emitted from:** `packages/server/src/mcp/mcp-outage.ts:53`. **Session-scoped:** no. **Payload block:** `outage`.
|
package/docs/telemetry.md
CHANGED
|
@@ -31,7 +31,6 @@ Eighteen kinds of events, each a single small JSON object. This is the whole lis
|
|
|
31
31
|
| `daemon_stopped` | The local daemon stops | A summary of the session; see below |
|
|
32
32
|
| `session_progress` | Periodically, from a daemon that is still running | The same summary as `daemon_stopped`, marked `final: false`. It exists so a long session is not invisible until it exits |
|
|
33
33
|
| `app_instrumented` | The first time your app's SDK connects in a given daemon run | Nothing beyond the common fields. It is the second half of the install funnel: registering the MCP server is one thing, getting the SDK into a running page is another |
|
|
34
|
-
| `instrumentation_stalled` | Ten minutes into a daemon run in which your app has still never connected | The same three facts as `app_instrumented`, with how long it waited instead of how long it took. It fires at most once per daemon run, and a run that starts slowly and then works reports nothing at all. It carries no diagnosis: whether a dev server is listening would need a live probe, and probing your machine to enrich a metric is collecting for the metric's sake |
|
|
35
34
|
| `verification_completed` | A verification produces a verdict | Whether it passed, whether Reticle refused to call a passing check verified, and **why** the verdict came out that way; see below |
|
|
36
35
|
| `project_profiled` | Once per daemon start | The shape of the project; see below |
|
|
37
36
|
| `version_changed` | You update or roll back | The two version numbers, and which direction |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Reticle bridge + MCP server. Hosts the browser WS endpoint and exposes MCP tools to the coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
23
23
|
"ws": "^8.21.3",
|
|
24
24
|
"zod": "^3.24.1",
|
|
25
|
-
"@reticlehq/core": "2.
|
|
25
|
+
"@reticlehq/core": "2.11.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/pngjs": "^6.0.5",
|
|
@@ -1,57 +0,0 @@
|
|
|
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;
|
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
}
|