@cesarus777/opencode-cgp 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 valid2 = (n) => n !== undefined && Number.isFinite(n) && n > 0;
217
- const output = reserve ?? (valid2(limits.output) ? limits.output : undefined);
218
- const contextBudget = valid2(limits.context) && output !== undefined ? Math.max(0, limits.context - output) : undefined;
219
- if (valid2(limits.input))
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 next = project(s, this.config, this.now());
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 registration = await rpc.register(monitorRpc, {
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 result = store.read(input);
477
- if (!isResult(result))
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((batch) => {
507
- if (!isBatch(batch))
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, ["enabled", "mode", "defaults", "policies", "safety", "telemetry"], "options");
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 nonnegative(v) {
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 (!nonnegative(c.count) || !nonnegative(c.failures) || !nonnegative(c.consecutiveFailures) || typeof c.armed !== "boolean")
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 (nonnegative(c.lastAttempt))
1874
+ if (nonnegative2(c.lastAttempt))
974
1875
  compaction.lastAttempt = c.lastAttempt;
975
- if (nonnegative(c.lastCompleted))
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
- ...nonnegative(item.sequence) ? { sequence: item.sequence } : {}
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 join("/home/julius/.local/state", "opencode-context-guard", `${key}.jsonl`);
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 mkdir(dirname(this.path), { recursive: true, mode: 448 });
1057
- this.bytes = await stat(this.path).then((s) => s.size).catch(() => 0);
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 open(this.path, "a", 384);
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 store = new SnapshotStore(config, () => monitor?.sessions ?? new Map);
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 stopSnapshots = await serveSnapshots(ctx.rpc, store).catch(() => {
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({ id: "context-guard", setup });
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
  };