@bli-cockpit/telemetry-core 0.1.37 → 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.
|
@@ -109,6 +109,9 @@ export declare const CollectorHeartbeatSchema: z.ZodObject<{
|
|
|
109
109
|
hook_via_direct_24h: z.ZodOptional<z.ZodNumber>;
|
|
110
110
|
hook_skipped_trivial_24h: z.ZodOptional<z.ZodNumber>;
|
|
111
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>;
|
|
112
115
|
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
113
116
|
hook_performance: z.ZodOptional<z.ZodObject<{
|
|
114
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
|
*
|
|
@@ -49,6 +85,7 @@ export declare const MemoryHookCountsSchema: z.ZodObject<{
|
|
|
49
85
|
via_direct: z.ZodOptional<z.ZodNumber>;
|
|
50
86
|
skipped_trivial: z.ZodOptional<z.ZodNumber>;
|
|
51
87
|
billing_exhausted: z.ZodOptional<z.ZodNumber>;
|
|
88
|
+
chars: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
52
89
|
}, z.core.$strict>;
|
|
53
90
|
export type MemoryHookCounts = z.infer<typeof MemoryHookCountsSchema>;
|
|
54
91
|
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
@@ -70,6 +107,7 @@ export declare const MemoryHookStatsFileSchema: z.ZodObject<{
|
|
|
70
107
|
via_direct: z.ZodOptional<z.ZodNumber>;
|
|
71
108
|
skipped_trivial: z.ZodOptional<z.ZodNumber>;
|
|
72
109
|
billing_exhausted: z.ZodOptional<z.ZodNumber>;
|
|
110
|
+
chars: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
73
111
|
}, z.core.$strict>>>;
|
|
74
112
|
}, z.core.$strict>;
|
|
75
113
|
export type MemoryHookStatsFile = z.infer<typeof MemoryHookStatsFileSchema>;
|
|
@@ -101,6 +139,16 @@ export interface MemoryHookWindow {
|
|
|
101
139
|
* older runs to zero.
|
|
102
140
|
*/
|
|
103
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;
|
|
104
152
|
/** Buckets that were inside the window and had something in them. */
|
|
105
153
|
hours: number;
|
|
106
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
|
*
|
|
@@ -93,6 +158,28 @@ export const MemoryHookCountsSchema = z
|
|
|
93
158
|
* `hook_stats_unrecognised_shape` overnight.
|
|
94
159
|
*/
|
|
95
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(),
|
|
96
183
|
})
|
|
97
184
|
.strict();
|
|
98
185
|
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
@@ -125,6 +212,10 @@ export function emptyMemoryHookCounts() {
|
|
|
125
212
|
skipped: 0,
|
|
126
213
|
skipped_trivial: 0,
|
|
127
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.
|
|
128
219
|
};
|
|
129
220
|
}
|
|
130
221
|
/**
|
|
@@ -149,6 +240,9 @@ export function summariseMemoryHookWindow(file, event, options) {
|
|
|
149
240
|
viaMeasured: false,
|
|
150
241
|
skippedTrivial: 0,
|
|
151
242
|
billingExhausted: 0,
|
|
243
|
+
chars: blankCharsHistogram(),
|
|
244
|
+
charsMeasured: false,
|
|
245
|
+
charsSamples: 0,
|
|
152
246
|
hours: 0,
|
|
153
247
|
};
|
|
154
248
|
for (const [bucket, events] of Object.entries(file.buckets)) {
|
|
@@ -175,6 +269,16 @@ export function summariseMemoryHookWindow(file, event, options) {
|
|
|
175
269
|
// which sums as zero and is honest — that machine genuinely had none it
|
|
176
270
|
// could name.
|
|
177
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
|
+
}
|
|
178
282
|
total.hours += 1;
|
|
179
283
|
}
|
|
180
284
|
return total;
|
|
@@ -117,6 +117,9 @@ export declare const MemoryInstallReceiptSchema: z.ZodObject<{
|
|
|
117
117
|
hook_via_direct_24h: z.ZodOptional<z.ZodNumber>;
|
|
118
118
|
hook_skipped_trivial_24h: z.ZodOptional<z.ZodNumber>;
|
|
119
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>;
|
|
120
123
|
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
121
124
|
hook_performance: z.ZodOptional<z.ZodObject<{
|
|
122
125
|
schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
|
|
@@ -163,6 +163,23 @@ export const MemoryInstallReceiptSchema = z
|
|
|
163
163
|
* Optional like the counts above — absent reads as "not reported".
|
|
164
164
|
*/
|
|
165
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(),
|
|
166
183
|
/** Named when the counts are absent because the file could not be read. */
|
|
167
184
|
hook_stats_reason: ReasonLabelSchema.optional(),
|
|
168
185
|
/** Observed ordinary invocations, not legacy floors or a full host trace. */
|