@cesarus777/opencode-cgp 0.3.1 → 0.4.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +23 -9
  3. package/dist/cli.js +1179 -92
  4. package/dist/core.js +49 -4
  5. package/dist/index.js +570 -9
  6. package/dist/rpc.js +129 -2
  7. package/dist/tui.js +1543 -707
  8. package/dist/types/adapter.d.ts +2 -0
  9. package/dist/types/core.d.ts +1 -0
  10. package/dist/types/dashboard/command.d.ts +1 -1
  11. package/dist/types/dashboard/connection.d.ts +2 -0
  12. package/dist/types/dashboard/controller.d.ts +4 -0
  13. package/dist/types/dashboard/models.d.ts +42 -0
  14. package/dist/types/dashboard/modelview.d.ts +30 -0
  15. package/dist/types/dashboard/port.d.ts +6 -0
  16. package/dist/types/dashboard/terminal.d.ts +7 -0
  17. package/dist/types/dashboard/terminalview.d.ts +7 -2
  18. package/dist/types/dashboard/view.d.ts +6 -1
  19. package/dist/types/history.d.ts +1 -0
  20. package/dist/types/historyclient.d.ts +15 -0
  21. package/dist/types/modelanalytics.d.ts +8 -0
  22. package/dist/types/modelanalyticsserver.d.ts +16 -0
  23. package/dist/types/modelseries.d.ts +43 -0
  24. package/dist/types/rpc.d.ts +28 -0
  25. package/dist/types/tui/chartformat.d.ts +1 -1
  26. package/dist/types/tui/charts.d.ts +2 -3
  27. package/dist/types/tui/compactformat.d.ts +12 -3
  28. package/dist/types/tui/historystate.d.ts +49 -0
  29. package/dist/types/tui/inventory.d.ts +12 -0
  30. package/dist/types/tui/panelcontrols.d.ts +8 -0
  31. package/dist/types/tui/panelformat.d.ts +12 -0
  32. package/dist/types/tui/panelview.d.ts +22 -0
  33. package/dist/types/tui/quantity.d.ts +1 -0
  34. package/dist/types/tui/sidebar.d.ts +0 -5
  35. package/dist/types/tui/state.d.ts +3 -1
  36. package/dist/types/tui/statusformat.d.ts +19 -0
  37. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -43,6 +43,12 @@ function isHistoryRead(value) {
43
43
  function isHistoryResult(value) {
44
44
  return valid(value, historyResultSchema);
45
45
  }
46
+ function isAnalyticsRead(value) {
47
+ return valid(value, analyticsReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from < value.to && value.to - value.from <= 604800000 && !(value.model !== undefined && value.unknownModel === true) && (!(value.modelCursor || value.requestCursor) || typeof value.snapshotId === "string");
48
+ }
49
+ function isAnalyticsResult(value) {
50
+ return valid(value, analyticsResultSchema);
51
+ }
46
52
  function record(x) {
47
53
  return typeof x === "object" && x !== null && !Array.isArray(x);
48
54
  }
@@ -100,7 +106,7 @@ var count, text, enumeration = (...values) => ({ type: "string", enum: values })
100
106
  type: "array",
101
107
  items,
102
108
  maxItems
103
- }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
109
+ }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc, modelSummarySchema, analyticsReadSchema, analyticsResultSchema, analyticsRpc;
104
110
  var init_rpc = __esm(() => {
105
111
  count = {
106
112
  type: "integer",
@@ -336,6 +342,123 @@ var init_rpc = __esm(() => {
336
342
  methods: { read: { input: historyReadSchema, output: historyResultSchema } },
337
343
  events: {}
338
344
  });
345
+ modelSummarySchema = object({
346
+ model: text,
347
+ sessions: count,
348
+ completedRequests: count,
349
+ partialRequests: count,
350
+ legacyAttributionRequests: count,
351
+ tpsRequests: count,
352
+ ttftRequests: count,
353
+ excludedTpsRequests: count,
354
+ excludedTtftRequests: count,
355
+ outputTokens: nonnegative,
356
+ durationMs: nonnegative,
357
+ averageTps: nonnegative,
358
+ meanTtftMs: nonnegative,
359
+ medianTtftMs: nonnegative,
360
+ p95TtftMs: nonnegative
361
+ }, [
362
+ "sessions",
363
+ "completedRequests",
364
+ "partialRequests",
365
+ "legacyAttributionRequests",
366
+ "tpsRequests",
367
+ "ttftRequests",
368
+ "excludedTpsRequests",
369
+ "excludedTtftRequests",
370
+ "outputTokens",
371
+ "durationMs"
372
+ ]);
373
+ analyticsReadSchema = object({
374
+ schemaVersion: { const: 1 },
375
+ from: count,
376
+ to: count,
377
+ buckets: { type: "integer", minimum: 1, maximum: 240 },
378
+ agent: text,
379
+ model: text,
380
+ unknownModel: { type: "boolean" },
381
+ sort: enumeration("requests", "tps", "ttft", "identity"),
382
+ snapshotId: text,
383
+ modelCursor: text,
384
+ requestCursor: text
385
+ }, ["schemaVersion", "from", "to", "buckets"]);
386
+ analyticsResultSchema = object({
387
+ schemaVersion: { const: 1 },
388
+ generatedAt: count,
389
+ expiresAt: count,
390
+ snapshotId: text,
391
+ enabled: { type: "boolean" },
392
+ from: count,
393
+ to: count,
394
+ agent: text,
395
+ models: array(modelSummarySchema, 50),
396
+ totalModels: count,
397
+ nextModelsCursor: text,
398
+ selected: modelSummarySchema,
399
+ buckets: array(object({ from: nonnegative, to: nonnegative, summary: modelSummarySchema }, ["from", "to"]), 240),
400
+ requests: array(object({
401
+ location: text,
402
+ sessionId: text,
403
+ requestId: text,
404
+ startedAt: count,
405
+ completedAt: count,
406
+ model: text,
407
+ agent: text,
408
+ attribution: enumeration("request", "legacy-record", "unknown"),
409
+ partial: { type: "boolean" },
410
+ outputTokens: nonnegative,
411
+ ttftMs: nonnegative
412
+ }, [
413
+ "location",
414
+ "sessionId",
415
+ "requestId",
416
+ "startedAt",
417
+ "completedAt",
418
+ "attribution",
419
+ "partial"
420
+ ]), 50),
421
+ nextRequestsCursor: text,
422
+ coverage: object({
423
+ source: enumeration("retained-request-records"),
424
+ scannedRecords: count,
425
+ unscannedRecords: count,
426
+ duplicateRecords: count,
427
+ conflictingRequests: count,
428
+ missingIdentityRecords: count,
429
+ incompleteRequests: count,
430
+ invalidTimingRequests: count,
431
+ gapRecords: count,
432
+ maxRequests: count,
433
+ partial: { type: "boolean" }
434
+ }),
435
+ metadata: historyResultSchema.properties.metadata ?? {},
436
+ gaps: array(count, 128),
437
+ omittedGaps: count
438
+ }, [
439
+ "schemaVersion",
440
+ "generatedAt",
441
+ "expiresAt",
442
+ "snapshotId",
443
+ "enabled",
444
+ "from",
445
+ "to",
446
+ "models",
447
+ "totalModels",
448
+ "buckets",
449
+ "requests",
450
+ "coverage",
451
+ "metadata",
452
+ "gaps",
453
+ "omittedGaps"
454
+ ]);
455
+ analyticsRpc = Rpc.define({
456
+ id: "context-guard.analytics.v1",
457
+ methods: {
458
+ read: { input: analyticsReadSchema, output: analyticsResultSchema }
459
+ },
460
+ events: {}
461
+ });
339
462
  });
340
463
 
341
464
  // src/history.ts
@@ -528,6 +651,7 @@ function scalarPerformance(p) {
528
651
  async function readPerformanceHistory(directory, filter = {}) {
529
652
  const records = [];
530
653
  const files = await segments(directory);
654
+ filter.signal?.throwIfAborted();
531
655
  const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
532
656
  let bytes = 0, skippedBytes = 0, malformedRecords = 0;
533
657
  const selected = [];
@@ -540,6 +664,7 @@ async function readPerformanceHistory(directory, filter = {}) {
540
664
  selected.unshift(file);
541
665
  }
542
666
  for (const file of selected) {
667
+ filter.signal?.throwIfAborted();
543
668
  const handle = await open(join(directory, file.name), "r");
544
669
  let source;
545
670
  try {
@@ -549,7 +674,12 @@ async function readPerformanceHistory(directory, filter = {}) {
549
674
  } finally {
550
675
  await handle.close();
551
676
  }
677
+ let scannedLines = 0;
552
678
  for (const line of source.split("\n")) {
679
+ if (filter.signal && ++scannedLines % 256 === 0) {
680
+ await new Promise((done) => setTimeout(done, 0));
681
+ filter.signal.throwIfAborted();
682
+ }
553
683
  if (!line)
554
684
  continue;
555
685
  try {
@@ -604,6 +734,205 @@ var init_history = __esm(() => {
604
734
  filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
605
735
  });
606
736
 
737
+ // src/modelanalytics.ts
738
+ function requestFact(record2) {
739
+ const p = record2.performance;
740
+ if (!p?.requestId || !record2.sessionId || !nonnegative2(p.requestStartedAt))
741
+ return;
742
+ const model = p.requestModel || record2.model;
743
+ const agent = p.requestAgent || record2.agent;
744
+ const ttft = p.ttftMs ?? (p.firstOutputAt === undefined ? undefined : p.firstOutputAt - p.requestStartedAt);
745
+ return {
746
+ location: "",
747
+ sessionId: record2.sessionId,
748
+ requestId: p.requestId,
749
+ startedAt: p.requestStartedAt,
750
+ ...p.streamEndedAt === undefined ? {} : { completedAt: p.streamEndedAt },
751
+ ...model ? { model } : {},
752
+ ...agent ? { agent } : {},
753
+ attribution: p.requestModel ? "request" : model ? "legacy-record" : "unknown",
754
+ partial: p.partial,
755
+ ...p.tokenSource === "reported" && nonnegative2(p.requestOutputTokens) ? { outputTokens: p.requestOutputTokens } : {},
756
+ ...ttft === undefined ? {} : { ttftMs: ttft }
757
+ };
758
+ }
759
+ function reconcile(a, b) {
760
+ for (const field of [
761
+ "completedAt",
762
+ "outputTokens",
763
+ "ttftMs",
764
+ "agent"
765
+ ])
766
+ if (a[field] !== undefined && b[field] !== undefined && a[field] !== b[field])
767
+ return;
768
+ if (a.model && b.model && a.model !== b.model)
769
+ return;
770
+ const rank = { unknown: 0, "legacy-record": 1, request: 2 };
771
+ const preferred = rank[b.attribution] > rank[a.attribution] ? b : a;
772
+ return {
773
+ ...a,
774
+ ...b,
775
+ ...preferred.model ? { model: preferred.model } : {},
776
+ attribution: preferred.attribution,
777
+ partial: a.partial || b.partial
778
+ };
779
+ }
780
+ function analyzeModelRequests(records, options) {
781
+ const maxRequests = options.maxRequests ?? 1e5;
782
+ if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
783
+ throw new Error("maxRequests must be an integer from 1 to 1000000");
784
+ if (!options.location)
785
+ throw new Error("Analytics requires a location identity");
786
+ for (const value of [options.from, options.to])
787
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
788
+ throw new Error("Invalid analytics timestamp");
789
+ if (options.from !== undefined && options.to !== undefined && options.from > options.to)
790
+ throw new Error("--from must precede --to");
791
+ const coverage = {
792
+ source: "retained-request-records",
793
+ scannedRecords: 0,
794
+ unscannedRecords: 0,
795
+ duplicateRecords: 0,
796
+ conflictingRequests: 0,
797
+ missingIdentityRecords: 0,
798
+ incompleteRequests: 0,
799
+ invalidTimingRequests: 0,
800
+ gapRecords: 0,
801
+ maxRequests,
802
+ partial: false
803
+ };
804
+ const requests = new Map;
805
+ for (const record2 of records) {
806
+ coverage.scannedRecords++;
807
+ if (record2.type === "gap") {
808
+ coverage.gapRecords++;
809
+ continue;
810
+ }
811
+ if (record2.type !== "request")
812
+ continue;
813
+ const fact = requestFact(record2);
814
+ if (!fact) {
815
+ coverage.missingIdentityRecords++;
816
+ continue;
817
+ }
818
+ fact.location = options.location;
819
+ const key = JSON.stringify([
820
+ fact.sessionId,
821
+ fact.requestId,
822
+ fact.startedAt
823
+ ]);
824
+ const previous = requests.get(key);
825
+ if (previous) {
826
+ coverage.duplicateRecords++;
827
+ const merged = reconcile(previous.fact, fact);
828
+ if (!merged)
829
+ previous.conflict = true;
830
+ else
831
+ previous.fact = merged;
832
+ } else {
833
+ if (requests.size === maxRequests) {
834
+ coverage.scannedRecords--;
835
+ coverage.unscannedRecords = records.length - coverage.scannedRecords;
836
+ break;
837
+ }
838
+ requests.set(key, { fact, conflict: false });
839
+ }
840
+ }
841
+ const accepted = [];
842
+ for (const { fact: r, conflict } of requests.values()) {
843
+ if (conflict) {
844
+ coverage.conflictingRequests++;
845
+ continue;
846
+ }
847
+ if (r.completedAt === undefined) {
848
+ coverage.incompleteRequests++;
849
+ continue;
850
+ }
851
+ const duration = r.completedAt - r.startedAt;
852
+ if (!nonnegative2(r.completedAt) || !Number.isFinite(duration) || duration <= 0) {
853
+ coverage.invalidTimingRequests++;
854
+ continue;
855
+ }
856
+ if (options.from !== undefined && r.completedAt < options.from || options.to !== undefined && r.completedAt >= options.to || options.model !== undefined && r.model !== options.model || options.agent !== undefined && r.agent !== options.agent)
857
+ continue;
858
+ accepted.push({ ...r, completedAt: r.completedAt });
859
+ }
860
+ const models = summarizeModelRequests(accepted);
861
+ coverage.partial = coverage.unscannedRecords > 0 || coverage.conflictingRequests > 0 || coverage.missingIdentityRecords > 0 || coverage.incompleteRequests > 0 || coverage.invalidTimingRequests > 0 || coverage.gapRecords > 0 || models.some((s) => s.partialRequests > 0 || s.legacyAttributionRequests > 0 || s.excludedTpsRequests > 0 || s.excludedTtftRequests > 0 || !s.model);
862
+ return {
863
+ schemaVersion: 1,
864
+ location: options.location,
865
+ models,
866
+ coverage,
867
+ requests: accepted
868
+ };
869
+ }
870
+ function summarizeModelRequests(requests) {
871
+ const groups = new Map;
872
+ for (const r of requests) {
873
+ const duration = r.completedAt - r.startedAt;
874
+ let group = groups.get(r.model);
875
+ if (!group) {
876
+ group = {
877
+ summary: {
878
+ ...r.model ? { model: r.model } : {},
879
+ sessions: 0,
880
+ completedRequests: 0,
881
+ partialRequests: 0,
882
+ legacyAttributionRequests: 0,
883
+ tpsRequests: 0,
884
+ ttftRequests: 0,
885
+ excludedTpsRequests: 0,
886
+ excludedTtftRequests: 0,
887
+ outputTokens: 0,
888
+ durationMs: 0
889
+ },
890
+ sessions: new Set,
891
+ ttfts: []
892
+ };
893
+ groups.set(r.model, group);
894
+ }
895
+ const s = group.summary;
896
+ group.sessions.add(r.sessionId);
897
+ s.completedRequests++;
898
+ if (r.partial)
899
+ s.partialRequests++;
900
+ if (r.attribution === "legacy-record")
901
+ s.legacyAttributionRequests++;
902
+ if (r.outputTokens !== undefined) {
903
+ s.tpsRequests++;
904
+ s.outputTokens += r.outputTokens;
905
+ s.durationMs += duration;
906
+ } else
907
+ s.excludedTpsRequests++;
908
+ if (nonnegative2(r.ttftMs) && r.ttftMs <= duration)
909
+ group.ttfts.push(r.ttftMs);
910
+ else
911
+ s.excludedTtftRequests++;
912
+ }
913
+ const models = [...groups.values()].map(({ summary: s, sessions, ttfts }) => {
914
+ s.sessions = sessions.size;
915
+ s.ttftRequests = ttfts.length;
916
+ if (s.tpsRequests)
917
+ s.averageTps = s.outputTokens * 1000 / s.durationMs;
918
+ ttfts.sort((a, b) => a - b);
919
+ if (ttfts.length) {
920
+ s.meanTtftMs = ttfts.reduce((sum, n) => sum + n, 0) / ttfts.length;
921
+ s.medianTtftMs = ((ttfts[Math.floor((ttfts.length - 1) / 2)] ?? 0) + (ttfts[Math.floor(ttfts.length / 2)] ?? 0)) / 2;
922
+ const p95 = ttfts[Math.ceil(ttfts.length * 0.95) - 1];
923
+ if (p95 !== undefined)
924
+ s.p95TtftMs = p95;
925
+ }
926
+ return s;
927
+ }).sort((a, b) => (a.model ?? "").localeCompare(b.model ?? ""));
928
+ return models;
929
+ }
930
+ function aggregateModels(records, options) {
931
+ const { requests: _requests, ...result } = analyzeModelRequests(records, options);
932
+ return result;
933
+ }
934
+ var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
935
+
607
936
  // src/index.ts
608
937
  import { Plugin } from "@opencode/plugin";
609
938
 
@@ -1056,6 +1385,229 @@ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = asyn
1056
1385
  };
1057
1386
  }
1058
1387
 
1388
+ // src/modelanalyticsserver.ts
1389
+ init_history();
1390
+ import { createHash } from "crypto";
1391
+
1392
+ // src/modelseries.ts
1393
+ function bucketModelRequests(requests, from, to, count2) {
1394
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || from >= to || !Number.isSafeInteger(count2) || count2 < 1 || count2 > 240)
1395
+ throw new Error("Invalid analytics buckets");
1396
+ const groups = Array.from({ length: count2 }, () => []);
1397
+ for (const request of requests) {
1398
+ if (request.completedAt < from || request.completedAt >= to)
1399
+ continue;
1400
+ const index = Math.min(count2 - 1, Math.floor((request.completedAt - from) / (to - from) * count2));
1401
+ groups[index]?.push(request);
1402
+ }
1403
+ return groups.map((group, index) => {
1404
+ const summaries = summarizeModelRequests(group);
1405
+ if (summaries.length > 1)
1406
+ throw new Error("Select one model for buckets");
1407
+ const summary = summaries[0];
1408
+ return {
1409
+ from: from + (to - from) * index / count2,
1410
+ to: from + (to - from) * (index + 1) / count2,
1411
+ ...summary ? { summary } : {}
1412
+ };
1413
+ });
1414
+ }
1415
+
1416
+ // src/modelanalyticsserver.ts
1417
+ init_rpc();
1418
+ var metadata = {
1419
+ schema: 1,
1420
+ retainedBytes: 0,
1421
+ skippedBytes: 0,
1422
+ malformedRecords: 0,
1423
+ completeness: "bounded-retention"
1424
+ };
1425
+ function createAnalyticsService(path, location, enabled, options = {}) {
1426
+ const now = options.now ?? Date.now;
1427
+ const reader = options.read ?? readPerformanceHistory;
1428
+ const snapshots = new Map;
1429
+ let stopped = false;
1430
+ let archive;
1431
+ let scan;
1432
+ async function load(signal) {
1433
+ if (archive && now() - archive.at < 30000)
1434
+ return archive;
1435
+ if (!scan || scan.controller.signal.aborted) {
1436
+ const controller = new AbortController;
1437
+ const job2 = {
1438
+ controller,
1439
+ waiters: 0,
1440
+ promise: Promise.resolve().then(async () => {
1441
+ const source = enabled ? await reader(path, { signal: controller.signal }) : { metadata, records: [] };
1442
+ controller.signal.throwIfAborted();
1443
+ if (stopped)
1444
+ throw new Error("Analytics service stopped");
1445
+ const analysis = analyzeModelRequests(source.records, { location });
1446
+ const result = {
1447
+ at: now(),
1448
+ source: {
1449
+ metadata: source.metadata,
1450
+ records: source.records.filter((r) => r.type === "gap")
1451
+ },
1452
+ analysis
1453
+ };
1454
+ archive = result;
1455
+ return result;
1456
+ }).finally(() => {
1457
+ if (scan === job2)
1458
+ scan = undefined;
1459
+ })
1460
+ };
1461
+ scan = job2;
1462
+ }
1463
+ const job = scan;
1464
+ job.waiters++;
1465
+ let onAbort = () => {
1466
+ };
1467
+ try {
1468
+ return await Promise.race([
1469
+ job.promise,
1470
+ new Promise((_resolve, reject) => {
1471
+ onAbort = () => reject(new Error("Analytics read cancelled"));
1472
+ signal.addEventListener("abort", onAbort, { once: true });
1473
+ if (signal.aborted)
1474
+ onAbort();
1475
+ })
1476
+ ]);
1477
+ } finally {
1478
+ signal.removeEventListener("abort", onAbort);
1479
+ if (--job.waiters === 0)
1480
+ job.controller.abort();
1481
+ }
1482
+ }
1483
+ const queryKey = (input) => JSON.stringify([input.from, input.to, input.agent ?? null]);
1484
+ function pageOffset(cursor2, snapshot, kind, binding) {
1485
+ if (!cursor2)
1486
+ return 0;
1487
+ try {
1488
+ const parsed = JSON.parse(Buffer.from(cursor2, "base64url").toString());
1489
+ if (Array.isArray(parsed) && parsed.length === 4 && parsed[0] === snapshot.id && parsed[1] === kind && parsed[3] === createHash("sha256").update(binding).digest("hex") && Number.isSafeInteger(parsed[2]) && parsed[2] >= 0 && parsed[2] % 50 === 0)
1490
+ return Number(parsed[2]);
1491
+ } catch {
1492
+ }
1493
+ throw new Error("Invalid analytics cursor; refresh Models");
1494
+ }
1495
+ const cursor = (snapshot, kind, offset, binding) => Buffer.from(JSON.stringify([
1496
+ snapshot.id,
1497
+ kind,
1498
+ offset,
1499
+ createHash("sha256").update(binding).digest("hex")
1500
+ ])).toString("base64url");
1501
+ return {
1502
+ async read(input, signal) {
1503
+ if (!isAnalyticsRead(input))
1504
+ throw new Error("Invalid analytics request");
1505
+ signal.throwIfAborted();
1506
+ if (stopped)
1507
+ throw new Error("Analytics service stopped");
1508
+ for (const [id, item] of snapshots)
1509
+ if (item.expiresAt <= now())
1510
+ snapshots.delete(id);
1511
+ let snapshot = input.snapshotId ? snapshots.get(input.snapshotId) : [...snapshots.values()].find((s) => s.key === queryKey(input) && now() - s.generatedAt < 30000);
1512
+ if (input.snapshotId && (!snapshot || snapshot.key !== queryKey(input)))
1513
+ throw new Error("Analytics snapshot expired; refresh Models");
1514
+ if (!snapshot) {
1515
+ const data = await load(signal);
1516
+ signal.throwIfAborted();
1517
+ const requests2 = data.analysis.requests.filter((r) => r.completedAt >= input.from && r.completedAt < input.to && (!input.agent || r.agent === input.agent));
1518
+ requests2.sort((a, b) => b.completedAt - a.completedAt || a.sessionId.localeCompare(b.sessionId) || a.requestId.localeCompare(b.requestId) || a.startedAt - b.startedAt);
1519
+ const gaps = data.source.records.filter((r) => r.time >= input.from && r.time < input.to).map((r) => r.time);
1520
+ snapshot = {
1521
+ id: crypto.randomUUID(),
1522
+ key: queryKey(input),
1523
+ generatedAt: data.at,
1524
+ expiresAt: now() + 90000,
1525
+ requests: requests2,
1526
+ models: summarizeModelRequests(requests2),
1527
+ coverage: data.analysis.coverage,
1528
+ metadata: data.source.metadata,
1529
+ gaps: gaps.slice(-128),
1530
+ omittedGaps: Math.max(0, gaps.length - 128)
1531
+ };
1532
+ snapshots.set(snapshot.id, snapshot);
1533
+ if (snapshots.size > 2) {
1534
+ const oldest = snapshots.keys().next().value;
1535
+ if (oldest)
1536
+ snapshots.delete(oldest);
1537
+ }
1538
+ }
1539
+ const sort = input.sort ?? "requests";
1540
+ const models = [...snapshot.models].sort((a, b) => {
1541
+ const key = sort === "tps" ? "averageTps" : sort === "ttft" ? "p95TtftMs" : "completedRequests";
1542
+ return (sort === "identity" ? 0 : (b[key] ?? -Infinity) - (a[key] ?? -Infinity)) || (a.model ?? "").localeCompare(b.model ?? "");
1543
+ });
1544
+ const modelOffset = pageOffset(input.modelCursor, snapshot, "models", sort);
1545
+ const selected = input.unknownModel ? models.find((m) => m.model === undefined) : input.model ? models.find((m) => m.model === input.model) : models[modelOffset];
1546
+ const selection = selected?.model ?? "";
1547
+ const requests = selected ? snapshot.requests.filter((r) => r.model === selected.model) : [];
1548
+ const requestOffset = pageOffset(input.requestCursor, snapshot, "requests", selection);
1549
+ const buckets = selected ? bucketModelRequests(requests, input.from, input.to, input.buckets) : [];
1550
+ const result = {
1551
+ schemaVersion: 1,
1552
+ generatedAt: snapshot.generatedAt,
1553
+ expiresAt: snapshot.expiresAt,
1554
+ snapshotId: snapshot.id,
1555
+ enabled,
1556
+ from: input.from,
1557
+ to: input.to,
1558
+ ...input.agent ? { agent: input.agent } : {},
1559
+ models: models.slice(modelOffset, modelOffset + 50).map((m) => ({ ...m })),
1560
+ totalModels: models.length,
1561
+ ...modelOffset + 50 < models.length ? {
1562
+ nextModelsCursor: cursor(snapshot, "models", modelOffset + 50, sort)
1563
+ } : {},
1564
+ ...selected ? { selected: { ...selected } } : {},
1565
+ buckets,
1566
+ requests: requests.slice(requestOffset, requestOffset + 50).map((r) => {
1567
+ const { ttftMs, ...fact } = r;
1568
+ return {
1569
+ ...fact,
1570
+ ...ttftMs !== undefined && Number.isFinite(ttftMs) && ttftMs >= 0 && ttftMs <= r.completedAt - r.startedAt ? { ttftMs } : {}
1571
+ };
1572
+ }),
1573
+ ...requestOffset + 50 < requests.length ? {
1574
+ nextRequestsCursor: cursor(snapshot, "requests", requestOffset + 50, selection)
1575
+ } : {},
1576
+ coverage: { ...snapshot.coverage },
1577
+ metadata: { ...snapshot.metadata },
1578
+ gaps: [...snapshot.gaps],
1579
+ omittedGaps: snapshot.omittedGaps
1580
+ };
1581
+ if (!isAnalyticsResult(result))
1582
+ throw new Error("Analytics response exceeds supported bounds");
1583
+ return result;
1584
+ },
1585
+ dispose() {
1586
+ stopped = true;
1587
+ scan?.controller.abort();
1588
+ snapshots.clear();
1589
+ archive = undefined;
1590
+ }
1591
+ };
1592
+ }
1593
+ async function serveModelAnalytics(port, path, location, enabled) {
1594
+ if (!port)
1595
+ return () => {
1596
+ };
1597
+ const service = createAnalyticsService(path, location, enabled);
1598
+ const registration = await port.register(analyticsRpc, {
1599
+ read: async (input, context) => {
1600
+ if (!isAnalyticsRead(input))
1601
+ throw new Error("Invalid analytics request");
1602
+ return service.read(input, context.signal);
1603
+ }
1604
+ });
1605
+ return async () => {
1606
+ service.dispose();
1607
+ await registration.dispose();
1608
+ };
1609
+ }
1610
+
1059
1611
  // src/adapter.ts
1060
1612
  init_history();
1061
1613
 
@@ -1888,7 +2440,7 @@ function estimate(value, maxChars = 2000000) {
1888
2440
  function record2(v) {
1889
2441
  return typeof v === "object" && v !== null && !Array.isArray(v);
1890
2442
  }
1891
- function nonnegative2(v) {
2443
+ function nonnegative3(v) {
1892
2444
  return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
1893
2445
  }
1894
2446
  function decodeRecovery(v, max) {
@@ -1899,7 +2451,7 @@ function decodeRecovery(v, max) {
1899
2451
  if (!record2(item) || typeof item.sessionId !== "string" || !record2(item.compaction))
1900
2452
  continue;
1901
2453
  const c = item.compaction;
1902
- if (!nonnegative2(c.count) || !nonnegative2(c.failures) || !nonnegative2(c.consecutiveFailures) || typeof c.armed !== "boolean")
2454
+ if (!nonnegative3(c.count) || !nonnegative3(c.failures) || !nonnegative3(c.consecutiveFailures) || typeof c.armed !== "boolean")
1903
2455
  continue;
1904
2456
  const phase = c.phase === "idle" || c.phase === "blocked" ? c.phase : "uncertain";
1905
2457
  const compaction = {
@@ -1909,9 +2461,9 @@ function decodeRecovery(v, max) {
1909
2461
  failures: c.failures,
1910
2462
  consecutiveFailures: c.consecutiveFailures
1911
2463
  };
1912
- if (nonnegative2(c.lastAttempt))
2464
+ if (nonnegative3(c.lastAttempt))
1913
2465
  compaction.lastAttempt = c.lastAttempt;
1914
- if (nonnegative2(c.lastCompleted))
2466
+ if (nonnegative3(c.lastCompleted))
1915
2467
  compaction.lastCompleted = c.lastCompleted;
1916
2468
  if (typeof c.requestId === "string")
1917
2469
  compaction.requestId = c.requestId;
@@ -1922,7 +2474,7 @@ function decodeRecovery(v, max) {
1922
2474
  result.push({
1923
2475
  sessionId: item.sessionId,
1924
2476
  compaction,
1925
- ...nonnegative2(item.sequence) ? { sequence: item.sequence } : {}
2477
+ ...nonnegative3(item.sequence) ? { sequence: item.sequence } : {}
1926
2478
  });
1927
2479
  }
1928
2480
  return result;
@@ -1949,9 +2501,9 @@ function encodeRecovery(rows) {
1949
2501
  import { mkdir as mkdir2, open as open2, rename, stat as stat2 } from "fs/promises";
1950
2502
  import { dirname, join as join2 } from "path";
1951
2503
  import { homedir } from "os";
1952
- import { createHash } from "crypto";
2504
+ import { createHash as createHash2 } from "crypto";
1953
2505
  function defaultPath(directory) {
1954
- const key = createHash("sha256").update(directory).digest("hex").slice(0, 16);
2506
+ const key = createHash2("sha256").update(directory).digest("hex").slice(0, 16);
1955
2507
  return join2(process.env.XDG_STATE_HOME || join2(homedir(), ".local/state"), "opencode-context-guard", `${key}.jsonl`);
1956
2508
  }
1957
2509
 
@@ -2118,7 +2670,16 @@ async function setup(ctx, injectedSink) {
2118
2670
  return () => {
2119
2671
  };
2120
2672
  });
2673
+ const stopAnalytics = await serveModelAnalytics(ctx.analyticsRpc, config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, ctx.location.directory, config.enabled && config.metrics.enabled && config.history.enabled).catch(() => {
2674
+ diagnostics.emit({
2675
+ time: Date.now(),
2676
+ type: "analytics-service-unavailable"
2677
+ });
2678
+ return () => {
2679
+ };
2680
+ });
2121
2681
  const stopSnapshots = async () => {
2682
+ await stopAnalytics();
2122
2683
  await stopHistory();
2123
2684
  await stopV2();
2124
2685
  await stopV1();
@@ -2667,7 +3228,7 @@ async function setup(ctx, injectedSink) {
2667
3228
  // src/index.ts
2668
3229
  var src_default = Plugin.define({
2669
3230
  id: "context-guard",
2670
- setup: (ctx, injectedSink) => setup({ ...ctx, historyRpc: ctx.rpc }, injectedSink)
3231
+ setup: (ctx, injectedSink) => setup({ ...ctx, historyRpc: ctx.rpc, analyticsRpc: ctx.rpc }, injectedSink)
2671
3232
  });
2672
3233
  export {
2673
3234
  src_default as default