@manyoars/oars 0.3.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 +44 -29
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +102 -32
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/gate/hook-main.js +13 -1
- package/dist/gate/hook-process.d.ts +11 -1
- package/dist/gate/hook-process.js +50 -2
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +2 -0
- package/dist/gate/hook.js +3 -0
- package/dist/gate/replay.js +2 -5
- package/dist/gate/report.js +1 -1
- package/dist/gate/transcripts.js +1 -1
- package/dist/hook-bin.js +27 -2
- package/dist/issuer.d.ts +7 -9
- package/dist/issuer.js +10 -32
- package/dist/main.js +3 -0
- package/dist/sessions/actions.js +78 -0
- package/dist/sessions/allow.d.ts +10 -1
- package/dist/sessions/allow.js +29 -9
- package/dist/sessions/baseline.d.ts +1 -1
- package/dist/sessions/baseline.js +6 -4
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/cell-increments.d.ts +5 -1
- package/dist/sessions/cell-increments.js +84 -50
- package/dist/sessions/cells.d.ts +33 -3
- package/dist/sessions/cells.js +89 -9
- package/dist/sessions/codex.js +93 -27
- package/dist/sessions/command.d.ts +11 -1
- package/dist/sessions/command.js +64 -18
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.js +2 -2
- package/dist/sessions/derive.js +72 -6
- package/dist/sessions/exclusion.d.ts +6 -0
- package/dist/sessions/exclusion.js +8 -0
- package/dist/sessions/extract.d.ts +4 -2
- package/dist/sessions/extract.js +58 -26
- package/dist/sessions/hook-calls.js +1 -1
- package/dist/sessions/hook-index.d.ts +14 -1
- package/dist/sessions/hook-index.js +10 -3
- package/dist/sessions/hooks.d.ts +5 -1
- package/dist/sessions/hooks.js +133 -22
- package/dist/sessions/identity.d.ts +8 -3
- package/dist/sessions/identity.js +45 -15
- package/dist/sessions/install.d.ts +18 -2
- package/dist/sessions/install.js +53 -8
- package/dist/sessions/mcp.d.ts +3 -2
- package/dist/sessions/mcp.js +8 -4
- package/dist/sessions/motif.d.ts +2 -1
- package/dist/sessions/motif.js +2 -2
- 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 +2 -0
- package/dist/sessions/package-resolve.js +3 -1
- 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 +76 -5
- package/dist/sessions/pool-command.js +263 -136
- 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 +15 -2
- package/dist/sessions/pool-outbox.js +79 -26
- package/dist/sessions/pool-print.d.ts +1 -1
- 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 +240 -30
- package/dist/sessions/pool-wire.js +695 -215
- package/dist/sessions/public-vocabulary.d.ts +2 -3
- package/dist/sessions/public-vocabulary.js +2 -9
- package/dist/sessions/query.d.ts +16 -4
- package/dist/sessions/query.js +32 -10
- package/dist/sessions/read-cost.js +14 -3
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read.d.ts +8 -1
- package/dist/sessions/read.js +6 -3
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -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 +2 -0
- package/dist/sessions/recall-object.js +1 -0
- package/dist/sessions/recall-push.d.ts +2 -2
- package/dist/sessions/recall-push.js +3 -51
- package/dist/sessions/recall-reads.d.ts +7 -0
- package/dist/sessions/recall-reads.js +7 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +11 -0
- package/dist/sessions/redact.js +59 -0
- package/dist/sessions/schema.d.ts +59 -9
- package/dist/sessions/schema.js +22 -3
- package/dist/sessions/session-shape.d.ts +6 -3
- package/dist/sessions/session-shape.js +19 -6
- package/dist/sessions/sources.js +75 -1
- package/dist/sessions/store.d.ts +13 -1
- package/dist/sessions/store.js +36 -16
- package/dist/sessions/survival.d.ts +2 -0
- package/dist/sessions/survival.js +2 -2
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/vscode-copilot.js +24 -7
- 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 +2 -2
|
@@ -21,7 +21,7 @@ const CWD_SCAN_LINES = 200;
|
|
|
21
21
|
const DEFAULT_REFS = ['refs/remotes/origin/main', 'refs/remotes/origin/master', 'refs/heads/main', 'refs/heads/master'];
|
|
22
22
|
const STATES = ['survived', 'reverted', 'unreachable', 'unknown'];
|
|
23
23
|
export const survivalFile = (storeDir) => join(storeDir, 'commit-survival.jsonl');
|
|
24
|
-
const runGit = (root, args, stdin) => new Promise((resolve, reject) => {
|
|
24
|
+
export const runGit = (root, args, stdin) => new Promise((resolve, reject) => {
|
|
25
25
|
const child = execFile('git', ['-C', root, ...args], {
|
|
26
26
|
encoding: 'utf8',
|
|
27
27
|
timeout: GIT_TIMEOUT_MS,
|
|
@@ -145,7 +145,7 @@ const cwdIn = async (file, project) => {
|
|
|
145
145
|
stream.destroy();
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
|
-
const cwdOfProject = async (runtime, project, sessions) => {
|
|
148
|
+
export const cwdOfProject = async (runtime, project, sessions) => {
|
|
149
149
|
for (const session of sessions) {
|
|
150
150
|
for (const root of [runtime.projectsRoot, paths(runtime.storeDir).raw]) {
|
|
151
151
|
const found = await cwdIn(join(root, project, `${session}.jsonl`), project);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A file's last bytes; a first line this cuts in two fails to parse and is skipped by whoever reads it. */
|
|
2
|
+
export declare const tailOf: (file: string) => Promise<string>;
|
|
3
|
+
export declare const isRecord: (value: unknown) => value is {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
/** What a transcript's tail states of the asker: the harness version, the model, and the effort, each the newest line's that states it. */
|
|
7
|
+
export interface Stated {
|
|
8
|
+
harnessVersion: string | null;
|
|
9
|
+
model: string | null;
|
|
10
|
+
effort: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare const statedOf: (tail: string) => Stated;
|
|
13
|
+
/** The same from a transcript file. A file that cannot be read states nothing: telemetry must never cost a hook its answer. */
|
|
14
|
+
export declare const statedAt: (file: string) => Promise<Stated>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The last bytes of a transcript and what they state, light enough for a hook to import: the bare `oars gate hook` loads this and never the recall machinery.
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
/** How far back a hook looks in a growing file: enough for the session's latest lines, and a bound on what a hook on every call can cost. */
|
|
4
|
+
const TAIL_BYTES = 64 * 1024;
|
|
5
|
+
const isMissing = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
6
|
+
const openIfThere = async (file) => {
|
|
7
|
+
try {
|
|
8
|
+
return await open(file, 'r');
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (isMissing(error))
|
|
12
|
+
return undefined;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** A file's last bytes; a first line this cuts in two fails to parse and is skipped by whoever reads it. */
|
|
17
|
+
export const tailOf = async (file) => {
|
|
18
|
+
const handle = await openIfThere(file);
|
|
19
|
+
if (handle === undefined)
|
|
20
|
+
return '';
|
|
21
|
+
try {
|
|
22
|
+
const { size } = await handle.stat();
|
|
23
|
+
const length = Math.min(size, TAIL_BYTES);
|
|
24
|
+
const buffer = Buffer.alloc(length);
|
|
25
|
+
await handle.read(buffer, 0, length, size - length);
|
|
26
|
+
return buffer.toString('utf8');
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await handle.close();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
const text = (value) => (typeof value === 'string' && value !== '' ? value : null);
|
|
34
|
+
const parsedLines = (tail) => tail.split('\n').flatMap((line) => {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(line);
|
|
37
|
+
return isRecord(parsed) ? [parsed] : [];
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
export const statedOf = (tail) => {
|
|
44
|
+
let harnessVersion = null;
|
|
45
|
+
let model = null;
|
|
46
|
+
let effort = null;
|
|
47
|
+
// Oldest first, so the newest line that states a field wins.
|
|
48
|
+
for (const line of parsedLines(tail)) {
|
|
49
|
+
harnessVersion = text(line.version) ?? harnessVersion;
|
|
50
|
+
effort = text(line.effort) ?? effort;
|
|
51
|
+
if (isRecord(line.message))
|
|
52
|
+
model = text(line.message.model) ?? model;
|
|
53
|
+
}
|
|
54
|
+
return { harnessVersion, model, effort };
|
|
55
|
+
};
|
|
56
|
+
/** The same from a transcript file. A file that cannot be read states nothing: telemetry must never cost a hook its answer. */
|
|
57
|
+
export const statedAt = async (file) => {
|
|
58
|
+
try {
|
|
59
|
+
return statedOf(await tailOf(file));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { harnessVersion: null, model: null, effort: null };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
// `hiddenFromTranscript` is the harness writing, not the operator, and its prompt is not read; its response still is
|
|
32
32
|
// timestamp epoch milliseconds, `responseTimestamp` for the response. A missing or implausible one is never made up: the time carries forward
|
|
33
33
|
// modelId the model
|
|
34
|
+
// agent `agent.extensionVersion` (participants/chatAgents.ts:71) is the Copilot Chat extension's version, the harness version, read only when `agent.extensionId.value` is `GitHub.copilot-chat` (a third-party participant carries its own): the newest request that names one
|
|
34
35
|
// response[] a markdown part is a bare `{value}` (a string in the old format); `kind:'thinking'` is skipped;
|
|
35
36
|
// `kind:'toolInvocationSerialized'` is a tool call (chatService/chatService.ts:1246-1270); `kind:'textEditGroup'` or `'notebookEditGroup'` `{uri}` is a file edited
|
|
36
37
|
// usage `completionTokens` on the request, the whole turn's output (model/chatModel.ts:1929-1934, 2086), and `modelTotals[{model, inputTokens, cachedTokens, outputTokens}]`
|
|
@@ -48,7 +49,8 @@
|
|
|
48
49
|
// button of a confirmation is the decline button and writes `{type:5, source:'user'}` (browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts:225,
|
|
49
50
|
// chatTerminalToolConfirmationSubPart.ts:380,507; browser/tools/languageModelToolsService.ts:711), so a user's refusal (`source` `user`) reads as Claude Code's refusal text and
|
|
50
51
|
// stores `user-rejected`; a hook's or a risk assessment's (languageModelToolsService.ts:683,1051) reads as a permission denial. A `{type:5}` with no `source` is the skip after
|
|
51
|
-
// approval (model/chatProgressTypes/chatToolInvocation.ts:516) and has no outcome. The file holds no
|
|
52
|
+
// approval (model/chatProgressTypes/chatToolInvocation.ts:516) and has no outcome. The file holds no working-directory change;
|
|
53
|
+
// its one harness version is the Copilot Chat agent's `extensionVersion` (see `agent` above).
|
|
52
54
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
53
55
|
import { homedir } from 'node:os';
|
|
54
56
|
import { basename, dirname, join } from 'node:path';
|
|
@@ -67,6 +69,8 @@ export const vscodeAppData = (platform, env, home = homedir()) => {
|
|
|
67
69
|
/** The `nameShort` of each build read. */
|
|
68
70
|
const SHELL_TOOLS = new Set(['run_in_terminal', 'run_task', 'create_and_run_task']);
|
|
69
71
|
const CHANNELS = ['Code', 'Code - Insiders', 'Code - OSS'];
|
|
72
|
+
// ExtensionIdentifier compares ids case-insensitively, so this is the lowercase form of `GitHub.copilot-chat`.
|
|
73
|
+
const COPILOT_CHAT_EXTENSION_ID = 'github.copilot-chat';
|
|
70
74
|
const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
71
75
|
const str = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
|
|
72
76
|
const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
|
|
@@ -229,6 +233,11 @@ export const chatLines = (data, fallbackId, folder) => {
|
|
|
229
233
|
};
|
|
230
234
|
for (const request of requests) {
|
|
231
235
|
const when = stamp(request.timestamp);
|
|
236
|
+
// The extension's version, from the agent VS Code serialized beside the request (ISerializableChatAgentData.extensionVersion); a later request supersedes. A
|
|
237
|
+
// third-party chat participant is serialized the same way with its own extension's version, so only Copilot Chat's own agent is read (id compared case-insensitively).
|
|
238
|
+
const extensionVersion = isObj(request.agent) && isObj(request.agent.extensionId) && str(request.agent.extensionId.value)?.toLowerCase() === COPILOT_CHAT_EXTENSION_ID ? str(request.agent.extensionVersion) : undefined;
|
|
239
|
+
if (extensionVersion !== undefined)
|
|
240
|
+
base.version = extensionVersion;
|
|
232
241
|
const message = request.message;
|
|
233
242
|
const text = typeof message === 'string' ? str(message) : isObj(message) ? str(message.text) : undefined;
|
|
234
243
|
const hidden = request.isSystemInitiated === true || request.requestHiddenFromTranscript === true || request.hiddenFromTranscript === true;
|
|
@@ -326,12 +335,20 @@ export const chatLines = (data, fallbackId, folder) => {
|
|
|
326
335
|
const totals = Array.isArray(request.modelTotals) ? request.modelTotals.filter(isObj) : [];
|
|
327
336
|
const completion = num(request.completionTokens);
|
|
328
337
|
const sum = (field) => totals.reduce((total, entry) => total + (num(entry[field]) ?? 0), 0);
|
|
329
|
-
// `inputTokens` is taken whole: the source does not say whether it includes `cachedTokens`.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
338
|
+
// `inputTokens` is taken whole: the source does not say whether it includes `cachedTokens`. A zero `inputTokens` means unknown (the extension API bridge never reports one,
|
|
339
|
+
// so the zero is a default), so it is left out rather than written as a measured zero. `cachedTokens` and `outputTokens` are values: a zero is kept when a total carried the
|
|
340
|
+
// field, since a request with no cache hit is a measured zero, and only a field no total carried is left out. When no category is left, the request has no usage.
|
|
341
|
+
// An all-zero `outputTokens` beside a positive `completionTokens` is the bridge's default beside the real count, so the completion count wins over that zero.
|
|
342
|
+
const carried = (field) => totals.some((entry) => num(entry[field]) !== undefined);
|
|
343
|
+
const fromCompletion = completion !== undefined && completion > 0 ? { output_tokens: completion } : {};
|
|
344
|
+
const fromTotals = {
|
|
345
|
+
...(sum('inputTokens') > 0 ? { input_tokens: sum('inputTokens') } : {}),
|
|
346
|
+
...(carried('cachedTokens') ? { cache_read_input_tokens: sum('cachedTokens') } : {}),
|
|
347
|
+
...(carried('outputTokens') ? { output_tokens: sum('outputTokens') } : {}),
|
|
348
|
+
...(sum('outputTokens') === 0 ? fromCompletion : {}),
|
|
349
|
+
};
|
|
350
|
+
const counted = totals.length > 0 && Object.keys(fromTotals).length > 0 ? fromTotals : fromCompletion;
|
|
351
|
+
const usage = Object.keys(counted).length > 0 ? counted : undefined;
|
|
335
352
|
if (content.length > 0 || usage !== undefined) {
|
|
336
353
|
const model = str(request.modelId);
|
|
337
354
|
const answered = stamp(request.responseTimestamp);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Outcome, RunDeps } from '../cli.js';
|
|
2
|
+
export declare const telemetryStatus: (deps: RunDeps) => Outcome;
|
|
3
|
+
export declare const telemetryOn: (deps: RunDeps) => Outcome;
|
|
4
|
+
export declare const telemetryOff: (deps: RunDeps) => Outcome;
|
|
5
|
+
/** The spool exactly as it is on disk, oldest first, as `pool outbox` prints a batch; the document carries the same events parsed. */
|
|
6
|
+
export declare const telemetryShow: (deps: RunDeps) => Outcome;
|
|
7
|
+
/** The user's own numbers from the spool, and the fleet's beside them when a pool server is configured and answers (docs/telemetry.md, "The report"). It writes nothing. */
|
|
8
|
+
export declare const telemetryReport: (deps: RunDeps) => Promise<Outcome>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// `oars telemetry status|on|off|show`.
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
4
|
+
import { consentOf, markNoticeShown, NOTICE, setEnabled } from './consent.js';
|
|
5
|
+
import { readFleet, reportOf, textOf } from './report.js';
|
|
6
|
+
import { readSpool, spoolPaths } from './spool.js';
|
|
7
|
+
const STEP = 'telemetry';
|
|
8
|
+
const runtimeOf = (deps) => {
|
|
9
|
+
if (deps.telemetry === undefined)
|
|
10
|
+
throw new CliError('The telemetry folder is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
11
|
+
return deps.telemetry;
|
|
12
|
+
};
|
|
13
|
+
const REASONS = {
|
|
14
|
+
default: 'on by default',
|
|
15
|
+
setting: 'by `oars telemetry on` or `off`',
|
|
16
|
+
OARS_TELEMETRY: 'by OARS_TELEMETRY',
|
|
17
|
+
DO_NOT_TRACK: 'by DO_NOT_TRACK',
|
|
18
|
+
};
|
|
19
|
+
const sizeOf = (path) => {
|
|
20
|
+
try {
|
|
21
|
+
return statSync(path).size;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const describe = (consent) => `Telemetry is ${consent.enabled ? 'on' : 'off'} (${REASONS[consent.reason]}).`;
|
|
28
|
+
export const telemetryStatus = (deps) => {
|
|
29
|
+
const { dir } = runtimeOf(deps);
|
|
30
|
+
const consent = consentOf(deps.env, dir);
|
|
31
|
+
const events = readSpool(dir).length;
|
|
32
|
+
const bytes = sizeOf(spoolPaths(dir).current) + sizeOf(spoolPaths(dir).previous);
|
|
33
|
+
const lines = [
|
|
34
|
+
describe(consent),
|
|
35
|
+
`The notice has ${consent.noticeShown ? '' : 'not '}been shown.`,
|
|
36
|
+
`${String(events)} events (${String(bytes)} bytes) in ${dir}.`,
|
|
37
|
+
'Events are sent to the pool server in OARS_POOL_URL, else to the Many Oars pool (pool.manyoars.com, not deployed yet: until it is, a send fails and the events stay on this machine), and only while telemetry is on and the notice has been shown.',
|
|
38
|
+
];
|
|
39
|
+
return { document: { ok: true, step: STEP, enabled: consent.enabled, reason: consent.reason, noticeShown: consent.noticeShown, events, bytes, dir }, text: lines.join('\n') };
|
|
40
|
+
};
|
|
41
|
+
const save = (dir, enabled) => {
|
|
42
|
+
try {
|
|
43
|
+
setEnabled(dir, enabled);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new CliError(`Could not save the telemetry setting in ${dir}: ${error instanceof Error ? error.message : String(error)}`, ExitCode.UNEXPECTED, 'telemetry-setting');
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export const telemetryOn = (deps) => {
|
|
50
|
+
const { dir } = runtimeOf(deps);
|
|
51
|
+
save(dir, true);
|
|
52
|
+
// Running `on` is the notice: it is printed here and recorded as shown.
|
|
53
|
+
markNoticeShown(dir, deps.now);
|
|
54
|
+
const consent = consentOf(deps.env, dir);
|
|
55
|
+
const override = consent.enabled ? '' : `\nIt stays off in this shell: ${consent.reason} is set.`;
|
|
56
|
+
return { document: { ok: true, step: STEP, enabled: consent.enabled, reason: consent.reason, notice: NOTICE }, text: `Telemetry is on.\n${NOTICE}${override}` };
|
|
57
|
+
};
|
|
58
|
+
export const telemetryOff = (deps) => {
|
|
59
|
+
const { dir } = runtimeOf(deps);
|
|
60
|
+
save(dir, false);
|
|
61
|
+
return {
|
|
62
|
+
document: { ok: true, step: STEP, enabled: false, reason: 'setting' },
|
|
63
|
+
text: `Telemetry is off: nothing more is recorded or sent. What is already in the spool stays until you delete ${dir}.`,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
const parsed = (line) => {
|
|
67
|
+
try {
|
|
68
|
+
return [JSON.parse(line)];
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/** The spool exactly as it is on disk, oldest first, as `pool outbox` prints a batch; the document carries the same events parsed. */
|
|
75
|
+
export const telemetryShow = (deps) => {
|
|
76
|
+
const { dir } = runtimeOf(deps);
|
|
77
|
+
const lines = readSpool(dir);
|
|
78
|
+
return { document: { ok: true, step: STEP, events: lines.flatMap(parsed) }, text: lines.length === 0 ? `The spool is empty (${dir}).` : lines.join('\n') };
|
|
79
|
+
};
|
|
80
|
+
/** The user's own numbers from the spool, and the fleet's beside them when a pool server is configured and answers (docs/telemetry.md, "The report"). It writes nothing. */
|
|
81
|
+
export const telemetryReport = async (deps) => {
|
|
82
|
+
const { dir } = runtimeOf(deps);
|
|
83
|
+
const report = reportOf(readSpool(dir), deps.now(), await readFleet(deps.env));
|
|
84
|
+
return { document: { ok: true, step: STEP, ...report }, text: textOf(report, dir) };
|
|
85
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Printed once on stderr, on the first run that is not a hook; also what `oars telemetry on` prints. */
|
|
2
|
+
export declare const NOTICE: string;
|
|
3
|
+
export type Reason = 'default' | 'setting' | 'OARS_TELEMETRY' | 'DO_NOT_TRACK';
|
|
4
|
+
export interface Consent {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
/** Which switch decided `enabled`. */
|
|
7
|
+
reason: Reason;
|
|
8
|
+
/** The notice was printed (or `on` was run) on this install. */
|
|
9
|
+
noticeShown: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const consentOf: (env: NodeJS.ProcessEnv, dir: string) => Consent;
|
|
12
|
+
/** Saves `on` or `off`. Throws when the folder cannot be written: the command that asked reports it. */
|
|
13
|
+
export declare const setEnabled: (dir: string, enabled: boolean) => void;
|
|
14
|
+
/** Records that the notice was shown; `false` when it could not be recorded (nothing is recorded or sent from a folder that cannot be written). */
|
|
15
|
+
export declare const markNoticeShown: (dir: string, now: () => number) => boolean;
|
|
16
|
+
/** Whether the sender (t875) may send: telemetry on, and the notice shown. Off, or notice not yet shown, means nothing sends. */
|
|
17
|
+
export declare const telemetrySendAllowed: (env: NodeJS.ProcessEnv, dir: string) => boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Whether telemetry records and may send, and the one-time notice. The off switches are `oars telemetry off` (a saved setting), `OARS_TELEMETRY=0`, and
|
|
2
|
+
// `DO_NOT_TRACK=1`; the environment wins over a saved `on`. `telemetrySendAllowed` is the check the sender must call before any byte leaves.
|
|
3
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { spoolPaths } from './spool.js';
|
|
5
|
+
/** Printed once on stderr, on the first run that is not a hook; also what `oars telemetry on` prints. */
|
|
6
|
+
export const NOTICE = [
|
|
7
|
+
'oars records how it is used, on this machine: one event per command with its path, flag names (never their values), how long it took, the exit code, and the class of any error; one per hook fire, recall, deposit, and fetch, as counts and closed words (which hook, how long, hit or miss, how many bytes, how many sessions a deposit counted and how many each gate kept out, never which); each with a random install id, the oars version, the OS, and the time. Once a night it also records counts and sizes of the session store and what the run cost. A crash is recorded as the error\'s class and a hash of its stack (never its message). Each pool deposit records, per batch, which fields it held and how many bytes each took, and each send (a wire-send event) its HTTP status, the pool\'s refusal code, and its size.',
|
|
8
|
+
'It never records session content, prompts, file paths, URLs, arguments, or anything you typed. oars sends the events in batches to the pool server in OARS_POOL_URL, else to the Many Oars pool (pool.manyoars.com, not deployed yet: until it is, a send fails and the events stay on this machine), and nowhere else.',
|
|
9
|
+
'See exactly what is kept: oars telemetry show. Turn it off: oars telemetry off, or set OARS_TELEMETRY=0 or DO_NOT_TRACK=1. More: docs/telemetry.md.',
|
|
10
|
+
].join('\n');
|
|
11
|
+
const readSettings = (dir) => {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(readFileSync(spoolPaths(dir).settings, 'utf8'));
|
|
14
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
15
|
+
return {};
|
|
16
|
+
const enabled = Reflect.get(parsed, 'enabled');
|
|
17
|
+
const shown = Reflect.get(parsed, 'noticeShownAt');
|
|
18
|
+
return { ...(typeof enabled === 'boolean' ? { enabled } : {}), ...(typeof shown === 'string' ? { noticeShownAt: shown } : {}) };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** Write to a sibling and rename over, so a crash leaves the old settings or the new, never half. */
|
|
25
|
+
const writeSettings = (dir, settings) => {
|
|
26
|
+
const { settings: path } = spoolPaths(dir);
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
const scratch = `${path}.tmp`;
|
|
29
|
+
writeFileSync(scratch, `${JSON.stringify(settings)}\n`);
|
|
30
|
+
renameSync(scratch, path);
|
|
31
|
+
};
|
|
32
|
+
const OFF_WORDS = new Set(['0', 'false', 'off', 'no']);
|
|
33
|
+
const DO_NOT_TRACK_WORDS = new Set(['1', 'true']);
|
|
34
|
+
export const consentOf = (env, dir) => {
|
|
35
|
+
const settings = readSettings(dir);
|
|
36
|
+
const noticeShown = settings.noticeShownAt !== undefined;
|
|
37
|
+
if (OFF_WORDS.has((env.OARS_TELEMETRY ?? '').toLowerCase()))
|
|
38
|
+
return { enabled: false, reason: 'OARS_TELEMETRY', noticeShown };
|
|
39
|
+
if (DO_NOT_TRACK_WORDS.has((env.DO_NOT_TRACK ?? '').toLowerCase()))
|
|
40
|
+
return { enabled: false, reason: 'DO_NOT_TRACK', noticeShown };
|
|
41
|
+
if (settings.enabled !== undefined)
|
|
42
|
+
return { enabled: settings.enabled, reason: 'setting', noticeShown };
|
|
43
|
+
return { enabled: true, reason: 'default', noticeShown };
|
|
44
|
+
};
|
|
45
|
+
/** Saves `on` or `off`. Throws when the folder cannot be written: the command that asked reports it. */
|
|
46
|
+
export const setEnabled = (dir, enabled) => {
|
|
47
|
+
writeSettings(dir, { ...readSettings(dir), enabled });
|
|
48
|
+
};
|
|
49
|
+
/** Records that the notice was shown; `false` when it could not be recorded (nothing is recorded or sent from a folder that cannot be written). */
|
|
50
|
+
export const markNoticeShown = (dir, now) => {
|
|
51
|
+
try {
|
|
52
|
+
writeSettings(dir, { ...readSettings(dir), noticeShownAt: new Date(now()).toISOString() });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/** Whether the sender (t875) may send: telemetry on, and the notice shown. Off, or notice not yet shown, means nothing sends. */
|
|
60
|
+
export const telemetrySendAllowed = (env, dir) => {
|
|
61
|
+
const consent = consentOf(env, dir);
|
|
62
|
+
return consent.enabled && consent.noticeShown;
|
|
63
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Emitter } from './emit.js';
|
|
2
|
+
import { type CrashClass } from './schema.js';
|
|
3
|
+
/** The class of a thrown value: a built-in error class by name, else `other`. A class a program or library defines, and a name set to text, are `other`. */
|
|
4
|
+
export declare const crashClassOf: (error: unknown) => CrashClass;
|
|
5
|
+
/** 16 hex characters from the frames of the stack, the message's lines left out (a message can run over many lines, and none begins with `at`). */
|
|
6
|
+
export declare const stackHashOf: (error: unknown) => string;
|
|
7
|
+
/** Records a crash. It reads the error for its class and its frames only; the message is never touched. */
|
|
8
|
+
export declare const recordCrash: (emitter: Emitter, error: unknown) => void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// A crash report is the error's class and a hash of its stack: never its message, which can hold a path. The stack's own text is read only to be
|
|
2
|
+
// hashed, and every file path in it is reduced to its place inside a package (or hashed whole when it sits nowhere known) before that, so the same
|
|
3
|
+
// code on two machines is the same crash and no home folder reaches the hash's input in clear.
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { emitEvent } from './emit.js';
|
|
6
|
+
import { CRASH_CLASSES } from './schema.js';
|
|
7
|
+
/** The class of a thrown value: a built-in error class by name, else `other`. A class a program or library defines, and a name set to text, are `other`. */
|
|
8
|
+
export const crashClassOf = (error) => {
|
|
9
|
+
if (!(error instanceof Error))
|
|
10
|
+
return 'other';
|
|
11
|
+
// A subclass of a built-in is that built-in; walk up until one is named.
|
|
12
|
+
for (let proto = Object.getPrototypeOf(error); typeof proto === 'object' && proto !== null; proto = Object.getPrototypeOf(proto)) {
|
|
13
|
+
const named = Reflect.get(proto, 'constructor');
|
|
14
|
+
const name = typeof named === 'function' ? Reflect.get(named, 'name') : '';
|
|
15
|
+
const found = CRASH_CLASSES.find((known) => known === name && known !== 'other');
|
|
16
|
+
if (found !== undefined)
|
|
17
|
+
return found;
|
|
18
|
+
}
|
|
19
|
+
return 'other';
|
|
20
|
+
};
|
|
21
|
+
const short = (text) => createHash('sha256').update(text).digest('hex').slice(0, 8);
|
|
22
|
+
/** `file:///C:/a/b.js`, `C:\a\b.js`, and `/a/b.js` as one slash-separated path, so two machines' separators agree. */
|
|
23
|
+
const plain = (location) => location.replace(/^file:\/\/\/?/, '').replaceAll('\\', '/');
|
|
24
|
+
/** Where a package's own files start in a path: after the last `dist/`, `src/`, or `node_modules/`. What lies before it is the machine's. */
|
|
25
|
+
const WITHIN = /^.*\/(?:dist|src|node_modules)\/(.*)$/;
|
|
26
|
+
const frameOf = (line) => {
|
|
27
|
+
const found = /^\s*at (?:(.*?) \()?(.*?)\)?$/.exec(line);
|
|
28
|
+
if (found === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const name = found[1] ?? '';
|
|
31
|
+
const place = /^(.*):(\d+):(\d+)$/.exec(found[2] ?? '');
|
|
32
|
+
// A frame with no line and column is an engine frame (`node:internal/...`, `<anonymous>`): nothing of the machine's.
|
|
33
|
+
if (place === null)
|
|
34
|
+
return `${name}@${found[2] ?? ''}`;
|
|
35
|
+
const path = plain(place[1] ?? '');
|
|
36
|
+
const within = WITHIN.exec(path)?.[1];
|
|
37
|
+
// Hashed either way: a package-relative path names no one, and one that is not inside a package is the machine's and is hashed whole.
|
|
38
|
+
return `${name}@${short(within ?? path)}:${place[2] ?? ''}:${place[3] ?? ''}`;
|
|
39
|
+
};
|
|
40
|
+
/** 16 hex characters from the frames of the stack, the message's lines left out (a message can run over many lines, and none begins with `at`). */
|
|
41
|
+
export const stackHashOf = (error) => {
|
|
42
|
+
const stack = error instanceof Error && typeof error.stack === 'string' ? error.stack : '';
|
|
43
|
+
const frames = stack
|
|
44
|
+
.split('\n')
|
|
45
|
+
.filter((line) => /^\s+at /.test(line))
|
|
46
|
+
.flatMap((line) => frameOf(line) ?? []);
|
|
47
|
+
return createHash('sha256').update(frames.join('\n')).digest('hex').slice(0, 16);
|
|
48
|
+
};
|
|
49
|
+
/** Records a crash. It reads the error for its class and its frames only; the message is never touched. */
|
|
50
|
+
export const recordCrash = (emitter, error) => {
|
|
51
|
+
emitEvent(emitter, 'crash', { errorClass: crashClassOf(error), stackHash: stackHashOf(error) });
|
|
52
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type TelemetryRuntime } from './record.js';
|
|
2
|
+
/** What recording needs from a run: where the folder is, the environment's switches, the version, and the clock. */
|
|
3
|
+
export interface Emitter {
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
telemetry?: TelemetryRuntime | undefined;
|
|
6
|
+
version: string;
|
|
7
|
+
now: () => number;
|
|
8
|
+
}
|
|
9
|
+
/** Whether this run records at all: a folder is set and consent is on. A caller that must do work to build an event asks first, so an opt-out pays nothing. */
|
|
10
|
+
export declare const recordsEvents: (emitter: Emitter) => boolean;
|
|
11
|
+
export declare const emitEvent: (emitter: Emitter, type: string, fields: object) => void;
|
|
12
|
+
/** Records one event of a known type; a no-op when telemetry is off, has no folder, or fails. */
|
|
13
|
+
export type Emit = (type: string, fields: object) => void;
|
|
14
|
+
export interface EmitContext {
|
|
15
|
+
env: NodeJS.ProcessEnv;
|
|
16
|
+
/** Absent: nothing is recorded. */
|
|
17
|
+
telemetry: TelemetryRuntime | undefined;
|
|
18
|
+
/** Read only when an event is about to be written, so a run with telemetry off never reads it. */
|
|
19
|
+
version: () => string;
|
|
20
|
+
now: () => number;
|
|
21
|
+
}
|
|
22
|
+
export declare const emitFor: ({ env, telemetry, version, now }: EmitContext) => Emit;
|
|
23
|
+
/** An emitter that records nothing, for a caller built without telemetry. */
|
|
24
|
+
export declare const noEmit: Emit;
|
|
25
|
+
/** The emitter for a CLI run: the run's environment, telemetry folder, version, and clock. */
|
|
26
|
+
export declare const emitOf: (deps: {
|
|
27
|
+
env: NodeJS.ProcessEnv;
|
|
28
|
+
telemetry?: TelemetryRuntime | undefined;
|
|
29
|
+
version: string;
|
|
30
|
+
now: () => number;
|
|
31
|
+
}) => Emit;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The one way a part of oars records an event: it checks the switches, passes the closed schema through `recordEvent`, and never throws,
|
|
2
|
+
// so no command's exit code or output can depend on it. A working path holds one `Emit`: one call, consent checked, so the line it carries is the whole cost to its caller.
|
|
3
|
+
import { consentOf } from './consent.js';
|
|
4
|
+
import { recordEvent } from './record.js';
|
|
5
|
+
/** Whether this run records at all: a folder is set and consent is on. A caller that must do work to build an event asks first, so an opt-out pays nothing. */
|
|
6
|
+
export const recordsEvents = (emitter) => {
|
|
7
|
+
try {
|
|
8
|
+
return emitter.telemetry !== undefined && consentOf(emitter.env, emitter.telemetry.dir).enabled;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const emitEvent = (emitter, type, fields) => {
|
|
15
|
+
try {
|
|
16
|
+
const runtime = emitter.telemetry;
|
|
17
|
+
if (runtime === undefined || !consentOf(emitter.env, runtime.dir).enabled)
|
|
18
|
+
return;
|
|
19
|
+
recordEvent({ ...runtime, version: emitter.version, now: emitter.now }, type, fields);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Telemetry is never allowed to change what a command or hook does.
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export const emitFor = ({ env, telemetry, version, now }) => {
|
|
26
|
+
const emitter = {
|
|
27
|
+
env,
|
|
28
|
+
telemetry,
|
|
29
|
+
// A getter, so `emitEvent` reads the version only after consent says an event will be written.
|
|
30
|
+
get version() {
|
|
31
|
+
return version();
|
|
32
|
+
},
|
|
33
|
+
now,
|
|
34
|
+
};
|
|
35
|
+
return (type, fields) => {
|
|
36
|
+
emitEvent(emitter, type, fields);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/** An emitter that records nothing, for a caller built without telemetry. */
|
|
40
|
+
export const noEmit = () => undefined;
|
|
41
|
+
/** The emitter for a CLI run: the run's environment, telemetry folder, version, and clock. */
|
|
42
|
+
export const emitOf = (deps) => emitFor({ env: deps.env, telemetry: deps.telemetry, version: () => deps.version, now: deps.now });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Recall } from '../sessions/recall-object.js';
|
|
2
|
+
/** The harness every hook oars installs runs under (Claude Code); one spelling for the session hooks and the gate hook, so the two emitters cannot drift. */
|
|
3
|
+
export declare const HOOK_HARNESS = "claude-code";
|
|
4
|
+
/** The harness a hook ran under, as the closed list names it, else `other`. */
|
|
5
|
+
export declare const harnessWord: (harness: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* The harness, harness version, and model a hook saw, as the public vocabulary knows them or `other`; a version or model the hook did not see is absent. A hook fire names its harness (the
|
|
8
|
+
* recall's frame when a push made one); the nightly task runs under none, so its caller passes `undefined` and the event carries no harness (it reads as `unrecorded`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const frameWords: (harness: string | undefined, harnessVersion: string | null | undefined, model: string | null | undefined) => {
|
|
11
|
+
harness?: string;
|
|
12
|
+
harnessVersion?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
};
|
|
15
|
+
/** A recall as counts: how it was asked, which families it served, how many reads and evidence rows, the size of the answer, and its arm. Never the query text, a name, or the rendered answer. */
|
|
16
|
+
export declare const recallFields: (recall: Recall, families: readonly string[], legacy?: boolean) => object;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { HARNESSES, OTHER } from '../sessions/pool-wire.js';
|
|
2
|
+
import { modelWord, versionWord } from './schema.js';
|
|
3
|
+
/** The harness every hook oars installs runs under (Claude Code); one spelling for the session hooks and the gate hook, so the two emitters cannot drift. */
|
|
4
|
+
export const HOOK_HARNESS = 'claude-code';
|
|
5
|
+
/** The harness a hook ran under, as the closed list names it, else `other`. */
|
|
6
|
+
export const harnessWord = (harness) => (HARNESSES.some((known) => known === harness) ? harness : OTHER);
|
|
7
|
+
/**
|
|
8
|
+
* The harness, harness version, and model a hook saw, as the public vocabulary knows them or `other`; a version or model the hook did not see is absent. A hook fire names its harness (the
|
|
9
|
+
* recall's frame when a push made one); the nightly task runs under none, so its caller passes `undefined` and the event carries no harness (it reads as `unrecorded`).
|
|
10
|
+
*/
|
|
11
|
+
export const frameWords = (harness, harnessVersion, model) => ({
|
|
12
|
+
...(harness === undefined ? {} : { harness: harnessWord(harness) }),
|
|
13
|
+
...(harnessVersion === undefined || harnessVersion === null ? {} : { harnessVersion: versionWord(harnessVersion) }),
|
|
14
|
+
...(model === undefined || model === null ? {} : { model: modelWord(model) }),
|
|
15
|
+
});
|
|
16
|
+
/** A recall as counts: how it was asked, which families it served, how many reads and evidence rows, the size of the answer, and its arm. Never the query text, a name, or the rendered answer. */
|
|
17
|
+
export const recallFields = (recall, families, legacy = false) => ({
|
|
18
|
+
form: legacy ? 'legacy' : recall.query.form,
|
|
19
|
+
mode: recall.mode,
|
|
20
|
+
families: [...new Set(families)],
|
|
21
|
+
kind: recall.answer.kind,
|
|
22
|
+
reads: recall.answer.reads.length,
|
|
23
|
+
evidence: recall.evidence.length,
|
|
24
|
+
tokens: recall.answer.tokens,
|
|
25
|
+
arm: recall.arm,
|
|
26
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type SessionsRuntime } from '../sessions/store.js';
|
|
2
|
+
import { type Emitter } from './emit.js';
|
|
3
|
+
/** What the nightly run needs to record: the emitter, and a reader for the process's peak memory (a test sets it). */
|
|
4
|
+
export interface NightlyTelemetry extends Emitter {
|
|
5
|
+
peakRssMb?: () => number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The distinct values seen, each kept when the pool wire's own public vocabulary admits it and written as `other` when it does not, sorted with `other`
|
|
9
|
+
* last. More than the schema's list length folds the tail into `other`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const closedList: (values: readonly string[], kind: "version" | "model") => string[];
|
|
12
|
+
/** The distinct whole-number versions among the stamps, ascending; a stamp that is not a whole number names no version and is left out. */
|
|
13
|
+
export declare const versionsHeld: (stamps: readonly string[]) => number[];
|
|
14
|
+
/** The store-health event of one nightly run. A store that cannot be read is recorded as that and nothing else; it never stops the run. */
|
|
15
|
+
export declare const recordStoreHealth: (telemetry: NightlyTelemetry, runtime: SessionsRuntime) => Promise<void>;
|
|
16
|
+
/** What a nightly run cost: the whole run, the deposit's parse, the process's peak memory, and whether it failed. */
|
|
17
|
+
export declare const recordRunCost: (telemetry: NightlyTelemetry, cost: {
|
|
18
|
+
totalMs: number;
|
|
19
|
+
parseMs: number;
|
|
20
|
+
failed: boolean;
|
|
21
|
+
}) => void;
|