@bli-cockpit/telemetry-core 0.1.36 → 0.1.38
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.
|
@@ -108,6 +108,10 @@ export declare const CollectorHeartbeatSchema: z.ZodObject<{
|
|
|
108
108
|
hook_via_daemon_24h: z.ZodOptional<z.ZodNumber>;
|
|
109
109
|
hook_via_direct_24h: z.ZodOptional<z.ZodNumber>;
|
|
110
110
|
hook_skipped_trivial_24h: z.ZodOptional<z.ZodNumber>;
|
|
111
|
+
hook_billing_exhausted_24h: z.ZodOptional<z.ZodNumber>;
|
|
112
|
+
hook_chars_p50: z.ZodOptional<z.ZodNumber>;
|
|
113
|
+
hook_chars_p95: z.ZodOptional<z.ZodNumber>;
|
|
114
|
+
hook_chars_samples: z.ZodOptional<z.ZodNumber>;
|
|
111
115
|
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
112
116
|
hook_performance: z.ZodOptional<z.ZodObject<{
|
|
113
117
|
schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
|
|
@@ -29,6 +29,42 @@ export declare const MEMORY_HOOK_STATS_FILE_NAME = "memory-hook-stats.json";
|
|
|
29
29
|
/** The three hook events, spelled as the bin's subcommands are. */
|
|
30
30
|
export declare const MEMORY_HOOK_STAT_EVENTS: readonly ["session-start", "prompt", "stop"];
|
|
31
31
|
export type MemoryHookStatEvent = (typeof MEMORY_HOOK_STAT_EVENTS)[number];
|
|
32
|
+
/**
|
|
33
|
+
* THE CHARACTER HISTOGRAM (ticket 3934).
|
|
34
|
+
*
|
|
35
|
+
* 250 characters a bin, 41 bins: `[0,250)`, … `[9750,10000)`, `[10000,∞)`.
|
|
36
|
+
*
|
|
37
|
+
* The width is chosen against what the hook can actually print, not against a
|
|
38
|
+
* round number: a recall is at most `RECALL_LIMIT` (5) memories truncated to
|
|
39
|
+
* `RECALL_LINE_CHARS` (300) each, plus at most `RECALL_TOWER_LIMIT` (3) Tower
|
|
40
|
+
* lines and the two block wrappers — call it 2,000 characters in the ordinary
|
|
41
|
+
* case. 250 puts eight bins across that range, which is enough resolution for
|
|
42
|
+
* a p50 to move visibly, and the open top bin catches a SessionStart profile
|
|
43
|
+
* block, which has no such limit and can be far larger.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MEMORY_HOOK_CHARS_BIN_WIDTH = 250;
|
|
46
|
+
export declare const MEMORY_HOOK_CHARS_BIN_COUNT = 41;
|
|
47
|
+
/** A fresh all-zero character histogram. */
|
|
48
|
+
export declare function blankCharsHistogram(): number[];
|
|
49
|
+
/**
|
|
50
|
+
* The bin one sample falls in. Clamped at both ends rather than throwing: this
|
|
51
|
+
* runs inside a hook, and a counter must never be the thing that fails a
|
|
52
|
+
* person's turn. A negative or non-finite length is impossible from
|
|
53
|
+
* `stdout.length` and is floored at bin 0 if it ever happens.
|
|
54
|
+
*/
|
|
55
|
+
export declare function memoryHookCharsBin(chars: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* The nearest-rank percentile of a character histogram, as BIN BOUNDS.
|
|
58
|
+
*
|
|
59
|
+
* `null` when nothing was sampled — which is the honest answer for a machine
|
|
60
|
+
* whose hooks never printed, and is never rendered as a zero. The top bin is
|
|
61
|
+
* open, so its `upper` is `null` too: "10,000 or more" is what the data says
|
|
62
|
+
* and inventing a ceiling would be a claim nobody measured.
|
|
63
|
+
*/
|
|
64
|
+
export declare function memoryHookCharsPercentile(histogram: readonly number[], percentile: number): {
|
|
65
|
+
lower: number;
|
|
66
|
+
upper: number | null;
|
|
67
|
+
} | null;
|
|
32
68
|
/**
|
|
33
69
|
* One hour of one event.
|
|
34
70
|
*
|
|
@@ -48,6 +84,8 @@ export declare const MemoryHookCountsSchema: z.ZodObject<{
|
|
|
48
84
|
via_daemon: z.ZodOptional<z.ZodNumber>;
|
|
49
85
|
via_direct: z.ZodOptional<z.ZodNumber>;
|
|
50
86
|
skipped_trivial: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
billing_exhausted: z.ZodOptional<z.ZodNumber>;
|
|
88
|
+
chars: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
51
89
|
}, z.core.$strict>;
|
|
52
90
|
export type MemoryHookCounts = z.infer<typeof MemoryHookCountsSchema>;
|
|
53
91
|
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
@@ -68,6 +106,8 @@ export declare const MemoryHookStatsFileSchema: z.ZodObject<{
|
|
|
68
106
|
via_daemon: z.ZodOptional<z.ZodNumber>;
|
|
69
107
|
via_direct: z.ZodOptional<z.ZodNumber>;
|
|
70
108
|
skipped_trivial: z.ZodOptional<z.ZodNumber>;
|
|
109
|
+
billing_exhausted: z.ZodOptional<z.ZodNumber>;
|
|
110
|
+
chars: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
71
111
|
}, z.core.$strict>>>;
|
|
72
112
|
}, z.core.$strict>;
|
|
73
113
|
export type MemoryHookStatsFile = z.infer<typeof MemoryHookStatsFileSchema>;
|
|
@@ -92,6 +132,23 @@ export interface MemoryHookWindow {
|
|
|
92
132
|
viaMeasured: boolean;
|
|
93
133
|
/** Trivial prompts the hook declined to search for (BLI-3881). A subset of skips. */
|
|
94
134
|
skippedTrivial: number;
|
|
135
|
+
/**
|
|
136
|
+
* BLI-3891: runs the provider refused because the account has no credit. A
|
|
137
|
+
* subset of `failed`, summed only over buckets that carry the field, so a
|
|
138
|
+
* window spanning an upgrade reports what was measured rather than crediting
|
|
139
|
+
* older runs to zero.
|
|
140
|
+
*/
|
|
141
|
+
billingExhausted: number;
|
|
142
|
+
/**
|
|
143
|
+
* Ticket 3934: the CHARACTERS injected per recall, summed bin by bin over
|
|
144
|
+
* buckets that CARRY the field. `charsMeasured` is false when no bucket in
|
|
145
|
+
* the window recorded one — a machine on an older memory-mcp — so a reader
|
|
146
|
+
* says "not recorded" rather than printing a percentile of nothing.
|
|
147
|
+
*/
|
|
148
|
+
chars: number[];
|
|
149
|
+
charsMeasured: boolean;
|
|
150
|
+
/** Runs that contributed a character sample. Never assumed equal to `printed`. */
|
|
151
|
+
charsSamples: number;
|
|
95
152
|
/** Buckets that were inside the window and had something in them. */
|
|
96
153
|
hours: number;
|
|
97
154
|
}
|
|
@@ -29,6 +29,71 @@ export const MEMORY_HOOK_STATS_SCHEMA_VERSION = "memory-hook-stats.v1";
|
|
|
29
29
|
export const MEMORY_HOOK_STATS_FILE_NAME = "memory-hook-stats.json";
|
|
30
30
|
/** The three hook events, spelled as the bin's subcommands are. */
|
|
31
31
|
export const MEMORY_HOOK_STAT_EVENTS = ["session-start", "prompt", "stop"];
|
|
32
|
+
/**
|
|
33
|
+
* THE CHARACTER HISTOGRAM (ticket 3934).
|
|
34
|
+
*
|
|
35
|
+
* 250 characters a bin, 41 bins: `[0,250)`, … `[9750,10000)`, `[10000,∞)`.
|
|
36
|
+
*
|
|
37
|
+
* The width is chosen against what the hook can actually print, not against a
|
|
38
|
+
* round number: a recall is at most `RECALL_LIMIT` (5) memories truncated to
|
|
39
|
+
* `RECALL_LINE_CHARS` (300) each, plus at most `RECALL_TOWER_LIMIT` (3) Tower
|
|
40
|
+
* lines and the two block wrappers — call it 2,000 characters in the ordinary
|
|
41
|
+
* case. 250 puts eight bins across that range, which is enough resolution for
|
|
42
|
+
* a p50 to move visibly, and the open top bin catches a SessionStart profile
|
|
43
|
+
* block, which has no such limit and can be far larger.
|
|
44
|
+
*/
|
|
45
|
+
export const MEMORY_HOOK_CHARS_BIN_WIDTH = 250;
|
|
46
|
+
export const MEMORY_HOOK_CHARS_BIN_COUNT = 41;
|
|
47
|
+
/** A fresh all-zero character histogram. */
|
|
48
|
+
export function blankCharsHistogram() {
|
|
49
|
+
return Array.from({ length: MEMORY_HOOK_CHARS_BIN_COUNT }, () => 0);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The bin one sample falls in. Clamped at both ends rather than throwing: this
|
|
53
|
+
* runs inside a hook, and a counter must never be the thing that fails a
|
|
54
|
+
* person's turn. A negative or non-finite length is impossible from
|
|
55
|
+
* `stdout.length` and is floored at bin 0 if it ever happens.
|
|
56
|
+
*/
|
|
57
|
+
export function memoryHookCharsBin(chars) {
|
|
58
|
+
// NaN has no size, so it goes in the smallest bin; a positive infinity is
|
|
59
|
+
// enormous and goes in the open top one. Collapsing both to bin 0 would file
|
|
60
|
+
// an impossibly large recall as the smallest thing on the machine.
|
|
61
|
+
if (Number.isNaN(chars) || chars <= 0)
|
|
62
|
+
return 0;
|
|
63
|
+
if (!Number.isFinite(chars))
|
|
64
|
+
return MEMORY_HOOK_CHARS_BIN_COUNT - 1;
|
|
65
|
+
return Math.min(Math.floor(chars / MEMORY_HOOK_CHARS_BIN_WIDTH), MEMORY_HOOK_CHARS_BIN_COUNT - 1);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The nearest-rank percentile of a character histogram, as BIN BOUNDS.
|
|
69
|
+
*
|
|
70
|
+
* `null` when nothing was sampled — which is the honest answer for a machine
|
|
71
|
+
* whose hooks never printed, and is never rendered as a zero. The top bin is
|
|
72
|
+
* open, so its `upper` is `null` too: "10,000 or more" is what the data says
|
|
73
|
+
* and inventing a ceiling would be a claim nobody measured.
|
|
74
|
+
*/
|
|
75
|
+
export function memoryHookCharsPercentile(histogram, percentile) {
|
|
76
|
+
if (!Number.isFinite(percentile) || percentile < 0 || percentile > 100) {
|
|
77
|
+
throw new RangeError("percentile must be between 0 and 100");
|
|
78
|
+
}
|
|
79
|
+
const samples = histogram.reduce((sum, count) => sum + count, 0);
|
|
80
|
+
if (samples === 0)
|
|
81
|
+
return null;
|
|
82
|
+
const rank = Math.max(1, Math.ceil((percentile / 100) * samples));
|
|
83
|
+
let cumulative = 0;
|
|
84
|
+
for (let index = 0; index < histogram.length; index += 1) {
|
|
85
|
+
cumulative += histogram[index] ?? 0;
|
|
86
|
+
if (cumulative >= rank) {
|
|
87
|
+
return {
|
|
88
|
+
lower: index * MEMORY_HOOK_CHARS_BIN_WIDTH,
|
|
89
|
+
upper: index === MEMORY_HOOK_CHARS_BIN_COUNT - 1
|
|
90
|
+
? null
|
|
91
|
+
: (index + 1) * MEMORY_HOOK_CHARS_BIN_WIDTH,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
32
97
|
/**
|
|
33
98
|
* One hour of one event.
|
|
34
99
|
*
|
|
@@ -76,6 +141,45 @@ export const MemoryHookCountsSchema = z
|
|
|
76
141
|
* failed + skipped` still holds.
|
|
77
142
|
*/
|
|
78
143
|
skipped_trivial: z.number().int().min(0).optional(),
|
|
144
|
+
/**
|
|
145
|
+
* BLI-3891 — the SUBSET of `failed` that was the ACCOUNT, not the code.
|
|
146
|
+
*
|
|
147
|
+
* The provider refused the embedding because there is no credit left
|
|
148
|
+
* (`provider_billing_exhausted`). On 2026-09-06 that state made every
|
|
149
|
+
* recall on every machine miss its deadline and print nothing, and the
|
|
150
|
+
* only number anywhere would have said "timeouts", which sends a person
|
|
151
|
+
* to re-fit a budget rather than to a billing page.
|
|
152
|
+
*
|
|
153
|
+
* Counted BESIDE `failed`, never instead of it, so
|
|
154
|
+
* `runs = printed + empty + timeouts + failed + skipped` still holds, and
|
|
155
|
+
* OPTIONAL for the same reason `skipped_trivial` is: a file written by an
|
|
156
|
+
* older memory-mcp has no such key, this schema is strict, and a required
|
|
157
|
+
* field would make every existing machine's history
|
|
158
|
+
* `hook_stats_unrecognised_shape` overnight.
|
|
159
|
+
*/
|
|
160
|
+
billing_exhausted: z.number().int().min(0).optional(),
|
|
161
|
+
/**
|
|
162
|
+
* HOW BIG WAS THE RECALL? (ticket 3934) — a counts-only histogram of the
|
|
163
|
+
* CHARACTERS the hook injected, one sample per run that printed a block.
|
|
164
|
+
*
|
|
165
|
+
* The timing histogram beside it (`memory-hook-performance.ts`) says how
|
|
166
|
+
* long a recall took. Nothing anywhere said how MUCH it put into a
|
|
167
|
+
* person's context, and that is the number behind two questions the fleet
|
|
168
|
+
* cannot otherwise answer: is the recall a line or a page, and is a slow
|
|
169
|
+
* turn slow because the door was slow or because the block was enormous.
|
|
170
|
+
*
|
|
171
|
+
* BINS, NEVER A LENGTH. A histogram of counts cannot leak a memory; a
|
|
172
|
+
* stored length could not either, but a bin is the smaller claim and the
|
|
173
|
+
* percentile is all any surface prints. The block's TEXT never comes near
|
|
174
|
+
* this file — see the header's prohibition, which this does not weaken.
|
|
175
|
+
*
|
|
176
|
+
* OPTIONAL, and it must stay optional. This schema is strict, a file
|
|
177
|
+
* written by an older memory-mcp has no such key, and a required field
|
|
178
|
+
* would make every existing machine's history `hook_stats_unrecognised_shape`
|
|
179
|
+
* overnight — the same rule `skipped_trivial` and `billing_exhausted` are
|
|
180
|
+
* kept under. Absent means NOT RECORDED, never "the recalls were empty".
|
|
181
|
+
*/
|
|
182
|
+
chars: z.array(z.number().int().min(0)).length(MEMORY_HOOK_CHARS_BIN_COUNT).optional(),
|
|
79
183
|
})
|
|
80
184
|
.strict();
|
|
81
185
|
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
@@ -99,7 +203,20 @@ export function memoryHookHourBucket(at) {
|
|
|
99
203
|
return at.toISOString().slice(0, 13);
|
|
100
204
|
}
|
|
101
205
|
export function emptyMemoryHookCounts() {
|
|
102
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
runs: 0,
|
|
208
|
+
printed: 0,
|
|
209
|
+
empty: 0,
|
|
210
|
+
timeouts: 0,
|
|
211
|
+
failed: 0,
|
|
212
|
+
skipped: 0,
|
|
213
|
+
skipped_trivial: 0,
|
|
214
|
+
billing_exhausted: 0,
|
|
215
|
+
// `chars` is deliberately NOT here (ticket 3934). An hour in which the
|
|
216
|
+
// hook ran and never printed has no recall to measure, and seeding 41
|
|
217
|
+
// zeroes into every event of every bucket would both bloat the file and
|
|
218
|
+
// make `charsMeasured` true for a machine that has measured nothing.
|
|
219
|
+
};
|
|
103
220
|
}
|
|
104
221
|
/**
|
|
105
222
|
* Sum one event's buckets over the last `hours` hours, ending at `now`.
|
|
@@ -122,6 +239,10 @@ export function summariseMemoryHookWindow(file, event, options) {
|
|
|
122
239
|
viaDirect: 0,
|
|
123
240
|
viaMeasured: false,
|
|
124
241
|
skippedTrivial: 0,
|
|
242
|
+
billingExhausted: 0,
|
|
243
|
+
chars: blankCharsHistogram(),
|
|
244
|
+
charsMeasured: false,
|
|
245
|
+
charsSamples: 0,
|
|
125
246
|
hours: 0,
|
|
126
247
|
};
|
|
127
248
|
for (const [bucket, events] of Object.entries(file.buckets)) {
|
|
@@ -144,6 +265,20 @@ export function summariseMemoryHookWindow(file, event, options) {
|
|
|
144
265
|
// Absent means an older writer never counted one, which sums as zero and
|
|
145
266
|
// is honest: that machine genuinely skipped none, because it could not.
|
|
146
267
|
total.skippedTrivial += counts.skipped_trivial ?? 0;
|
|
268
|
+
// Same rule (BLI-3891): absent means an older writer never counted one,
|
|
269
|
+
// which sums as zero and is honest — that machine genuinely had none it
|
|
270
|
+
// could name.
|
|
271
|
+
total.billingExhausted += counts.billing_exhausted ?? 0;
|
|
272
|
+
// Ticket 3934: summed only over buckets that CARRY the histogram, so a
|
|
273
|
+
// window spanning an upgrade reports what was measured rather than
|
|
274
|
+
// crediting the older hours with zero-length recalls.
|
|
275
|
+
if (counts.chars) {
|
|
276
|
+
total.charsMeasured = true;
|
|
277
|
+
for (let index = 0; index < total.chars.length; index += 1) {
|
|
278
|
+
total.chars[index] = (total.chars[index] ?? 0) + (counts.chars[index] ?? 0);
|
|
279
|
+
total.charsSamples += counts.chars[index] ?? 0;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
147
282
|
total.hours += 1;
|
|
148
283
|
}
|
|
149
284
|
return total;
|
|
@@ -116,6 +116,10 @@ export declare const MemoryInstallReceiptSchema: z.ZodObject<{
|
|
|
116
116
|
hook_via_daemon_24h: z.ZodOptional<z.ZodNumber>;
|
|
117
117
|
hook_via_direct_24h: z.ZodOptional<z.ZodNumber>;
|
|
118
118
|
hook_skipped_trivial_24h: z.ZodOptional<z.ZodNumber>;
|
|
119
|
+
hook_billing_exhausted_24h: z.ZodOptional<z.ZodNumber>;
|
|
120
|
+
hook_chars_p50: z.ZodOptional<z.ZodNumber>;
|
|
121
|
+
hook_chars_p95: z.ZodOptional<z.ZodNumber>;
|
|
122
|
+
hook_chars_samples: z.ZodOptional<z.ZodNumber>;
|
|
119
123
|
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
120
124
|
hook_performance: z.ZodOptional<z.ZodObject<{
|
|
121
125
|
schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
|
|
@@ -154,6 +154,32 @@ export const MemoryInstallReceiptSchema = z
|
|
|
154
154
|
* none.
|
|
155
155
|
*/
|
|
156
156
|
hook_skipped_trivial_24h: z.number().int().min(0).optional(),
|
|
157
|
+
/**
|
|
158
|
+
* BLI-3891: runs whose recall failed because the PROVIDER ACCOUNT has no
|
|
159
|
+
* credit. A subset of `hook_failed_24h`, and the reason it is on the
|
|
160
|
+
* receipt at all: on 2026-09-06 every machine's recall died for four hours
|
|
161
|
+
* and the only word any surface had for it was "missed the deadline",
|
|
162
|
+
* which sends a person to re-fit a budget instead of to a billing page.
|
|
163
|
+
* Optional like the counts above — absent reads as "not reported".
|
|
164
|
+
*/
|
|
165
|
+
hook_billing_exhausted_24h: z.number().int().min(0).optional(),
|
|
166
|
+
/**
|
|
167
|
+
* HOW BIG WAS A RECALL (ticket 3934) — the p50 and p95 CHARACTERS the
|
|
168
|
+
* prompt hook injected over the same 24 hours, as the lower bound of the
|
|
169
|
+
* bin each percentile fell in, with the number of runs behind them.
|
|
170
|
+
*
|
|
171
|
+
* The timing counters above say whether a recall ARRIVED. These say how
|
|
172
|
+
* much of a person's context it took when it did, which is the other half
|
|
173
|
+
* of "is the recall worth its budget" and had no surface anywhere.
|
|
174
|
+
*
|
|
175
|
+
* All three optional, and ABSENT on a machine whose memory-mcp is older
|
|
176
|
+
* than the histogram — which reads as "not recorded", never as "the
|
|
177
|
+
* recalls were empty". They are not derivable from the counts above and
|
|
178
|
+
* must never be inferred from them.
|
|
179
|
+
*/
|
|
180
|
+
hook_chars_p50: z.number().int().min(0).optional(),
|
|
181
|
+
hook_chars_p95: z.number().int().min(0).optional(),
|
|
182
|
+
hook_chars_samples: z.number().int().min(0).optional(),
|
|
157
183
|
/** Named when the counts are absent because the file could not be read. */
|
|
158
184
|
hook_stats_reason: ReasonLabelSchema.optional(),
|
|
159
185
|
/** Observed ordinary invocations, not legacy floors or a full host trace. */
|