@airprompter/agent-telemetry 0.1.0
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/README.md +32 -0
- package/dist/cjs/index.js +44 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/spool/writer.js +387 -0
- package/dist/cjs/spool/writer.js.map +1 -0
- package/dist/cjs/uploader.js +627 -0
- package/dist/cjs/uploader.js.map +1 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/spool/writer.d.ts +152 -0
- package/dist/esm/spool/writer.js +375 -0
- package/dist/esm/spool/writer.js.map +1 -0
- package/dist/esm/uploader.d.ts +257 -0
- package/dist/esm/uploader.js +617 -0
- package/dist/esm/uploader.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spool writer (`protocol/spool-format.md`, D52/D66).
|
|
3
|
+
*
|
|
4
|
+
* Minute windows accumulate in memory per dimension set
|
|
5
|
+
* `(tag, versionId, arm, model, status, errorClass)` and are written as
|
|
6
|
+
* `window` rows when the minute closes. Segments are append-only NDJSON
|
|
7
|
+
* under `<store>/spool/telemetry/`, open as `seg-<inst>-<epochMinute>-<n>.ndjson.open`,
|
|
8
|
+
* closed by fsync + rename; rotated at the minute boundary or 1 MiB. Nothing
|
|
9
|
+
* here can carry prompt text, output, or an end-user identifier — the row
|
|
10
|
+
* shape has no field for them. Serverless hosts use the memory sink and
|
|
11
|
+
* flush at invocation end.
|
|
12
|
+
*/
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { nodeFs, fsFailureCode, latencyBucketIndex, minuteOf, epochMinute, LATENCY_BUCKET_EDGES_MS } from "@airprompter/agent-core";
|
|
15
|
+
export { LATENCY_BUCKET_EDGES_MS, latencyBucketIndex, minuteOf, epochMinute } from "@airprompter/agent-core";
|
|
16
|
+
export const SEGMENT_MAX_BYTES = 1024 * 1024;
|
|
17
|
+
/** A host keeps this much closed, unsent spool before the oldest segments are evicted (spool-format.md). */
|
|
18
|
+
export const HOST_SPOOL_BUDGET_BYTES = 100 * 1024 * 1024;
|
|
19
|
+
/** A serverless invocation keeps this much in memory; beyond it the oldest rows go and a `dropped` row says so. */
|
|
20
|
+
export const SERVERLESS_BUFFER_BYTES = 256 * 1024;
|
|
21
|
+
export function segmentName(instanceId, minute, n) {
|
|
22
|
+
return `seg-${instanceId}-${minute}-${n}.ndjson`;
|
|
23
|
+
}
|
|
24
|
+
/** Which segment an appended line lands in: a new one on a new minute, or when the line would push past 1 MiB. Pure; `protocol/vectors/spool.json` pins it. */
|
|
25
|
+
export class SegmentPlanner {
|
|
26
|
+
instanceId;
|
|
27
|
+
openMinute = null;
|
|
28
|
+
openBytes = 0;
|
|
29
|
+
n = 0;
|
|
30
|
+
constructor(instanceId) {
|
|
31
|
+
this.instanceId = instanceId;
|
|
32
|
+
}
|
|
33
|
+
append(epochMs, lineBytes) {
|
|
34
|
+
const minute = epochMinute(epochMs);
|
|
35
|
+
const rotated = this.openMinute === null || minute !== this.openMinute || this.openBytes + lineBytes > SEGMENT_MAX_BYTES;
|
|
36
|
+
if (rotated) {
|
|
37
|
+
this.n = minute === this.openMinute ? this.n + 1 : 0;
|
|
38
|
+
this.openMinute = minute;
|
|
39
|
+
this.openBytes = 0;
|
|
40
|
+
}
|
|
41
|
+
this.openBytes += lineBytes;
|
|
42
|
+
return { segment: segmentName(this.instanceId, this.openMinute, this.n), rotated };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The serverless buffer: rows in memory up to `budgetBytes` (256 KiB by
|
|
47
|
+
* default). When a row would push past the budget the OLDEST rows are
|
|
48
|
+
* evicted and counted; the next flush (invocation end) hands back the
|
|
49
|
+
* surviving rows followed by one `dropped` row carrying the count and the
|
|
50
|
+
* bytes lost, so an over-chatty invocation is reported, never silent.
|
|
51
|
+
*/
|
|
52
|
+
export class MemorySink {
|
|
53
|
+
identity;
|
|
54
|
+
budgetBytes;
|
|
55
|
+
kind = "memory";
|
|
56
|
+
rows = [];
|
|
57
|
+
bytes = 0;
|
|
58
|
+
droppedRows = 0;
|
|
59
|
+
droppedBytes = 0;
|
|
60
|
+
constructor(identity = null, budgetBytes = SERVERLESS_BUFFER_BYTES) {
|
|
61
|
+
this.identity = identity;
|
|
62
|
+
this.budgetBytes = budgetBytes;
|
|
63
|
+
}
|
|
64
|
+
append(row) {
|
|
65
|
+
const size = Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
|
|
66
|
+
this.rows.push(row);
|
|
67
|
+
this.bytes += size;
|
|
68
|
+
while (this.bytes > this.budgetBytes && this.rows.length > 1) {
|
|
69
|
+
const oldest = this.rows.shift();
|
|
70
|
+
const lost = Buffer.byteLength(JSON.stringify(oldest), "utf8") + 1;
|
|
71
|
+
this.bytes -= lost;
|
|
72
|
+
this.droppedRows += 1;
|
|
73
|
+
this.droppedBytes += lost;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
flush(_nowMs) { }
|
|
77
|
+
/** The buffered rows, then a `dropped` row when eviction happened since the last drain. */
|
|
78
|
+
drain(nowMs = Date.now()) {
|
|
79
|
+
const rows = this.rows.splice(0, this.rows.length);
|
|
80
|
+
this.bytes = 0;
|
|
81
|
+
if (this.droppedRows > 0 && this.identity) {
|
|
82
|
+
rows.push({ type: "dropped", v: 1, at: new Date(nowMs).toISOString().replace(/\.\d{3}Z$/, "Z"), instanceId: this.identity.instanceId, segments: this.droppedRows, bytes: this.droppedBytes });
|
|
83
|
+
this.droppedRows = 0;
|
|
84
|
+
this.droppedBytes = 0;
|
|
85
|
+
}
|
|
86
|
+
return rows;
|
|
87
|
+
}
|
|
88
|
+
get dropped() {
|
|
89
|
+
return { rows: this.droppedRows, bytes: this.droppedBytes };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Segments on disk, through an `FsPort`. Never throws: the request path calls
|
|
94
|
+
* `append`, and a full disk, an I/O error or a file a sibling process took
|
|
95
|
+
* away are counted, reported as a `dropped` row when writing works again,
|
|
96
|
+
* and surfaced on `faults` — never surfaced to the caller as an exception.
|
|
97
|
+
*/
|
|
98
|
+
export class DirectorySink {
|
|
99
|
+
dir;
|
|
100
|
+
instanceId;
|
|
101
|
+
budgetBytes;
|
|
102
|
+
kind = "directory";
|
|
103
|
+
fd = null;
|
|
104
|
+
openPath = null;
|
|
105
|
+
planner;
|
|
106
|
+
fs;
|
|
107
|
+
faults = { pendingRows: 0, pendingBytes: 0, byCode: {}, last: null };
|
|
108
|
+
constructor(dir, instanceId, budgetBytes = HOST_SPOOL_BUDGET_BYTES, fs = nodeFs) {
|
|
109
|
+
this.dir = dir;
|
|
110
|
+
this.instanceId = instanceId;
|
|
111
|
+
this.budgetBytes = budgetBytes;
|
|
112
|
+
this.fs = fs;
|
|
113
|
+
this.planner = new SegmentPlanner(instanceId);
|
|
114
|
+
this.guard("open_spool", () => {
|
|
115
|
+
this.fs.mkdirp(join(dir, "exported"), 0o700);
|
|
116
|
+
this.fs.mkdirp(join(dir, "quarantine"), 0o700);
|
|
117
|
+
});
|
|
118
|
+
this.recoverOpenSegments();
|
|
119
|
+
}
|
|
120
|
+
/** Run a filesystem step; on failure count it by code and return false. The sink never throws. */
|
|
121
|
+
guard(step, run) {
|
|
122
|
+
try {
|
|
123
|
+
run();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const code = fsFailureCode(error);
|
|
128
|
+
this.faults.byCode[code] = (this.faults.byCode[code] ?? 0) + 1;
|
|
129
|
+
this.faults.last = `${step}: ${code}`;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** A writer that crashed left `.open` files; the same writer closes them on its next start (a partial last line is the daemon's to skip). */
|
|
134
|
+
recoverOpenSegments() {
|
|
135
|
+
let names = [];
|
|
136
|
+
this.guard("list_spool", () => void (names = this.fs.list(this.dir)));
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
if (name.startsWith(`seg-${this.instanceId}-`) && name.endsWith(".ndjson.open")) {
|
|
139
|
+
const path = join(this.dir, name);
|
|
140
|
+
this.guard("recover_open_segment", () => {
|
|
141
|
+
const fd = this.fs.open(path, "r+");
|
|
142
|
+
try {
|
|
143
|
+
this.fs.fsync(fd);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
this.fs.close(fd);
|
|
147
|
+
}
|
|
148
|
+
this.fs.rename(path, path.slice(0, -".open".length));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
append(row, nowMs) {
|
|
154
|
+
const line = Buffer.from(`${JSON.stringify(row)}\n`, "utf8");
|
|
155
|
+
const plan = this.planner.append(nowMs, line.length);
|
|
156
|
+
if (plan.rotated || this.fd === null) {
|
|
157
|
+
this.flush(nowMs);
|
|
158
|
+
const opened = this.guard("open_segment", () => {
|
|
159
|
+
// A name already on disk (a previous process of the same instance in the same minute) is skipped, never appended to.
|
|
160
|
+
let name = plan.segment;
|
|
161
|
+
while (this.fs.exists(join(this.dir, name)) || this.fs.exists(join(this.dir, `${name}.open`))) {
|
|
162
|
+
this.planner.n += 1;
|
|
163
|
+
name = segmentName(this.instanceId, this.planner.openMinute, this.planner.n);
|
|
164
|
+
}
|
|
165
|
+
const openPath = join(this.dir, `${name}.open`);
|
|
166
|
+
this.fd = this.fs.open(openPath, "a", 0o600);
|
|
167
|
+
this.openPath = openPath;
|
|
168
|
+
});
|
|
169
|
+
if (!opened) {
|
|
170
|
+
this.fd = null;
|
|
171
|
+
this.openPath = null;
|
|
172
|
+
this.lose(1, line.length);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.reportPendingLoss(nowMs);
|
|
176
|
+
if (this.fd === null) {
|
|
177
|
+
// The loss could not even be said (still no space): this row joins it.
|
|
178
|
+
this.lose(1, line.length);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const fd = this.fd;
|
|
183
|
+
if (!this.guard("write_row", () => this.fs.write(fd, line))) {
|
|
184
|
+
// What was written before this row is good; close the segment (a partial last line is the daemon's to skip) and count the row.
|
|
185
|
+
this.closeOpen();
|
|
186
|
+
this.lose(1, line.length);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** A row the sink could not keep. Counted now; said in a `dropped` row when a write succeeds again. */
|
|
190
|
+
lose(rows, bytes) {
|
|
191
|
+
this.faults.pendingRows += rows;
|
|
192
|
+
this.faults.pendingBytes += bytes;
|
|
193
|
+
}
|
|
194
|
+
/** Rows lost to failures become one `dropped` row (rows counted as `segments`, as the memory sink does) in the segment just opened. */
|
|
195
|
+
reportPendingLoss(nowMs) {
|
|
196
|
+
if (this.faults.pendingRows === 0 || this.fd === null)
|
|
197
|
+
return;
|
|
198
|
+
const rows = this.faults.pendingRows;
|
|
199
|
+
const bytes = this.faults.pendingBytes;
|
|
200
|
+
const line = Buffer.from(`${JSON.stringify({ type: "dropped", v: 1, at: isoSeconds(nowMs), instanceId: this.instanceId, segments: rows, bytes })}\n`, "utf8");
|
|
201
|
+
const fd = this.fd;
|
|
202
|
+
if (this.guard("write_dropped_row", () => this.fs.write(fd, line))) {
|
|
203
|
+
this.faults.pendingRows = 0;
|
|
204
|
+
this.faults.pendingBytes = 0;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.closeOpen();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
flush(nowMs) {
|
|
211
|
+
if (this.fd === null || !this.openPath)
|
|
212
|
+
return;
|
|
213
|
+
this.closeOpen();
|
|
214
|
+
// Over budget after this close: evict the oldest, then write the loss as its own small closed segment, at once.
|
|
215
|
+
const evicted = this.enforceBudget();
|
|
216
|
+
if (evicted) {
|
|
217
|
+
const at = nowMs ?? Date.now();
|
|
218
|
+
this.append({ type: "dropped", v: 1, at: isoSeconds(at), instanceId: this.instanceId, segments: evicted.segments, bytes: evicted.bytes }, at);
|
|
219
|
+
this.closeOpen();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
closeOpen() {
|
|
223
|
+
if (this.fd === null || !this.openPath)
|
|
224
|
+
return;
|
|
225
|
+
const fd = this.fd;
|
|
226
|
+
const openPath = this.openPath;
|
|
227
|
+
this.fd = null;
|
|
228
|
+
this.openPath = null;
|
|
229
|
+
const synced = this.guard("fsync_segment", () => this.fs.fsync(fd));
|
|
230
|
+
this.guard("close_segment", () => this.fs.close(fd));
|
|
231
|
+
// A segment that did not fsync is not closed: it stays `.open` for the next start to recover (its bytes are on disk or they are not).
|
|
232
|
+
if (synced)
|
|
233
|
+
this.guard("close_segment", () => this.fs.rename(openPath, openPath.slice(0, -".open".length)));
|
|
234
|
+
}
|
|
235
|
+
/** Over the host budget: evict the OLDEST closed, unsent segments and say how much went (spool-format.md). A file a sibling took away is skipped. */
|
|
236
|
+
enforceBudget() {
|
|
237
|
+
const sizes = [];
|
|
238
|
+
let total = 0;
|
|
239
|
+
for (const name of this.closedSegments()) {
|
|
240
|
+
this.guard("stat_segment", () => {
|
|
241
|
+
const size = this.fs.stat(join(this.dir, name)).size;
|
|
242
|
+
sizes.push({ name, size });
|
|
243
|
+
total += size;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
let evicted = 0;
|
|
247
|
+
let evictedBytes = 0;
|
|
248
|
+
for (const { name, size } of sizes) {
|
|
249
|
+
if (total <= this.budgetBytes)
|
|
250
|
+
break;
|
|
251
|
+
// Gone already (the daemon or a sibling evicted it): it no longer counts, and nothing was lost here.
|
|
252
|
+
const removed = this.guard("evict_segment", () => this.fs.unlink(join(this.dir, name)));
|
|
253
|
+
total -= size;
|
|
254
|
+
if (!removed)
|
|
255
|
+
continue;
|
|
256
|
+
evicted += 1;
|
|
257
|
+
evictedBytes += size;
|
|
258
|
+
}
|
|
259
|
+
return evicted > 0 ? { segments: evicted, bytes: evictedBytes } : null;
|
|
260
|
+
}
|
|
261
|
+
closedSegments() {
|
|
262
|
+
let names = [];
|
|
263
|
+
this.guard("list_spool", () => void (names = this.fs.list(this.dir)));
|
|
264
|
+
return names.filter((name) => name.startsWith("seg-") && name.endsWith(".ndjson")).sort();
|
|
265
|
+
}
|
|
266
|
+
depth() {
|
|
267
|
+
const segments = this.closedSegments();
|
|
268
|
+
let bytes = 0;
|
|
269
|
+
for (const name of segments)
|
|
270
|
+
this.guard("stat_segment", () => void (bytes += this.fs.stat(join(this.dir, name)).size));
|
|
271
|
+
return { segments: segments.length, bytes };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** ISO-8601 to the second, the spool's timestamp form. */
|
|
275
|
+
function isoSeconds(ms) {
|
|
276
|
+
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
277
|
+
}
|
|
278
|
+
/** Accumulates observations into minute windows and hands closed windows to the sink. */
|
|
279
|
+
export class SpoolWriter {
|
|
280
|
+
sink;
|
|
281
|
+
identity;
|
|
282
|
+
open = new Map();
|
|
283
|
+
openMinute = null;
|
|
284
|
+
constructor(sink, identity) {
|
|
285
|
+
this.sink = sink;
|
|
286
|
+
this.identity = identity;
|
|
287
|
+
}
|
|
288
|
+
window(dimensions, nowMs) {
|
|
289
|
+
const minute = minuteOf(nowMs);
|
|
290
|
+
if (this.openMinute !== null && this.openMinute !== minute)
|
|
291
|
+
this.closeWindows(nowMs);
|
|
292
|
+
this.openMinute = minute;
|
|
293
|
+
const errorClass = dimensions.errorClass ?? null;
|
|
294
|
+
const key = [dimensions.tag, dimensions.versionId, dimensions.arm, dimensions.model, dimensions.status, errorClass ?? ""].join("\u0000");
|
|
295
|
+
let row = this.open.get(key);
|
|
296
|
+
if (!row) {
|
|
297
|
+
row = {
|
|
298
|
+
type: "window",
|
|
299
|
+
v: 1,
|
|
300
|
+
minute,
|
|
301
|
+
instanceId: this.identity.instanceId,
|
|
302
|
+
instanceClass: this.identity.instanceClass,
|
|
303
|
+
tag: dimensions.tag,
|
|
304
|
+
versionId: dimensions.versionId,
|
|
305
|
+
arm: dimensions.arm,
|
|
306
|
+
model: dimensions.model,
|
|
307
|
+
status: dimensions.status,
|
|
308
|
+
errorClass,
|
|
309
|
+
usageSource: dimensions.usageSource ?? "reported",
|
|
310
|
+
count: 0,
|
|
311
|
+
latencyMs: { buckets: new Array(LATENCY_BUCKET_EDGES_MS.length).fill(0), sum: 0 },
|
|
312
|
+
tokens: { input: 0, output: 0 },
|
|
313
|
+
sdk: this.identity.sdk,
|
|
314
|
+
};
|
|
315
|
+
this.open.set(key, row);
|
|
316
|
+
}
|
|
317
|
+
return row;
|
|
318
|
+
}
|
|
319
|
+
observe(observation, nowMs) {
|
|
320
|
+
const row = this.window(observation, nowMs);
|
|
321
|
+
row.count += 1;
|
|
322
|
+
const bucket = latencyBucketIndex(observation.latencyMs);
|
|
323
|
+
row.latencyMs.buckets[bucket] = (row.latencyMs.buckets[bucket] ?? 0) + 1;
|
|
324
|
+
row.latencyMs.sum += Math.max(0, Math.round(observation.latencyMs));
|
|
325
|
+
row.tokens.input += observation.tokens?.input ?? 0;
|
|
326
|
+
row.tokens.output += observation.tokens?.output ?? 0;
|
|
327
|
+
if (observation.tokens?.cachedInput)
|
|
328
|
+
row.tokens.cachedInput = (row.tokens.cachedInput ?? 0) + observation.tokens.cachedInput;
|
|
329
|
+
if (observation.checks) {
|
|
330
|
+
row.checks = { passed: (row.checks?.passed ?? 0) + (observation.checks.passed ?? 0), failed: (row.checks?.failed ?? 0) + (observation.checks.failed ?? 0) };
|
|
331
|
+
}
|
|
332
|
+
if (observation.outcomes)
|
|
333
|
+
mergeOutcomes(row, observation.outcomes);
|
|
334
|
+
}
|
|
335
|
+
/** T29: output-check counts against a run already counted (an app that evaluated after the fact): the run's window, no extra count. */
|
|
336
|
+
checks(dimensions, counts, nowMs) {
|
|
337
|
+
const row = this.window({ ...dimensions, status: "ok" }, nowMs);
|
|
338
|
+
row.checks = { passed: (row.checks?.passed ?? 0) + counts.passed, failed: (row.checks?.failed ?? 0) + counts.failed };
|
|
339
|
+
}
|
|
340
|
+
/** Quality signals against a run already counted: they ride on the run's window (status ok) and never add to `count`. */
|
|
341
|
+
outcomes(dimensions, outcomes, nowMs) {
|
|
342
|
+
mergeOutcomes(this.window({ ...dimensions, status: "ok" }, nowMs), outcomes);
|
|
343
|
+
}
|
|
344
|
+
refusal(row, nowMs) {
|
|
345
|
+
this.sink.append({ type: "refusal", v: 1, instanceId: this.identity.instanceId, ...row }, nowMs);
|
|
346
|
+
}
|
|
347
|
+
/** Write every open window and close the segment. Called at the minute boundary, at shutdown, and at invocation end on serverless. */
|
|
348
|
+
closeWindows(nowMs) {
|
|
349
|
+
for (const row of this.open.values())
|
|
350
|
+
this.sink.append(row, nowMs);
|
|
351
|
+
this.open.clear();
|
|
352
|
+
this.openMinute = null;
|
|
353
|
+
this.sink.flush(nowMs);
|
|
354
|
+
}
|
|
355
|
+
/** S6: close the windows of a minute that has passed (and the segment with them) without touching the current minute — the runtime's spool timer calls this, so an idle writer never leaves the last minute of a burst parked in an `.open` file. */
|
|
356
|
+
closeStaleWindows(nowMs) {
|
|
357
|
+
if (this.openMinute !== null && this.openMinute !== minuteOf(nowMs))
|
|
358
|
+
this.closeWindows(nowMs);
|
|
359
|
+
}
|
|
360
|
+
get openWindowCount() {
|
|
361
|
+
return this.open.size;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function mergeOutcomes(row, outcomes) {
|
|
365
|
+
row.outcomes ??= {};
|
|
366
|
+
for (const [name, value] of Object.entries(outcomes)) {
|
|
367
|
+
if (!/^[a-z][a-zA-Z0-9]{0,31}$/.test(name))
|
|
368
|
+
continue;
|
|
369
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
370
|
+
continue;
|
|
371
|
+
const current = row.outcomes[name] ?? { n: 0, sum: 0 };
|
|
372
|
+
row.outcomes[name] = { n: current.n + 1, sum: current.sum + (typeof value === "boolean" ? (value ? 1 : 0) : value) };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.js","sourceRoot":"","sources":["../../../src/spool/writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spool uploader (T26 P4, D52/D66): closed segments from ANY writer in
|
|
3
|
+
* `<store>/spool/telemetry/` are validated line by line against the spool
|
|
4
|
+
* row contract, quarantined when they do not fit, and POSTed straight to S3
|
|
5
|
+
* under the heartbeat's presigned grant — one in flight per host, oldest
|
|
6
|
+
* first, exponential backoff with full jitter (1 s → 5 min), acknowledged
|
|
7
|
+
* segments DELETED (S6: S3 keys are idempotent, a lost response is a
|
|
8
|
+
* replay, nothing needs keeping), `quarantine/` and `exported/` capped in
|
|
9
|
+
* bytes and swept by age, abandoned `.open` files reclaimed, the host
|
|
10
|
+
* budget enforced across writers with the loss written as a `dropped` row.
|
|
11
|
+
* Nothing here reads a row for anything but its shape.
|
|
12
|
+
*
|
|
13
|
+
* S6 — the disk budget is a published invariant (spool-format.md draft 2):
|
|
14
|
+
*
|
|
15
|
+
* tree ≤ budget + (writers × 1 MiB open) + quarantine cap + exported cap
|
|
16
|
+
*
|
|
17
|
+
* Closed unsent segments are the budget; each live writer holds at most one
|
|
18
|
+
* open segment of at most 1 MiB; quarantine/ and exported/ hold at most
|
|
19
|
+
* their caps; nothing else is ever parked under the spool.
|
|
20
|
+
*
|
|
21
|
+
* A grant is per INSTANCE prefix (`org/{org}/agent/{agent}/{target}/{instance}/`)
|
|
22
|
+
* and the ingest processor holds every row to the prefix it arrived under,
|
|
23
|
+
* so a daemon that uploads for several writers holds one grant per writer:
|
|
24
|
+
* `grantFor(instanceId)` is the daemon's heartbeat carrying that writer's
|
|
25
|
+
* instance id. The serverless path uses the same `postSegment` with the
|
|
26
|
+
* runtime's own grant at invocation end.
|
|
27
|
+
*/
|
|
28
|
+
import { type FsPort } from "@airprompter/agent-core";
|
|
29
|
+
import { type SpoolRow } from "./spool/writer.js";
|
|
30
|
+
import type { FetchLike, UploadSink } from "@airprompter/agent-core";
|
|
31
|
+
export declare const UPLOAD_BACKOFF_BASE_MS = 1000;
|
|
32
|
+
export declare const UPLOAD_BACKOFF_CAP_MS: number;
|
|
33
|
+
export declare const QUARANTINE_RETENTION_MS: number;
|
|
34
|
+
/** S6: `quarantine/` and `exported/` are capped in bytes, oldest first — a buggy third-party writer cannot fill the disk through quarantine. */
|
|
35
|
+
export declare const QUARANTINE_CAP_BYTES: number;
|
|
36
|
+
export declare const EXPORTED_CAP_BYTES: number;
|
|
37
|
+
/** S6: an `.open` segment untouched this long has no writer behind it (a live one closes every minute it has traffic, and its stale windows within one); it is closed and uploaded like any other. */
|
|
38
|
+
export declare const OPEN_SEGMENT_RECLAIM_MS: number;
|
|
39
|
+
/** S6: the uploader stamps its last acknowledged upload here (mtime), so `airprompter status` can say it without a daemon. */
|
|
40
|
+
export declare const LAST_UPLOAD_MARKER = ".last-upload";
|
|
41
|
+
/** A grant is refreshed this long before its `expiresAt`, so an upload never starts on one about to lapse. */
|
|
42
|
+
export declare const GRANT_REFRESH_MARGIN_MS: number;
|
|
43
|
+
export declare const SEGMENT_NAME: RegExp;
|
|
44
|
+
export declare const OPEN_SEGMENT_NAME: RegExp;
|
|
45
|
+
/** The heartbeat's `uploadGrant` (protocol heartbeat.schema.json). */
|
|
46
|
+
export interface UploadGrant {
|
|
47
|
+
grantId: string;
|
|
48
|
+
url: string;
|
|
49
|
+
fields: Record<string, string>;
|
|
50
|
+
keyPrefix: string;
|
|
51
|
+
expiresAt: string;
|
|
52
|
+
maxObjectBytes: number;
|
|
53
|
+
contentType?: "application/x-ndjson";
|
|
54
|
+
}
|
|
55
|
+
export type GrantDecision = {
|
|
56
|
+
kind: "grant";
|
|
57
|
+
grant: UploadGrant;
|
|
58
|
+
uploadIntervalSeconds?: number;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "hold";
|
|
61
|
+
retryAfterSeconds: number;
|
|
62
|
+
reason?: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "unavailable";
|
|
65
|
+
reason: string;
|
|
66
|
+
};
|
|
67
|
+
export type RowVerdict = {
|
|
68
|
+
ok: true;
|
|
69
|
+
row: SpoolRow;
|
|
70
|
+
} | {
|
|
71
|
+
ok: false;
|
|
72
|
+
reason: string;
|
|
73
|
+
};
|
|
74
|
+
/** One parsed line against the contract. The reason names the first field that does not fit — never the value. */
|
|
75
|
+
export declare function validateSpoolRow(value: unknown): RowVerdict;
|
|
76
|
+
export interface SegmentInspection {
|
|
77
|
+
rows: SpoolRow[];
|
|
78
|
+
/** Line numbers (1-based) and reasons; empty means the segment fits the contract. */
|
|
79
|
+
invalid: Array<{
|
|
80
|
+
line: number;
|
|
81
|
+
reason: string;
|
|
82
|
+
}>;
|
|
83
|
+
/** A last line without its `\n` (a crashed writer): skipped, never counted as invalid. */
|
|
84
|
+
partialTail: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** Every line of a segment against the contract; `instanceId` (from the file name) is authoritative for every row. */
|
|
87
|
+
export declare function inspectSegment(bytes: Uint8Array, instanceId: string): SegmentInspection;
|
|
88
|
+
export declare function multipartBody(boundary: string, fields: Array<[string, string]>, file: {
|
|
89
|
+
name: string;
|
|
90
|
+
contentType: string;
|
|
91
|
+
bytes: Uint8Array;
|
|
92
|
+
}): Buffer;
|
|
93
|
+
export type PostOutcome = {
|
|
94
|
+
status: "ok";
|
|
95
|
+
key: string;
|
|
96
|
+
} | {
|
|
97
|
+
status: "refused";
|
|
98
|
+
httpStatus: number;
|
|
99
|
+
expired: boolean;
|
|
100
|
+
} | {
|
|
101
|
+
status: "too_large";
|
|
102
|
+
bytes: number;
|
|
103
|
+
} | {
|
|
104
|
+
status: "network";
|
|
105
|
+
reason: string;
|
|
106
|
+
};
|
|
107
|
+
/** One segment under one grant. S3 PUT is idempotent by key, so a replay after a lost response overwrites identically. */
|
|
108
|
+
export declare function postSegment(input: {
|
|
109
|
+
grant: UploadGrant;
|
|
110
|
+
segment: string;
|
|
111
|
+
bytes: Uint8Array;
|
|
112
|
+
fetch: FetchLike;
|
|
113
|
+
now?: () => number;
|
|
114
|
+
boundary?: string;
|
|
115
|
+
}): Promise<PostOutcome>;
|
|
116
|
+
/** Full jitter: uniform in [0, min(cap, base × 2^attempt)]. */
|
|
117
|
+
export declare function backoffDelayMs(attempt: number, random?: () => number): number;
|
|
118
|
+
export interface UploaderOptions {
|
|
119
|
+
dir: string;
|
|
120
|
+
/** The daemon's own instance id: `dropped` rows written by the budget sweep name it. */
|
|
121
|
+
instanceId: string;
|
|
122
|
+
/** A grant for one writer's prefix — the heartbeat carrying that writer's instance id (AirPrompter's sink). */
|
|
123
|
+
grantFor?: (instanceId: string) => Promise<GrantDecision>;
|
|
124
|
+
fetch?: FetchLike;
|
|
125
|
+
/**
|
|
126
|
+
* S13: where validated segments go. Absent: AirPrompter's sink over `grantFor` + `fetch`. The OpenTelemetry bridge
|
|
127
|
+
* (`@airprompter/otel-bridge`) or a customer's own sink takes the same segments with no AirPrompter grant at all.
|
|
128
|
+
*/
|
|
129
|
+
sink?: UploadSink;
|
|
130
|
+
now?: () => number;
|
|
131
|
+
/** The filesystem (S2): the Node port by default; a fake that fills, fails or loses files in tests. */
|
|
132
|
+
fs?: FsPort;
|
|
133
|
+
random?: () => number;
|
|
134
|
+
logger?: (event: Record<string, unknown>) => void;
|
|
135
|
+
budgetBytes?: number;
|
|
136
|
+
quarantineRetentionMs?: number;
|
|
137
|
+
/** S6: byte caps on `quarantine/` and `exported/` (10 MiB each by default), oldest first. */
|
|
138
|
+
quarantineCapBytes?: number;
|
|
139
|
+
exportedCapBytes?: number;
|
|
140
|
+
/** S6: how long an `.open` segment may sit untouched before it is closed as abandoned (1 h by default). */
|
|
141
|
+
openReclaimMs?: number;
|
|
142
|
+
/** The cadence between passes when no grant has said otherwise (the grant's `uploadIntervalSeconds` wins). */
|
|
143
|
+
intervalSeconds?: number;
|
|
144
|
+
}
|
|
145
|
+
export interface UploaderStatus {
|
|
146
|
+
/** S13: which sink the segments go to. */
|
|
147
|
+
sink: string;
|
|
148
|
+
lastUploadAt: string | null;
|
|
149
|
+
lastError: string | null;
|
|
150
|
+
backoffUntil: string | null;
|
|
151
|
+
attempt: number;
|
|
152
|
+
inFlight: boolean;
|
|
153
|
+
intervalSeconds: number;
|
|
154
|
+
nextPassAt: string | null;
|
|
155
|
+
sentSegments: number;
|
|
156
|
+
quarantinedSegments: number;
|
|
157
|
+
droppedSegments: number;
|
|
158
|
+
/** Live grants by writer instance and when each lapses. */
|
|
159
|
+
grants: Array<{
|
|
160
|
+
instanceId: string;
|
|
161
|
+
expiresAt: string;
|
|
162
|
+
}>;
|
|
163
|
+
depth: {
|
|
164
|
+
segments: number;
|
|
165
|
+
bytes: number;
|
|
166
|
+
};
|
|
167
|
+
/** S6: the invariant's other terms — open segments (one per live writer, ≤ 1 MiB each), quarantine/ and exported/ bytes — and the whole tree. */
|
|
168
|
+
tree: {
|
|
169
|
+
openSegments: number;
|
|
170
|
+
openBytes: number;
|
|
171
|
+
quarantineBytes: number;
|
|
172
|
+
exportedBytes: number;
|
|
173
|
+
totalBytes: number;
|
|
174
|
+
};
|
|
175
|
+
/** S6: abandoned `.open` segments closed by the sweep, and quarantined / exported files evicted past their caps. */
|
|
176
|
+
reclaimedSegments: number;
|
|
177
|
+
capEvictedFiles: number;
|
|
178
|
+
}
|
|
179
|
+
export interface PassResult {
|
|
180
|
+
uploaded: string[];
|
|
181
|
+
quarantined: string[];
|
|
182
|
+
dropped: number;
|
|
183
|
+
held: boolean;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* AirPrompter's sink: one grant per writer prefix (the daemon's heartbeat carrying that writer's instance id), a PUT of
|
|
187
|
+
* exactly the whole lines to the customer's own prefix; a grant that lapsed between the check and the bucket's clock is
|
|
188
|
+
* refreshed once. `onGrant` lets the uploader take the grant's cadence.
|
|
189
|
+
*/
|
|
190
|
+
export declare function airprompterUploadSink(input: {
|
|
191
|
+
grantFor: (instanceId: string) => Promise<GrantDecision>;
|
|
192
|
+
fetch: FetchLike;
|
|
193
|
+
now?: () => number;
|
|
194
|
+
onGrant?: (decision: Extract<GrantDecision, {
|
|
195
|
+
kind: "grant";
|
|
196
|
+
}>) => void;
|
|
197
|
+
}): UploadSink & {
|
|
198
|
+
readonly grants: Map<string, UploadGrant>;
|
|
199
|
+
};
|
|
200
|
+
export declare class SpoolUploader {
|
|
201
|
+
private readonly options;
|
|
202
|
+
private readonly sink;
|
|
203
|
+
private lastUploadMs;
|
|
204
|
+
private lastError;
|
|
205
|
+
private backoffUntilMs;
|
|
206
|
+
private attempt;
|
|
207
|
+
private inFlight;
|
|
208
|
+
private intervalSeconds;
|
|
209
|
+
private nextPassMs;
|
|
210
|
+
private sentSegments;
|
|
211
|
+
private quarantinedSegments;
|
|
212
|
+
private droppedSegments;
|
|
213
|
+
private reclaimedSegments;
|
|
214
|
+
private capEvictedFiles;
|
|
215
|
+
private timer;
|
|
216
|
+
private stopped;
|
|
217
|
+
private readonly fs;
|
|
218
|
+
/** Filesystem failures by code — a sweep that could not stat, an evict that found the file gone (S2). */
|
|
219
|
+
readonly fsFaults: Record<string, number>;
|
|
220
|
+
constructor(options: UploaderOptions);
|
|
221
|
+
/** Run a filesystem step; a failure is counted by code and returns false (a segment a sibling took away is not an error). */
|
|
222
|
+
private guard;
|
|
223
|
+
private now;
|
|
224
|
+
private log;
|
|
225
|
+
/** Closed, unsent segments, oldest first (by epoch minute, then n, then name). */
|
|
226
|
+
closedSegments(): string[];
|
|
227
|
+
depth(): {
|
|
228
|
+
segments: number;
|
|
229
|
+
bytes: number;
|
|
230
|
+
};
|
|
231
|
+
/** Bytes under one subdirectory (files only), oldest-first names beside it. */
|
|
232
|
+
private dirBytes;
|
|
233
|
+
/** S6: the invariant's terms as they stand — what a host actually has parked under the spool. */
|
|
234
|
+
tree(): UploaderStatus["tree"];
|
|
235
|
+
/** S6: the published bound for this uploader's settings — `budget + writers × 1 MiB + quarantine cap + exported cap`. */
|
|
236
|
+
bound(writers: number): number;
|
|
237
|
+
/** Attached SDK processes and the daemon both write here; over the host budget the OLDEST unsent segments go and the loss is one `dropped` row under the daemon's own id. */
|
|
238
|
+
enforceBudget(): number;
|
|
239
|
+
/** The segment's bytes, or null when it is gone (counted as a fault, never thrown). */
|
|
240
|
+
private readSegment;
|
|
241
|
+
/**
|
|
242
|
+
* S6: `quarantine/` entries older than their retention are deleted; `quarantine/` and `exported/` are held under their
|
|
243
|
+
* byte caps, oldest first; an `.open` segment untouched past the reclaim age has no writer behind it and is closed so it
|
|
244
|
+
* uploads (a partial last line is skipped at inspection) and counts against the budget like any other.
|
|
245
|
+
*/
|
|
246
|
+
sweep(): void;
|
|
247
|
+
private quarantine;
|
|
248
|
+
/** One pass: sweep, budget, then each closed segment oldest first — validate, grant, POST, move — until the spool is empty, a hold, or a failure. Never throws. */
|
|
249
|
+
runOnce(): Promise<PassResult>;
|
|
250
|
+
private pass;
|
|
251
|
+
private fail;
|
|
252
|
+
/** Passes every `intervalSeconds` (the grant's `uploadIntervalSeconds` once one has answered), with a random phase offset so a fleet does not upload together. */
|
|
253
|
+
start(): void;
|
|
254
|
+
private schedule;
|
|
255
|
+
stop(): Promise<void>;
|
|
256
|
+
status(): UploaderStatus;
|
|
257
|
+
}
|