@bli-cockpit/telemetry-core 0.1.30 → 0.1.32
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 +7 -0
- package/dist/collector-heartbeat.js +31 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/memory-hook-stats.d.ts +105 -0
- package/dist/memory-hook-stats.js +122 -0
- package/dist/memory-install-receipt.d.ts +5 -0
- package/dist/memory-install-receipt.js +30 -0
- package/package.json +1 -1
|
@@ -58,6 +58,8 @@ export declare const CollectorHeartbeatSchema: z.ZodObject<{
|
|
|
58
58
|
sessions_outside_root: z.ZodOptional<z.ZodNumber>;
|
|
59
59
|
sessions_new_this_tick: z.ZodOptional<z.ZodNumber>;
|
|
60
60
|
sessions_pending_upload: z.ZodOptional<z.ZodNumber>;
|
|
61
|
+
staging_uncommitted_bytes: z.ZodOptional<z.ZodNumber>;
|
|
62
|
+
staging_uncommitted_reason: z.ZodOptional<z.ZodString>;
|
|
61
63
|
memory_install: z.ZodOptional<z.ZodObject<{
|
|
62
64
|
schema_version: z.ZodLiteral<"memory-install-receipt.v1">;
|
|
63
65
|
checked_at: z.ZodString;
|
|
@@ -99,6 +101,11 @@ export declare const CollectorHeartbeatSchema: z.ZodObject<{
|
|
|
99
101
|
}, z.core.$strict>;
|
|
100
102
|
reasons: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
101
103
|
bin_found: z.ZodOptional<z.ZodBoolean>;
|
|
104
|
+
hook_runs_24h: z.ZodOptional<z.ZodNumber>;
|
|
105
|
+
hook_timeouts_24h: z.ZodOptional<z.ZodNumber>;
|
|
106
|
+
hook_printed_24h: z.ZodOptional<z.ZodNumber>;
|
|
107
|
+
hook_failed_24h: z.ZodOptional<z.ZodNumber>;
|
|
108
|
+
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
102
109
|
}, z.core.$strict>>;
|
|
103
110
|
setup_receipt: z.ZodOptional<z.ZodObject<{
|
|
104
111
|
schema_version: z.ZodLiteral<"setup-receipt.v1">;
|
|
@@ -114,6 +114,37 @@ export const CollectorHeartbeatSchema = z
|
|
|
114
114
|
.nonnegative()
|
|
115
115
|
.max(1_000_000)
|
|
116
116
|
.optional(),
|
|
117
|
+
/**
|
|
118
|
+
* BLI-3797. How many staged raw-evidence bytes on this machine the upload
|
|
119
|
+
* ledger cannot vouch for, and the one label that explains most of them.
|
|
120
|
+
*
|
|
121
|
+
* These are the "collection has not landed" numbers. A machine can be
|
|
122
|
+
* alive, syncing, green on every other counter here and still be holding
|
|
123
|
+
* gigabytes of transcript that never reached storage — on the reference Mac
|
|
124
|
+
* 1,836 MB across 234 objects, the oldest staged 45 days earlier, with the
|
|
125
|
+
* server itself answering `unknown_to_server` when asked. Nothing in this
|
|
126
|
+
* heartbeat could say so, so `cockpit ops` could not either.
|
|
127
|
+
*
|
|
128
|
+
* `staging_uncommitted_reason` follows `last_sync_reason`'s
|
|
129
|
+
* `nothing_in_root:47` idiom — `<dominant reason>:<object count>` — so one
|
|
130
|
+
* bounded label carries both halves and a reader prints it verbatim rather
|
|
131
|
+
* than parsing it. Both fields are optional: a collector too old to compute
|
|
132
|
+
* them keeps posting, and an absent value reads as "this collector predates
|
|
133
|
+
* the counter", never as "this machine holds nothing".
|
|
134
|
+
*/
|
|
135
|
+
staging_uncommitted_bytes: z
|
|
136
|
+
.number()
|
|
137
|
+
.int()
|
|
138
|
+
.nonnegative()
|
|
139
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
140
|
+
.optional(),
|
|
141
|
+
staging_uncommitted_reason: z
|
|
142
|
+
.string()
|
|
143
|
+
.trim()
|
|
144
|
+
.min(1)
|
|
145
|
+
.max(120)
|
|
146
|
+
.regex(/^[a-z0-9_:.-]+$/iu)
|
|
147
|
+
.optional(),
|
|
117
148
|
/**
|
|
118
149
|
* BLI-3729. Is BLI Memory actually switched on for both agent hosts on
|
|
119
150
|
* this machine? Six words computed by reading the host configs BACK, not
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./evidence-reconcile.js";
|
|
|
9
9
|
export * from "./evidence-upload.js";
|
|
10
10
|
export * from "./ingest-dto.js";
|
|
11
11
|
export * from "./local-config.js";
|
|
12
|
+
export * from "./memory-hook-stats.js";
|
|
12
13
|
export * from "./memory-install-receipt.js";
|
|
13
14
|
export * from "./paths.js";
|
|
14
15
|
export * from "./privacy.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./evidence-reconcile.js";
|
|
|
9
9
|
export * from "./evidence-upload.js";
|
|
10
10
|
export * from "./ingest-dto.js";
|
|
11
11
|
export * from "./local-config.js";
|
|
12
|
+
export * from "./memory-hook-stats.js";
|
|
12
13
|
export * from "./memory-install-receipt.js";
|
|
13
14
|
export * from "./paths.js";
|
|
14
15
|
export * from "./privacy.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE HOOK COUNTER, SHARED (BLI-3788).
|
|
3
|
+
*
|
|
4
|
+
* `@bli-cockpit/memory-mcp` WRITES this file — one hook run at a time, on the
|
|
5
|
+
* machine, counts only — and `@bli-cockpit/local-collector` READS it once a
|
|
6
|
+
* tick to put `hook_timeouts_24h` on the heartbeat's memory receipt. The two
|
|
7
|
+
* packages are published separately and never import each other, so the
|
|
8
|
+
* contract between them lives here, in the one package they both depend on:
|
|
9
|
+
* the file's address, its shape, and the rule for turning hourly buckets into
|
|
10
|
+
* a 24-hour number.
|
|
11
|
+
*
|
|
12
|
+
* The alternative was two copies of a JSON shape kept in step by comment, and
|
|
13
|
+
* BLI-2541 is what that costs — a fixture built from our own formatting rather
|
|
14
|
+
* than from the real thing, green while the fleet was broken.
|
|
15
|
+
*
|
|
16
|
+
* WHY IT EXISTS AT ALL: a prompt hook that misses its deadline prints NOTHING,
|
|
17
|
+
* deliberately. That is right for the person and blind for the operator, and
|
|
18
|
+
* QA tick 18 found the recall silently missing on 4 of 6 runs with no surface
|
|
19
|
+
* anywhere reporting it. These counts are that silence made countable.
|
|
20
|
+
*
|
|
21
|
+
* WHAT MAY NEVER BE IN IT: a prompt, a memory, a container tag, a path, a
|
|
22
|
+
* token, or any string that did not come from the hook's own closed reason
|
|
23
|
+
* vocabulary. Counts and hour labels, nothing else.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
export declare const MEMORY_HOOK_STATS_SCHEMA_VERSION = "memory-hook-stats.v1";
|
|
27
|
+
/** The file name, in the collector's state directory. One spelling, here. */
|
|
28
|
+
export declare const MEMORY_HOOK_STATS_FILE_NAME = "memory-hook-stats.json";
|
|
29
|
+
/** The three hook events, spelled as the bin's subcommands are. */
|
|
30
|
+
export declare const MEMORY_HOOK_STAT_EVENTS: readonly ["session-start", "prompt", "stop"];
|
|
31
|
+
export type MemoryHookStatEvent = (typeof MEMORY_HOOK_STAT_EVENTS)[number];
|
|
32
|
+
/**
|
|
33
|
+
* One hour of one event.
|
|
34
|
+
*
|
|
35
|
+
* `timeouts` is counted apart from `failed` (a refusal, an outage, an
|
|
36
|
+
* unpaired machine — different fixes) and apart from `empty` (the record was
|
|
37
|
+
* genuinely silent, which is an answer and not a fault). Collapsing them is
|
|
38
|
+
* exactly how "the door is slow" and "the shelf is empty" became one
|
|
39
|
+
* unreadable number in the first place.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MemoryHookCountsSchema: z.ZodObject<{
|
|
42
|
+
runs: z.ZodNumber;
|
|
43
|
+
printed: z.ZodNumber;
|
|
44
|
+
empty: z.ZodNumber;
|
|
45
|
+
timeouts: z.ZodNumber;
|
|
46
|
+
failed: z.ZodNumber;
|
|
47
|
+
skipped: z.ZodNumber;
|
|
48
|
+
}, z.core.$strict>;
|
|
49
|
+
export type MemoryHookCounts = z.infer<typeof MemoryHookCountsSchema>;
|
|
50
|
+
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
51
|
+
export declare const MEMORY_HOOK_BUCKET_PATTERN: RegExp;
|
|
52
|
+
export declare const MemoryHookStatsFileSchema: z.ZodObject<{
|
|
53
|
+
schema_version: z.ZodLiteral<"memory-hook-stats.v1">;
|
|
54
|
+
buckets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodEnum<{
|
|
55
|
+
"session-start": "session-start";
|
|
56
|
+
prompt: "prompt";
|
|
57
|
+
stop: "stop";
|
|
58
|
+
}> & z.core.$partial, z.ZodObject<{
|
|
59
|
+
runs: z.ZodNumber;
|
|
60
|
+
printed: z.ZodNumber;
|
|
61
|
+
empty: z.ZodNumber;
|
|
62
|
+
timeouts: z.ZodNumber;
|
|
63
|
+
failed: z.ZodNumber;
|
|
64
|
+
skipped: z.ZodNumber;
|
|
65
|
+
}, z.core.$strict>>>;
|
|
66
|
+
}, z.core.$strict>;
|
|
67
|
+
export type MemoryHookStatsFile = z.infer<typeof MemoryHookStatsFileSchema>;
|
|
68
|
+
export declare function memoryHookStatsFilePath(homeDir: string): string;
|
|
69
|
+
/** The hour a moment belongs to. UTC, so two machines agree on a boundary. */
|
|
70
|
+
export declare function memoryHookHourBucket(at: Date): string;
|
|
71
|
+
export declare function emptyMemoryHookCounts(): MemoryHookCounts;
|
|
72
|
+
export interface MemoryHookWindow {
|
|
73
|
+
runs: number;
|
|
74
|
+
printed: number;
|
|
75
|
+
timeouts: number;
|
|
76
|
+
failed: number;
|
|
77
|
+
/** Buckets that were inside the window and had something in them. */
|
|
78
|
+
hours: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Sum one event's buckets over the last `hours` hours, ending at `now`.
|
|
82
|
+
*
|
|
83
|
+
* The window is inclusive of the hour `now` falls in, so a run three minutes
|
|
84
|
+
* ago counts; that makes the last bucket partial, which is the honest
|
|
85
|
+
* direction for a gauge whose whole job is to notice that something is going
|
|
86
|
+
* wrong RIGHT NOW.
|
|
87
|
+
*/
|
|
88
|
+
export declare function summariseMemoryHookWindow(file: MemoryHookStatsFile, event: MemoryHookStatEvent, options: {
|
|
89
|
+
now: Date;
|
|
90
|
+
hours?: number;
|
|
91
|
+
}): MemoryHookWindow;
|
|
92
|
+
/**
|
|
93
|
+
* Parse what is on disk, or say why not.
|
|
94
|
+
*
|
|
95
|
+
* A file this version does not recognise is REPLACED by the writer and
|
|
96
|
+
* reported as unreadable by the reader — never merged into a shape it was not
|
|
97
|
+
* written in, which would publish a number nobody can explain.
|
|
98
|
+
*/
|
|
99
|
+
export declare function parseMemoryHookStats(raw: string): {
|
|
100
|
+
ok: true;
|
|
101
|
+
file: MemoryHookStatsFile;
|
|
102
|
+
} | {
|
|
103
|
+
ok: false;
|
|
104
|
+
reason: string;
|
|
105
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE HOOK COUNTER, SHARED (BLI-3788).
|
|
3
|
+
*
|
|
4
|
+
* `@bli-cockpit/memory-mcp` WRITES this file — one hook run at a time, on the
|
|
5
|
+
* machine, counts only — and `@bli-cockpit/local-collector` READS it once a
|
|
6
|
+
* tick to put `hook_timeouts_24h` on the heartbeat's memory receipt. The two
|
|
7
|
+
* packages are published separately and never import each other, so the
|
|
8
|
+
* contract between them lives here, in the one package they both depend on:
|
|
9
|
+
* the file's address, its shape, and the rule for turning hourly buckets into
|
|
10
|
+
* a 24-hour number.
|
|
11
|
+
*
|
|
12
|
+
* The alternative was two copies of a JSON shape kept in step by comment, and
|
|
13
|
+
* BLI-2541 is what that costs — a fixture built from our own formatting rather
|
|
14
|
+
* than from the real thing, green while the fleet was broken.
|
|
15
|
+
*
|
|
16
|
+
* WHY IT EXISTS AT ALL: a prompt hook that misses its deadline prints NOTHING,
|
|
17
|
+
* deliberately. That is right for the person and blind for the operator, and
|
|
18
|
+
* QA tick 18 found the recall silently missing on 4 of 6 runs with no surface
|
|
19
|
+
* anywhere reporting it. These counts are that silence made countable.
|
|
20
|
+
*
|
|
21
|
+
* WHAT MAY NEVER BE IN IT: a prompt, a memory, a container tag, a path, a
|
|
22
|
+
* token, or any string that did not come from the hook's own closed reason
|
|
23
|
+
* vocabulary. Counts and hour labels, nothing else.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { getUserLocalCockpitPaths } from "./paths.js";
|
|
27
|
+
export const MEMORY_HOOK_STATS_SCHEMA_VERSION = "memory-hook-stats.v1";
|
|
28
|
+
/** The file name, in the collector's state directory. One spelling, here. */
|
|
29
|
+
export const MEMORY_HOOK_STATS_FILE_NAME = "memory-hook-stats.json";
|
|
30
|
+
/** The three hook events, spelled as the bin's subcommands are. */
|
|
31
|
+
export const MEMORY_HOOK_STAT_EVENTS = ["session-start", "prompt", "stop"];
|
|
32
|
+
/**
|
|
33
|
+
* One hour of one event.
|
|
34
|
+
*
|
|
35
|
+
* `timeouts` is counted apart from `failed` (a refusal, an outage, an
|
|
36
|
+
* unpaired machine — different fixes) and apart from `empty` (the record was
|
|
37
|
+
* genuinely silent, which is an answer and not a fault). Collapsing them is
|
|
38
|
+
* exactly how "the door is slow" and "the shelf is empty" became one
|
|
39
|
+
* unreadable number in the first place.
|
|
40
|
+
*/
|
|
41
|
+
export const MemoryHookCountsSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
runs: z.number().int().min(0),
|
|
44
|
+
printed: z.number().int().min(0),
|
|
45
|
+
empty: z.number().int().min(0),
|
|
46
|
+
timeouts: z.number().int().min(0),
|
|
47
|
+
failed: z.number().int().min(0),
|
|
48
|
+
skipped: z.number().int().min(0),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
52
|
+
export const MEMORY_HOOK_BUCKET_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}$/u;
|
|
53
|
+
export const MemoryHookStatsFileSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
schema_version: z.literal(MEMORY_HOOK_STATS_SCHEMA_VERSION),
|
|
56
|
+
// `partialRecord`, not `record`: a record keyed by an enum is EXHAUSTIVE
|
|
57
|
+
// in zod 4 — every event would be required — and an hour in which only
|
|
58
|
+
// the prompt hook ran is the ordinary case, not a malformed file.
|
|
59
|
+
buckets: z.record(z.string().regex(MEMORY_HOOK_BUCKET_PATTERN), z.partialRecord(z.enum(MEMORY_HOOK_STAT_EVENTS), MemoryHookCountsSchema)),
|
|
60
|
+
})
|
|
61
|
+
.strict();
|
|
62
|
+
export function memoryHookStatsFilePath(homeDir) {
|
|
63
|
+
const paths = getUserLocalCockpitPaths(homeDir);
|
|
64
|
+
const separator = paths.state_dir.includes("\\") ? "\\" : "/";
|
|
65
|
+
return `${paths.state_dir}${separator}${MEMORY_HOOK_STATS_FILE_NAME}`;
|
|
66
|
+
}
|
|
67
|
+
/** The hour a moment belongs to. UTC, so two machines agree on a boundary. */
|
|
68
|
+
export function memoryHookHourBucket(at) {
|
|
69
|
+
return at.toISOString().slice(0, 13);
|
|
70
|
+
}
|
|
71
|
+
export function emptyMemoryHookCounts() {
|
|
72
|
+
return { runs: 0, printed: 0, empty: 0, timeouts: 0, failed: 0, skipped: 0 };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Sum one event's buckets over the last `hours` hours, ending at `now`.
|
|
76
|
+
*
|
|
77
|
+
* The window is inclusive of the hour `now` falls in, so a run three minutes
|
|
78
|
+
* ago counts; that makes the last bucket partial, which is the honest
|
|
79
|
+
* direction for a gauge whose whole job is to notice that something is going
|
|
80
|
+
* wrong RIGHT NOW.
|
|
81
|
+
*/
|
|
82
|
+
export function summariseMemoryHookWindow(file, event, options) {
|
|
83
|
+
const hours = options.hours ?? 24;
|
|
84
|
+
const earliest = memoryHookHourBucket(new Date(options.now.getTime() - (hours - 1) * 3_600_000));
|
|
85
|
+
const latest = memoryHookHourBucket(options.now);
|
|
86
|
+
const total = { runs: 0, printed: 0, timeouts: 0, failed: 0, hours: 0 };
|
|
87
|
+
for (const [bucket, events] of Object.entries(file.buckets)) {
|
|
88
|
+
// Lexical order is chronological for this key, which is the only reason a
|
|
89
|
+
// string comparison is allowed to stand in for a date one here.
|
|
90
|
+
if (bucket < earliest || bucket > latest)
|
|
91
|
+
continue;
|
|
92
|
+
const counts = events[event];
|
|
93
|
+
if (!counts)
|
|
94
|
+
continue;
|
|
95
|
+
total.runs += counts.runs;
|
|
96
|
+
total.printed += counts.printed;
|
|
97
|
+
total.timeouts += counts.timeouts;
|
|
98
|
+
total.failed += counts.failed;
|
|
99
|
+
total.hours += 1;
|
|
100
|
+
}
|
|
101
|
+
return total;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse what is on disk, or say why not.
|
|
105
|
+
*
|
|
106
|
+
* A file this version does not recognise is REPLACED by the writer and
|
|
107
|
+
* reported as unreadable by the reader — never merged into a shape it was not
|
|
108
|
+
* written in, which would publish a number nobody can explain.
|
|
109
|
+
*/
|
|
110
|
+
export function parseMemoryHookStats(raw) {
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(raw);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { ok: false, reason: "hook_stats_unparseable" };
|
|
117
|
+
}
|
|
118
|
+
const result = MemoryHookStatsFileSchema.safeParse(parsed);
|
|
119
|
+
if (!result.success)
|
|
120
|
+
return { ok: false, reason: "hook_stats_unrecognised_shape" };
|
|
121
|
+
return { ok: true, file: result.data };
|
|
122
|
+
}
|
|
@@ -109,6 +109,11 @@ export declare const MemoryInstallReceiptSchema: z.ZodObject<{
|
|
|
109
109
|
}, z.core.$strict>;
|
|
110
110
|
reasons: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
111
111
|
bin_found: z.ZodOptional<z.ZodBoolean>;
|
|
112
|
+
hook_runs_24h: z.ZodOptional<z.ZodNumber>;
|
|
113
|
+
hook_timeouts_24h: z.ZodOptional<z.ZodNumber>;
|
|
114
|
+
hook_printed_24h: z.ZodOptional<z.ZodNumber>;
|
|
115
|
+
hook_failed_24h: z.ZodOptional<z.ZodNumber>;
|
|
116
|
+
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
112
117
|
}, z.core.$strict>;
|
|
113
118
|
export type MemoryInstallReceipt = z.infer<typeof MemoryInstallReceiptSchema>;
|
|
114
119
|
/** Every piece is `ok`. The one question the board asks first. */
|
|
@@ -103,6 +103,36 @@ export const MemoryInstallReceiptSchema = z
|
|
|
103
103
|
* (`no_bin_no_write`).
|
|
104
104
|
*/
|
|
105
105
|
bin_found: z.boolean().optional(),
|
|
106
|
+
/**
|
|
107
|
+
* DID THE HOOKS ACTUALLY WORK? (BLI-3788)
|
|
108
|
+
*
|
|
109
|
+
* The five words above say a hook is REGISTERED. They cannot say whether
|
|
110
|
+
* it did anything, and a prompt hook that loses its deadline prints
|
|
111
|
+
* nothing by design — so a machine could read `memory: claude mcp ✓ hooks
|
|
112
|
+
* ✓` while two thirds of that person's turns silently got no recall (QA
|
|
113
|
+
* tick 18). These four counts, summed over the last 24 hours from the
|
|
114
|
+
* hook's own on-disk counter, are that gap closed:
|
|
115
|
+
*
|
|
116
|
+
* `hook_runs_24h` hooks that ran at all — the denominator, without
|
|
117
|
+
* which a timeout count says nothing
|
|
118
|
+
* `hook_timeouts_24h` runs that lost the clock and printed nothing
|
|
119
|
+
* `hook_printed_24h` runs that injected a block
|
|
120
|
+
* `hook_failed_24h` runs that could not run (a refusal, an outage)
|
|
121
|
+
*
|
|
122
|
+
* An honestly EMPTY shelf is in none of them but `runs`, because zero hits
|
|
123
|
+
* is a real answer and not a fault. All four are optional: a collector or
|
|
124
|
+
* a memory-mcp older than this ticket reports none, and absent must read
|
|
125
|
+
* as "not reported", never as zero.
|
|
126
|
+
*
|
|
127
|
+
* They are a FLOOR. Two hooks finishing in the same millisecond can lose
|
|
128
|
+
* one increment to the file rename, which understates and cannot invent.
|
|
129
|
+
*/
|
|
130
|
+
hook_runs_24h: z.number().int().min(0).optional(),
|
|
131
|
+
hook_timeouts_24h: z.number().int().min(0).optional(),
|
|
132
|
+
hook_printed_24h: z.number().int().min(0).optional(),
|
|
133
|
+
hook_failed_24h: z.number().int().min(0).optional(),
|
|
134
|
+
/** Named when the counts are absent because the file could not be read. */
|
|
135
|
+
hook_stats_reason: ReasonLabelSchema.optional(),
|
|
106
136
|
})
|
|
107
137
|
.strict();
|
|
108
138
|
/** Every piece is `ok`. The one question the board asks first. */
|