@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.
- package/dist/collector-heartbeat.d.ts +42 -0
- package/dist/evidence-completeness.d.ts +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ingest-dto.d.ts +2 -2
- package/dist/memory-hook-performance.d.ts +102 -0
- package/dist/memory-hook-performance.js +208 -0
- package/dist/memory-hook-stats.d.ts +4 -0
- package/dist/memory-hook-stats.js +29 -2
- package/dist/memory-install-receipt.d.ts +42 -0
- package/dist/memory-install-receipt.js +12 -0
- package/package.json +1 -1
|
@@ -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";
|
package/dist/ingest-dto.d.ts
CHANGED
|
@@ -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 = {
|
|
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. */
|