@manyoars/oars 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +10 -4
- package/dist/cli.js +440 -91
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +3 -3
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +47 -0
- package/dist/gate/hook-process.d.ts +24 -0
- package/dist/gate/hook-process.js +112 -0
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +48 -0
- package/dist/gate/hook.js +259 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +254 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +50 -0
- package/dist/issuer.d.ts +10 -6
- package/dist/issuer.js +19 -24
- package/dist/main.d.ts +1 -0
- package/dist/main.js +67 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +429 -0
- package/dist/sessions/allow.d.ts +23 -0
- package/dist/sessions/allow.js +71 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +190 -0
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +47 -0
- package/dist/sessions/cell-increments.js +243 -0
- package/dist/sessions/cells.d.ts +109 -0
- package/dist/sessions/cells.js +261 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +745 -0
- package/dist/sessions/command.d.ts +127 -0
- package/dist/sessions/command.js +419 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +386 -0
- package/dist/sessions/exclusion.d.ts +87 -0
- package/dist/sessions/exclusion.js +361 -0
- package/dist/sessions/extract.d.ts +181 -0
- package/dist/sessions/extract.js +1806 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +66 -0
- package/dist/sessions/hook-index.js +164 -0
- package/dist/sessions/hooks.d.ts +17 -0
- package/dist/sessions/hooks.js +281 -0
- package/dist/sessions/identity.d.ts +27 -0
- package/dist/sessions/identity.js +131 -0
- package/dist/sessions/install.d.ts +70 -0
- package/dist/sessions/install.js +246 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +7 -0
- package/dist/sessions/mcp.js +157 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +44 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/opencode.d.ts +33 -0
- package/dist/sessions/opencode.js +319 -0
- package/dist/sessions/otel.d.ts +60 -0
- package/dist/sessions/otel.js +494 -0
- package/dist/sessions/package-books.d.ts +48 -0
- package/dist/sessions/package-books.js +120 -0
- package/dist/sessions/package-resolve.d.ts +32 -0
- package/dist/sessions/package-resolve.js +185 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pi.d.ts +36 -0
- package/dist/sessions/pi.js +353 -0
- package/dist/sessions/pool-books.d.ts +21 -0
- package/dist/sessions/pool-books.js +75 -0
- package/dist/sessions/pool-choices.d.ts +5 -0
- package/dist/sessions/pool-choices.js +11 -0
- package/dist/sessions/pool-command.d.ts +115 -0
- package/dist/sessions/pool-command.js +669 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-nightly-deps.d.ts +3 -0
- package/dist/sessions/pool-nightly-deps.js +11 -0
- package/dist/sessions/pool-nightly.d.ts +10 -0
- package/dist/sessions/pool-nightly.js +85 -0
- package/dist/sessions/pool-outbox.d.ts +76 -0
- package/dist/sessions/pool-outbox.js +313 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-raw.d.ts +27 -0
- package/dist/sessions/pool-raw.js +68 -0
- package/dist/sessions/pool-replica.d.ts +35 -0
- package/dist/sessions/pool-replica.js +91 -0
- package/dist/sessions/pool-send-lock.d.ts +20 -0
- package/dist/sessions/pool-send-lock.js +87 -0
- package/dist/sessions/pool-sent.d.ts +4 -0
- package/dist/sessions/pool-sent.js +26 -0
- package/dist/sessions/pool-totals.d.ts +10 -0
- package/dist/sessions/pool-totals.js +21 -0
- package/dist/sessions/pool-wire.d.ts +435 -0
- package/dist/sessions/pool-wire.js +1100 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +72 -0
- package/dist/sessions/public-vocabulary.js +295 -0
- package/dist/sessions/query.d.ts +123 -0
- package/dist/sessions/query.js +268 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +48 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +86 -0
- package/dist/sessions/read.js +87 -0
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-frames.d.ts +18 -0
- package/dist/sessions/recall-frames.js +44 -0
- package/dist/sessions/recall-next.d.ts +145 -0
- package/dist/sessions/recall-next.js +241 -0
- package/dist/sessions/recall-object.d.ts +139 -0
- package/dist/sessions/recall-object.js +78 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +47 -0
- package/dist/sessions/recall-reads.d.ts +124 -0
- package/dist/sessions/recall-reads.js +417 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +17 -0
- package/dist/sessions/redact.js +113 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +694 -0
- package/dist/sessions/schema.js +167 -0
- package/dist/sessions/session-shape.d.ts +76 -0
- package/dist/sessions/session-shape.js +355 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +233 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +309 -0
- package/dist/sessions/store.js +1498 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +27 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +494 -0
- package/dist/telemetry/command.d.ts +8 -0
- package/dist/telemetry/command.js +85 -0
- package/dist/telemetry/consent.d.ts +17 -0
- package/dist/telemetry/consent.js +63 -0
- package/dist/telemetry/crash.d.ts +8 -0
- package/dist/telemetry/crash.js +52 -0
- package/dist/telemetry/emit.d.ts +31 -0
- package/dist/telemetry/emit.js +42 -0
- package/dist/telemetry/fields.d.ts +16 -0
- package/dist/telemetry/fields.js +26 -0
- package/dist/telemetry/health.d.ts +21 -0
- package/dist/telemetry/health.js +108 -0
- package/dist/telemetry/hook-emit.d.ts +4 -0
- package/dist/telemetry/hook-emit.js +18 -0
- package/dist/telemetry/measures.d.ts +32 -0
- package/dist/telemetry/measures.js +128 -0
- package/dist/telemetry/observe.d.ts +13 -0
- package/dist/telemetry/observe.js +51 -0
- package/dist/telemetry/read-events.d.ts +9 -0
- package/dist/telemetry/read-events.js +17 -0
- package/dist/telemetry/record.d.ts +17 -0
- package/dist/telemetry/record.js +26 -0
- package/dist/telemetry/report.d.ts +77 -0
- package/dist/telemetry/report.js +188 -0
- package/dist/telemetry/schema.d.ts +40 -0
- package/dist/telemetry/schema.js +187 -0
- package/dist/telemetry/send-main.d.ts +1 -0
- package/dist/telemetry/send-main.js +11 -0
- package/dist/telemetry/send.d.ts +36 -0
- package/dist/telemetry/send.js +172 -0
- package/dist/telemetry/spool.d.ts +17 -0
- package/dist/telemetry/spool.js +70 -0
- package/dist/telemetry/wire.d.ts +25 -0
- package/dist/telemetry/wire.js +76 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -38
- package/dist/commands/deploy-input.js +0 -150
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -124
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/templates.d.ts +0 -11
- package/dist/commands/templates.js +0 -32
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The spool's events read back as typed values, by type: for the nightly figures that sum earlier events and for the tests that check what was written.
|
|
2
|
+
// A line that is not an event is left out.
|
|
3
|
+
import { readSpool } from './spool.js';
|
|
4
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
const eventOf = (line) => {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(line);
|
|
8
|
+
if (isRecord(parsed) && typeof parsed.type === 'string' && typeof parsed.at === 'string' && isRecord(parsed.data))
|
|
9
|
+
return { type: parsed.type, at: parsed.at, data: parsed.data };
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Not JSON: not an event.
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
};
|
|
16
|
+
/** Every spooled event, oldest first, or only those of `type`. */
|
|
17
|
+
export const spoolEvents = (dir, type) => readSpool(dir).flatMap((line) => eventOf(line) ?? []).filter((event) => type === undefined || event.type === type);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Where telemetry keeps its folder; absent from a run, nothing is recorded. */
|
|
2
|
+
export interface TelemetryRuntime {
|
|
3
|
+
/** `<config>/oars/telemetry`. */
|
|
4
|
+
dir: string;
|
|
5
|
+
platform: NodeJS.Platform;
|
|
6
|
+
/** Starts the detached send (`spawnDetachedSend`); only `main.ts` supplies it, so a run without one never sends. */
|
|
7
|
+
spawnSend?: (dir: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface RecordContext extends TelemetryRuntime {
|
|
10
|
+
/** The CLI's version. */
|
|
11
|
+
version: string;
|
|
12
|
+
/** Epoch milliseconds. */
|
|
13
|
+
now: () => number;
|
|
14
|
+
/** The spool's byte cap per file; absent means `SPOOL_CAP_BYTES`. */
|
|
15
|
+
capBytes?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare const recordEvent: (context: RecordContext, type: string, fields: object) => void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Records one event: closed schema, then the spool. It never throws and returns nothing, so a command's exit code and output cannot depend on it.
|
|
2
|
+
import { SCHEMA_VERSION, sanitize } from './schema.js';
|
|
3
|
+
import { appendLine, installId } from './spool.js';
|
|
4
|
+
const OS = ['win32', 'darwin', 'linux'];
|
|
5
|
+
const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]{1,32})?$/;
|
|
6
|
+
export const recordEvent = (context, type, fields) => {
|
|
7
|
+
try {
|
|
8
|
+
const checked = sanitize(type, fields);
|
|
9
|
+
if (checked === undefined)
|
|
10
|
+
return;
|
|
11
|
+
const event = {
|
|
12
|
+
v: SCHEMA_VERSION,
|
|
13
|
+
id: installId(context.dir),
|
|
14
|
+
at: new Date(context.now()).toISOString(),
|
|
15
|
+
type,
|
|
16
|
+
cli: VERSION.test(context.version) ? context.version : 'unknown',
|
|
17
|
+
os: OS.includes(context.platform) ? context.platform : 'other',
|
|
18
|
+
...(checked.dropped > 0 ? { dropped: checked.dropped } : {}),
|
|
19
|
+
data: checked.data,
|
|
20
|
+
};
|
|
21
|
+
appendLine(context.dir, JSON.stringify(event), context.capBytes);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Telemetry is never allowed to change what a command does.
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type Kind, type Measure } from './measures.js';
|
|
2
|
+
/** The fleet's number for one cell: percentiles of the fleet's latency, or the median install's rate. */
|
|
3
|
+
export interface FleetNumbers {
|
|
4
|
+
p50: number;
|
|
5
|
+
p95?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface FleetCell extends FleetNumbers {
|
|
8
|
+
metric: string;
|
|
9
|
+
dimension: Measure['dimension'];
|
|
10
|
+
value: string;
|
|
11
|
+
kind: Kind;
|
|
12
|
+
}
|
|
13
|
+
export interface OwnCell {
|
|
14
|
+
metric: string;
|
|
15
|
+
dimension: Measure['dimension'];
|
|
16
|
+
value: string;
|
|
17
|
+
kind: Kind;
|
|
18
|
+
/** Observations in the window. */
|
|
19
|
+
n: number;
|
|
20
|
+
p50?: number;
|
|
21
|
+
p95?: number;
|
|
22
|
+
/** For a rate: the share of the user's observations that were a yes. */
|
|
23
|
+
rate?: number;
|
|
24
|
+
fleet?: FleetNumbers;
|
|
25
|
+
}
|
|
26
|
+
export interface StoreSnapshot {
|
|
27
|
+
at: string;
|
|
28
|
+
readFailed: boolean;
|
|
29
|
+
sessions?: number;
|
|
30
|
+
subagents?: number;
|
|
31
|
+
skeletons?: number;
|
|
32
|
+
unparseable?: number;
|
|
33
|
+
verifyChecked?: number;
|
|
34
|
+
verifyFailed?: number;
|
|
35
|
+
rawBytes?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The user's own cells over the last `days`, ordered by metric and then value; with `asOf`, over the `days` ending with that day instead. A latency is counted in the same buckets the fleet's is,
|
|
39
|
+
* so the user's p50 and the fleet's are the same kind of number (the bucket's upper edge) and a spool identical to a fleet cell reads equal to it.
|
|
40
|
+
*/
|
|
41
|
+
export declare const ownCells: (lines: readonly string[], now: number, days: number, asOf?: string) => OwnCell[];
|
|
42
|
+
/** The newest store-health snapshot in the window: its counts as the nightly recorded them. */
|
|
43
|
+
export declare const latestStore: (lines: readonly string[], now: number, days: number) => StoreSnapshot | undefined;
|
|
44
|
+
/** The user's cells with the fleet's number put on each cell the fleet serves; the fleet is `undefined` when no read answered. */
|
|
45
|
+
export declare const besideFleet: (own: readonly OwnCell[], fleet: readonly FleetCell[] | undefined) => OwnCell[];
|
|
46
|
+
export interface FleetWindow {
|
|
47
|
+
days: number;
|
|
48
|
+
cells: FleetCell[];
|
|
49
|
+
}
|
|
50
|
+
export type FleetRead = {
|
|
51
|
+
status: 'not-configured';
|
|
52
|
+
} | {
|
|
53
|
+
status: 'unavailable';
|
|
54
|
+
server?: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
} | {
|
|
57
|
+
status: 'answered';
|
|
58
|
+
server: string;
|
|
59
|
+
asOf: string;
|
|
60
|
+
floor: number;
|
|
61
|
+
windows: FleetWindow[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The fleet's numbers from the pool server in `OARS_POOL_URL`, or why there are none. The request is a bare GET: it names no install and sends no credential, cookie, or event.
|
|
65
|
+
* Anything wrong, a refusal, a timeout, or an answer of the wrong shape, comes back as `unavailable`; it never fails the report.
|
|
66
|
+
*/
|
|
67
|
+
export declare const readFleet: (env: NodeJS.ProcessEnv) => Promise<FleetRead>;
|
|
68
|
+
export interface Report {
|
|
69
|
+
windows: {
|
|
70
|
+
days: number;
|
|
71
|
+
cells: OwnCell[];
|
|
72
|
+
}[];
|
|
73
|
+
store: StoreSnapshot | undefined;
|
|
74
|
+
fleet: FleetRead;
|
|
75
|
+
}
|
|
76
|
+
export declare const reportOf: (lines: readonly string[], now: number, fleet: FleetRead) => Report;
|
|
77
|
+
export declare const textOf: (report: Report, dir: string) => string;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// `oars telemetry report`: what telemetry measured about the user's own use, from the spool on this machine, beside the fleet's number for the same cell when a pool server is configured and answers.
|
|
2
|
+
// The own numbers need no server and no account. The fleet read is a plain GET that carries nothing of the user's: no credential, no install id, no event. Nothing here writes the spool.
|
|
3
|
+
import { CliError } from '../errors.js';
|
|
4
|
+
import { bucketOf, cellKey, histogramPercentile, measuresOf, WINDOWS } from './measures.js';
|
|
5
|
+
const DAY_MS = 86_400_000;
|
|
6
|
+
const FLEET_TIMEOUT_MS = 10_000;
|
|
7
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
const fieldOf = (value, key) => (isObject(value) ? Reflect.get(value, key) : undefined);
|
|
9
|
+
/** The spool's events that parse and carry a time. A line that is not an event is skipped; the spool is the user's file and may hold anything. */
|
|
10
|
+
const eventsOf = (lines) => lines.flatMap((line) => {
|
|
11
|
+
let event;
|
|
12
|
+
try {
|
|
13
|
+
event = JSON.parse(line);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const raw = fieldOf(event, 'at');
|
|
19
|
+
const at = typeof raw === 'string' ? Date.parse(raw) : Number.NaN;
|
|
20
|
+
return Number.isNaN(at) ? [] : [{ at, event }];
|
|
21
|
+
});
|
|
22
|
+
const within = (parsed, now, days) => parsed.filter(({ at }) => at >= now - days * DAY_MS && at <= now + DAY_MS);
|
|
23
|
+
/** The fleet's window ends with the day it is as of, a whole UTC day: the user's own window must end there too, or a cell reported only today reads as one the fleet does not serve. */
|
|
24
|
+
const upToDay = (parsed, days, asOf) => {
|
|
25
|
+
const end = Date.parse(`${asOf}T00:00:00Z`) + DAY_MS;
|
|
26
|
+
return Number.isNaN(end) ? [] : parsed.filter(({ at }) => at >= end - days * DAY_MS && at < end);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The user's own cells over the last `days`, ordered by metric and then value; with `asOf`, over the `days` ending with that day instead. A latency is counted in the same buckets the fleet's is,
|
|
30
|
+
* so the user's p50 and the fleet's are the same kind of number (the bucket's upper edge) and a spool identical to a fleet cell reads equal to it.
|
|
31
|
+
*/
|
|
32
|
+
export const ownCells = (lines, now, days, asOf) => {
|
|
33
|
+
const groups = new Map();
|
|
34
|
+
const parsed = eventsOf(lines);
|
|
35
|
+
for (const { event } of asOf === undefined ? within(parsed, now, days) : upToDay(parsed, days, asOf)) {
|
|
36
|
+
for (const measure of measuresOf(event)) {
|
|
37
|
+
const key = cellKey(measure);
|
|
38
|
+
const group = groups.get(key) ?? { head: measure, xs: [] };
|
|
39
|
+
group.xs.push(measure.x);
|
|
40
|
+
groups.set(key, group);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...groups.values()]
|
|
44
|
+
.map(({ head, xs }) => {
|
|
45
|
+
const { metric, dimension, value, kind } = head;
|
|
46
|
+
if (kind === 'rate')
|
|
47
|
+
return { metric, dimension, value, kind, n: xs.length, rate: xs.reduce((sum, x) => sum + x, 0) / xs.length };
|
|
48
|
+
const buckets = new Map();
|
|
49
|
+
for (const x of xs)
|
|
50
|
+
buckets.set(bucketOf(x), (buckets.get(bucketOf(x)) ?? 0) + 1);
|
|
51
|
+
return { metric, dimension, value, kind, n: xs.length, p50: histogramPercentile([...buckets], 0.5), p95: histogramPercentile([...buckets], 0.95) };
|
|
52
|
+
})
|
|
53
|
+
.sort((a, b) => a.metric.localeCompare(b.metric) || a.value.localeCompare(b.value));
|
|
54
|
+
};
|
|
55
|
+
const COUNTS = ['sessions', 'subagents', 'skeletons', 'unparseable', 'verifyChecked', 'verifyFailed', 'rawBytes'];
|
|
56
|
+
/** The newest store-health snapshot in the window: its counts as the nightly recorded them. */
|
|
57
|
+
export const latestStore = (lines, now, days) => {
|
|
58
|
+
const newest = within(eventsOf(lines), now, days)
|
|
59
|
+
.filter(({ event }) => fieldOf(event, 'type') === 'store-health')
|
|
60
|
+
.sort((a, b) => b.at - a.at)[0];
|
|
61
|
+
if (newest === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
const data = fieldOf(newest.event, 'data');
|
|
64
|
+
const field = (name) => fieldOf(data, name);
|
|
65
|
+
const snapshot = { at: new Date(newest.at).toISOString(), readFailed: field('readFailed') === 1 };
|
|
66
|
+
for (const name of COUNTS) {
|
|
67
|
+
const found = field(name);
|
|
68
|
+
if (typeof found === 'number' && Number.isSafeInteger(found) && found >= 0)
|
|
69
|
+
snapshot[name] = found;
|
|
70
|
+
}
|
|
71
|
+
return snapshot;
|
|
72
|
+
};
|
|
73
|
+
/** The user's cells with the fleet's number put on each cell the fleet serves; the fleet is `undefined` when no read answered. */
|
|
74
|
+
export const besideFleet = (own, fleet) => own.map((cell) => {
|
|
75
|
+
const match = fleet?.find((one) => one.metric === cell.metric && one.dimension === cell.dimension && one.value === cell.value);
|
|
76
|
+
if (match === undefined)
|
|
77
|
+
return cell;
|
|
78
|
+
return { ...cell, fleet: match.p95 === undefined ? { p50: match.p50 } : { p50: match.p50, p95: match.p95 } };
|
|
79
|
+
});
|
|
80
|
+
const fleetCellOf = (raw) => {
|
|
81
|
+
const metric = fieldOf(raw, 'metric');
|
|
82
|
+
const dimension = fieldOf(raw, 'dimension');
|
|
83
|
+
const value = fieldOf(raw, 'value');
|
|
84
|
+
const kind = fieldOf(raw, 'kind');
|
|
85
|
+
const p50 = fieldOf(raw, 'p50');
|
|
86
|
+
const p95 = fieldOf(raw, 'p95');
|
|
87
|
+
if (typeof metric !== 'string' || typeof value !== 'string' || typeof p50 !== 'number')
|
|
88
|
+
return undefined;
|
|
89
|
+
if (dimension !== 'harnessVersion' && dimension !== 'cli')
|
|
90
|
+
return undefined;
|
|
91
|
+
if (kind !== 'latency' && kind !== 'rate')
|
|
92
|
+
return undefined;
|
|
93
|
+
return { metric, dimension, value, kind, p50, ...(typeof p95 === 'number' ? { p95 } : {}) };
|
|
94
|
+
};
|
|
95
|
+
/** The answer as a fleet read, or undefined when it is not one; a cell that is not a cell is left out. */
|
|
96
|
+
const fleetOf = (server, body) => {
|
|
97
|
+
const asOf = fieldOf(body, 'asOf');
|
|
98
|
+
const floor = fieldOf(body, 'floor');
|
|
99
|
+
const windows = fieldOf(body, 'windows');
|
|
100
|
+
if (typeof asOf !== 'string' || typeof floor !== 'number' || !Array.isArray(windows))
|
|
101
|
+
return undefined;
|
|
102
|
+
const read = [];
|
|
103
|
+
for (const window of windows) {
|
|
104
|
+
const days = fieldOf(window, 'days');
|
|
105
|
+
const cells = fieldOf(window, 'cells');
|
|
106
|
+
if (typeof days !== 'number' || !Array.isArray(cells))
|
|
107
|
+
return undefined;
|
|
108
|
+
read.push({ days, cells: cells.flatMap((cell) => fleetCellOf(cell) ?? []) });
|
|
109
|
+
}
|
|
110
|
+
return { status: 'answered', server, asOf, floor, windows: read };
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* The fleet's numbers from the pool server in `OARS_POOL_URL`, or why there are none. The request is a bare GET: it names no install and sends no credential, cookie, or event.
|
|
114
|
+
* Anything wrong, a refusal, a timeout, or an answer of the wrong shape, comes back as `unavailable`; it never fails the report.
|
|
115
|
+
*/
|
|
116
|
+
export const readFleet = async (env) => {
|
|
117
|
+
if ((env.OARS_POOL_URL ?? '') === '')
|
|
118
|
+
return { status: 'not-configured' };
|
|
119
|
+
let server;
|
|
120
|
+
try {
|
|
121
|
+
const { poolServer } = await import('../sessions/pool-command.js');
|
|
122
|
+
server = poolServer(env, 'telemetry');
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return { status: 'unavailable', reason: error instanceof CliError ? error.message : 'the setting is not a usable server' };
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const reply = await fetch(`${server}/v1/telemetry/fleet`, { method: 'GET', headers: { accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(FLEET_TIMEOUT_MS) });
|
|
129
|
+
if (!reply.ok)
|
|
130
|
+
return { status: 'unavailable', server, reason: `HTTP ${String(reply.status)}` };
|
|
131
|
+
const body = await reply.json().catch(() => undefined);
|
|
132
|
+
return fleetOf(server, body) ?? { status: 'unavailable', server, reason: 'the answer is not a fleet read' };
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return { status: 'unavailable', server, reason: 'it could not be reached' };
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const percent = (rate) => `${String(Math.round(rate * 100))}%`;
|
|
139
|
+
const numbersOf = (cell) => cell.kind === 'rate' ? percent(cell.rate ?? 0) : `p50 ${String(cell.p50 ?? 0)} ms, p95 ${String(cell.p95 ?? 0)} ms`;
|
|
140
|
+
const fleetNumbers = (cell, fleet) => (cell.kind === 'rate' ? `the fleet's median install: ${percent(fleet.p50)}` : `fleet p50 ${String(fleet.p50)} ms, p95 ${String(fleet.p95 ?? 0)} ms`);
|
|
141
|
+
const cellLines = (cell, answered, floor) => {
|
|
142
|
+
const own = cell.kind === 'rate' ? `your rate ${percent(cell.rate ?? 0)}` : numbersOf(cell);
|
|
143
|
+
const lines = [` ${cell.metric} (${cell.dimension === 'harnessVersion' ? 'harness/version' : cell.dimension} ${cell.value}) n=${String(cell.n)} ${own}`];
|
|
144
|
+
if (cell.fleet !== undefined)
|
|
145
|
+
lines.push(` ${fleetNumbers(cell, cell.fleet)}`);
|
|
146
|
+
else if (answered)
|
|
147
|
+
lines.push(` fleet: not served (fewer than ${String(floor)} installs report this)`);
|
|
148
|
+
return lines;
|
|
149
|
+
};
|
|
150
|
+
const storeLine = (store) => {
|
|
151
|
+
if (store.readFailed)
|
|
152
|
+
return `Store health, as of ${store.at}: the store could not be read.`;
|
|
153
|
+
const parts = COUNTS.flatMap((name) => (store[name] === undefined ? [] : [`${name} ${String(store[name])}`]));
|
|
154
|
+
return `Store health, as of ${store.at}: ${parts.join(', ')}.`;
|
|
155
|
+
};
|
|
156
|
+
export const reportOf = (lines, now, fleet) => ({
|
|
157
|
+
windows: WINDOWS.map((days) => {
|
|
158
|
+
const window = fleet.status === 'answered' ? fleet.windows.find((one) => one.days === days) : undefined;
|
|
159
|
+
return { days, cells: besideFleet(ownCells(lines, now, days, fleet.status === 'answered' ? fleet.asOf : undefined), window?.cells) };
|
|
160
|
+
}),
|
|
161
|
+
store: latestStore(lines, now, Math.max(...WINDOWS)),
|
|
162
|
+
fleet,
|
|
163
|
+
});
|
|
164
|
+
export const textOf = (report, dir) => {
|
|
165
|
+
const { fleet } = report;
|
|
166
|
+
const answered = fleet.status === 'answered';
|
|
167
|
+
const floor = fleet.status === 'answered' ? fleet.floor : 0;
|
|
168
|
+
const out = [`Your telemetry, from the spool in ${dir}.`];
|
|
169
|
+
if (report.windows.every((window) => window.cells.length === 0) && report.store === undefined)
|
|
170
|
+
out.push('Nothing is measured in the spool yet: the hooks and nightly task record as they run.');
|
|
171
|
+
for (const window of report.windows) {
|
|
172
|
+
if (window.cells.length === 0)
|
|
173
|
+
continue;
|
|
174
|
+
out.push('', `Last ${String(window.days)} days`);
|
|
175
|
+
for (const cell of window.cells)
|
|
176
|
+
out.push(...cellLines(cell, answered, floor));
|
|
177
|
+
}
|
|
178
|
+
if (report.store !== undefined)
|
|
179
|
+
out.push('', storeLine(report.store));
|
|
180
|
+
out.push('');
|
|
181
|
+
if (fleet.status === 'not-configured')
|
|
182
|
+
out.push('No pool server is configured (OARS_POOL_URL), so these are your numbers only.');
|
|
183
|
+
else if (fleet.status === 'unavailable')
|
|
184
|
+
out.push(`The pool server${fleet.server === undefined ? '' : ` at ${fleet.server}`} did not give a fleet read (${fleet.reason}), so these are your numbers only.`);
|
|
185
|
+
else
|
|
186
|
+
out.push(`Fleet numbers are as of ${fleet.asOf}, from ${fleet.server}: percentiles over installs, served only where at least ${String(fleet.floor)} installs report. The read sent nothing of yours.`);
|
|
187
|
+
return out.join('\n');
|
|
188
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ExitCode } from '../errors.js';
|
|
2
|
+
/** Bumped when an event type or field changes meaning; every spooled event carries it. */
|
|
3
|
+
export declare const SCHEMA_VERSION = 1;
|
|
4
|
+
/** What an event field may hold: a closed word, a whole number, or a list of either. */
|
|
5
|
+
export type Value = string | number | string[] | number[];
|
|
6
|
+
/** A model or harness version as the public vocabulary knows it, else the closed word `other`: a private or gateway name never reaches the spool. */
|
|
7
|
+
export declare const publicWord: (shape: (value: string | null) => string | null, value: unknown) => string;
|
|
8
|
+
export declare const modelWord: (value: unknown) => string;
|
|
9
|
+
export declare const versionWord: (value: unknown) => string;
|
|
10
|
+
/** What went wrong, as a closed word chosen from the exit code; the error's own text never comes here. */
|
|
11
|
+
export declare const ERROR_CLASSES: readonly ["usage", "auth", "invalid", "conflict", "rate-limited", "unreachable", "capped", "unexpected"];
|
|
12
|
+
export type ErrorClass = (typeof ERROR_CLASSES)[number];
|
|
13
|
+
/** Which hook fired: the six session hooks, and `gate` for `oars gate hook`. */
|
|
14
|
+
export declare const HOOK_NAMES: readonly ["session-start", "prompt", "pre-tool", "post-tool", "stop", "nightly", "gate"];
|
|
15
|
+
/** What a hook came to: it found something, found nothing, found something and held it back (the unexposed arm), or failed. */
|
|
16
|
+
export declare const HOOK_OUTCOMES: readonly ["hit", "miss", "held", "error"];
|
|
17
|
+
/** How a recall was asked and by whom. */
|
|
18
|
+
export declare const RECALL_FORMS: readonly ["pattern", "entity", "question", "text", "legacy"];
|
|
19
|
+
export declare const RECALL_MODES: readonly ["pull", "push"];
|
|
20
|
+
export declare const RECALL_ARMS: readonly ["shown", "held"];
|
|
21
|
+
/** The read families a recall can serve, as the program names them. */
|
|
22
|
+
export declare const RECALL_FAMILY_NAMES: readonly ["recovery", "fit", "cost", "change", "live", "adoption", "method", "next-step", "tool-day", "version-boundary", "substitution", "interaction", "day-move", "entity"];
|
|
23
|
+
export declare const FETCH_HARNESSES: readonly ["claude-code", "cursor", "codex", "copilot", "gemini", "grok", "opencode", "pi", "vscode-copilot", "otel", "other"];
|
|
24
|
+
/** The built-in error classes a crash may name; any other class (a library's, a program's, or text set as a name) is `other`. */
|
|
25
|
+
export declare const CRASH_CLASSES: readonly ["Error", "TypeError", "RangeError", "SyntaxError", "ReferenceError", "EvalError", "URIError", "AggregateError", "other"];
|
|
26
|
+
export type CrashClass = (typeof CRASH_CLASSES)[number];
|
|
27
|
+
export declare const EVENT_TYPES: readonly string[];
|
|
28
|
+
/** The field names a type admits, so a test can feed every one. */
|
|
29
|
+
export declare const fieldNamesOf: (type: string) => string[];
|
|
30
|
+
/** The error class for an exit code: closed words only. */
|
|
31
|
+
export declare const errorClassOf: (exit: ExitCode) => ErrorClass | undefined;
|
|
32
|
+
/** What a field set came to: the fields that fit, and how many did not (an unknown name, or a value its rule refused). */
|
|
33
|
+
export interface Sanitized {
|
|
34
|
+
data: {
|
|
35
|
+
[field: string]: Value;
|
|
36
|
+
};
|
|
37
|
+
dropped: number;
|
|
38
|
+
}
|
|
39
|
+
/** Passes `fields` through the rules of `type`; `undefined` for a type the schema does not know. */
|
|
40
|
+
export declare const sanitize: (type: string, fields: object) => Sanitized | undefined;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// The closed schema every telemetry event passes before it reaches the spool. A field the schema does not know is dropped and counted, and a value
|
|
2
|
+
// that does not fit its rule is dropped the same way, so what the spool holds is bounded by this file, not by whoever wrote the event. A rule admits
|
|
3
|
+
// a closed word, a small whole number, or a short list of closed words: never free text, a path, or a URL (none of them fits `TOKEN`).
|
|
4
|
+
// Later briefs add event types: add one line to `EVENTS`.
|
|
5
|
+
import { ExitCode } from '../errors.js';
|
|
6
|
+
import { FIELD_ROWS } from '../sessions/pool-outbox.js';
|
|
7
|
+
import { OTHER, HARNESSES } from '../sessions/pool-wire.js';
|
|
8
|
+
import { LITERAL, publicModel, publicVersion } from '../sessions/public-vocabulary.js';
|
|
9
|
+
/** Bumped when an event type or field changes meaning; every spooled event carries it. */
|
|
10
|
+
export const SCHEMA_VERSION = 1;
|
|
11
|
+
/** A command or option name as the program defines it: letters, digits, and hyphens. No space, slash, dot, colon, or quote. */
|
|
12
|
+
const TOKEN = /^[a-z][A-Za-z0-9-]{0,31}$/;
|
|
13
|
+
const tokens = (max) => (value) => Array.isArray(value) && value.length <= max && value.every((entry) => typeof entry === 'string' && TOKEN.test(entry)) ? value.map(String) : undefined;
|
|
14
|
+
const whole = (max) => (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= max ? value : undefined;
|
|
15
|
+
const oneOf = (allowed) => (value) => typeof value === 'string' && allowed.includes(value) ? value : undefined;
|
|
16
|
+
/** A model or harness version as the public vocabulary knows it, else the closed word `other`: a private or gateway name never reaches the spool. */
|
|
17
|
+
export const publicWord = (shape, value) => {
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
return 'other';
|
|
20
|
+
const named = shape(value);
|
|
21
|
+
return named === null || named === LITERAL ? 'other' : named;
|
|
22
|
+
};
|
|
23
|
+
export const modelWord = (value) => publicWord(publicModel, value);
|
|
24
|
+
export const versionWord = (value) => publicWord(publicVersion, value);
|
|
25
|
+
const MODEL_OR_VERSION = (word) => (value) => typeof value === 'string' && word(value) === value ? value : undefined;
|
|
26
|
+
/** A short list whose every entry is one of `allowed`. */
|
|
27
|
+
const listOf = (allowed, max) => (value) => Array.isArray(value) && value.length <= max && value.every((entry) => typeof entry === 'string' && allowed.includes(entry)) ? value.map(String) : undefined;
|
|
28
|
+
/** A list of whole numbers, none above `max`: the counts that sit beside a list of names. */
|
|
29
|
+
const counts = (length, max) => (value) => Array.isArray(value) && value.length <= length && value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry) && entry >= 0 && entry <= max) ? value.map(Number) : undefined;
|
|
30
|
+
/** A list of names, every one of them in `allowed`. */
|
|
31
|
+
const namesIn = (allowed, length) => (value) => Array.isArray(value) && value.length <= length && value.every((entry) => typeof entry === 'string' && allowed.has(entry)) ? value.map(String) : undefined;
|
|
32
|
+
/** A list of values the pool wire's own public vocabulary admits, or the word `other` for one it does not: the caller maps, the schema checks again. */
|
|
33
|
+
const publicList = (visible, length) => (value) => Array.isArray(value) && value.length <= length && value.every((entry) => typeof entry === 'string' && (entry === OTHER || (entry !== LITERAL && visible(entry) === entry))) ? value.map(String) : undefined;
|
|
34
|
+
/** A fixed number of lowercase hexadecimal digits: a hash, which names nothing. */
|
|
35
|
+
const hex = (digits) => (value) => typeof value === 'string' && new RegExp(`^[0-9a-f]{${String(digits)}}$`).test(value) ? value : undefined;
|
|
36
|
+
/** A problem code as a server names it: lowercase words joined with underscores. No space, slash, dot, colon, or quote, so no path, URL, or sentence fits. */
|
|
37
|
+
const CODE = /^[a-z][a-z0-9_]{0,39}$/;
|
|
38
|
+
const code = (value) => (typeof value === 'string' && CODE.test(value) ? value : undefined);
|
|
39
|
+
const rules = (fields) => new Map(Object.entries(fields));
|
|
40
|
+
const DAY_MS = 86_400_000;
|
|
41
|
+
/** What went wrong, as a closed word chosen from the exit code; the error's own text never comes here. */
|
|
42
|
+
export const ERROR_CLASSES = ['usage', 'auth', 'invalid', 'conflict', 'rate-limited', 'unreachable', 'capped', 'unexpected'];
|
|
43
|
+
/** Which hook fired: the six session hooks, and `gate` for `oars gate hook`. */
|
|
44
|
+
export const HOOK_NAMES = ['session-start', 'prompt', 'pre-tool', 'post-tool', 'stop', 'nightly', 'gate'];
|
|
45
|
+
/** What a hook came to: it found something, found nothing, found something and held it back (the unexposed arm), or failed. */
|
|
46
|
+
export const HOOK_OUTCOMES = ['hit', 'miss', 'held', 'error'];
|
|
47
|
+
/** How a recall was asked and by whom. */
|
|
48
|
+
export const RECALL_FORMS = ['pattern', 'entity', 'question', 'text', 'legacy'];
|
|
49
|
+
export const RECALL_MODES = ['pull', 'push'];
|
|
50
|
+
export const RECALL_ARMS = ['shown', 'held'];
|
|
51
|
+
/** The read families a recall can serve, as the program names them. */
|
|
52
|
+
export const RECALL_FAMILY_NAMES = ['recovery', 'fit', 'cost', 'change', 'live', 'adoption', 'method', 'next-step', 'tool-day', 'version-boundary', 'substitution', 'interaction', 'day-move', 'entity'];
|
|
53
|
+
export const FETCH_HARNESSES = ['claude-code', 'cursor', 'codex', 'copilot', 'gemini', 'grok', 'opencode', 'pi', 'vscode-copilot', 'otel', 'other'];
|
|
54
|
+
const BYTES = 1 << 26;
|
|
55
|
+
const COUNT = 1_000_000;
|
|
56
|
+
/** The built-in error classes a crash may name; any other class (a library's, a program's, or text set as a name) is `other`. */
|
|
57
|
+
export const CRASH_CLASSES = ['Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError', 'EvalError', 'URIError', 'AggregateError', 'other'];
|
|
58
|
+
const HUGE = 2 ** 50;
|
|
59
|
+
const ENTRIES = 1_000_000_000;
|
|
60
|
+
const WIRE_NAMES = new Set(Object.keys(FIELD_ROWS));
|
|
61
|
+
const WIRE_HARNESSES = [...HARNESSES, OTHER];
|
|
62
|
+
/** At most as many names as the register holds, so a batch's whole field list fits. */
|
|
63
|
+
const WIRE_LENGTH = WIRE_NAMES.size;
|
|
64
|
+
/** One line per event type. */
|
|
65
|
+
const EVENTS = new Map([
|
|
66
|
+
['command', rules({ path: tokens(4), flags: tokens(40), durationMs: whole(DAY_MS), exitCode: whole(255), errorClass: oneOf(ERROR_CLASSES) })],
|
|
67
|
+
['hook', rules({ name: oneOf(HOOK_NAMES), latencyMs: whole(DAY_MS), outcome: oneOf(HOOK_OUTCOMES), bytes: whole(BYTES), harness: oneOf(WIRE_HARNESSES), harnessVersion: MODEL_OR_VERSION(versionWord), model: MODEL_OR_VERSION(modelWord) })],
|
|
68
|
+
[
|
|
69
|
+
'recall',
|
|
70
|
+
rules({ form: oneOf(RECALL_FORMS), mode: oneOf(RECALL_MODES), families: listOf(RECALL_FAMILY_NAMES, 16), kind: oneOf(['hit', 'miss']), reads: whole(COUNT), evidence: whole(COUNT), tokens: whole(COUNT), arm: oneOf(RECALL_ARMS) }),
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
'deposit',
|
|
74
|
+
rules({
|
|
75
|
+
step: oneOf(['build', 'send']),
|
|
76
|
+
sessions: whole(COUNT),
|
|
77
|
+
batched: whole(COUNT),
|
|
78
|
+
sent: whole(COUNT),
|
|
79
|
+
bytes: whole(BYTES),
|
|
80
|
+
excludedFolder: whole(COUNT),
|
|
81
|
+
excludedTerm: whole(COUNT),
|
|
82
|
+
excludedNoRaw: whole(COUNT),
|
|
83
|
+
excludedNoTerms: whole(COUNT),
|
|
84
|
+
excludedUnreadable: whole(COUNT),
|
|
85
|
+
notEnded: whole(COUNT),
|
|
86
|
+
uncaptured: whole(COUNT),
|
|
87
|
+
refused: whole(COUNT),
|
|
88
|
+
withdrawn: whole(COUNT),
|
|
89
|
+
discarded: whole(COUNT),
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
['fetch', rules({ harness: oneOf(FETCH_HARNESSES), fetches: whole(COUNT), etagHits: whole(COUNT), capHits: whole(COUNT), reads: whole(COUNT) })],
|
|
93
|
+
// The nightly snapshot of the session store. `readFailed` is 1 when the store could not be read, and then no count is sent.
|
|
94
|
+
[
|
|
95
|
+
'store-health',
|
|
96
|
+
rules({
|
|
97
|
+
readFailed: whole(1),
|
|
98
|
+
sessions: whole(ENTRIES),
|
|
99
|
+
subagents: whole(ENTRIES),
|
|
100
|
+
skeletons: whole(ENTRIES),
|
|
101
|
+
unparseable: whole(ENTRIES),
|
|
102
|
+
rawBytes: whole(HUGE),
|
|
103
|
+
verifyChecked: whole(ENTRIES),
|
|
104
|
+
verifyFailed: whole(ENTRIES),
|
|
105
|
+
captureVersions: counts(32, 10_000),
|
|
106
|
+
extractorVersions: counts(32, 10_000),
|
|
107
|
+
harnessVersions: publicList(publicVersion, 32),
|
|
108
|
+
models: publicList(publicModel, 32),
|
|
109
|
+
outboxBytes: whole(HUGE),
|
|
110
|
+
egressBytes: whole(HUGE),
|
|
111
|
+
}),
|
|
112
|
+
],
|
|
113
|
+
// What one nightly run cost. `failed` is 1 when the run failed.
|
|
114
|
+
['run-cost', rules({ totalMs: whole(DAY_MS), parseMs: whole(DAY_MS), peakRssMb: whole(1_000_000), failed: whole(1) })],
|
|
115
|
+
// An error nobody planned for: its class and a hash of its stack. Never its message.
|
|
116
|
+
['crash', rules({ errorClass: oneOf(CRASH_CLASSES), stackHash: hex(16) })],
|
|
117
|
+
// One deposit pass, for one harness: the register's field names, and beside them how many batches held each and how many bytes it took.
|
|
118
|
+
[
|
|
119
|
+
'wire',
|
|
120
|
+
rules({
|
|
121
|
+
harness: oneOf(WIRE_HARNESSES),
|
|
122
|
+
batches: whole(ENTRIES),
|
|
123
|
+
bytes: whole(HUGE),
|
|
124
|
+
fields: namesIn(WIRE_NAMES, WIRE_LENGTH),
|
|
125
|
+
present: counts(WIRE_LENGTH, ENTRIES),
|
|
126
|
+
fieldBytes: counts(WIRE_LENGTH, HUGE),
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
// One send of one batch: whether the pool took it, the HTTP status, and the code of its refusal.
|
|
130
|
+
['wire-send', rules({ sent: whole(1), status: whole(599), code, bytes: whole(HUGE) })],
|
|
131
|
+
]);
|
|
132
|
+
export const EVENT_TYPES = [...EVENTS.keys()];
|
|
133
|
+
/** The field names a type admits, so a test can feed every one. */
|
|
134
|
+
export const fieldNamesOf = (type) => [...(EVENTS.get(type)?.keys() ?? [])];
|
|
135
|
+
/** The error class for an exit code: closed words only. */
|
|
136
|
+
export const errorClassOf = (exit) => {
|
|
137
|
+
switch (exit) {
|
|
138
|
+
case ExitCode.OK:
|
|
139
|
+
return undefined;
|
|
140
|
+
case ExitCode.USAGE:
|
|
141
|
+
return 'usage';
|
|
142
|
+
case ExitCode.AUTH:
|
|
143
|
+
return 'auth';
|
|
144
|
+
case ExitCode.INVALID:
|
|
145
|
+
return 'invalid';
|
|
146
|
+
case ExitCode.CONFLICT:
|
|
147
|
+
return 'conflict';
|
|
148
|
+
case ExitCode.RATE_LIMITED:
|
|
149
|
+
return 'rate-limited';
|
|
150
|
+
case ExitCode.UNREACHABLE:
|
|
151
|
+
return 'unreachable';
|
|
152
|
+
case ExitCode.CAPPED:
|
|
153
|
+
return 'capped';
|
|
154
|
+
default:
|
|
155
|
+
return 'unexpected';
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
/** The lists of a `wire` event that are read index by index, so one refused (or a different length) would leave the others misaligned. */
|
|
159
|
+
const PARALLEL_LISTS = ['fields', 'present', 'fieldBytes'];
|
|
160
|
+
/** Keeps the parallel lists only when every one sent came through and they are the same length; otherwise none of them, each kept one counted as dropped. */
|
|
161
|
+
const alignParallel = (sent, data, dropped) => {
|
|
162
|
+
const given = PARALLEL_LISTS.filter((name) => name in sent);
|
|
163
|
+
const kept = given.filter((name) => data[name] !== undefined);
|
|
164
|
+
// Every rule for these three names returns a list, so `flat` of the one-element wrapper is that list's length.
|
|
165
|
+
const lengths = new Set(kept.map((name) => [data[name]].flat().length));
|
|
166
|
+
if (kept.length === given.length && lengths.size <= 1)
|
|
167
|
+
return { data, dropped };
|
|
168
|
+
for (const name of kept)
|
|
169
|
+
Reflect.deleteProperty(data, name);
|
|
170
|
+
return { data, dropped: dropped + kept.length };
|
|
171
|
+
};
|
|
172
|
+
/** Passes `fields` through the rules of `type`; `undefined` for a type the schema does not know. */
|
|
173
|
+
export const sanitize = (type, fields) => {
|
|
174
|
+
const known = EVENTS.get(type);
|
|
175
|
+
if (known === undefined)
|
|
176
|
+
return undefined;
|
|
177
|
+
const data = {};
|
|
178
|
+
let dropped = 0;
|
|
179
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
180
|
+
const accepted = known.get(name)?.(value);
|
|
181
|
+
if (accepted === undefined)
|
|
182
|
+
dropped += 1;
|
|
183
|
+
else
|
|
184
|
+
data[name] = accepted;
|
|
185
|
+
}
|
|
186
|
+
return type === 'wire' ? alignParallel(fields, data, dropped) : { data, dropped };
|
|
187
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The entry `startDailySend` starts, detached: `node send-main.js <telemetry folder>`. It prints nothing, answers nothing, and exits 0 whatever happens; nobody waits on it.
|
|
2
|
+
import { sendSpool } from './send.js';
|
|
3
|
+
const [dir] = process.argv.slice(2);
|
|
4
|
+
try {
|
|
5
|
+
if (dir !== undefined)
|
|
6
|
+
await sendSpool({ env: process.env, dir, now: Date.now });
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// Fail-open: the spool is sent at the next try.
|
|
10
|
+
}
|
|
11
|
+
process.exit(0);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HelperSpawn } from '../gate/hook-fill.js';
|
|
2
|
+
import type { TelemetryRuntime } from './record.js';
|
|
3
|
+
/** The pool route takes at most this many events (Analytics Engine's per-request write limit). */
|
|
4
|
+
export declare const BATCH_EVENTS = 250;
|
|
5
|
+
/** One request's whole life, connect to last byte; a server that is slower than this is treated as down. */
|
|
6
|
+
export declare const SEND_TIMEOUT_MS = 10000;
|
|
7
|
+
export interface SendDeps {
|
|
8
|
+
env: NodeJS.ProcessEnv;
|
|
9
|
+
/** `<config>/oars/telemetry`. */
|
|
10
|
+
dir: string;
|
|
11
|
+
now: () => number;
|
|
12
|
+
/** The network, for a test; absent means the global `fetch`. */
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SendResult {
|
|
17
|
+
outcome: 'sent' | 'failed' | 'empty' | 'not-allowed' | 'no-server';
|
|
18
|
+
/** Events the server took (and that were cleared from the spool). */
|
|
19
|
+
sent: number;
|
|
20
|
+
/** Requests made, including one that failed. */
|
|
21
|
+
requests: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const writeMark: (dir: string, now: number) => void;
|
|
24
|
+
/** Whether a day has passed since the last send was started. A missing, unreadable, or future mark counts as due. */
|
|
25
|
+
export declare const dueForSend: (dir: string, now: number) => boolean;
|
|
26
|
+
/** Sends what the spool holds. Never throws. */
|
|
27
|
+
export declare const sendSpool: (deps: SendDeps) => Promise<SendResult>;
|
|
28
|
+
/** The nightly task's send, in the telemetry folder under the config folder. */
|
|
29
|
+
export declare const sendNightly: (env: NodeJS.ProcessEnv, platform: NodeJS.Platform, home: string, now: () => number) => Promise<SendResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Called after an interactive command: starts one detached send, at most once a day, when sending is allowed. The mark is written first, so two commands at
|
|
32
|
+
* once start one. Nothing here throws or waits, and the run has no way to start a send unless `main.ts` gave it one.
|
|
33
|
+
*/
|
|
34
|
+
export declare const startDailySend: (runtime: TelemetryRuntime, env: NodeJS.ProcessEnv, now: number) => void;
|
|
35
|
+
/** Starts `send-main.js` detached with no stdio and never waits for it, as the gate's helper is started. */
|
|
36
|
+
export declare const spawnDetachedSend: (dir: string, start?: HelperSpawn) => void;
|