@bli-cockpit/telemetry-core 0.1.32 → 0.1.34

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.
@@ -105,7 +105,49 @@ export declare const CollectorHeartbeatSchema: z.ZodObject<{
105
105
  hook_timeouts_24h: z.ZodOptional<z.ZodNumber>;
106
106
  hook_printed_24h: z.ZodOptional<z.ZodNumber>;
107
107
  hook_failed_24h: z.ZodOptional<z.ZodNumber>;
108
+ hook_skipped_trivial_24h: z.ZodOptional<z.ZodNumber>;
108
109
  hook_stats_reason: z.ZodOptional<z.ZodString>;
110
+ hook_performance: z.ZodOptional<z.ZodObject<{
111
+ schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
112
+ window_start: z.ZodString;
113
+ window_end: z.ZodString;
114
+ sampling_since: z.ZodNullable<z.ZodString>;
115
+ samples: z.ZodNumber;
116
+ outcomes: z.ZodObject<{
117
+ printed: z.ZodNumber;
118
+ empty: z.ZodNumber;
119
+ timeouts: z.ZodNumber;
120
+ failed: z.ZodNumber;
121
+ skipped: z.ZodNumber;
122
+ }, z.core.$strict>;
123
+ duration_by_cache: z.ZodObject<{
124
+ hit: z.ZodArray<z.ZodNumber>;
125
+ miss: z.ZodArray<z.ZodNumber>;
126
+ shared: z.ZodArray<z.ZodNumber>;
127
+ unknown: z.ZodArray<z.ZodNumber>;
128
+ }, z.core.$strict>;
129
+ producer_versions: z.ZodArray<z.ZodObject<{
130
+ version: z.ZodString;
131
+ samples: z.ZodNumber;
132
+ }, z.core.$strict>>;
133
+ invalid_samples: z.ZodNumber;
134
+ incomplete_samples: z.ZodNumber;
135
+ unreadable_samples: z.ZodNumber;
136
+ capped: z.ZodBoolean;
137
+ reasons: z.ZodArray<z.ZodEnum<{
138
+ no_samples: "no_samples";
139
+ partial_window: "partial_window";
140
+ lease_absent: "lease_absent";
141
+ lease_expired: "lease_expired";
142
+ lease_invalid: "lease_invalid";
143
+ read_failed: "read_failed";
144
+ read_limit: "read_limit";
145
+ invalid_samples: "invalid_samples";
146
+ incomplete_samples: "incomplete_samples";
147
+ unreadable_samples: "unreadable_samples";
148
+ cleanup_failed: "cleanup_failed";
149
+ }>>;
150
+ }, z.core.$strict>>;
109
151
  }, z.core.$strict>>;
110
152
  setup_receipt: z.ZodOptional<z.ZodObject<{
111
153
  schema_version: z.ZodLiteral<"setup-receipt.v1">;
@@ -2,9 +2,9 @@ import { z } from "zod";
2
2
  export declare const EvidenceCompletenessStatusSchema: z.ZodEnum<{
3
3
  unknown: "unknown";
4
4
  failed: "failed";
5
+ empty: "empty";
5
6
  complete: "complete";
6
7
  partial: "partial";
7
- empty: "empty";
8
8
  unavailable: "unavailable";
9
9
  }>;
10
10
  export type EvidenceCompletenessStatus = z.infer<typeof EvidenceCompletenessStatusSchema>;
@@ -71,9 +71,9 @@ export declare const EvidenceCompletenessPayloadSchema: z.ZodObject<{
71
71
  status: z.ZodEnum<{
72
72
  unknown: "unknown";
73
73
  failed: "failed";
74
+ empty: "empty";
74
75
  complete: "complete";
75
76
  partial: "partial";
76
- empty: "empty";
77
77
  unavailable: "unavailable";
78
78
  }>;
79
79
  generated_at: z.ZodString;
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./evidence-upload.js";
10
10
  export * from "./ingest-dto.js";
11
11
  export * from "./local-config.js";
12
12
  export * from "./memory-hook-stats.js";
13
+ export * from "./memory-hook-performance.js";
13
14
  export * from "./memory-install-receipt.js";
14
15
  export * from "./paths.js";
15
16
  export * from "./privacy.js";
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export * from "./evidence-upload.js";
10
10
  export * from "./ingest-dto.js";
11
11
  export * from "./local-config.js";
12
12
  export * from "./memory-hook-stats.js";
13
+ export * from "./memory-hook-performance.js";
13
14
  export * from "./memory-install-receipt.js";
14
15
  export * from "./paths.js";
15
16
  export * from "./privacy.js";
@@ -105,9 +105,9 @@ export declare const TelemetryIngestEventDtoSchema: z.ZodObject<{
105
105
  status: z.ZodEnum<{
106
106
  unknown: "unknown";
107
107
  failed: "failed";
108
+ empty: "empty";
108
109
  complete: "complete";
109
110
  partial: "partial";
110
- empty: "empty";
111
111
  unavailable: "unavailable";
112
112
  }>;
113
113
  generated_at: z.ZodString;
@@ -584,9 +584,9 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
584
584
  status: z.ZodEnum<{
585
585
  unknown: "unknown";
586
586
  failed: "failed";
587
+ empty: "empty";
587
588
  complete: "complete";
588
589
  partial: "partial";
589
- empty: "empty";
590
590
  unavailable: "unavailable";
591
591
  }>;
592
592
  generated_at: z.ZodString;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Content-free prompt-hook performance samples (BLI-3876).
3
+ *
4
+ * This module is the shared wire and local-file contract only. It deliberately
5
+ * has no filesystem effects: the hook writer and collector reader own I/O.
6
+ * Samples carry closed labels, durations and timestamps, never prompt text,
7
+ * hashes or memory content.
8
+ */
9
+ import { z } from "zod";
10
+ export declare const MEMORY_HOOK_SAMPLE_SCHEMA_VERSION = "memory-hook-sample.v1";
11
+ export declare const MEMORY_HOOK_SAMPLING_LEASE_SCHEMA_VERSION = "memory-hook-sampling-lease.v1";
12
+ export declare const MEMORY_HOOK_PERFORMANCE_SCHEMA_VERSION = "memory-hook-performance.v1";
13
+ export declare const MEMORY_HOOK_CACHE_OUTCOMES: readonly ["hit", "miss", "shared", "unknown"];
14
+ export type MemoryHookCacheOutcome = (typeof MEMORY_HOOK_CACHE_OUTCOMES)[number];
15
+ export declare const MEMORY_HOOK_SAMPLE_OUTCOMES: readonly ["printed", "empty", "timeouts", "failed", "skipped"];
16
+ export type MemoryHookSampleOutcome = (typeof MEMORY_HOOK_SAMPLE_OUTCOMES)[number];
17
+ export declare const MemoryHookSampleSchema: z.ZodObject<{
18
+ schema_version: z.ZodLiteral<"memory-hook-sample.v1">;
19
+ recorded_at: z.ZodString;
20
+ elapsed_ms: z.ZodNumber;
21
+ outcome: z.ZodEnum<{
22
+ failed: "failed";
23
+ skipped: "skipped";
24
+ printed: "printed";
25
+ empty: "empty";
26
+ timeouts: "timeouts";
27
+ }>;
28
+ embed_cache: z.ZodEnum<{
29
+ unknown: "unknown";
30
+ hit: "hit";
31
+ miss: "miss";
32
+ shared: "shared";
33
+ }>;
34
+ producer_version: z.ZodString;
35
+ }, z.core.$strict>;
36
+ export type MemoryHookSample = z.infer<typeof MemoryHookSampleSchema>;
37
+ export declare const MemoryHookSamplingLeaseSchema: z.ZodObject<{
38
+ schema_version: z.ZodLiteral<"memory-hook-sampling-lease.v1">;
39
+ continuous_since: z.ZodString;
40
+ expires_at: z.ZodString;
41
+ }, z.core.$strict>;
42
+ export type MemoryHookSamplingLease = z.infer<typeof MemoryHookSamplingLeaseSchema>;
43
+ /** The samples directory below the user's existing Cockpit state directory. */
44
+ export declare const MEMORY_HOOK_SAMPLES_DIRECTORY_NAME = "memory-hook-samples";
45
+ export declare function memoryHookSamplesDirectory(homeDir: string): string;
46
+ export declare const MEMORY_HOOK_HISTOGRAM_BIN_WIDTH_MS = 100;
47
+ export declare const MEMORY_HOOK_HISTOGRAM_LENGTH = 51;
48
+ /** A fresh [0,100), ... [4900,5000), [5000,infinity) histogram. */
49
+ export declare function blankHistogram(): number[];
50
+ /** Add one valid duration and return the same histogram for convenient chaining. */
51
+ export declare function addDuration(histogram: number[], durationMs: number): number[];
52
+ /**
53
+ * Return the nearest-rank bin bounds. The result never interpolates between
54
+ * bins, and the open-ended final bin has no finite upper bound.
55
+ */
56
+ export declare function histogramPercentileBounds(histogram: number[], percentile: number): {
57
+ lower_ms: number;
58
+ upper_ms: number | null;
59
+ } | null;
60
+ export declare const MEMORY_HOOK_PERFORMANCE_REASONS: readonly ["no_samples", "partial_window", "lease_absent", "lease_expired", "lease_invalid", "read_failed", "read_limit", "invalid_samples", "incomplete_samples", "unreadable_samples", "cleanup_failed"];
61
+ export declare const MemoryHookPerformanceSchema: z.ZodObject<{
62
+ schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
63
+ window_start: z.ZodString;
64
+ window_end: z.ZodString;
65
+ sampling_since: z.ZodNullable<z.ZodString>;
66
+ samples: z.ZodNumber;
67
+ outcomes: z.ZodObject<{
68
+ printed: z.ZodNumber;
69
+ empty: z.ZodNumber;
70
+ timeouts: z.ZodNumber;
71
+ failed: z.ZodNumber;
72
+ skipped: z.ZodNumber;
73
+ }, z.core.$strict>;
74
+ duration_by_cache: z.ZodObject<{
75
+ hit: z.ZodArray<z.ZodNumber>;
76
+ miss: z.ZodArray<z.ZodNumber>;
77
+ shared: z.ZodArray<z.ZodNumber>;
78
+ unknown: z.ZodArray<z.ZodNumber>;
79
+ }, z.core.$strict>;
80
+ producer_versions: z.ZodArray<z.ZodObject<{
81
+ version: z.ZodString;
82
+ samples: z.ZodNumber;
83
+ }, z.core.$strict>>;
84
+ invalid_samples: z.ZodNumber;
85
+ incomplete_samples: z.ZodNumber;
86
+ unreadable_samples: z.ZodNumber;
87
+ capped: z.ZodBoolean;
88
+ reasons: z.ZodArray<z.ZodEnum<{
89
+ no_samples: "no_samples";
90
+ partial_window: "partial_window";
91
+ lease_absent: "lease_absent";
92
+ lease_expired: "lease_expired";
93
+ lease_invalid: "lease_invalid";
94
+ read_failed: "read_failed";
95
+ read_limit: "read_limit";
96
+ invalid_samples: "invalid_samples";
97
+ incomplete_samples: "incomplete_samples";
98
+ unreadable_samples: "unreadable_samples";
99
+ cleanup_failed: "cleanup_failed";
100
+ }>>;
101
+ }, z.core.$strict>;
102
+ export type MemoryHookPerformance = z.infer<typeof MemoryHookPerformanceSchema>;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Content-free prompt-hook performance samples (BLI-3876).
3
+ *
4
+ * This module is the shared wire and local-file contract only. It deliberately
5
+ * has no filesystem effects: the hook writer and collector reader own I/O.
6
+ * Samples carry closed labels, durations and timestamps, never prompt text,
7
+ * hashes or memory content.
8
+ */
9
+ import { z } from "zod";
10
+ import { IsoDateTimeSchema } from "./common.js";
11
+ import { getUserLocalCockpitPaths } from "./paths.js";
12
+ export const MEMORY_HOOK_SAMPLE_SCHEMA_VERSION = "memory-hook-sample.v1";
13
+ export const MEMORY_HOOK_SAMPLING_LEASE_SCHEMA_VERSION = "memory-hook-sampling-lease.v1";
14
+ export const MEMORY_HOOK_PERFORMANCE_SCHEMA_VERSION = "memory-hook-performance.v1";
15
+ export const MEMORY_HOOK_CACHE_OUTCOMES = [
16
+ "hit",
17
+ "miss",
18
+ "shared",
19
+ "unknown",
20
+ ];
21
+ export const MEMORY_HOOK_SAMPLE_OUTCOMES = [
22
+ "printed",
23
+ "empty",
24
+ "timeouts",
25
+ "failed",
26
+ "skipped",
27
+ ];
28
+ const CacheOutcomeSchema = z.enum(MEMORY_HOOK_CACHE_OUTCOMES);
29
+ const SampleOutcomeSchema = z.enum(MEMORY_HOOK_SAMPLE_OUTCOMES);
30
+ const CountSchema = z.number().int().nonnegative();
31
+ const SemverSchema = z
32
+ .string()
33
+ .min(1)
34
+ .max(40)
35
+ .regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u);
36
+ export const MemoryHookSampleSchema = z
37
+ .object({
38
+ schema_version: z.literal(MEMORY_HOOK_SAMPLE_SCHEMA_VERSION),
39
+ recorded_at: IsoDateTimeSchema,
40
+ elapsed_ms: z.number().finite().int().nonnegative().max(60_000),
41
+ outcome: SampleOutcomeSchema,
42
+ embed_cache: CacheOutcomeSchema,
43
+ producer_version: SemverSchema,
44
+ })
45
+ .strict();
46
+ export const MemoryHookSamplingLeaseSchema = z
47
+ .object({
48
+ schema_version: z.literal(MEMORY_HOOK_SAMPLING_LEASE_SCHEMA_VERSION),
49
+ continuous_since: IsoDateTimeSchema,
50
+ expires_at: IsoDateTimeSchema,
51
+ })
52
+ .strict();
53
+ /** The samples directory below the user's existing Cockpit state directory. */
54
+ export const MEMORY_HOOK_SAMPLES_DIRECTORY_NAME = "memory-hook-samples";
55
+ export function memoryHookSamplesDirectory(homeDir) {
56
+ const paths = getUserLocalCockpitPaths(homeDir);
57
+ const separator = paths.state_dir.includes("\\") ? "\\" : "/";
58
+ return `${paths.state_dir}${separator}${MEMORY_HOOK_SAMPLES_DIRECTORY_NAME}`;
59
+ }
60
+ export const MEMORY_HOOK_HISTOGRAM_BIN_WIDTH_MS = 100;
61
+ export const MEMORY_HOOK_HISTOGRAM_LENGTH = 51;
62
+ /** A fresh [0,100), ... [4900,5000), [5000,infinity) histogram. */
63
+ export function blankHistogram() {
64
+ return Array.from({ length: MEMORY_HOOK_HISTOGRAM_LENGTH }, () => 0);
65
+ }
66
+ function validateHistogram(histogram) {
67
+ if (histogram.length !== MEMORY_HOOK_HISTOGRAM_LENGTH) {
68
+ throw new RangeError(`histogram must have ${MEMORY_HOOK_HISTOGRAM_LENGTH} bins`);
69
+ }
70
+ for (const count of histogram) {
71
+ if (!Number.isInteger(count) || count < 0) {
72
+ throw new RangeError("histogram counts must be non-negative integers");
73
+ }
74
+ }
75
+ }
76
+ /** Add one valid duration and return the same histogram for convenient chaining. */
77
+ export function addDuration(histogram, durationMs) {
78
+ validateHistogram(histogram);
79
+ if (!Number.isFinite(durationMs) || !Number.isInteger(durationMs) || durationMs < 0) {
80
+ throw new RangeError("duration must be a non-negative integer");
81
+ }
82
+ if (durationMs > 60_000) {
83
+ throw new RangeError("duration must be at most 60000 ms");
84
+ }
85
+ const bin = Math.min(Math.floor(durationMs / MEMORY_HOOK_HISTOGRAM_BIN_WIDTH_MS), MEMORY_HOOK_HISTOGRAM_LENGTH - 1);
86
+ histogram[bin] += 1;
87
+ return histogram;
88
+ }
89
+ /**
90
+ * Return the nearest-rank bin bounds. The result never interpolates between
91
+ * bins, and the open-ended final bin has no finite upper bound.
92
+ */
93
+ export function histogramPercentileBounds(histogram, percentile) {
94
+ validateHistogram(histogram);
95
+ if (!Number.isFinite(percentile) || percentile < 0 || percentile > 100) {
96
+ throw new RangeError("percentile must be between 0 and 100");
97
+ }
98
+ const samples = histogram.reduce((sum, count) => sum + count, 0);
99
+ if (samples === 0)
100
+ return null;
101
+ const rank = Math.max(1, Math.ceil((percentile / 100) * samples));
102
+ let cumulative = 0;
103
+ for (let index = 0; index < histogram.length; index += 1) {
104
+ cumulative += histogram[index] ?? 0;
105
+ if (cumulative >= rank) {
106
+ return {
107
+ lower_ms: index * MEMORY_HOOK_HISTOGRAM_BIN_WIDTH_MS,
108
+ upper_ms: index === MEMORY_HOOK_HISTOGRAM_LENGTH - 1
109
+ ? null
110
+ : (index + 1) * MEMORY_HOOK_HISTOGRAM_BIN_WIDTH_MS,
111
+ };
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ const OutcomeCountsSchema = z
117
+ .object({
118
+ printed: CountSchema,
119
+ empty: CountSchema,
120
+ timeouts: CountSchema,
121
+ failed: CountSchema,
122
+ skipped: CountSchema,
123
+ })
124
+ .strict();
125
+ const HistogramSchema = z
126
+ .array(CountSchema)
127
+ .length(MEMORY_HOOK_HISTOGRAM_LENGTH);
128
+ const DurationByCacheSchema = z
129
+ .object({
130
+ hit: HistogramSchema,
131
+ miss: HistogramSchema,
132
+ shared: HistogramSchema,
133
+ unknown: HistogramSchema,
134
+ })
135
+ .strict();
136
+ const ProducerVersionCountSchema = z
137
+ .object({
138
+ version: SemverSchema,
139
+ samples: CountSchema,
140
+ })
141
+ .strict();
142
+ export const MEMORY_HOOK_PERFORMANCE_REASONS = [
143
+ "no_samples",
144
+ "partial_window",
145
+ "lease_absent",
146
+ "lease_expired",
147
+ "lease_invalid",
148
+ "read_failed",
149
+ "read_limit",
150
+ "invalid_samples",
151
+ "incomplete_samples",
152
+ "unreadable_samples",
153
+ "cleanup_failed",
154
+ ];
155
+ const PerformanceReasonSchema = z.enum(MEMORY_HOOK_PERFORMANCE_REASONS);
156
+ export const MemoryHookPerformanceSchema = z
157
+ .object({
158
+ schema_version: z.literal(MEMORY_HOOK_PERFORMANCE_SCHEMA_VERSION),
159
+ window_start: IsoDateTimeSchema,
160
+ window_end: IsoDateTimeSchema,
161
+ sampling_since: IsoDateTimeSchema.nullable(),
162
+ samples: CountSchema,
163
+ outcomes: OutcomeCountsSchema,
164
+ duration_by_cache: DurationByCacheSchema,
165
+ producer_versions: z.array(ProducerVersionCountSchema).max(16),
166
+ invalid_samples: CountSchema,
167
+ incomplete_samples: CountSchema,
168
+ unreadable_samples: CountSchema,
169
+ capped: z.boolean(),
170
+ reasons: z.array(PerformanceReasonSchema).max(MEMORY_HOOK_PERFORMANCE_REASONS.length),
171
+ })
172
+ .strict()
173
+ .superRefine((value, context) => {
174
+ const outcomeTotal = Object.values(value.outcomes).reduce((sum, count) => sum + count, 0);
175
+ if (outcomeTotal !== value.samples) {
176
+ context.addIssue({
177
+ code: "custom",
178
+ path: ["outcomes"],
179
+ message: "outcome counts must sum to samples",
180
+ });
181
+ }
182
+ const histogramTotal = Object.values(value.duration_by_cache)
183
+ .flat()
184
+ .reduce((sum, count) => sum + count, 0);
185
+ if (histogramTotal !== value.samples) {
186
+ context.addIssue({
187
+ code: "custom",
188
+ path: ["duration_by_cache"],
189
+ message: "duration histograms must sum to samples",
190
+ });
191
+ }
192
+ const versionTotal = value.producer_versions.reduce((sum, item) => sum + item.samples, 0);
193
+ if (versionTotal !== value.samples) {
194
+ context.addIssue({
195
+ code: "custom",
196
+ path: ["producer_versions"],
197
+ message: "producer version counts must sum to samples",
198
+ });
199
+ }
200
+ const versions = value.producer_versions.map((item) => item.version);
201
+ if (new Set(versions).size !== versions.length) {
202
+ context.addIssue({
203
+ code: "custom",
204
+ path: ["producer_versions"],
205
+ message: "producer versions must be unique",
206
+ });
207
+ }
208
+ });
@@ -45,6 +45,7 @@ export declare const MemoryHookCountsSchema: z.ZodObject<{
45
45
  timeouts: z.ZodNumber;
46
46
  failed: z.ZodNumber;
47
47
  skipped: z.ZodNumber;
48
+ skipped_trivial: z.ZodOptional<z.ZodNumber>;
48
49
  }, z.core.$strict>;
49
50
  export type MemoryHookCounts = z.infer<typeof MemoryHookCountsSchema>;
50
51
  /** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
@@ -62,6 +63,7 @@ export declare const MemoryHookStatsFileSchema: z.ZodObject<{
62
63
  timeouts: z.ZodNumber;
63
64
  failed: z.ZodNumber;
64
65
  skipped: z.ZodNumber;
66
+ skipped_trivial: z.ZodOptional<z.ZodNumber>;
65
67
  }, z.core.$strict>>>;
66
68
  }, z.core.$strict>;
67
69
  export type MemoryHookStatsFile = z.infer<typeof MemoryHookStatsFileSchema>;
@@ -74,6 +76,8 @@ export interface MemoryHookWindow {
74
76
  printed: number;
75
77
  timeouts: number;
76
78
  failed: number;
79
+ /** Trivial prompts the hook declined to search for (BLI-3881). A subset of skips. */
80
+ skippedTrivial: number;
77
81
  /** Buckets that were inside the window and had something in them. */
78
82
  hours: number;
79
83
  }
@@ -46,6 +46,23 @@ export const MemoryHookCountsSchema = z
46
46
  timeouts: z.number().int().min(0),
47
47
  failed: z.number().int().min(0),
48
48
  skipped: z.number().int().min(0),
49
+ /**
50
+ * BLI-3881 — the SUBSET of `skipped` the hook declined to search for.
51
+ *
52
+ * "ok", "continue", "y", a slash command: a turn whose whole text carries
53
+ * no subject to recall against. Searching one spends a person's whole
54
+ * budget on a query that can only return noise, so the hook returns early
55
+ * and this counts how often — because a hook that stops doing something
56
+ * and says nothing is indistinguishable from a hook that broke.
57
+ *
58
+ * OPTIONAL, and it must stay optional: a file written by a memory-mcp
59
+ * older than this ticket has no such key, and `MemoryHookCountsSchema` is
60
+ * strict, so a required field would make every existing machine's history
61
+ * `hook_stats_unrecognised_shape` overnight. It is counted BESIDE
62
+ * `skipped`, never instead of it, so `runs = printed + empty + timeouts +
63
+ * failed + skipped` still holds.
64
+ */
65
+ skipped_trivial: z.number().int().min(0).optional(),
49
66
  })
50
67
  .strict();
51
68
  /** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
@@ -69,7 +86,7 @@ export function memoryHookHourBucket(at) {
69
86
  return at.toISOString().slice(0, 13);
70
87
  }
71
88
  export function emptyMemoryHookCounts() {
72
- return { runs: 0, printed: 0, empty: 0, timeouts: 0, failed: 0, skipped: 0 };
89
+ return { runs: 0, printed: 0, empty: 0, timeouts: 0, failed: 0, skipped: 0, skipped_trivial: 0 };
73
90
  }
74
91
  /**
75
92
  * Sum one event's buckets over the last `hours` hours, ending at `now`.
@@ -83,7 +100,14 @@ export function summariseMemoryHookWindow(file, event, options) {
83
100
  const hours = options.hours ?? 24;
84
101
  const earliest = memoryHookHourBucket(new Date(options.now.getTime() - (hours - 1) * 3_600_000));
85
102
  const latest = memoryHookHourBucket(options.now);
86
- const total = { runs: 0, printed: 0, timeouts: 0, failed: 0, hours: 0 };
103
+ const total = {
104
+ runs: 0,
105
+ printed: 0,
106
+ timeouts: 0,
107
+ failed: 0,
108
+ skippedTrivial: 0,
109
+ hours: 0,
110
+ };
87
111
  for (const [bucket, events] of Object.entries(file.buckets)) {
88
112
  // Lexical order is chronological for this key, which is the only reason a
89
113
  // string comparison is allowed to stand in for a date one here.
@@ -96,6 +120,9 @@ export function summariseMemoryHookWindow(file, event, options) {
96
120
  total.printed += counts.printed;
97
121
  total.timeouts += counts.timeouts;
98
122
  total.failed += counts.failed;
123
+ // Absent means an older writer never counted one, which sums as zero and
124
+ // is honest: that machine genuinely skipped none, because it could not.
125
+ total.skippedTrivial += counts.skipped_trivial ?? 0;
99
126
  total.hours += 1;
100
127
  }
101
128
  return total;
@@ -113,7 +113,49 @@ export declare const MemoryInstallReceiptSchema: z.ZodObject<{
113
113
  hook_timeouts_24h: z.ZodOptional<z.ZodNumber>;
114
114
  hook_printed_24h: z.ZodOptional<z.ZodNumber>;
115
115
  hook_failed_24h: z.ZodOptional<z.ZodNumber>;
116
+ hook_skipped_trivial_24h: z.ZodOptional<z.ZodNumber>;
116
117
  hook_stats_reason: z.ZodOptional<z.ZodString>;
118
+ hook_performance: z.ZodOptional<z.ZodObject<{
119
+ schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
120
+ window_start: z.ZodString;
121
+ window_end: z.ZodString;
122
+ sampling_since: z.ZodNullable<z.ZodString>;
123
+ samples: z.ZodNumber;
124
+ outcomes: z.ZodObject<{
125
+ printed: z.ZodNumber;
126
+ empty: z.ZodNumber;
127
+ timeouts: z.ZodNumber;
128
+ failed: z.ZodNumber;
129
+ skipped: z.ZodNumber;
130
+ }, z.core.$strict>;
131
+ duration_by_cache: z.ZodObject<{
132
+ hit: z.ZodArray<z.ZodNumber>;
133
+ miss: z.ZodArray<z.ZodNumber>;
134
+ shared: z.ZodArray<z.ZodNumber>;
135
+ unknown: z.ZodArray<z.ZodNumber>;
136
+ }, z.core.$strict>;
137
+ producer_versions: z.ZodArray<z.ZodObject<{
138
+ version: z.ZodString;
139
+ samples: z.ZodNumber;
140
+ }, z.core.$strict>>;
141
+ invalid_samples: z.ZodNumber;
142
+ incomplete_samples: z.ZodNumber;
143
+ unreadable_samples: z.ZodNumber;
144
+ capped: z.ZodBoolean;
145
+ reasons: z.ZodArray<z.ZodEnum<{
146
+ no_samples: "no_samples";
147
+ partial_window: "partial_window";
148
+ lease_absent: "lease_absent";
149
+ lease_expired: "lease_expired";
150
+ lease_invalid: "lease_invalid";
151
+ read_failed: "read_failed";
152
+ read_limit: "read_limit";
153
+ invalid_samples: "invalid_samples";
154
+ incomplete_samples: "incomplete_samples";
155
+ unreadable_samples: "unreadable_samples";
156
+ cleanup_failed: "cleanup_failed";
157
+ }>>;
158
+ }, z.core.$strict>>;
117
159
  }, z.core.$strict>;
118
160
  export type MemoryInstallReceipt = z.infer<typeof MemoryInstallReceiptSchema>;
119
161
  /** Every piece is `ok`. The one question the board asks first. */
@@ -33,6 +33,7 @@
33
33
  */
34
34
  import { z } from "zod";
35
35
  import { IsoDateTimeSchema } from "./common.js";
36
+ import { MemoryHookPerformanceSchema } from "./memory-hook-performance.js";
36
37
  export const MEMORY_INSTALL_RECEIPT_SCHEMA_VERSION = "memory-install-receipt.v1";
37
38
  /**
38
39
  * What one piece of the installation is.
@@ -131,8 +132,19 @@ export const MemoryInstallReceiptSchema = z
131
132
  hook_timeouts_24h: z.number().int().min(0).optional(),
132
133
  hook_printed_24h: z.number().int().min(0).optional(),
133
134
  hook_failed_24h: z.number().int().min(0).optional(),
135
+ /**
136
+ * BLI-3881: runs the hook DECLINED to search for — "ok", "continue", a
137
+ * slash command. It is a subset of the skips inside `hook_runs_24h`, and
138
+ * it exists because a hook that quietly stops searching looks exactly like
139
+ * a hook that broke. Optional like the four above: absent means the
140
+ * machine is on a memory-mcp older than the rule, never that it skipped
141
+ * none.
142
+ */
143
+ hook_skipped_trivial_24h: z.number().int().min(0).optional(),
134
144
  /** Named when the counts are absent because the file could not be read. */
135
145
  hook_stats_reason: ReasonLabelSchema.optional(),
146
+ /** Observed ordinary invocations, not legacy floors or a full host trace. */
147
+ hook_performance: MemoryHookPerformanceSchema.optional(),
136
148
  })
137
149
  .strict();
138
150
  /** Every piece is `ok`. The one question the board asks first. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/telemetry-core",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",