@bli-cockpit/telemetry-core 0.1.35 → 0.1.37
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 +1 -0
- package/dist/memory-experience.d.ts +37 -0
- package/dist/memory-experience.js +85 -0
- package/dist/memory-hook-stats.d.ts +9 -0
- package/dist/memory-hook-stats.js +32 -1
- package/dist/memory-install-receipt.d.ts +1 -0
- package/dist/memory-install-receipt.js +9 -0
- package/package.json +5 -1
|
@@ -108,6 +108,7 @@ 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>;
|
|
111
112
|
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
112
113
|
hook_performance: z.ZodOptional<z.ZodObject<{
|
|
113
114
|
schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type ExperienceStore = "bli" | "supermemory" | "both";
|
|
2
|
+
export type ExperienceVerdict = "win" | "loss" | "noise";
|
|
3
|
+
export interface MemoryExperience {
|
|
4
|
+
id: string;
|
|
5
|
+
store: ExperienceStore;
|
|
6
|
+
verdict: ExperienceVerdict;
|
|
7
|
+
reason: string;
|
|
8
|
+
created_at: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExperienceReceipt extends MemoryExperience {
|
|
11
|
+
shipped: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type ExperienceSender = (entry: MemoryExperience) => Promise<{
|
|
14
|
+
ok: boolean;
|
|
15
|
+
reason?: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function validateExperience(store: string, verdict: string, reason: string): void;
|
|
18
|
+
export declare function experienceLine(entry: MemoryExperience, agent: string, project: string): string;
|
|
19
|
+
export declare function appendExperience(input: {
|
|
20
|
+
store: ExperienceStore;
|
|
21
|
+
verdict: ExperienceVerdict;
|
|
22
|
+
reason: string;
|
|
23
|
+
}, options: {
|
|
24
|
+
homeDir?: string;
|
|
25
|
+
agent: string;
|
|
26
|
+
project: string;
|
|
27
|
+
}): Promise<ExperienceReceipt>;
|
|
28
|
+
export declare function shipExperience(entry: ExperienceReceipt, send: ExperienceSender, homeDir?: string): Promise<{
|
|
29
|
+
shipped: boolean;
|
|
30
|
+
reason: string;
|
|
31
|
+
}>;
|
|
32
|
+
/** Bounded work, no throwing into collection. UUIDs make concurrent drains idempotent. */
|
|
33
|
+
export declare function drainExperiences(send: ExperienceSender, homeDir?: string): Promise<{
|
|
34
|
+
attempted: number;
|
|
35
|
+
shipped: number;
|
|
36
|
+
reason: string;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Append-only experience receipts shared by the CLI and MCP. BLI-3893. */
|
|
2
|
+
import { appendFile, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
export function validateExperience(store, verdict, reason) {
|
|
7
|
+
if (!["bli", "supermemory", "both"].includes(store))
|
|
8
|
+
throw new Error("store must be bli, supermemory or both");
|
|
9
|
+
if (!["win", "loss", "noise"].includes(verdict))
|
|
10
|
+
throw new Error("verdict must be win, loss or noise");
|
|
11
|
+
if (!reason.trim() || [...reason].length > 500 || /[\r\n\x00-\x1f\x7f]/u.test(reason)) {
|
|
12
|
+
throw new Error("reason must be one nonempty line of at most 500 characters; opinions only, no prompts, memory bodies or secrets");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const markers = { bli: "[bli]", supermemory: "◪", both: "[bli]+◪" };
|
|
16
|
+
export function experienceLine(entry, agent, project) {
|
|
17
|
+
const stamp = new Date(Date.parse(entry.created_at) + 7 * 3600000).toISOString().slice(0, 16).replace("T", " ");
|
|
18
|
+
const label = (value) => value.replace(/[\r\n·]/gu, " ");
|
|
19
|
+
return `- ${stamp} ICT · ${label(agent)} · ${label(project)} · ${entry.store} ${markers[entry.store]} · ${entry.verdict}: ${entry.reason}`;
|
|
20
|
+
}
|
|
21
|
+
function paths(home) {
|
|
22
|
+
return { log: path.join(home, ".codex", "AGENT-EXPERIENCE.md"), queue: path.join(home, ".codex", "agent-experience-outbox") };
|
|
23
|
+
}
|
|
24
|
+
async function saveReceipt(home, entry) {
|
|
25
|
+
const target = path.join(paths(home).queue, `${entry.id}.json`);
|
|
26
|
+
const temp = `${target}.${randomUUID()}.tmp`;
|
|
27
|
+
await writeFile(temp, JSON.stringify(entry), { mode: 0o600 });
|
|
28
|
+
await rename(temp, target);
|
|
29
|
+
}
|
|
30
|
+
export async function appendExperience(input, options) {
|
|
31
|
+
validateExperience(input.store, input.verdict, input.reason);
|
|
32
|
+
const home = options.homeDir ?? homedir();
|
|
33
|
+
const entry = { ...input, id: randomUUID(), created_at: new Date().toISOString(), shipped: false };
|
|
34
|
+
await mkdir(paths(home).queue, { recursive: true });
|
|
35
|
+
// Persist the retry before the human receipt so a crash never loses delivery.
|
|
36
|
+
await saveReceipt(home, entry);
|
|
37
|
+
await appendFile(paths(home).log, `${experienceLine(entry, options.agent, options.project)}\n`, { mode: 0o600 });
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
export async function shipExperience(entry, send, homeDir = homedir()) {
|
|
41
|
+
try {
|
|
42
|
+
const result = await send(entry);
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
return { shipped: false, reason: result.reason ?? "experience_refused" };
|
|
45
|
+
await saveReceipt(homeDir, { ...entry, shipped: true });
|
|
46
|
+
return { shipped: true, reason: "accepted" };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { shipped: false, reason: "experience_delivery_failed" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Bounded work, no throwing into collection. UUIDs make concurrent drains idempotent. */
|
|
53
|
+
export async function drainExperiences(send, homeDir = homedir()) {
|
|
54
|
+
let attempted = 0, shipped = 0;
|
|
55
|
+
try {
|
|
56
|
+
let files;
|
|
57
|
+
try {
|
|
58
|
+
files = await readdir(paths(homeDir).queue);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === "ENOENT")
|
|
62
|
+
return { attempted, shipped, reason: "empty" };
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
for (const file of files.sort()) {
|
|
66
|
+
if (!/^[0-9a-f-]{36}\.json$/u.test(file))
|
|
67
|
+
continue;
|
|
68
|
+
const entry = JSON.parse(await readFile(path.join(paths(homeDir).queue, file), "utf8"));
|
|
69
|
+
if (entry.shipped)
|
|
70
|
+
continue;
|
|
71
|
+
validateExperience(entry.store, entry.verdict, entry.reason);
|
|
72
|
+
attempted++;
|
|
73
|
+
const result = await shipExperience(entry, send, homeDir);
|
|
74
|
+
if (!result.shipped)
|
|
75
|
+
return { attempted, shipped, reason: result.reason };
|
|
76
|
+
shipped++;
|
|
77
|
+
if (attempted >= 20)
|
|
78
|
+
return { attempted, shipped, reason: "batch_cap" };
|
|
79
|
+
}
|
|
80
|
+
return { attempted, shipped, reason: "exhausted" };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return { attempted, shipped, reason: "experience_outbox_unreadable" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -48,6 +48,7 @@ export declare const MemoryHookCountsSchema: z.ZodObject<{
|
|
|
48
48
|
via_daemon: z.ZodOptional<z.ZodNumber>;
|
|
49
49
|
via_direct: z.ZodOptional<z.ZodNumber>;
|
|
50
50
|
skipped_trivial: z.ZodOptional<z.ZodNumber>;
|
|
51
|
+
billing_exhausted: z.ZodOptional<z.ZodNumber>;
|
|
51
52
|
}, z.core.$strict>;
|
|
52
53
|
export type MemoryHookCounts = z.infer<typeof MemoryHookCountsSchema>;
|
|
53
54
|
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
@@ -68,6 +69,7 @@ export declare const MemoryHookStatsFileSchema: z.ZodObject<{
|
|
|
68
69
|
via_daemon: z.ZodOptional<z.ZodNumber>;
|
|
69
70
|
via_direct: z.ZodOptional<z.ZodNumber>;
|
|
70
71
|
skipped_trivial: z.ZodOptional<z.ZodNumber>;
|
|
72
|
+
billing_exhausted: z.ZodOptional<z.ZodNumber>;
|
|
71
73
|
}, z.core.$strict>>>;
|
|
72
74
|
}, z.core.$strict>;
|
|
73
75
|
export type MemoryHookStatsFile = z.infer<typeof MemoryHookStatsFileSchema>;
|
|
@@ -92,6 +94,13 @@ export interface MemoryHookWindow {
|
|
|
92
94
|
viaMeasured: boolean;
|
|
93
95
|
/** Trivial prompts the hook declined to search for (BLI-3881). A subset of skips. */
|
|
94
96
|
skippedTrivial: number;
|
|
97
|
+
/**
|
|
98
|
+
* BLI-3891: runs the provider refused because the account has no credit. A
|
|
99
|
+
* subset of `failed`, summed only over buckets that carry the field, so a
|
|
100
|
+
* window spanning an upgrade reports what was measured rather than crediting
|
|
101
|
+
* older runs to zero.
|
|
102
|
+
*/
|
|
103
|
+
billingExhausted: number;
|
|
95
104
|
/** Buckets that were inside the window and had something in them. */
|
|
96
105
|
hours: number;
|
|
97
106
|
}
|
|
@@ -76,6 +76,23 @@ export const MemoryHookCountsSchema = z
|
|
|
76
76
|
* failed + skipped` still holds.
|
|
77
77
|
*/
|
|
78
78
|
skipped_trivial: z.number().int().min(0).optional(),
|
|
79
|
+
/**
|
|
80
|
+
* BLI-3891 — the SUBSET of `failed` that was the ACCOUNT, not the code.
|
|
81
|
+
*
|
|
82
|
+
* The provider refused the embedding because there is no credit left
|
|
83
|
+
* (`provider_billing_exhausted`). On 2026-09-06 that state made every
|
|
84
|
+
* recall on every machine miss its deadline and print nothing, and the
|
|
85
|
+
* only number anywhere would have said "timeouts", which sends a person
|
|
86
|
+
* to re-fit a budget rather than to a billing page.
|
|
87
|
+
*
|
|
88
|
+
* Counted BESIDE `failed`, never instead of it, so
|
|
89
|
+
* `runs = printed + empty + timeouts + failed + skipped` still holds, and
|
|
90
|
+
* OPTIONAL for the same reason `skipped_trivial` is: a file written by an
|
|
91
|
+
* older memory-mcp has no such key, this schema is strict, and a required
|
|
92
|
+
* field would make every existing machine's history
|
|
93
|
+
* `hook_stats_unrecognised_shape` overnight.
|
|
94
|
+
*/
|
|
95
|
+
billing_exhausted: z.number().int().min(0).optional(),
|
|
79
96
|
})
|
|
80
97
|
.strict();
|
|
81
98
|
/** `YYYY-MM-DDTHH` in UTC — the bucket key, and the reason the window is exact. */
|
|
@@ -99,7 +116,16 @@ export function memoryHookHourBucket(at) {
|
|
|
99
116
|
return at.toISOString().slice(0, 13);
|
|
100
117
|
}
|
|
101
118
|
export function emptyMemoryHookCounts() {
|
|
102
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
runs: 0,
|
|
121
|
+
printed: 0,
|
|
122
|
+
empty: 0,
|
|
123
|
+
timeouts: 0,
|
|
124
|
+
failed: 0,
|
|
125
|
+
skipped: 0,
|
|
126
|
+
skipped_trivial: 0,
|
|
127
|
+
billing_exhausted: 0,
|
|
128
|
+
};
|
|
103
129
|
}
|
|
104
130
|
/**
|
|
105
131
|
* Sum one event's buckets over the last `hours` hours, ending at `now`.
|
|
@@ -122,6 +148,7 @@ export function summariseMemoryHookWindow(file, event, options) {
|
|
|
122
148
|
viaDirect: 0,
|
|
123
149
|
viaMeasured: false,
|
|
124
150
|
skippedTrivial: 0,
|
|
151
|
+
billingExhausted: 0,
|
|
125
152
|
hours: 0,
|
|
126
153
|
};
|
|
127
154
|
for (const [bucket, events] of Object.entries(file.buckets)) {
|
|
@@ -144,6 +171,10 @@ export function summariseMemoryHookWindow(file, event, options) {
|
|
|
144
171
|
// Absent means an older writer never counted one, which sums as zero and
|
|
145
172
|
// is honest: that machine genuinely skipped none, because it could not.
|
|
146
173
|
total.skippedTrivial += counts.skipped_trivial ?? 0;
|
|
174
|
+
// Same rule (BLI-3891): absent means an older writer never counted one,
|
|
175
|
+
// which sums as zero and is honest — that machine genuinely had none it
|
|
176
|
+
// could name.
|
|
177
|
+
total.billingExhausted += counts.billing_exhausted ?? 0;
|
|
147
178
|
total.hours += 1;
|
|
148
179
|
}
|
|
149
180
|
return total;
|
|
@@ -116,6 +116,7 @@ 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>;
|
|
119
120
|
hook_stats_reason: z.ZodOptional<z.ZodString>;
|
|
120
121
|
hook_performance: z.ZodOptional<z.ZodObject<{
|
|
121
122
|
schema_version: z.ZodLiteral<"memory-hook-performance.v1">;
|
|
@@ -154,6 +154,15 @@ 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(),
|
|
157
166
|
/** Named when the counts are absent because the file could not be read. */
|
|
158
167
|
hook_stats_reason: ReasonLabelSchema.optional(),
|
|
159
168
|
/** Observed ordinary invocations, not legacy floors or a full host trace. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/telemetry-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
".": {
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./memory-experience": {
|
|
18
|
+
"types": "./dist/memory-experience.d.ts",
|
|
19
|
+
"import": "./dist/memory-experience.js"
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"publishConfig": {
|