@cesarus777/opencode-cgp 0.1.0 → 0.2.2
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 +7 -3
- package/dist/cli.js +1411 -0
- package/dist/core.js +424 -3
- package/dist/index.js +1087 -29
- package/dist/rpc.js +159 -0
- package/dist/tui.js +653 -82
- package/dist/types/adapter.d.ts +2 -0
- package/dist/types/chartreport.d.ts +14 -0
- package/dist/types/charts.d.ts +54 -0
- package/dist/types/core.d.ts +2 -0
- package/dist/types/family.d.ts +1 -1
- package/dist/types/history.d.ts +59 -0
- package/dist/types/historycharts.d.ts +17 -0
- package/dist/types/historyserver.d.ts +7 -0
- package/dist/types/performance.d.ts +57 -0
- package/dist/types/rpc.d.ts +78 -0
- package/dist/types/snapshot.d.ts +4 -1
- package/dist/types/snapshotserver.d.ts +3 -3
- package/dist/types/tui/chartformat.d.ts +46 -0
- package/dist/types/tui/charts.d.ts +11 -0
- package/dist/types/tui/compactformat.d.ts +3 -2
- package/dist/types/tui/contextchart.d.ts +19 -0
- package/dist/types/tui/format.d.ts +1 -0
- package/dist/types/tui/state.d.ts +1 -0
- package/dist/types/types.d.ts +11 -0
- package/package.json +9 -3
package/dist/index.js
CHANGED
|
@@ -114,6 +114,154 @@ var monitorRpc = Rpc.define({
|
|
|
114
114
|
methods: { read: { input: readSchema, output: resultSchema } },
|
|
115
115
|
events: { changed: { schema: batchSchema } }
|
|
116
116
|
});
|
|
117
|
+
var nonnegative = { type: "number", minimum: 0 };
|
|
118
|
+
var performanceSchema = object({
|
|
119
|
+
observedSince: count,
|
|
120
|
+
at: count,
|
|
121
|
+
sessionCreatedAt: count,
|
|
122
|
+
sessionOutputTokens: count,
|
|
123
|
+
status: enumeration("waiting", "streaming", "idle", "unavailable"),
|
|
124
|
+
partial: { type: "boolean" },
|
|
125
|
+
liveTps: nonnegative,
|
|
126
|
+
averageModelTps: nonnegative,
|
|
127
|
+
averageSessionTps: nonnegative,
|
|
128
|
+
ttftMs: nonnegative,
|
|
129
|
+
latestRequestTps: nonnegative,
|
|
130
|
+
requestId: text,
|
|
131
|
+
requestAgent: text,
|
|
132
|
+
requestModel: text,
|
|
133
|
+
requestOutputTokens: count,
|
|
134
|
+
requestStartedAt: count,
|
|
135
|
+
firstOutputAt: count,
|
|
136
|
+
streamEndedAt: count,
|
|
137
|
+
estimatedOutputTokens: nonnegative,
|
|
138
|
+
modelOutputTokens: nonnegative,
|
|
139
|
+
modelDurationMs: nonnegative,
|
|
140
|
+
completedRequests: count,
|
|
141
|
+
tokenSource: enumeration("estimated", "reported", "unavailable")
|
|
142
|
+
}, [
|
|
143
|
+
"sessionCreatedAt",
|
|
144
|
+
"observedSince",
|
|
145
|
+
"at",
|
|
146
|
+
"status",
|
|
147
|
+
"partial",
|
|
148
|
+
"estimatedOutputTokens",
|
|
149
|
+
"modelOutputTokens",
|
|
150
|
+
"modelDurationMs",
|
|
151
|
+
"completedRequests",
|
|
152
|
+
"tokenSource"
|
|
153
|
+
]);
|
|
154
|
+
var performanceRow = {
|
|
155
|
+
...row,
|
|
156
|
+
properties: { ...row.properties, performance: performanceSchema }
|
|
157
|
+
};
|
|
158
|
+
var performanceBatchSchema = object({
|
|
159
|
+
...batchFields,
|
|
160
|
+
upserts: array(performanceRow)
|
|
161
|
+
});
|
|
162
|
+
var performanceResultSchema = object({
|
|
163
|
+
...batchFields,
|
|
164
|
+
upserts: array(performanceRow),
|
|
165
|
+
unavailable: resultSchema.properties.unavailable ?? {}
|
|
166
|
+
});
|
|
167
|
+
var performanceRpc = Rpc.define({
|
|
168
|
+
id: "context-guard.monitor.v2",
|
|
169
|
+
methods: { read: { input: readSchema, output: performanceResultSchema } },
|
|
170
|
+
events: { changed: { schema: performanceBatchSchema } }
|
|
171
|
+
});
|
|
172
|
+
function isPerformanceBatch(value) {
|
|
173
|
+
return valid(value, performanceBatchSchema) && uniqueRows(value);
|
|
174
|
+
}
|
|
175
|
+
function isPerformanceResult(value) {
|
|
176
|
+
return valid(value, performanceResultSchema) && uniqueRows(value);
|
|
177
|
+
}
|
|
178
|
+
function isPerformanceSnapshot(value) {
|
|
179
|
+
return valid(value, performanceSchema);
|
|
180
|
+
}
|
|
181
|
+
var chartPointSchema = object({
|
|
182
|
+
time: count,
|
|
183
|
+
agent: text,
|
|
184
|
+
model: text,
|
|
185
|
+
breakBefore: { type: "boolean" },
|
|
186
|
+
partial: { type: "boolean" },
|
|
187
|
+
liveTps: nonnegative,
|
|
188
|
+
averageModelTps: nonnegative,
|
|
189
|
+
averageSessionTps: nonnegative,
|
|
190
|
+
sessionOutputTokens: count
|
|
191
|
+
}, ["time", "breakBefore", "partial"]);
|
|
192
|
+
var chartRequestSchema = object({
|
|
193
|
+
time: count,
|
|
194
|
+
requestId: text,
|
|
195
|
+
started: count,
|
|
196
|
+
agent: text,
|
|
197
|
+
model: text,
|
|
198
|
+
ttftMs: nonnegative,
|
|
199
|
+
outputTokens: count,
|
|
200
|
+
durationMs: nonnegative,
|
|
201
|
+
tps: nonnegative,
|
|
202
|
+
partial: { type: "boolean" }
|
|
203
|
+
}, ["time", "requestId", "started", "partial"]);
|
|
204
|
+
var historyReadSchema = object({
|
|
205
|
+
schemaVersion: { const: 1 },
|
|
206
|
+
sessionId: text,
|
|
207
|
+
from: count,
|
|
208
|
+
to: count,
|
|
209
|
+
maxPoints: { type: "integer", minimum: 16, maximum: 500 }
|
|
210
|
+
});
|
|
211
|
+
var historyResultSchema = object({
|
|
212
|
+
schemaVersion: { const: 1 },
|
|
213
|
+
generatedAt: count,
|
|
214
|
+
enabled: { type: "boolean" },
|
|
215
|
+
truncated: { type: "boolean" },
|
|
216
|
+
metadata: object({
|
|
217
|
+
schema: { const: 1 },
|
|
218
|
+
retainedFrom: count,
|
|
219
|
+
retainedBytes: count,
|
|
220
|
+
skippedBytes: count,
|
|
221
|
+
malformedRecords: count,
|
|
222
|
+
completeness: enumeration("bounded-retention")
|
|
223
|
+
}, [
|
|
224
|
+
"schema",
|
|
225
|
+
"retainedBytes",
|
|
226
|
+
"skippedBytes",
|
|
227
|
+
"malformedRecords",
|
|
228
|
+
"completeness"
|
|
229
|
+
]),
|
|
230
|
+
data: object({
|
|
231
|
+
schema: { const: 1 },
|
|
232
|
+
omittedSessions: count,
|
|
233
|
+
omittedGaps: count,
|
|
234
|
+
maxPoints: count,
|
|
235
|
+
gaps: array(object({ time: count, sessionId: text, reason: text }), 128),
|
|
236
|
+
series: array(object({
|
|
237
|
+
sessionId: text,
|
|
238
|
+
parentId: text,
|
|
239
|
+
agents: array(text, 1024),
|
|
240
|
+
models: array(text, 1024),
|
|
241
|
+
points: array(chartPointSchema, 500),
|
|
242
|
+
requests: array(chartRequestSchema, 500),
|
|
243
|
+
originalPoints: count
|
|
244
|
+
}, [
|
|
245
|
+
"sessionId",
|
|
246
|
+
"agents",
|
|
247
|
+
"models",
|
|
248
|
+
"points",
|
|
249
|
+
"requests",
|
|
250
|
+
"originalPoints"
|
|
251
|
+
]), 1)
|
|
252
|
+
}, ["schema", "omittedSessions", "maxPoints", "gaps", "series"])
|
|
253
|
+
});
|
|
254
|
+
var historyRpc = Rpc.define({
|
|
255
|
+
id: "context-guard.history.v1",
|
|
256
|
+
methods: { read: { input: historyReadSchema, output: historyResultSchema } },
|
|
257
|
+
events: {}
|
|
258
|
+
});
|
|
259
|
+
function isHistoryRead(value) {
|
|
260
|
+
return valid(value, historyReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from <= value.to && value.to - value.from <= 7 * 86400000;
|
|
261
|
+
}
|
|
262
|
+
function isHistoryResult(value) {
|
|
263
|
+
return valid(value, historyResultSchema);
|
|
264
|
+
}
|
|
117
265
|
function record(x) {
|
|
118
266
|
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
119
267
|
}
|
|
@@ -166,6 +314,715 @@ function uniqueRows(value) {
|
|
|
166
314
|
// src/index.ts
|
|
167
315
|
import { Plugin } from "@opencode/plugin";
|
|
168
316
|
|
|
317
|
+
// src/performance.ts
|
|
318
|
+
var valid2 = (n) => Number.isFinite(n) && n >= 0;
|
|
319
|
+
var rate = (tokens, ms) => valid2(tokens) && ms > 0 ? tokens * 1000 / ms : undefined;
|
|
320
|
+
function outputTokens(tokens) {
|
|
321
|
+
const n = tokens.output + tokens.reasoning;
|
|
322
|
+
return Number.isSafeInteger(tokens.output) && tokens.output >= 0 && Number.isSafeInteger(tokens.reasoning) && tokens.reasoning >= 0 && Number.isSafeInteger(n) ? n : undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
class PerformanceTracker {
|
|
326
|
+
maxSessions;
|
|
327
|
+
windowMs;
|
|
328
|
+
now;
|
|
329
|
+
rows = new Map;
|
|
330
|
+
constructor(maxSessions, windowMs = 2000, now = Date.now) {
|
|
331
|
+
this.maxSessions = maxSessions;
|
|
332
|
+
this.windowMs = windowMs;
|
|
333
|
+
this.now = now;
|
|
334
|
+
}
|
|
335
|
+
observe(id, created, cumulative) {
|
|
336
|
+
let row2 = this.rows.get(id);
|
|
337
|
+
if (!row2) {
|
|
338
|
+
if (this.rows.size >= this.maxSessions)
|
|
339
|
+
return;
|
|
340
|
+
row2 = {
|
|
341
|
+
observedSince: this.now(),
|
|
342
|
+
created,
|
|
343
|
+
partial: cumulative !== 0,
|
|
344
|
+
output: 0,
|
|
345
|
+
duration: 0,
|
|
346
|
+
completed: 0,
|
|
347
|
+
sequence: -1
|
|
348
|
+
};
|
|
349
|
+
this.rows.set(id, row2);
|
|
350
|
+
}
|
|
351
|
+
if (valid2(created))
|
|
352
|
+
row2.created = created;
|
|
353
|
+
if (cumulative !== undefined && valid2(cumulative))
|
|
354
|
+
row2.cumulative = cumulative;
|
|
355
|
+
}
|
|
356
|
+
accept(id, sequence) {
|
|
357
|
+
const row2 = this.rows.get(id);
|
|
358
|
+
if (!row2 || sequence <= row2.sequence)
|
|
359
|
+
return false;
|
|
360
|
+
row2.sequence = sequence;
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
start(id, requestId, agent, model, started) {
|
|
364
|
+
const row2 = this.rows.get(id);
|
|
365
|
+
if (!row2 || !valid2(started))
|
|
366
|
+
return;
|
|
367
|
+
const old = row2.request;
|
|
368
|
+
if (old?.id === requestId && old.started === started)
|
|
369
|
+
return;
|
|
370
|
+
if (old && !old.settled)
|
|
371
|
+
row2.partial = true;
|
|
372
|
+
row2.request = {
|
|
373
|
+
id: requestId,
|
|
374
|
+
agent,
|
|
375
|
+
model,
|
|
376
|
+
started,
|
|
377
|
+
chars: 0,
|
|
378
|
+
settled: false,
|
|
379
|
+
events: new Set,
|
|
380
|
+
buckets: new Map
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
delta(id, requestId, eventId, at, characters) {
|
|
384
|
+
const row2 = this.rows.get(id), r = row2?.request;
|
|
385
|
+
if (!row2 || !r || r.id !== requestId || r.settled || r.ended !== undefined || !valid2(at) || at < r.started || !valid2(characters) || !characters || r.events.has(eventId))
|
|
386
|
+
return;
|
|
387
|
+
if (r.events.size >= 256)
|
|
388
|
+
r.events.delete(r.events.values().next().value ?? "");
|
|
389
|
+
r.events.add(eventId);
|
|
390
|
+
r.first = r.first === undefined ? at : Math.min(r.first, at);
|
|
391
|
+
r.chars += characters;
|
|
392
|
+
const bucket = Math.floor(at / 100) * 100;
|
|
393
|
+
r.buckets.set(bucket, (r.buckets.get(bucket) ?? 0) + characters);
|
|
394
|
+
this.prune(r, Math.max(at, this.now()));
|
|
395
|
+
while (r.buckets.size > Math.ceil(this.windowMs / 100) + 2) {
|
|
396
|
+
r.buckets.delete(Math.min(...r.buckets.keys()));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
streamed(id, requestId, at) {
|
|
400
|
+
const row2 = this.rows.get(id), r = row2?.request;
|
|
401
|
+
if (!row2 || !r || r.id !== requestId || r.settled || r.ended !== undefined)
|
|
402
|
+
return;
|
|
403
|
+
if (!valid2(at) || at <= r.started || r.first !== undefined && at < r.first) {
|
|
404
|
+
row2.partial = true;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
r.ended = at;
|
|
408
|
+
}
|
|
409
|
+
settle(id, requestId, tokens) {
|
|
410
|
+
const row2 = this.rows.get(id), r = row2?.request;
|
|
411
|
+
if (!row2)
|
|
412
|
+
return;
|
|
413
|
+
if (!r || r.id !== requestId) {
|
|
414
|
+
row2.partial = true;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (r.settled)
|
|
418
|
+
return;
|
|
419
|
+
r.settled = true;
|
|
420
|
+
if (tokens !== undefined && valid2(tokens))
|
|
421
|
+
r.output = tokens;
|
|
422
|
+
if (tokens !== undefined && valid2(tokens) && r.ended !== undefined) {
|
|
423
|
+
row2.output += tokens;
|
|
424
|
+
row2.duration += r.ended - r.started;
|
|
425
|
+
row2.completed++;
|
|
426
|
+
} else
|
|
427
|
+
row2.partial = true;
|
|
428
|
+
const snapshot = this.snapshot(id);
|
|
429
|
+
if (snapshot) {
|
|
430
|
+
const { latestRequestTps: _previousRate, ...current } = snapshot;
|
|
431
|
+
row2.latest = {
|
|
432
|
+
...current,
|
|
433
|
+
tokenSource: tokens === undefined ? "unavailable" : "reported",
|
|
434
|
+
...tokens !== undefined && r.ended !== undefined ? { latestRequestTps: tokens * 1000 / (r.ended - r.started) } : {}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
r.events.clear();
|
|
438
|
+
r.buckets.clear();
|
|
439
|
+
}
|
|
440
|
+
gap(id) {
|
|
441
|
+
for (const [key, row2] of this.rows) {
|
|
442
|
+
if (id !== undefined && key !== id)
|
|
443
|
+
continue;
|
|
444
|
+
row2.partial = true;
|
|
445
|
+
delete row2.request;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
prune(r, at) {
|
|
449
|
+
for (const time of r.buckets.keys())
|
|
450
|
+
if (time < at - this.windowMs)
|
|
451
|
+
r.buckets.delete(time);
|
|
452
|
+
}
|
|
453
|
+
snapshot(id) {
|
|
454
|
+
const row2 = this.rows.get(id);
|
|
455
|
+
if (!row2)
|
|
456
|
+
return;
|
|
457
|
+
const at = this.now(), r = row2.request;
|
|
458
|
+
const active = r && !r.settled && r.ended === undefined;
|
|
459
|
+
if (r)
|
|
460
|
+
this.prune(r, at);
|
|
461
|
+
const live = active ? rate([...r.buckets.values()].reduce((a, b) => a + b, 0) / 4, Math.min(this.windowMs, at - r.started)) : 0;
|
|
462
|
+
const averageModelTps = rate(row2.output, row2.duration);
|
|
463
|
+
const averageSessionTps = row2.cumulative === undefined ? undefined : rate(row2.cumulative, at - row2.created);
|
|
464
|
+
return {
|
|
465
|
+
observedSince: row2.observedSince,
|
|
466
|
+
at,
|
|
467
|
+
sessionCreatedAt: row2.created,
|
|
468
|
+
...row2.cumulative === undefined ? {} : { sessionOutputTokens: row2.cumulative },
|
|
469
|
+
status: active ? r.first === undefined ? "waiting" : "streaming" : !r && row2.partial ? "unavailable" : "idle",
|
|
470
|
+
partial: row2.partial,
|
|
471
|
+
...live === undefined || !r && row2.partial ? {} : { liveTps: live },
|
|
472
|
+
...averageModelTps === undefined ? {} : { averageModelTps },
|
|
473
|
+
...averageSessionTps === undefined ? {} : { averageSessionTps },
|
|
474
|
+
...r ? {
|
|
475
|
+
requestId: r.id,
|
|
476
|
+
requestAgent: r.agent,
|
|
477
|
+
requestModel: r.model,
|
|
478
|
+
requestStartedAt: r.started,
|
|
479
|
+
...r.output === undefined ? {} : { requestOutputTokens: r.output },
|
|
480
|
+
...r.first === undefined ? {} : { firstOutputAt: r.first, ttftMs: r.first - r.started },
|
|
481
|
+
...r.ended === undefined ? {} : { streamEndedAt: r.ended }
|
|
482
|
+
} : row2.latest ? {
|
|
483
|
+
...row2.latest.ttftMs === undefined ? {} : { ttftMs: row2.latest.ttftMs }
|
|
484
|
+
} : {},
|
|
485
|
+
...row2.latest?.latestRequestTps === undefined ? {} : { latestRequestTps: row2.latest.latestRequestTps },
|
|
486
|
+
estimatedOutputTokens: r ? r.chars / 4 : 0,
|
|
487
|
+
modelOutputTokens: row2.output,
|
|
488
|
+
modelDurationMs: row2.duration,
|
|
489
|
+
completedRequests: row2.completed,
|
|
490
|
+
tokenSource: r && !r.settled ? "estimated" : row2.latest?.tokenSource ?? "unavailable"
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
checkpoint() {
|
|
494
|
+
return [...this.rows].map(([sessionId, r]) => ({
|
|
495
|
+
sessionId,
|
|
496
|
+
observedSince: r.observedSince,
|
|
497
|
+
created: r.created,
|
|
498
|
+
output: r.output,
|
|
499
|
+
duration: r.duration,
|
|
500
|
+
completed: r.completed,
|
|
501
|
+
sequence: r.sequence
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
restore(value) {
|
|
505
|
+
if (!Array.isArray(value))
|
|
506
|
+
return;
|
|
507
|
+
for (const item of value.slice(0, this.maxSessions)) {
|
|
508
|
+
if (!item || typeof item !== "object")
|
|
509
|
+
continue;
|
|
510
|
+
const r = item;
|
|
511
|
+
if (typeof r.sessionId !== "string" || !r.sessionId.length || r.sessionId.length > 512 || ![r.observedSince, r.created, r.output, r.duration, r.completed].every((n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0) || typeof r.sequence !== "number" || !Number.isSafeInteger(r.sequence) || r.sequence < -1)
|
|
512
|
+
continue;
|
|
513
|
+
const { observedSince, created, output, duration, completed, sequence } = r;
|
|
514
|
+
if (typeof observedSince !== "number" || typeof created !== "number" || typeof output !== "number" || typeof duration !== "number" || typeof completed !== "number")
|
|
515
|
+
continue;
|
|
516
|
+
this.rows.set(r.sessionId, {
|
|
517
|
+
observedSince,
|
|
518
|
+
created,
|
|
519
|
+
output,
|
|
520
|
+
duration,
|
|
521
|
+
completed,
|
|
522
|
+
sequence,
|
|
523
|
+
partial: true
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
remove(id) {
|
|
528
|
+
this.rows.delete(id);
|
|
529
|
+
}
|
|
530
|
+
ids() {
|
|
531
|
+
return this.rows.keys();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/charts.ts
|
|
536
|
+
var rateFields = [
|
|
537
|
+
"liveTps",
|
|
538
|
+
"averageModelTps",
|
|
539
|
+
"averageSessionTps",
|
|
540
|
+
"sessionOutputTokens"
|
|
541
|
+
];
|
|
542
|
+
function downsamplePoints(points, limit) {
|
|
543
|
+
if (!Number.isSafeInteger(limit) || limit < 16 || limit > 5000)
|
|
544
|
+
throw new Error("maxPoints must be an integer from 16 to 5000");
|
|
545
|
+
if (points.length <= limit)
|
|
546
|
+
return [...points];
|
|
547
|
+
const buckets = Math.max(1, Math.floor(limit / (2 + rateFields.length * 2)));
|
|
548
|
+
const selected = new Set;
|
|
549
|
+
for (let b = 0;b < buckets; b++) {
|
|
550
|
+
const start = Math.floor(b * points.length / buckets), end = Math.floor((b + 1) * points.length / buckets);
|
|
551
|
+
selected.add(start);
|
|
552
|
+
selected.add(end - 1);
|
|
553
|
+
for (const field of rateFields) {
|
|
554
|
+
let low, high;
|
|
555
|
+
for (let i = start;i < end; i++) {
|
|
556
|
+
const value = points[i]?.[field];
|
|
557
|
+
if (value === undefined)
|
|
558
|
+
continue;
|
|
559
|
+
if (low === undefined || value < (points[low]?.[field] ?? Infinity))
|
|
560
|
+
low = i;
|
|
561
|
+
if (high === undefined || value > (points[high]?.[field] ?? -Infinity))
|
|
562
|
+
high = i;
|
|
563
|
+
}
|
|
564
|
+
if (low !== undefined)
|
|
565
|
+
selected.add(low);
|
|
566
|
+
if (high !== undefined)
|
|
567
|
+
selected.add(high);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
let previous = -1;
|
|
571
|
+
return [...selected].sort((a, b) => a - b).flatMap((index) => {
|
|
572
|
+
const point = points[index];
|
|
573
|
+
if (!point)
|
|
574
|
+
return [];
|
|
575
|
+
const skippedGap = points.slice(previous + 1, index + 1).some((p) => p.breakBefore);
|
|
576
|
+
previous = index;
|
|
577
|
+
return [{ ...point, breakBefore: point.breakBefore || skippedGap }];
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function buildChartData(records, options = {}) {
|
|
581
|
+
const maxPoints = options.maxPoints ?? 1000, maxSessions = options.maxSessions ?? 128;
|
|
582
|
+
if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 1024)
|
|
583
|
+
throw new Error("maxSessions must be from 1 to 1024");
|
|
584
|
+
downsamplePoints([], maxPoints);
|
|
585
|
+
const sorted = [...records].sort((a, b) => a.time - b.time || a.recordId.localeCompare(b.recordId));
|
|
586
|
+
const lastSeen = new Map;
|
|
587
|
+
for (const r of sorted)
|
|
588
|
+
if (r.performance && (!options.sessionId || r.sessionId === options.sessionId))
|
|
589
|
+
lastSeen.set(r.sessionId, r.time);
|
|
590
|
+
const sessions = [...lastSeen].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
591
|
+
const selected = new Set(sessions.slice(0, maxSessions).map(([id]) => id));
|
|
592
|
+
const series = new Map, gaps = [];
|
|
593
|
+
const seen = new Set, requestSeen = new Set;
|
|
594
|
+
const pendingGaps = new Set;
|
|
595
|
+
const observedSince = new Map;
|
|
596
|
+
for (const r of sorted) {
|
|
597
|
+
if (seen.has(r.recordId))
|
|
598
|
+
continue;
|
|
599
|
+
seen.add(r.recordId);
|
|
600
|
+
if (r.type === "gap") {
|
|
601
|
+
if (r.sessionId === "__history__" || selected.has(r.sessionId)) {
|
|
602
|
+
gaps.push({
|
|
603
|
+
time: r.time,
|
|
604
|
+
sessionId: r.sessionId,
|
|
605
|
+
reason: r.event ?? "observation-gap"
|
|
606
|
+
});
|
|
607
|
+
if (r.sessionId === "__history__")
|
|
608
|
+
for (const id of selected)
|
|
609
|
+
pendingGaps.add(id);
|
|
610
|
+
else
|
|
611
|
+
pendingGaps.add(r.sessionId);
|
|
612
|
+
}
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const p = r.performance;
|
|
616
|
+
if (!p || !selected.has(r.sessionId))
|
|
617
|
+
continue;
|
|
618
|
+
let s = series.get(r.sessionId);
|
|
619
|
+
if (!s) {
|
|
620
|
+
s = {
|
|
621
|
+
sessionId: r.sessionId,
|
|
622
|
+
...r.parentId ? { parentId: r.parentId } : {},
|
|
623
|
+
agents: [],
|
|
624
|
+
models: [],
|
|
625
|
+
points: [],
|
|
626
|
+
requests: [],
|
|
627
|
+
originalPoints: 0
|
|
628
|
+
};
|
|
629
|
+
series.set(r.sessionId, s);
|
|
630
|
+
}
|
|
631
|
+
const agent = p.requestAgent ?? r.agent, model = p.requestModel ?? r.model;
|
|
632
|
+
if (agent && !s.agents.includes(agent))
|
|
633
|
+
s.agents.push(agent);
|
|
634
|
+
if (model && !s.models.includes(model))
|
|
635
|
+
s.models.push(model);
|
|
636
|
+
const previous = s.points.at(-1);
|
|
637
|
+
const restart = observedSince.has(r.sessionId) && observedSince.get(r.sessionId) !== p.observedSince;
|
|
638
|
+
const reset = previous?.sessionOutputTokens !== undefined && p.sessionOutputTokens !== undefined && p.sessionOutputTokens < previous.sessionOutputTokens;
|
|
639
|
+
const discontinuity = pendingGaps.has(r.sessionId) || restart || reset || !previous || previous.partial !== p.partial || previous.liveTps === undefined || p.liveTps === undefined || r.time - previous.time > 2500 || previous.agent !== agent || previous.model !== model;
|
|
640
|
+
const point = {
|
|
641
|
+
time: r.time,
|
|
642
|
+
...agent ? { agent } : {},
|
|
643
|
+
...model ? { model } : {},
|
|
644
|
+
breakBefore: discontinuity,
|
|
645
|
+
partial: p.partial,
|
|
646
|
+
...p.liveTps === undefined ? {} : { liveTps: p.liveTps },
|
|
647
|
+
...p.averageModelTps === undefined ? {} : { averageModelTps: p.averageModelTps },
|
|
648
|
+
...p.averageSessionTps === undefined ? {} : { averageSessionTps: p.averageSessionTps },
|
|
649
|
+
...p.sessionOutputTokens === undefined ? {} : { sessionOutputTokens: p.sessionOutputTokens }
|
|
650
|
+
};
|
|
651
|
+
if (previous?.time === point.time)
|
|
652
|
+
s.points[s.points.length - 1] = {
|
|
653
|
+
...point,
|
|
654
|
+
breakBefore: point.breakBefore || previous.breakBefore
|
|
655
|
+
};
|
|
656
|
+
else
|
|
657
|
+
s.points.push(point);
|
|
658
|
+
observedSince.set(r.sessionId, p.observedSince);
|
|
659
|
+
pendingGaps.delete(r.sessionId);
|
|
660
|
+
if (r.type === "request" && p.requestId !== undefined && p.requestStartedAt !== undefined) {
|
|
661
|
+
const key = JSON.stringify([
|
|
662
|
+
r.sessionId,
|
|
663
|
+
p.requestId,
|
|
664
|
+
p.requestStartedAt
|
|
665
|
+
]);
|
|
666
|
+
if (requestSeen.has(key))
|
|
667
|
+
continue;
|
|
668
|
+
requestSeen.add(key);
|
|
669
|
+
const duration = p.streamEndedAt === undefined ? undefined : p.streamEndedAt - p.requestStartedAt;
|
|
670
|
+
const validDuration = duration !== undefined && duration > 0;
|
|
671
|
+
const output = p.tokenSource === "reported" ? p.requestOutputTokens : undefined;
|
|
672
|
+
s.requests.push({
|
|
673
|
+
time: r.time,
|
|
674
|
+
requestId: p.requestId,
|
|
675
|
+
started: p.requestStartedAt,
|
|
676
|
+
...agent ? { agent } : {},
|
|
677
|
+
...model ? { model } : {},
|
|
678
|
+
partial: p.partial,
|
|
679
|
+
...p.ttftMs === undefined ? {} : { ttftMs: p.ttftMs },
|
|
680
|
+
...output === undefined ? {} : { outputTokens: output },
|
|
681
|
+
...validDuration ? { durationMs: duration } : {},
|
|
682
|
+
...output !== undefined && validDuration ? { tps: output * 1000 / duration } : {}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
schema: 1,
|
|
688
|
+
series: [...series.values()].map((s) => ({
|
|
689
|
+
...s,
|
|
690
|
+
originalPoints: s.points.length,
|
|
691
|
+
points: downsamplePoints(s.points, maxPoints)
|
|
692
|
+
})),
|
|
693
|
+
gaps: gaps.slice(-1024),
|
|
694
|
+
omittedGaps: Math.max(0, gaps.length - 1024),
|
|
695
|
+
omittedSessions: Math.max(0, sessions.length - selected.size),
|
|
696
|
+
maxPoints
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/history.ts
|
|
701
|
+
import { mkdir, readdir, stat, open, unlink } from "fs/promises";
|
|
702
|
+
import { join } from "path";
|
|
703
|
+
var filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
|
|
704
|
+
async function segments(directory) {
|
|
705
|
+
const names = await readdir(directory).catch((error) => {
|
|
706
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
707
|
+
return [];
|
|
708
|
+
throw error;
|
|
709
|
+
});
|
|
710
|
+
return (await Promise.all(names.filter((n) => filename.test(n)).map(async (name) => ({
|
|
711
|
+
name,
|
|
712
|
+
time: Number(filename.exec(name)?.[1]),
|
|
713
|
+
bytes: (await stat(join(directory, name))).size
|
|
714
|
+
})))).sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
class PerformanceHistory {
|
|
718
|
+
path;
|
|
719
|
+
config;
|
|
720
|
+
now;
|
|
721
|
+
report;
|
|
722
|
+
pending = [];
|
|
723
|
+
working;
|
|
724
|
+
active;
|
|
725
|
+
bytes = 0;
|
|
726
|
+
started = 0;
|
|
727
|
+
disabled = false;
|
|
728
|
+
dropped = 0;
|
|
729
|
+
droppedTotal = 0;
|
|
730
|
+
constructor(path, config, now = Date.now, report = () => {
|
|
731
|
+
}) {
|
|
732
|
+
this.path = path;
|
|
733
|
+
this.config = config;
|
|
734
|
+
this.now = now;
|
|
735
|
+
this.report = report;
|
|
736
|
+
}
|
|
737
|
+
emit(record2) {
|
|
738
|
+
if (!this.config.enabled || this.disabled)
|
|
739
|
+
return;
|
|
740
|
+
const line = JSON.stringify({
|
|
741
|
+
schema: 1,
|
|
742
|
+
recordId: record2.recordId,
|
|
743
|
+
time: record2.time,
|
|
744
|
+
type: record2.type,
|
|
745
|
+
sessionId: record2.sessionId,
|
|
746
|
+
...record2.parentId ? { parentId: record2.parentId } : {},
|
|
747
|
+
...record2.agent ? { agent: record2.agent } : {},
|
|
748
|
+
...record2.model ? { model: record2.model } : {},
|
|
749
|
+
...record2.event ? { event: record2.event } : {},
|
|
750
|
+
...record2.droppedRecords === undefined ? {} : { droppedRecords: record2.droppedRecords },
|
|
751
|
+
...record2.performance ? { performance: scalarPerformance(record2.performance) } : {}
|
|
752
|
+
}) + "\n";
|
|
753
|
+
if (this.pending.length >= 256 || Buffer.byteLength(line) > Math.min(4 * 1024 * 1024, this.config.maxBytes)) {
|
|
754
|
+
this.dropped++;
|
|
755
|
+
this.droppedTotal++;
|
|
756
|
+
if (this.dropped === 1)
|
|
757
|
+
this.report("history-records-dropped");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
this.pending.push(line);
|
|
761
|
+
this.kick();
|
|
762
|
+
}
|
|
763
|
+
kick() {
|
|
764
|
+
if (this.working || this.disabled)
|
|
765
|
+
return;
|
|
766
|
+
this.working = this.drain().catch(() => {
|
|
767
|
+
this.disabled = true;
|
|
768
|
+
this.pending = [];
|
|
769
|
+
this.report("history-unavailable");
|
|
770
|
+
}).finally(() => {
|
|
771
|
+
this.working = undefined;
|
|
772
|
+
if (this.pending.length)
|
|
773
|
+
this.kick();
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
async prune(incoming = 0) {
|
|
777
|
+
const files = await segments(this.path);
|
|
778
|
+
let total = files.reduce((n, f) => n + f.bytes, 0);
|
|
779
|
+
let truncated = false;
|
|
780
|
+
for (const file of files) {
|
|
781
|
+
if (file.time >= this.now() - this.config.retentionDays * 86400000 && total + incoming <= this.config.maxBytes)
|
|
782
|
+
break;
|
|
783
|
+
await unlink(join(this.path, file.name));
|
|
784
|
+
if (file.name === this.active) {
|
|
785
|
+
this.active = undefined;
|
|
786
|
+
this.bytes = 0;
|
|
787
|
+
}
|
|
788
|
+
total -= file.bytes;
|
|
789
|
+
truncated = true;
|
|
790
|
+
}
|
|
791
|
+
if (truncated)
|
|
792
|
+
this.report("history-truncated");
|
|
793
|
+
}
|
|
794
|
+
gapLine(event, droppedRecords) {
|
|
795
|
+
return JSON.stringify({
|
|
796
|
+
schema: 1,
|
|
797
|
+
recordId: crypto.randomUUID(),
|
|
798
|
+
time: this.now(),
|
|
799
|
+
type: "gap",
|
|
800
|
+
sessionId: "__history__",
|
|
801
|
+
event,
|
|
802
|
+
...droppedRecords === undefined ? {} : { droppedRecords }
|
|
803
|
+
}) + "\n";
|
|
804
|
+
}
|
|
805
|
+
async drain() {
|
|
806
|
+
await mkdir(this.path, { recursive: true, mode: 448 });
|
|
807
|
+
await this.prune();
|
|
808
|
+
while (this.pending.length) {
|
|
809
|
+
let line = this.pending.shift();
|
|
810
|
+
if (!line)
|
|
811
|
+
continue;
|
|
812
|
+
if (this.dropped > 0) {
|
|
813
|
+
this.pending.unshift(line);
|
|
814
|
+
line = this.gapLine("history-records-dropped", this.dropped);
|
|
815
|
+
this.dropped = 0;
|
|
816
|
+
}
|
|
817
|
+
const size = Buffer.byteLength(line);
|
|
818
|
+
const now = this.now();
|
|
819
|
+
if (!this.active || this.bytes + size > Math.min(4 * 1024 * 1024, this.config.maxBytes) || now - this.started >= 3600000) {
|
|
820
|
+
this.active = undefined;
|
|
821
|
+
this.bytes = 0;
|
|
822
|
+
await this.prune(size);
|
|
823
|
+
this.started = now;
|
|
824
|
+
this.active = `${now}-${crypto.randomUUID()}.jsonl`;
|
|
825
|
+
} else
|
|
826
|
+
await this.prune(size);
|
|
827
|
+
if (!this.active) {
|
|
828
|
+
this.started = now;
|
|
829
|
+
this.active = `${now}-${crypto.randomUUID()}.jsonl`;
|
|
830
|
+
}
|
|
831
|
+
const file = await open(join(this.path, this.active), "a", 384);
|
|
832
|
+
try {
|
|
833
|
+
await file.writeFile(line);
|
|
834
|
+
} finally {
|
|
835
|
+
await file.close();
|
|
836
|
+
}
|
|
837
|
+
this.bytes += size;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
health() {
|
|
841
|
+
return { unavailable: this.disabled, droppedRecords: this.droppedTotal };
|
|
842
|
+
}
|
|
843
|
+
async flush() {
|
|
844
|
+
while (this.working)
|
|
845
|
+
await this.working;
|
|
846
|
+
}
|
|
847
|
+
async initialize() {
|
|
848
|
+
if (!this.config.enabled)
|
|
849
|
+
return;
|
|
850
|
+
await mkdir(this.path, { recursive: true, mode: 448 }).then(() => this.prune()).catch(() => {
|
|
851
|
+
this.disabled = true;
|
|
852
|
+
this.report("history-unavailable");
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function scalarPerformance(p) {
|
|
857
|
+
return {
|
|
858
|
+
observedSince: p.observedSince,
|
|
859
|
+
sessionCreatedAt: p.sessionCreatedAt,
|
|
860
|
+
at: p.at,
|
|
861
|
+
status: p.status,
|
|
862
|
+
partial: p.partial,
|
|
863
|
+
estimatedOutputTokens: p.estimatedOutputTokens,
|
|
864
|
+
modelOutputTokens: p.modelOutputTokens,
|
|
865
|
+
modelDurationMs: p.modelDurationMs,
|
|
866
|
+
completedRequests: p.completedRequests,
|
|
867
|
+
tokenSource: p.tokenSource,
|
|
868
|
+
...Object.fromEntries([
|
|
869
|
+
"sessionOutputTokens",
|
|
870
|
+
"liveTps",
|
|
871
|
+
"averageModelTps",
|
|
872
|
+
"averageSessionTps",
|
|
873
|
+
"ttftMs",
|
|
874
|
+
"latestRequestTps",
|
|
875
|
+
"requestId",
|
|
876
|
+
"requestAgent",
|
|
877
|
+
"requestModel",
|
|
878
|
+
"requestOutputTokens",
|
|
879
|
+
"requestStartedAt",
|
|
880
|
+
"firstOutputAt",
|
|
881
|
+
"streamEndedAt"
|
|
882
|
+
].flatMap((key) => {
|
|
883
|
+
const value = p[key];
|
|
884
|
+
return value === undefined ? [] : [[key, value]];
|
|
885
|
+
}))
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
async function readPerformanceHistory(directory, filter = {}) {
|
|
889
|
+
const records = [];
|
|
890
|
+
const files = await segments(directory);
|
|
891
|
+
const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
|
|
892
|
+
let bytes = 0, skippedBytes = 0, malformedRecords = 0;
|
|
893
|
+
const selected = [];
|
|
894
|
+
for (const file of files.slice().reverse()) {
|
|
895
|
+
if (bytes + file.bytes > maxBytes) {
|
|
896
|
+
skippedBytes += file.bytes;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
bytes += file.bytes;
|
|
900
|
+
selected.unshift(file);
|
|
901
|
+
}
|
|
902
|
+
for (const file of selected) {
|
|
903
|
+
const handle = await open(join(directory, file.name), "r");
|
|
904
|
+
let source;
|
|
905
|
+
try {
|
|
906
|
+
const buffer = Buffer.alloc(file.bytes);
|
|
907
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
908
|
+
source = buffer.subarray(0, bytesRead).toString("utf8");
|
|
909
|
+
} finally {
|
|
910
|
+
await handle.close();
|
|
911
|
+
}
|
|
912
|
+
for (const line of source.split("\n")) {
|
|
913
|
+
if (!line)
|
|
914
|
+
continue;
|
|
915
|
+
try {
|
|
916
|
+
const value = JSON.parse(line);
|
|
917
|
+
if (!isHistoryRecord(value)) {
|
|
918
|
+
malformedRecords++;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (filter.sessionId !== undefined && value.sessionId !== filter.sessionId && value.sessionId !== "__history__" || value.type !== "gap" && filter.agent !== undefined && (value.performance?.requestAgent ?? value.agent) !== filter.agent || value.type !== "gap" && filter.model !== undefined && (value.performance?.requestModel ?? value.model) !== filter.model || filter.from !== undefined && value.time < filter.from || filter.to !== undefined && value.time > filter.to)
|
|
922
|
+
continue;
|
|
923
|
+
records.push(value);
|
|
924
|
+
} catch {
|
|
925
|
+
malformedRecords++;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const metadata = {
|
|
930
|
+
schema: 1,
|
|
931
|
+
...files[0] ? { retainedFrom: files[0].time } : {},
|
|
932
|
+
retainedBytes: files.reduce((n, f) => n + f.bytes, 0),
|
|
933
|
+
skippedBytes,
|
|
934
|
+
malformedRecords,
|
|
935
|
+
completeness: "bounded-retention"
|
|
936
|
+
};
|
|
937
|
+
return { metadata, records };
|
|
938
|
+
}
|
|
939
|
+
function isRecord(value) {
|
|
940
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
941
|
+
}
|
|
942
|
+
function isHistoryRecord(value) {
|
|
943
|
+
if (!isRecord(value))
|
|
944
|
+
return false;
|
|
945
|
+
const r = value;
|
|
946
|
+
const allowed = new Set([
|
|
947
|
+
"schema",
|
|
948
|
+
"recordId",
|
|
949
|
+
"time",
|
|
950
|
+
"type",
|
|
951
|
+
"sessionId",
|
|
952
|
+
"parentId",
|
|
953
|
+
"agent",
|
|
954
|
+
"model",
|
|
955
|
+
"event",
|
|
956
|
+
"performance",
|
|
957
|
+
"droppedRecords"
|
|
958
|
+
]);
|
|
959
|
+
return Object.keys(r).every((k) => allowed.has(k)) && r.schema === 1 && typeof r.recordId === "string" && typeof r.time === "number" && Number.isSafeInteger(r.time) && r.time >= 0 && typeof r.sessionId === "string" && typeof r.type === "string" && ["sample", "request", "lifecycle", "gap", "terminal"].includes(r.type) && ["parentId", "agent", "model", "event"].every((k) => r[k] === undefined || typeof r[k] === "string") && (r.performance === undefined || isPerformanceSnapshot(r.performance)) && (r.droppedRecords === undefined || typeof r.droppedRecords === "number" && Number.isSafeInteger(r.droppedRecords) && r.droppedRecords >= 0);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/historyserver.ts
|
|
963
|
+
async function serveHistory(rpc, path, enabled, now = Date.now, authorize = async () => true) {
|
|
964
|
+
if (!rpc)
|
|
965
|
+
return () => {
|
|
966
|
+
};
|
|
967
|
+
let reading = false, stopped = false;
|
|
968
|
+
const registration = await rpc.register(historyRpc, {
|
|
969
|
+
read: async (input, context) => {
|
|
970
|
+
if (!isHistoryRead(input))
|
|
971
|
+
throw new Error("Invalid history request (maximum window: seven days)");
|
|
972
|
+
if (reading || stopped)
|
|
973
|
+
throw new Error("History service busy or stopped; retry later");
|
|
974
|
+
reading = true;
|
|
975
|
+
try {
|
|
976
|
+
const allowed = !enabled || await authorize(input.sessionId, context.signal);
|
|
977
|
+
if (!allowed)
|
|
978
|
+
throw new Error("Session is outside this location");
|
|
979
|
+
const result = enabled ? await readPerformanceHistory(path, {
|
|
980
|
+
sessionId: input.sessionId,
|
|
981
|
+
from: input.from,
|
|
982
|
+
to: input.to
|
|
983
|
+
}) : {
|
|
984
|
+
metadata: {
|
|
985
|
+
schema: 1,
|
|
986
|
+
retainedBytes: 0,
|
|
987
|
+
skippedBytes: 0,
|
|
988
|
+
malformedRecords: 0,
|
|
989
|
+
completeness: "bounded-retention"
|
|
990
|
+
},
|
|
991
|
+
records: []
|
|
992
|
+
};
|
|
993
|
+
if (context.signal.aborted || stopped)
|
|
994
|
+
throw new Error("History read cancelled");
|
|
995
|
+
const data = buildChartData(result.records, {
|
|
996
|
+
sessionId: input.sessionId,
|
|
997
|
+
maxPoints: input.maxPoints,
|
|
998
|
+
maxSessions: 1
|
|
999
|
+
});
|
|
1000
|
+
const truncated = data.gaps.length > 128 || data.series.some((s) => s.requests.length > input.maxPoints || s.originalPoints > s.points.length) || result.metadata.skippedBytes > 0;
|
|
1001
|
+
data.gaps = data.gaps.slice(-128);
|
|
1002
|
+
for (const s of data.series)
|
|
1003
|
+
s.requests = s.requests.slice(-input.maxPoints);
|
|
1004
|
+
const response = {
|
|
1005
|
+
schemaVersion: 1,
|
|
1006
|
+
generatedAt: now(),
|
|
1007
|
+
enabled,
|
|
1008
|
+
truncated,
|
|
1009
|
+
metadata: { ...result.metadata, completeness: "bounded-retention" },
|
|
1010
|
+
data
|
|
1011
|
+
};
|
|
1012
|
+
if (!isHistoryResult(response))
|
|
1013
|
+
throw new Error("History response exceeds the supported bounds");
|
|
1014
|
+
return response;
|
|
1015
|
+
} finally {
|
|
1016
|
+
reading = false;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
return async () => {
|
|
1021
|
+
stopped = true;
|
|
1022
|
+
await registration.dispose();
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
169
1026
|
// src/policy.ts
|
|
170
1027
|
function glob(pattern, text2) {
|
|
171
1028
|
let p = 0, t = 0, star = -1, retry = 0;
|
|
@@ -213,10 +1070,10 @@ class PolicyResolver {
|
|
|
213
1070
|
function usableInput(limits, reserve) {
|
|
214
1071
|
if (!limits)
|
|
215
1072
|
return;
|
|
216
|
-
const
|
|
217
|
-
const output = reserve ?? (
|
|
218
|
-
const contextBudget =
|
|
219
|
-
if (
|
|
1073
|
+
const valid3 = (n) => n !== undefined && Number.isFinite(n) && n > 0;
|
|
1074
|
+
const output = reserve ?? (valid3(limits.output) ? limits.output : undefined);
|
|
1075
|
+
const contextBudget = valid3(limits.context) && output !== undefined ? Math.max(0, limits.context - output) : undefined;
|
|
1076
|
+
if (valid3(limits.input))
|
|
220
1077
|
return contextBudget === undefined ? limits.input : Math.min(limits.input, contextBudget);
|
|
221
1078
|
return contextBudget;
|
|
222
1079
|
}
|
|
@@ -327,6 +1184,7 @@ class SnapshotStore {
|
|
|
327
1184
|
config;
|
|
328
1185
|
sessions;
|
|
329
1186
|
now;
|
|
1187
|
+
performanceSnapshot;
|
|
330
1188
|
epoch;
|
|
331
1189
|
sequence = 0;
|
|
332
1190
|
revision = 0;
|
|
@@ -336,10 +1194,11 @@ class SnapshotStore {
|
|
|
336
1194
|
listeners = new Set;
|
|
337
1195
|
healthKey = "";
|
|
338
1196
|
reasons = new Set;
|
|
339
|
-
constructor(config, sessions, now = Date.now, epoch = crypto.randomUUID()) {
|
|
1197
|
+
constructor(config, sessions, now = Date.now, epoch = crypto.randomUUID(), performanceSnapshot) {
|
|
340
1198
|
this.config = config;
|
|
341
1199
|
this.sessions = sessions;
|
|
342
1200
|
this.now = now;
|
|
1201
|
+
this.performanceSnapshot = performanceSnapshot;
|
|
343
1202
|
this.epoch = epoch;
|
|
344
1203
|
}
|
|
345
1204
|
health() {
|
|
@@ -400,7 +1259,11 @@ class SnapshotStore {
|
|
|
400
1259
|
const s = resident.get(id);
|
|
401
1260
|
if (!s)
|
|
402
1261
|
continue;
|
|
403
|
-
const
|
|
1262
|
+
const performance = this.performanceSnapshot?.(id);
|
|
1263
|
+
const next = freezeSnapshot({
|
|
1264
|
+
...project(s, this.config, this.now()),
|
|
1265
|
+
...performance ? { performance } : {}
|
|
1266
|
+
});
|
|
404
1267
|
const old = this.rows.get(id);
|
|
405
1268
|
if (old && JSON.stringify({ ...old, revision: 0 }) === JSON.stringify(next))
|
|
406
1269
|
continue;
|
|
@@ -450,6 +1313,8 @@ class SnapshotStore {
|
|
|
450
1313
|
}
|
|
451
1314
|
}
|
|
452
1315
|
function freezeSnapshot(row2) {
|
|
1316
|
+
if (row2.performance)
|
|
1317
|
+
Object.freeze(row2.performance);
|
|
453
1318
|
if (row2.measurement)
|
|
454
1319
|
Object.freeze(row2.measurement);
|
|
455
1320
|
if (row2.limits)
|
|
@@ -465,16 +1330,23 @@ function freezeSnapshot(row2) {
|
|
|
465
1330
|
}
|
|
466
1331
|
|
|
467
1332
|
// src/snapshotserver.ts
|
|
468
|
-
async function serveSnapshots(rpc, store) {
|
|
1333
|
+
async function serveSnapshots(rpc, store, performance = false) {
|
|
469
1334
|
if (!rpc)
|
|
470
1335
|
return () => {
|
|
471
1336
|
};
|
|
472
|
-
const
|
|
1337
|
+
const stripPerformance = (batch) => ({
|
|
1338
|
+
...batch,
|
|
1339
|
+
upserts: batch.upserts.map(({ performance: _performance, ...row2 }) => row2)
|
|
1340
|
+
});
|
|
1341
|
+
const batchValid = performance ? isPerformanceBatch : isBatch;
|
|
1342
|
+
const resultValid = performance ? isPerformanceResult : isResult;
|
|
1343
|
+
const registration = await rpc.register(performance ? performanceRpc : monitorRpc, {
|
|
473
1344
|
read: async (input) => {
|
|
474
1345
|
if (!isRead(input))
|
|
475
1346
|
throw new Error("Invalid snapshot request");
|
|
476
|
-
const
|
|
477
|
-
|
|
1347
|
+
const raw = store.read(input);
|
|
1348
|
+
const result = performance ? raw : stripPerformance(raw);
|
|
1349
|
+
if (!resultValid(result))
|
|
478
1350
|
throw new Error("Invalid snapshot projection");
|
|
479
1351
|
return result;
|
|
480
1352
|
}
|
|
@@ -503,8 +1375,9 @@ async function serveSnapshots(rpc, store) {
|
|
|
503
1375
|
send();
|
|
504
1376
|
});
|
|
505
1377
|
}
|
|
506
|
-
const unlisten = store.listen((
|
|
507
|
-
|
|
1378
|
+
const unlisten = store.listen((raw) => {
|
|
1379
|
+
const batch = performance ? raw : stripPerformance(raw);
|
|
1380
|
+
if (!batchValid(batch))
|
|
508
1381
|
return;
|
|
509
1382
|
queue.push(batch);
|
|
510
1383
|
if (queue.length > capacity)
|
|
@@ -570,7 +1443,16 @@ function threshold(v, path) {
|
|
|
570
1443
|
}
|
|
571
1444
|
function parseConfig(value = {}) {
|
|
572
1445
|
const o = object2(value, "options");
|
|
573
|
-
keys(o, [
|
|
1446
|
+
keys(o, [
|
|
1447
|
+
"enabled",
|
|
1448
|
+
"mode",
|
|
1449
|
+
"defaults",
|
|
1450
|
+
"policies",
|
|
1451
|
+
"safety",
|
|
1452
|
+
"telemetry",
|
|
1453
|
+
"metrics",
|
|
1454
|
+
"history"
|
|
1455
|
+
], "options");
|
|
574
1456
|
if (o.mode !== undefined && o.mode !== "observe" && o.mode !== "enforce")
|
|
575
1457
|
throw new Error("mode: expected observe or enforce");
|
|
576
1458
|
const d = object2(o.defaults ?? {}, "defaults");
|
|
@@ -586,6 +1468,12 @@ function parseConfig(value = {}) {
|
|
|
586
1468
|
"sessionTtlMs",
|
|
587
1469
|
"outputReserveTokens"
|
|
588
1470
|
], "safety");
|
|
1471
|
+
const metrics = object2(o.metrics ?? {}, "metrics");
|
|
1472
|
+
keys(metrics, ["enabled", "sampleIntervalMs", "rollingWindowMs"], "metrics");
|
|
1473
|
+
const history = object2(o.history ?? {}, "history");
|
|
1474
|
+
keys(history, ["enabled", "retentionDays", "maxBytes", "path"], "history");
|
|
1475
|
+
if (history.path !== undefined && (typeof history.path !== "string" || !history.path.startsWith("/")))
|
|
1476
|
+
throw new Error("history.path: expected absolute directory path");
|
|
589
1477
|
const t = object2(o.telemetry ?? {}, "telemetry");
|
|
590
1478
|
keys(t, ["enabled", "jsonl", "path", "maxBytes"], "telemetry");
|
|
591
1479
|
if (t.path !== undefined && (typeof t.path !== "string" || !t.path.startsWith("/")))
|
|
@@ -633,6 +1521,17 @@ function parseConfig(value = {}) {
|
|
|
633
1521
|
outputReserveTokens: number(s.outputReserveTokens, 0, "outputReserveTokens")
|
|
634
1522
|
}
|
|
635
1523
|
},
|
|
1524
|
+
metrics: {
|
|
1525
|
+
enabled: bool(metrics.enabled, true, "metrics.enabled"),
|
|
1526
|
+
sampleIntervalMs: number(metrics.sampleIntervalMs, 500, "metrics.sampleIntervalMs", 100),
|
|
1527
|
+
rollingWindowMs: number(metrics.rollingWindowMs, 2000, "metrics.rollingWindowMs", 100)
|
|
1528
|
+
},
|
|
1529
|
+
history: {
|
|
1530
|
+
enabled: bool(history.enabled, true, "history.enabled"),
|
|
1531
|
+
retentionDays: number(history.retentionDays, 7, "history.retentionDays", 1),
|
|
1532
|
+
maxBytes: number(history.maxBytes, 64 * 1024 * 1024, "history.maxBytes", 4096),
|
|
1533
|
+
...typeof history.path === "string" ? { path: history.path } : {}
|
|
1534
|
+
},
|
|
636
1535
|
telemetry: {
|
|
637
1536
|
enabled: bool(t.enabled, true, "telemetry.enabled"),
|
|
638
1537
|
jsonl: bool(t.jsonl, true, "telemetry.jsonl"),
|
|
@@ -640,6 +1539,8 @@ function parseConfig(value = {}) {
|
|
|
640
1539
|
...typeof t.path === "string" ? { path: t.path } : {}
|
|
641
1540
|
}
|
|
642
1541
|
};
|
|
1542
|
+
if (config.metrics.rollingWindowMs < config.metrics.sampleIntervalMs || config.metrics.rollingWindowMs > 60000)
|
|
1543
|
+
throw new Error("metrics.rollingWindowMs must be >= sampleIntervalMs and <= 60000");
|
|
643
1544
|
for (const policy of [config.defaults, ...config.policies]) {
|
|
644
1545
|
if (policy.threshold.type === "tokens" && config.safety.minReclaimTokens >= policy.threshold.value)
|
|
645
1546
|
throw new Error("minReclaimTokens must be smaller than each absolute threshold");
|
|
@@ -949,7 +1850,7 @@ function estimate(value, maxChars = 2000000) {
|
|
|
949
1850
|
function record2(v) {
|
|
950
1851
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
951
1852
|
}
|
|
952
|
-
function
|
|
1853
|
+
function nonnegative2(v) {
|
|
953
1854
|
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
954
1855
|
}
|
|
955
1856
|
function decodeRecovery(v, max) {
|
|
@@ -960,7 +1861,7 @@ function decodeRecovery(v, max) {
|
|
|
960
1861
|
if (!record2(item) || typeof item.sessionId !== "string" || !record2(item.compaction))
|
|
961
1862
|
continue;
|
|
962
1863
|
const c = item.compaction;
|
|
963
|
-
if (!
|
|
1864
|
+
if (!nonnegative2(c.count) || !nonnegative2(c.failures) || !nonnegative2(c.consecutiveFailures) || typeof c.armed !== "boolean")
|
|
964
1865
|
continue;
|
|
965
1866
|
const phase = c.phase === "idle" || c.phase === "blocked" ? c.phase : "uncertain";
|
|
966
1867
|
const compaction = {
|
|
@@ -970,9 +1871,9 @@ function decodeRecovery(v, max) {
|
|
|
970
1871
|
failures: c.failures,
|
|
971
1872
|
consecutiveFailures: c.consecutiveFailures
|
|
972
1873
|
};
|
|
973
|
-
if (
|
|
1874
|
+
if (nonnegative2(c.lastAttempt))
|
|
974
1875
|
compaction.lastAttempt = c.lastAttempt;
|
|
975
|
-
if (
|
|
1876
|
+
if (nonnegative2(c.lastCompleted))
|
|
976
1877
|
compaction.lastCompleted = c.lastCompleted;
|
|
977
1878
|
if (typeof c.requestId === "string")
|
|
978
1879
|
compaction.requestId = c.requestId;
|
|
@@ -983,7 +1884,7 @@ function decodeRecovery(v, max) {
|
|
|
983
1884
|
result.push({
|
|
984
1885
|
sessionId: item.sessionId,
|
|
985
1886
|
compaction,
|
|
986
|
-
...
|
|
1887
|
+
...nonnegative2(item.sequence) ? { sequence: item.sequence } : {}
|
|
987
1888
|
});
|
|
988
1889
|
}
|
|
989
1890
|
return result;
|
|
@@ -1007,12 +1908,13 @@ function encodeRecovery(rows) {
|
|
|
1007
1908
|
}
|
|
1008
1909
|
|
|
1009
1910
|
// src/telemetry.ts
|
|
1010
|
-
import { mkdir, open, rename, stat } from "fs/promises";
|
|
1011
|
-
import { dirname, join } from "path";
|
|
1911
|
+
import { mkdir as mkdir2, open as open2, rename, stat as stat2 } from "fs/promises";
|
|
1912
|
+
import { dirname, join as join2 } from "path";
|
|
1913
|
+
import { homedir } from "os";
|
|
1012
1914
|
import { createHash } from "crypto";
|
|
1013
1915
|
function defaultPath(directory) {
|
|
1014
1916
|
const key = createHash("sha256").update(directory).digest("hex").slice(0, 16);
|
|
1015
|
-
return
|
|
1917
|
+
return join2(process.env.XDG_STATE_HOME || join2(homedir(), ".local/state"), "opencode-context-guard", `${key}.jsonl`);
|
|
1016
1918
|
}
|
|
1017
1919
|
|
|
1018
1920
|
class JsonlTelemetrySink {
|
|
@@ -1053,8 +1955,8 @@ class JsonlTelemetrySink {
|
|
|
1053
1955
|
async drain() {
|
|
1054
1956
|
try {
|
|
1055
1957
|
if (!this.ready) {
|
|
1056
|
-
await
|
|
1057
|
-
this.bytes = await
|
|
1958
|
+
await mkdir2(dirname(this.path), { recursive: true, mode: 448 });
|
|
1959
|
+
this.bytes = await stat2(this.path).then((s) => s.size).catch(() => 0);
|
|
1058
1960
|
this.ready = true;
|
|
1059
1961
|
}
|
|
1060
1962
|
while (this.pending.length) {
|
|
@@ -1071,7 +1973,7 @@ class JsonlTelemetrySink {
|
|
|
1071
1973
|
});
|
|
1072
1974
|
this.bytes = 0;
|
|
1073
1975
|
}
|
|
1074
|
-
const file = await
|
|
1976
|
+
const file = await open2(this.path, "a", 384);
|
|
1075
1977
|
try {
|
|
1076
1978
|
await file.writeFile(line);
|
|
1077
1979
|
} finally {
|
|
@@ -1090,13 +1992,41 @@ class JsonlTelemetrySink {
|
|
|
1090
1992
|
await this.working;
|
|
1091
1993
|
}
|
|
1092
1994
|
}
|
|
1995
|
+
async function readJournal(path, maxBytes = 16 * 1024 * 1024) {
|
|
1996
|
+
const records = [];
|
|
1997
|
+
for (const name of [`${path}.1`, path]) {
|
|
1998
|
+
const size = await stat2(name).then((s) => s.size).catch(() => 0);
|
|
1999
|
+
if (!size)
|
|
2000
|
+
continue;
|
|
2001
|
+
const file = await open2(name, "r");
|
|
2002
|
+
const buffer = Buffer.alloc(Math.min(size, maxBytes));
|
|
2003
|
+
try {
|
|
2004
|
+
await file.read(buffer, 0, buffer.length, Math.max(0, size - maxBytes));
|
|
2005
|
+
} finally {
|
|
2006
|
+
await file.close();
|
|
2007
|
+
}
|
|
2008
|
+
const lines = buffer.toString("utf8").split("\n");
|
|
2009
|
+
if (size > maxBytes)
|
|
2010
|
+
lines.shift();
|
|
2011
|
+
for (const line of lines) {
|
|
2012
|
+
if (!line)
|
|
2013
|
+
continue;
|
|
2014
|
+
try {
|
|
2015
|
+
records.push(JSON.parse(line));
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return records;
|
|
2021
|
+
}
|
|
1093
2022
|
|
|
1094
2023
|
// src/adapter.ts
|
|
1095
2024
|
async function setup(ctx, injectedSink) {
|
|
1096
2025
|
const config = parseConfig(ctx.options);
|
|
1097
2026
|
const disk = new JsonlTelemetrySink(config.telemetry.path ?? defaultPath(ctx.location.directory), config.telemetry);
|
|
1098
2027
|
const diagnostics = injectedSink ?? disk;
|
|
1099
|
-
const
|
|
2028
|
+
const performance = new PerformanceTracker(config.safety.maxSessions, config.metrics.rollingWindowMs);
|
|
2029
|
+
const store = new SnapshotStore(config, () => monitor?.sessions ?? new Map, Date.now, crypto.randomUUID(), config.metrics.enabled ? (id) => performance.snapshot(id) : undefined);
|
|
1100
2030
|
let monitor;
|
|
1101
2031
|
const sink = {
|
|
1102
2032
|
emit(event) {
|
|
@@ -1123,7 +2053,7 @@ async function setup(ctx, injectedSink) {
|
|
|
1123
2053
|
diagnostics.emit(event);
|
|
1124
2054
|
}
|
|
1125
2055
|
};
|
|
1126
|
-
const
|
|
2056
|
+
const stopV1 = await serveSnapshots(ctx.rpc, store).catch(() => {
|
|
1127
2057
|
diagnostics.emit({
|
|
1128
2058
|
time: Date.now(),
|
|
1129
2059
|
type: "snapshot-service-unavailable"
|
|
@@ -1131,6 +2061,27 @@ async function setup(ctx, injectedSink) {
|
|
|
1131
2061
|
return () => {
|
|
1132
2062
|
};
|
|
1133
2063
|
});
|
|
2064
|
+
const stopV2 = await serveSnapshots(ctx.rpc, store, true).catch(() => {
|
|
2065
|
+
diagnostics.emit({
|
|
2066
|
+
time: Date.now(),
|
|
2067
|
+
type: "performance-service-unavailable"
|
|
2068
|
+
});
|
|
2069
|
+
return () => {
|
|
2070
|
+
};
|
|
2071
|
+
});
|
|
2072
|
+
const stopHistory = await serveHistory(ctx.historyRpc, config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, config.enabled && config.metrics.enabled && config.history.enabled, Date.now, async (sessionId, signal) => {
|
|
2073
|
+
const info = await ctx.session.get({ sessionID: sessionId }, { signal });
|
|
2074
|
+
return info.location.directory === ctx.location.directory;
|
|
2075
|
+
}).catch(() => {
|
|
2076
|
+
diagnostics.emit({ time: Date.now(), type: "history-service-unavailable" });
|
|
2077
|
+
return () => {
|
|
2078
|
+
};
|
|
2079
|
+
});
|
|
2080
|
+
const stopSnapshots = async () => {
|
|
2081
|
+
await stopHistory();
|
|
2082
|
+
await stopV2();
|
|
2083
|
+
await stopV1();
|
|
2084
|
+
};
|
|
1134
2085
|
if (!config.enabled) {
|
|
1135
2086
|
sink.emit({ time: Date.now(), type: "disabled" });
|
|
1136
2087
|
return async () => {
|
|
@@ -1138,6 +2089,46 @@ async function setup(ctx, injectedSink) {
|
|
|
1138
2089
|
await disk.flush();
|
|
1139
2090
|
};
|
|
1140
2091
|
}
|
|
2092
|
+
const history = new PerformanceHistory(config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, {
|
|
2093
|
+
...config.history,
|
|
2094
|
+
enabled: config.history.enabled && config.metrics.enabled
|
|
2095
|
+
}, Date.now, (type) => diagnostics.emit({ time: Date.now(), type }));
|
|
2096
|
+
await history.initialize();
|
|
2097
|
+
if (config.metrics.enabled) {
|
|
2098
|
+
performance.restore(await ctx.storage.get("performance.v1").catch(() => {
|
|
2099
|
+
return;
|
|
2100
|
+
}));
|
|
2101
|
+
}
|
|
2102
|
+
const performanceResidents = new Set;
|
|
2103
|
+
const recordPerformance = (id, type, event) => {
|
|
2104
|
+
if (!config.metrics.enabled)
|
|
2105
|
+
return;
|
|
2106
|
+
const state = monitor?.sessions.get(id), snapshot = performance.snapshot(id);
|
|
2107
|
+
if (!state || !snapshot)
|
|
2108
|
+
return;
|
|
2109
|
+
history.emit({
|
|
2110
|
+
schema: 1,
|
|
2111
|
+
recordId: crypto.randomUUID(),
|
|
2112
|
+
time: Date.now(),
|
|
2113
|
+
type,
|
|
2114
|
+
sessionId: id,
|
|
2115
|
+
...state.parentId ? { parentId: state.parentId } : {},
|
|
2116
|
+
...state.agent ? { agent: state.agent } : {},
|
|
2117
|
+
...state.model ? { model: state.model } : {},
|
|
2118
|
+
...event ? { event } : {},
|
|
2119
|
+
performance: snapshot
|
|
2120
|
+
});
|
|
2121
|
+
};
|
|
2122
|
+
const persistPerformance = async () => {
|
|
2123
|
+
if (!config.metrics.enabled)
|
|
2124
|
+
return;
|
|
2125
|
+
await ctx.storage.set("performance.v1", performance.checkpoint().map((row2) => ({ ...row2 }))).catch(() => {
|
|
2126
|
+
diagnostics.emit({
|
|
2127
|
+
time: Date.now(),
|
|
2128
|
+
type: "performance-persistence-unavailable"
|
|
2129
|
+
});
|
|
2130
|
+
});
|
|
2131
|
+
};
|
|
1141
2132
|
const controller = new AbortController;
|
|
1142
2133
|
let stopped = false, persistenceHealthy = true;
|
|
1143
2134
|
let work = Promise.resolve();
|
|
@@ -1245,6 +2236,15 @@ async function setup(ctx, injectedSink) {
|
|
|
1245
2236
|
s.sequence = recovery.sequence ?? -1;
|
|
1246
2237
|
recoveries.delete(sessionId);
|
|
1247
2238
|
}
|
|
2239
|
+
if (config.metrics.enabled) {
|
|
2240
|
+
if (!performance.snapshot(sessionId) && [...performance.ids()].length >= config.safety.maxSessions) {
|
|
2241
|
+
const dormant = [...performance.ids()].find((id) => !performanceResidents.has(id));
|
|
2242
|
+
if (dormant)
|
|
2243
|
+
performance.remove(dormant);
|
|
2244
|
+
}
|
|
2245
|
+
performance.observe(sessionId, info.time.created, outputTokens(info.tokens));
|
|
2246
|
+
performanceResidents.add(sessionId);
|
|
2247
|
+
}
|
|
1248
2248
|
s.usage.cumulative = info.tokens;
|
|
1249
2249
|
s.usage.cumulativeScope = "session";
|
|
1250
2250
|
store.mark(s.sessionId);
|
|
@@ -1317,12 +2317,15 @@ async function setup(ctx, injectedSink) {
|
|
|
1317
2317
|
} catch (error) {
|
|
1318
2318
|
for (const r of registrations)
|
|
1319
2319
|
await r.dispose();
|
|
2320
|
+
await history.flush();
|
|
1320
2321
|
await stopSnapshots();
|
|
1321
2322
|
throw error;
|
|
1322
2323
|
}
|
|
1323
2324
|
const relevant = new Set([
|
|
2325
|
+
"session.usage.updated",
|
|
1324
2326
|
"session.created",
|
|
1325
2327
|
"session.step.started",
|
|
2328
|
+
"session.step.streamed",
|
|
1326
2329
|
"session.step.ended",
|
|
1327
2330
|
"session.step.failed",
|
|
1328
2331
|
"session.compaction.started",
|
|
@@ -1338,12 +2341,23 @@ async function setup(ctx, injectedSink) {
|
|
|
1338
2341
|
"session.tool.called"
|
|
1339
2342
|
]);
|
|
1340
2343
|
async function handle(event) {
|
|
2344
|
+
if (event.location && event.location.directory !== ctx.location.directory)
|
|
2345
|
+
return;
|
|
2346
|
+
if (event.type === "session.text.delta" || event.type === "session.reasoning.delta") {
|
|
2347
|
+
if (config.metrics.enabled)
|
|
2348
|
+
performance.delta(event.data.sessionID, event.data.assistantMessageID, event.id, event.created, event.data.delta.length);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
1341
2351
|
if (!relevant.has(event.type) || !("sessionID" in event.data))
|
|
1342
2352
|
return;
|
|
1343
2353
|
const id = event.data.sessionID;
|
|
1344
2354
|
if (event.location && event.location.directory !== ctx.location.directory)
|
|
1345
2355
|
return;
|
|
1346
2356
|
if (event.type === "session.deleted") {
|
|
2357
|
+
recordPerformance(id, "terminal", "session.deleted");
|
|
2358
|
+
performance.remove(id);
|
|
2359
|
+
performanceResidents.delete(id);
|
|
2360
|
+
await persistPerformance();
|
|
1347
2361
|
store.remove(id);
|
|
1348
2362
|
requests.delete(id);
|
|
1349
2363
|
activeMonitor.remove(id);
|
|
@@ -1357,8 +2371,11 @@ async function setup(ctx, injectedSink) {
|
|
|
1357
2371
|
return;
|
|
1358
2372
|
if ("durable" in event && event.durable && !activeMonitor.accept(s, event.durable.seq))
|
|
1359
2373
|
return;
|
|
2374
|
+
const perfAccepted = config.metrics.enabled && (!("durable" in event) || !event.durable || performance.accept(id, event.durable.seq));
|
|
1360
2375
|
switch (event.type) {
|
|
1361
2376
|
case "session.step.started":
|
|
2377
|
+
if (perfAccepted)
|
|
2378
|
+
performance.start(id, event.data.assistantMessageID, event.data.agent, `${event.data.model.providerID}/${event.data.model.id}`, event.data.started);
|
|
1362
2379
|
activeMonitor.touch({
|
|
1363
2380
|
sessionId: id,
|
|
1364
2381
|
agent: event.data.agent,
|
|
@@ -1371,8 +2388,14 @@ async function setup(ctx, injectedSink) {
|
|
|
1371
2388
|
s.usage.modelRequests++;
|
|
1372
2389
|
s.active = true;
|
|
1373
2390
|
break;
|
|
2391
|
+
case "session.step.streamed":
|
|
2392
|
+
if (perfAccepted)
|
|
2393
|
+
performance.streamed(id, event.data.assistantMessageID, event.created);
|
|
2394
|
+
break;
|
|
1374
2395
|
case "session.step.ended":
|
|
1375
2396
|
case "session.step.failed": {
|
|
2397
|
+
if (perfAccepted)
|
|
2398
|
+
performance.settle(id, event.data.assistantMessageID, event.data.tokens ? outputTokens(event.data.tokens) : undefined);
|
|
1376
2399
|
const tokens = event.data.tokens && promptTokens(event.data.tokens);
|
|
1377
2400
|
const request = requests.get(id);
|
|
1378
2401
|
if (tokens !== undefined && request?.id === event.data.assistantMessageID && request.model === s.model)
|
|
@@ -1411,6 +2434,8 @@ async function setup(ctx, injectedSink) {
|
|
|
1411
2434
|
case "session.execution.failed":
|
|
1412
2435
|
case "session.execution.interrupted":
|
|
1413
2436
|
s.active = false;
|
|
2437
|
+
if (performance.snapshot(id)?.tokenSource === "estimated")
|
|
2438
|
+
performance.gap(id);
|
|
1414
2439
|
s.lastExecutionEnd = event.created;
|
|
1415
2440
|
break;
|
|
1416
2441
|
case "session.tool.called":
|
|
@@ -1418,9 +2443,26 @@ async function setup(ctx, injectedSink) {
|
|
|
1418
2443
|
break;
|
|
1419
2444
|
}
|
|
1420
2445
|
sink.emit({ time: Date.now(), type: "session-state", session: s });
|
|
2446
|
+
if (perfAccepted) {
|
|
2447
|
+
recordPerformance(id, event.type === "session.step.ended" || event.type === "session.step.failed" ? "request" : event.type.startsWith("session.execution.") && event.type !== "session.execution.started" ? "terminal" : "lifecycle", event.type);
|
|
2448
|
+
await persistPerformance();
|
|
2449
|
+
}
|
|
1421
2450
|
await persist();
|
|
1422
2451
|
}
|
|
2452
|
+
let lastPerformanceSample = 0, lastHistorySample = 0;
|
|
1423
2453
|
const timer = setInterval(() => {
|
|
2454
|
+
const now = Date.now();
|
|
2455
|
+
if (config.metrics.enabled && now - lastPerformanceSample >= config.metrics.sampleIntervalMs) {
|
|
2456
|
+
lastPerformanceSample = now;
|
|
2457
|
+
for (const id of performanceResidents) {
|
|
2458
|
+
store.mark(id);
|
|
2459
|
+
const snapshot = performance.snapshot(id);
|
|
2460
|
+
if (now - lastHistorySample >= 1000 && snapshot && (snapshot.status === "waiting" || snapshot.status === "streaming"))
|
|
2461
|
+
recordPerformance(id, "sample");
|
|
2462
|
+
}
|
|
2463
|
+
if (now - lastHistorySample >= 1000)
|
|
2464
|
+
lastHistorySample = now;
|
|
2465
|
+
}
|
|
1424
2466
|
store.flush();
|
|
1425
2467
|
if (draining || stopped)
|
|
1426
2468
|
return;
|
|
@@ -1430,6 +2472,11 @@ async function setup(ctx, injectedSink) {
|
|
|
1430
2472
|
activeMonitor.cleanup();
|
|
1431
2473
|
if (activeMonitor.sessions.size + recoveries.size < config.safety.maxSessions)
|
|
1432
2474
|
store.recover("capacity-exceeded");
|
|
2475
|
+
for (const id of performanceResidents)
|
|
2476
|
+
if (!activeMonitor.sessions.has(id)) {
|
|
2477
|
+
performance.remove(id);
|
|
2478
|
+
performanceResidents.delete(id);
|
|
2479
|
+
}
|
|
1433
2480
|
for (const id of requests.keys())
|
|
1434
2481
|
if (!activeMonitor.sessions.has(id))
|
|
1435
2482
|
requests.delete(id);
|
|
@@ -1451,6 +2498,12 @@ async function setup(ctx, injectedSink) {
|
|
|
1451
2498
|
} catch {
|
|
1452
2499
|
}
|
|
1453
2500
|
if (!stopped) {
|
|
2501
|
+
performance.gap();
|
|
2502
|
+
for (const id of performanceResidents) {
|
|
2503
|
+
store.mark(id);
|
|
2504
|
+
recordPerformance(id, "gap", "event-stream-gap");
|
|
2505
|
+
}
|
|
2506
|
+
await persistPerformance();
|
|
1454
2507
|
sink.emit({
|
|
1455
2508
|
time: Date.now(),
|
|
1456
2509
|
type: "event-stream-gap",
|
|
@@ -1476,11 +2529,11 @@ async function setup(ctx, injectedSink) {
|
|
|
1476
2529
|
detail: "Enforcement disabled; safety state could not be read"
|
|
1477
2530
|
});
|
|
1478
2531
|
if (!injectedSink)
|
|
1479
|
-
console.error(`context-guard: ${config.mode}; diagnostics ${disk.path}`);
|
|
2532
|
+
console.error(`context-guard: ${config.mode}; diagnostics ${disk.path}; metrics ${history.path}`);
|
|
1480
2533
|
sink.emit({
|
|
1481
2534
|
time: Date.now(),
|
|
1482
2535
|
type: "ready",
|
|
1483
|
-
detail: `SDK 2.0.29; runtime ${ctx.app.version}; ${config.mode}; active-subagent=step-boundary; next-request-barrier=false; journal=${disk.path}`
|
|
2536
|
+
detail: `SDK 2.0.29; runtime ${ctx.app.version}; ${config.mode}; active-subagent=step-boundary; next-request-barrier=false; journal=${disk.path}; metrics=${history.path}`
|
|
1484
2537
|
});
|
|
1485
2538
|
return async () => {
|
|
1486
2539
|
stopped = true;
|
|
@@ -1495,13 +2548,18 @@ async function setup(ctx, injectedSink) {
|
|
|
1495
2548
|
await work;
|
|
1496
2549
|
await scheduler.flush();
|
|
1497
2550
|
await persist();
|
|
2551
|
+
await persistPerformance();
|
|
2552
|
+
await history.flush();
|
|
1498
2553
|
await stopSnapshots();
|
|
1499
2554
|
await disk.flush();
|
|
1500
2555
|
};
|
|
1501
2556
|
}
|
|
1502
2557
|
|
|
1503
2558
|
// src/index.ts
|
|
1504
|
-
var src_default = Plugin.define({
|
|
2559
|
+
var src_default = Plugin.define({
|
|
2560
|
+
id: "context-guard",
|
|
2561
|
+
setup: (ctx, injectedSink) => setup({ ...ctx, historyRpc: ctx.rpc }, injectedSink)
|
|
2562
|
+
});
|
|
1505
2563
|
export {
|
|
1506
2564
|
src_default as default
|
|
1507
2565
|
};
|