@cesarus777/opencode-cgp 0.4.1 → 0.4.3

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
@@ -473,15 +473,109 @@ async function segments(directory) {
473
473
  return (await Promise.all(names.filter((n) => filename.test(n)).map(async (name) => ({
474
474
  name,
475
475
  time: Number(filename.exec(name)?.[1]),
476
- bytes: (await stat(join(directory, name))).size
476
+ ...fileIdentity(await stat(join(directory, name)))
477
477
  })))).sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
478
478
  }
479
+ function fileIdentity(info) {
480
+ return {
481
+ bytes: info.size,
482
+ dev: info.dev,
483
+ ino: info.ino,
484
+ mtimeMs: info.mtimeMs,
485
+ ctimeMs: info.ctimeMs
486
+ };
487
+ }
488
+ function scanScheduler(signal, options = {}) {
489
+ const now = options.now ?? (() => performance.now());
490
+ const pause = options.yield ?? (() => new Promise((done) => setTimeout(done, 0)));
491
+ let started = now();
492
+ return () => {
493
+ signal?.throwIfAborted();
494
+ if (now() - started >= 4)
495
+ return pause().then(() => {
496
+ signal?.throwIfAborted();
497
+ started = now();
498
+ });
499
+ };
500
+ }
501
+ function matchesHistory(value, filter) {
502
+ return !(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);
503
+ }
504
+ async function scanHistorySegment(handle, size, filter = {}, options = {}, start = 0) {
505
+ const records = [];
506
+ let malformedRecords = 0, position = start, boundary = start;
507
+ let pending = Buffer.alloc(0);
508
+ const checkpoint = scanScheduler(filter.signal, options);
509
+ function parse(line) {
510
+ if (!line.length)
511
+ return;
512
+ options.onParse?.();
513
+ let value;
514
+ try {
515
+ value = JSON.parse(line.toString("utf8"));
516
+ } catch {
517
+ malformedRecords++;
518
+ return;
519
+ }
520
+ if (!isHistoryRecord(value)) {
521
+ malformedRecords++;
522
+ return;
523
+ }
524
+ if (matchesHistory(value, filter))
525
+ records.push(value);
526
+ }
527
+ while (position < size) {
528
+ {
529
+ const waiting = checkpoint();
530
+ if (waiting)
531
+ await waiting;
532
+ }
533
+ const buffer = Buffer.alloc(Math.min(options.chunkBytes ?? 65536, size - position));
534
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
535
+ if (!bytesRead)
536
+ throw new Error("Archive changed during read");
537
+ options.onRead?.(bytesRead);
538
+ position += bytesRead;
539
+ const chunk = pending.length ? Buffer.concat([pending, buffer.subarray(0, bytesRead)]) : buffer.subarray(0, bytesRead);
540
+ let offset = 0;
541
+ for (;; ) {
542
+ const end = chunk.indexOf(10, offset);
543
+ if (end < 0)
544
+ break;
545
+ {
546
+ const waiting = checkpoint();
547
+ if (waiting)
548
+ await waiting;
549
+ }
550
+ parse(chunk.subarray(offset, end));
551
+ offset = end + 1;
552
+ }
553
+ pending = Buffer.from(chunk.subarray(offset));
554
+ boundary = position - pending.length;
555
+ }
556
+ const completeRecords = records.length, completeMalformed = malformedRecords;
557
+ {
558
+ const waiting = checkpoint();
559
+ if (waiting)
560
+ await waiting;
561
+ }
562
+ parse(pending);
563
+ filter.signal?.throwIfAborted();
564
+ return {
565
+ records,
566
+ malformedRecords,
567
+ boundary,
568
+ completeRecords,
569
+ completeMalformed
570
+ };
571
+ }
479
572
 
480
573
  class PerformanceHistory {
481
574
  path;
482
575
  config;
483
576
  now;
484
577
  report;
578
+ changed;
485
579
  pending = [];
486
580
  working;
487
581
  active;
@@ -491,11 +585,13 @@ class PerformanceHistory {
491
585
  dropped = 0;
492
586
  droppedTotal = 0;
493
587
  constructor(path, config, now = Date.now, report = () => {
588
+ }, changed = () => {
494
589
  }) {
495
590
  this.path = path;
496
591
  this.config = config;
497
592
  this.now = now;
498
593
  this.report = report;
594
+ this.changed = changed;
499
595
  }
500
596
  emit(record2) {
501
597
  if (!this.config.enabled || this.disabled)
@@ -544,6 +640,7 @@ class PerformanceHistory {
544
640
  if (file.time >= this.now() - this.config.retentionDays * 86400000 && total + incoming <= this.config.maxBytes)
545
641
  break;
546
642
  await unlink(join(this.path, file.name));
643
+ this.changed(file.name);
547
644
  if (file.name === this.active) {
548
645
  this.active = undefined;
549
646
  this.bytes = 0;
@@ -594,6 +691,7 @@ class PerformanceHistory {
594
691
  const file = await open(join(this.path, this.active), "a", 384);
595
692
  try {
596
693
  await file.writeFile(line);
694
+ this.changed(this.active);
597
695
  } finally {
598
696
  await file.close();
599
697
  }
@@ -648,7 +746,8 @@ function scalarPerformance(p) {
648
746
  }))
649
747
  };
650
748
  }
651
- async function readPerformanceHistory(directory, filter = {}) {
749
+ async function readPerformanceHistory(directory, filter = {}, options = {}) {
750
+ filter.signal?.throwIfAborted();
652
751
  const records = [];
653
752
  const files = await segments(directory);
654
753
  filter.signal?.throwIfAborted();
@@ -666,36 +765,16 @@ async function readPerformanceHistory(directory, filter = {}) {
666
765
  for (const file of selected) {
667
766
  filter.signal?.throwIfAborted();
668
767
  const handle = await open(join(directory, file.name), "r");
669
- let source;
670
768
  try {
671
- const buffer = Buffer.alloc(file.bytes);
672
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
673
- source = buffer.subarray(0, bytesRead).toString("utf8");
769
+ const result = await scanHistorySegment(handle, file.bytes, filter, options);
770
+ for (const record2 of result.records)
771
+ records.push(record2);
772
+ malformedRecords += result.malformedRecords;
674
773
  } finally {
675
774
  await handle.close();
676
775
  }
677
- let scannedLines = 0;
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
- }
683
- if (!line)
684
- continue;
685
- try {
686
- const value = JSON.parse(line);
687
- if (!isHistoryRecord(value)) {
688
- malformedRecords++;
689
- continue;
690
- }
691
- 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)
692
- continue;
693
- records.push(value);
694
- } catch {
695
- malformedRecords++;
696
- }
697
- }
698
776
  }
777
+ filter.signal?.throwIfAborted();
699
778
  const metadata = {
700
779
  schema: 1,
701
780
  ...files[0] ? { retainedFrom: files[0].time } : {},
@@ -713,7 +792,13 @@ function isHistoryRecord(value) {
713
792
  if (!isRecord(value))
714
793
  return false;
715
794
  const r = value;
716
- const allowed = new Set([
795
+ return Object.keys(r).every((k) => historyFields.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);
796
+ }
797
+ var filename, historyFields;
798
+ var init_history = __esm(() => {
799
+ init_rpc();
800
+ filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
801
+ historyFields = new Set([
717
802
  "schema",
718
803
  "recordId",
719
804
  "time",
@@ -726,12 +811,6 @@ function isHistoryRecord(value) {
726
811
  "performance",
727
812
  "droppedRecords"
728
813
  ]);
729
- 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);
730
- }
731
- var filename;
732
- var init_history = __esm(() => {
733
- init_rpc();
734
- filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
735
814
  });
736
815
 
737
816
  // src/modelanalytics.ts
@@ -778,6 +857,11 @@ function reconcile(a, b) {
778
857
  };
779
858
  }
780
859
  function analyzeModelRequests(records, options) {
860
+ const accumulator = createModelAccumulator(options);
861
+ accumulator.append(records);
862
+ return accumulator.result();
863
+ }
864
+ function createModelAccumulator(options) {
781
865
  const maxRequests = options.maxRequests ?? 1e5;
782
866
  if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
783
867
  throw new Error("maxRequests must be an integer from 1 to 1000000");
@@ -788,7 +872,7 @@ function analyzeModelRequests(records, options) {
788
872
  throw new Error("Invalid analytics timestamp");
789
873
  if (options.from !== undefined && options.to !== undefined && options.from > options.to)
790
874
  throw new Error("--from must precede --to");
791
- const coverage = {
875
+ const counters = {
792
876
  source: "retained-request-records",
793
877
  scannedRecords: 0,
794
878
  unscannedRecords: 0,
@@ -802,74 +886,98 @@ function analyzeModelRequests(records, options) {
802
886
  partial: false
803
887
  };
804
888
  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;
889
+ let totalRecords = 0, stopped = false;
890
+ function append(records) {
891
+ totalRecords += records.length;
892
+ for (const record2 of records) {
893
+ if (stopped)
836
894
  break;
895
+ counters.scannedRecords++;
896
+ if (record2.type === "gap") {
897
+ counters.gapRecords++;
898
+ continue;
899
+ }
900
+ if (record2.type !== "request")
901
+ continue;
902
+ const fact = requestFact(record2);
903
+ if (!fact) {
904
+ counters.missingIdentityRecords++;
905
+ continue;
906
+ }
907
+ fact.location = options.location;
908
+ const key = JSON.stringify([
909
+ fact.sessionId,
910
+ fact.requestId,
911
+ fact.startedAt
912
+ ]);
913
+ const previous = requests.get(key);
914
+ if (previous) {
915
+ counters.duplicateRecords++;
916
+ const merged = reconcile(previous.fact, fact);
917
+ if (!merged)
918
+ previous.conflict = true;
919
+ else
920
+ previous.fact = merged;
921
+ } else {
922
+ if (requests.size === maxRequests) {
923
+ counters.scannedRecords--;
924
+ stopped = true;
925
+ break;
926
+ }
927
+ requests.set(key, { fact, conflict: false });
837
928
  }
838
- requests.set(key, { fact, conflict: false });
839
929
  }
840
930
  }
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;
931
+ function* steps() {
932
+ const coverage = {
933
+ ...counters,
934
+ unscannedRecords: totalRecords - counters.scannedRecords
935
+ };
936
+ const accepted = [];
937
+ for (const { fact: r, conflict } of requests.values()) {
938
+ yield;
939
+ if (conflict) {
940
+ coverage.conflictingRequests++;
941
+ continue;
942
+ }
943
+ if (r.completedAt === undefined) {
944
+ coverage.incompleteRequests++;
945
+ continue;
946
+ }
947
+ const duration = r.completedAt - r.startedAt;
948
+ if (!nonnegative2(r.completedAt) || !Number.isFinite(duration) || duration <= 0) {
949
+ coverage.invalidTimingRequests++;
950
+ continue;
951
+ }
952
+ 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)
953
+ continue;
954
+ accepted.push({ ...r, completedAt: r.completedAt });
855
955
  }
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 });
956
+ const models = yield* summarizeModelRequestSteps(accepted);
957
+ 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);
958
+ return {
959
+ schemaVersion: 1,
960
+ location: options.location,
961
+ models,
962
+ coverage,
963
+ requests: accepted
964
+ };
859
965
  }
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
- };
966
+ return { append, steps, result: () => finishSteps(steps()) };
967
+ }
968
+ function finishSteps(steps) {
969
+ let step = steps.next();
970
+ while (!step.done)
971
+ step = steps.next();
972
+ return step.value;
869
973
  }
870
974
  function summarizeModelRequests(requests) {
975
+ return finishSteps(summarizeModelRequestSteps(requests));
976
+ }
977
+ function* summarizeModelRequestSteps(requests) {
871
978
  const groups = new Map;
872
979
  for (const r of requests) {
980
+ yield;
873
981
  const duration = r.completedAt - r.startedAt;
874
982
  let group = groups.get(r.model);
875
983
  if (!group) {
@@ -936,6 +1044,324 @@ var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
936
1044
  // src/index.ts
937
1045
  import { Plugin } from "@opencode/plugin";
938
1046
 
1047
+ // src/archive.ts
1048
+ init_history();
1049
+ import { open as open2 } from "fs/promises";
1050
+ import { join as join2 } from "path";
1051
+ var signature = (file) => JSON.stringify(file);
1052
+ var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
1053
+ function freezeRecord(record2) {
1054
+ if (record2.performance)
1055
+ Object.freeze(record2.performance);
1056
+ return Object.freeze(record2);
1057
+ }
1058
+
1059
+ class PerformanceArchive {
1060
+ path;
1061
+ options;
1062
+ entries = new Map;
1063
+ generation = 0;
1064
+ stopped = false;
1065
+ dirty = new Map;
1066
+ forcedGeneration = 0;
1067
+ job;
1068
+ stats = {
1069
+ readBytes: 0,
1070
+ parsedRecords: 0,
1071
+ cacheBytes: 0,
1072
+ cacheRecords: 0
1073
+ };
1074
+ constructor(path, options = {}) {
1075
+ this.path = path;
1076
+ this.options = options;
1077
+ }
1078
+ invalidate(name) {
1079
+ this.generation++;
1080
+ if (name) {
1081
+ if (this.dirty.size >= 256 && !this.dirty.has(name)) {
1082
+ this.entries.clear();
1083
+ this.dirty.clear();
1084
+ this.forcedGeneration = this.generation;
1085
+ }
1086
+ this.dirty.set(name, this.generation);
1087
+ } else {
1088
+ this.entries.clear();
1089
+ this.dirty.clear();
1090
+ this.forcedGeneration = this.generation;
1091
+ }
1092
+ this.account();
1093
+ }
1094
+ dispose() {
1095
+ this.stopped = true;
1096
+ this.job?.controller.abort();
1097
+ this.entries.clear();
1098
+ this.dirty.clear();
1099
+ this.account();
1100
+ }
1101
+ account() {
1102
+ this.stats.cacheBytes = [...this.entries.values()].reduce((n, e) => n + e.file.bytes, 0);
1103
+ this.stats.cacheRecords = [...this.entries.values()].reduce((n, e) => n + e.records.length, 0);
1104
+ }
1105
+ retain(name, entry) {
1106
+ this.entries.delete(name);
1107
+ this.entries.set(name, entry);
1108
+ this.account();
1109
+ while (this.entries.size > (this.options.maxCacheSegments ?? 256) || this.stats.cacheBytes > (this.options.maxCacheBytes ?? 64 * 1024 * 1024) || this.stats.cacheRecords > (this.options.maxCacheRecords ?? 200000)) {
1110
+ const oldest = this.entries.keys().next().value;
1111
+ if (oldest === undefined)
1112
+ break;
1113
+ this.entries.delete(oldest);
1114
+ this.dirty.delete(oldest);
1115
+ this.account();
1116
+ }
1117
+ }
1118
+ validCapture(expected, actual, generation) {
1119
+ return signature(expected) === signature(actual) || sameFile(expected, actual) && actual.bytes > expected.bytes && (this.dirty.get(expected.name) ?? -1) > generation;
1120
+ }
1121
+ async scan(maxBytes, signal) {
1122
+ for (let attempt = 0;attempt < 3; attempt++) {
1123
+ try {
1124
+ signal.throwIfAborted();
1125
+ const generation = this.generation;
1126
+ const files = await segments(this.path);
1127
+ signal.throwIfAborted();
1128
+ const names = new Set(files.map((f) => f.name));
1129
+ for (const name of this.entries.keys())
1130
+ if (!names.has(name)) {
1131
+ this.entries.delete(name);
1132
+ this.dirty.delete(name);
1133
+ }
1134
+ this.account();
1135
+ const selected = [];
1136
+ let bytes = 0, skippedBytes = 0, malformedRecords = 0;
1137
+ for (const file of files.slice().reverse()) {
1138
+ if (bytes + file.bytes > maxBytes) {
1139
+ skippedBytes += file.bytes;
1140
+ continue;
1141
+ }
1142
+ selected.unshift(file);
1143
+ bytes += file.bytes;
1144
+ }
1145
+ const parts = [];
1146
+ let unstable = false;
1147
+ for (const file of selected) {
1148
+ signal.throwIfAborted();
1149
+ const handle = await open2(join2(this.path, file.name), "r");
1150
+ try {
1151
+ const captured = {
1152
+ name: file.name,
1153
+ time: file.time,
1154
+ ...fileIdentity(await handle.stat())
1155
+ };
1156
+ if (!this.validCapture(file, captured, generation)) {
1157
+ unstable = true;
1158
+ break;
1159
+ }
1160
+ const previous = this.entries.get(file.name);
1161
+ let entry;
1162
+ if (previous && previous.generation >= this.forcedGeneration && !this.dirty.has(file.name) && signature(previous.file) === signature(file)) {
1163
+ entry = previous;
1164
+ } else {
1165
+ const append = previous && previous.generation >= this.forcedGeneration && sameFile(previous.file, file) && file.bytes > previous.file.bytes && this.dirty.has(file.name);
1166
+ const parsed = await scanHistorySegment(handle, file.bytes, { signal }, {
1167
+ ...this.options,
1168
+ onRead: (n) => {
1169
+ this.stats.readBytes += n;
1170
+ this.options.onRead?.(n);
1171
+ },
1172
+ onParse: () => {
1173
+ this.stats.parsedRecords++;
1174
+ this.options.onParse?.();
1175
+ }
1176
+ }, append ? previous.boundary : 0);
1177
+ entry = {
1178
+ ...parsed,
1179
+ file,
1180
+ generation,
1181
+ sessions: new Map,
1182
+ minTime: Infinity,
1183
+ maxTime: -Infinity
1184
+ };
1185
+ if (append) {
1186
+ entry.records = [
1187
+ ...previous.records.slice(0, previous.completeRecords),
1188
+ ...parsed.records
1189
+ ];
1190
+ entry.malformedRecords += previous.completeMalformed;
1191
+ entry.completeRecords += previous.completeRecords;
1192
+ entry.completeMalformed += previous.completeMalformed;
1193
+ }
1194
+ const checkpoint = scanScheduler(signal, this.options);
1195
+ for (const [index, record2] of entry.records.entries()) {
1196
+ if (index % 64 === 0) {
1197
+ const waiting = checkpoint();
1198
+ if (waiting)
1199
+ await waiting;
1200
+ }
1201
+ freezeRecord(record2);
1202
+ entry.minTime = Math.min(entry.minTime, record2.time);
1203
+ entry.maxTime = Math.max(entry.maxTime, record2.time);
1204
+ const list = entry.sessions.get(record2.sessionId) ?? [];
1205
+ list.push(index);
1206
+ entry.sessions.set(record2.sessionId, list);
1207
+ }
1208
+ Object.freeze(entry.records);
1209
+ }
1210
+ const after = {
1211
+ name: file.name,
1212
+ time: file.time,
1213
+ ...fileIdentity(await handle.stat())
1214
+ };
1215
+ if (!this.validCapture(file, after, generation)) {
1216
+ unstable = true;
1217
+ break;
1218
+ }
1219
+ signal.throwIfAborted();
1220
+ this.retain(file.name, entry);
1221
+ parts.push(entry);
1222
+ malformedRecords += entry.malformedRecords;
1223
+ } finally {
1224
+ await handle.close();
1225
+ }
1226
+ }
1227
+ signal.throwIfAborted();
1228
+ const current = await segments(this.path);
1229
+ const currentByName = new Map(current.map((file) => [file.name, file]));
1230
+ const originalNames = new Set(files.map((file) => file.name));
1231
+ if (unstable || this.forcedGeneration > generation || files.some((file) => {
1232
+ const actual = currentByName.get(file.name);
1233
+ return !actual || !this.validCapture(file, actual, generation);
1234
+ }) || current.some((file) => !originalNames.has(file.name) && (this.dirty.get(file.name) ?? -1) <= generation))
1235
+ continue;
1236
+ for (const [name, changedAt] of this.dirty)
1237
+ if (changedAt <= generation)
1238
+ this.dirty.delete(name);
1239
+ const metadata = {
1240
+ schema: 1,
1241
+ ...files[0] ? { retainedFrom: files[0].time } : {},
1242
+ retainedBytes: files.reduce((n, f) => n + f.bytes, 0),
1243
+ skippedBytes,
1244
+ malformedRecords,
1245
+ completeness: "bounded-retention"
1246
+ };
1247
+ Object.freeze(metadata);
1248
+ let flattened;
1249
+ return Object.freeze({
1250
+ revision: JSON.stringify([generation, maxBytes, files]),
1251
+ metadata,
1252
+ parts,
1253
+ get records() {
1254
+ if (!flattened) {
1255
+ flattened = [];
1256
+ for (const part of parts)
1257
+ for (const record2 of part.records)
1258
+ flattened.push(record2);
1259
+ Object.freeze(flattened);
1260
+ }
1261
+ return flattened;
1262
+ }
1263
+ });
1264
+ } catch (error) {
1265
+ signal.throwIfAborted();
1266
+ if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
1267
+ throw error;
1268
+ }
1269
+ }
1270
+ throw new Error("Archive changed repeatedly during read; retry later");
1271
+ }
1272
+ async read(filter = {}) {
1273
+ filter.signal?.throwIfAborted();
1274
+ if (this.stopped)
1275
+ throw new Error("Archive stopped");
1276
+ const key = filter.maxBytes ?? 64 * 1024 * 1024;
1277
+ if (this.job && this.job.key !== key) {
1278
+ await this.wait(this.job, filter.signal);
1279
+ return this.read(filter);
1280
+ }
1281
+ if (this.job?.controller.signal.aborted) {
1282
+ await this.job.promise.catch(() => {
1283
+ });
1284
+ return this.read(filter);
1285
+ }
1286
+ if (!this.job) {
1287
+ const controller = new AbortController;
1288
+ const job = {
1289
+ key,
1290
+ controller,
1291
+ waiters: 0,
1292
+ promise: Promise.resolve().then(() => this.scan(key, controller.signal))
1293
+ };
1294
+ job.promise = job.promise.finally(() => {
1295
+ if (this.job === job)
1296
+ this.job = undefined;
1297
+ });
1298
+ this.job = job;
1299
+ }
1300
+ const snapshot = await this.wait(this.job, filter.signal);
1301
+ const parts = snapshot.parts;
1302
+ const source = { revision: snapshot.revision, metadata: snapshot.metadata };
1303
+ if (filter.sessionId === undefined && filter.agent === undefined && filter.model === undefined && filter.from === undefined && filter.to === undefined)
1304
+ return Object.freeze({
1305
+ ...source,
1306
+ get records() {
1307
+ return snapshot.records;
1308
+ },
1309
+ *iterateRecords() {
1310
+ for (const part of parts)
1311
+ for (const record2 of part.records)
1312
+ yield record2;
1313
+ }
1314
+ });
1315
+ const records = [];
1316
+ const checkpoint = scanScheduler(filter.signal, this.options);
1317
+ for (const part of parts) {
1318
+ if (filter.from !== undefined && part.maxTime < filter.from || filter.to !== undefined && part.minTime > filter.to)
1319
+ continue;
1320
+ const offsets = filter.sessionId === undefined ? undefined : [
1321
+ ...part.sessions.get(filter.sessionId) ?? [],
1322
+ ...filter.sessionId === "__history__" ? [] : part.sessions.get("__history__") ?? []
1323
+ ].sort((a, b) => a - b);
1324
+ const candidates = offsets ? offsets.map((i) => part.records[i]).filter((r) => r !== undefined) : part.records;
1325
+ for (const record2 of candidates) {
1326
+ {
1327
+ const waiting = checkpoint();
1328
+ if (waiting)
1329
+ await waiting;
1330
+ }
1331
+ if (matchesHistory(record2, filter))
1332
+ records.push(record2);
1333
+ }
1334
+ }
1335
+ filter.signal?.throwIfAborted();
1336
+ Object.freeze(records);
1337
+ return Object.freeze({ ...source, records });
1338
+ }
1339
+ async wait(job, signal) {
1340
+ job.waiters++;
1341
+ let onAbort = () => {
1342
+ };
1343
+ try {
1344
+ return await Promise.race([
1345
+ job.promise,
1346
+ new Promise((_resolve, reject) => {
1347
+ onAbort = () => reject(signal?.reason ?? job.controller.signal.reason ?? new Error("Archive read cancelled"));
1348
+ signal?.addEventListener("abort", onAbort, { once: true });
1349
+ job.controller.signal.addEventListener("abort", onAbort, {
1350
+ once: true
1351
+ });
1352
+ if (signal?.aborted || job.controller.signal.aborted)
1353
+ onAbort();
1354
+ })
1355
+ ]);
1356
+ } finally {
1357
+ signal?.removeEventListener("abort", onAbort);
1358
+ job.controller.signal.removeEventListener("abort", onAbort);
1359
+ if (--job.waiters === 0)
1360
+ job.controller.abort();
1361
+ }
1362
+ }
1363
+ }
1364
+
939
1365
  // src/performance.ts
940
1366
  var valid2 = (n) => Number.isFinite(n) && n >= 0;
941
1367
  var rate = (tokens, ms) => valid2(tokens) && ms > 0 ? tokens * 1000 / ms : undefined;
@@ -1322,10 +1748,11 @@ function buildChartData(records, options = {}) {
1322
1748
  // src/historyserver.ts
1323
1749
  init_history();
1324
1750
  init_rpc();
1325
- async function serveHistory(rpc, path, enabled, now = Date.now, authorize = async () => true) {
1751
+ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = async () => true, reader = readPerformanceHistory) {
1326
1752
  if (!rpc)
1327
1753
  return () => {
1328
1754
  };
1755
+ const shutdown = new AbortController;
1329
1756
  let reading = false, stopped = false;
1330
1757
  const registration = await rpc.register(historyRpc, {
1331
1758
  read: async (input, context) => {
@@ -1333,12 +1760,15 @@ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = asyn
1333
1760
  throw new Error("Invalid history request (maximum window: seven days)");
1334
1761
  if (reading || stopped)
1335
1762
  throw new Error("History service busy or stopped; retry later");
1763
+ const signal = AbortSignal.any([context.signal, shutdown.signal]);
1764
+ signal.throwIfAborted();
1336
1765
  reading = true;
1337
1766
  try {
1338
- const allowed = !enabled || await authorize(input.sessionId, context.signal);
1767
+ const allowed = !enabled || await authorize(input.sessionId, signal);
1339
1768
  if (!allowed)
1340
1769
  throw new Error("Session is outside this location");
1341
- const result = enabled ? await readPerformanceHistory(path, {
1770
+ const result = enabled ? await reader(path, {
1771
+ signal,
1342
1772
  sessionId: input.sessionId,
1343
1773
  from: input.from,
1344
1774
  to: input.to
@@ -1381,6 +1811,7 @@ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = asyn
1381
1811
  });
1382
1812
  return async () => {
1383
1813
  stopped = true;
1814
+ shutdown.abort();
1384
1815
  await registration.dispose();
1385
1816
  };
1386
1817
  }
@@ -1422,15 +1853,62 @@ var metadata = {
1422
1853
  malformedRecords: 0,
1423
1854
  completeness: "bounded-retention"
1424
1855
  };
1856
+ async function responsive(steps, signal) {
1857
+ const checkpoint = scanScheduler(signal);
1858
+ let step = steps.next(), iterations = 0;
1859
+ while (!step.done) {
1860
+ if (++iterations % 64 === 0) {
1861
+ const waiting = checkpoint();
1862
+ if (waiting)
1863
+ await waiting;
1864
+ }
1865
+ step = steps.next();
1866
+ }
1867
+ signal.throwIfAborted();
1868
+ return step.value;
1869
+ }
1870
+ function* collectRecords(values) {
1871
+ const records = [];
1872
+ for (const record2 of values) {
1873
+ yield;
1874
+ records.push(record2);
1875
+ }
1876
+ return records;
1877
+ }
1878
+ function* sortRequests(requests) {
1879
+ const compare = (a, b) => b.completedAt - a.completedAt || a.sessionId.localeCompare(b.sessionId) || a.requestId.localeCompare(b.requestId) || a.startedAt - b.startedAt;
1880
+ let source = requests, target = requests.slice();
1881
+ for (let size = 1;size < requests.length; size *= 2) {
1882
+ for (let start = 0;start < requests.length; start += 2 * size) {
1883
+ const middle = Math.min(start + size, requests.length), end = Math.min(start + 2 * size, requests.length);
1884
+ let left = start, right = middle;
1885
+ for (let output = start;output < end; output++) {
1886
+ yield;
1887
+ const a = source[left], b = source[right];
1888
+ if (left < middle && a && (right >= end || !b || compare(a, b) <= 0)) {
1889
+ target[output] = a;
1890
+ left++;
1891
+ } else if (b) {
1892
+ target[output] = b;
1893
+ right++;
1894
+ }
1895
+ }
1896
+ }
1897
+ [source, target] = [target, source];
1898
+ }
1899
+ return source;
1900
+ }
1425
1901
  function createAnalyticsService(path, location, enabled, options = {}) {
1426
1902
  const now = options.now ?? Date.now;
1427
1903
  const reader = options.read ?? readPerformanceHistory;
1428
1904
  const snapshots = new Map;
1429
- let stopped = false;
1905
+ const shutdown = new AbortController;
1906
+ let stopped = false, projecting = 0;
1430
1907
  let archive;
1431
1908
  let scan;
1909
+ let incremental;
1432
1910
  async function load(signal) {
1433
- if (archive && now() - archive.at < 30000)
1911
+ if (archive && !options.revisioned && now() - archive.at < 30000)
1434
1912
  return archive;
1435
1913
  if (!scan || scan.controller.signal.aborted) {
1436
1914
  const controller = new AbortController;
@@ -1442,12 +1920,35 @@ function createAnalyticsService(path, location, enabled, options = {}) {
1442
1920
  controller.signal.throwIfAborted();
1443
1921
  if (stopped)
1444
1922
  throw new Error("Analytics service stopped");
1445
- const analysis = analyzeModelRequests(source.records, { location });
1923
+ const revision = "revision" in source && typeof source.revision === "string" ? source.revision : undefined;
1924
+ if (archive && revision !== undefined && archive.revision === revision) {
1925
+ archive = { ...archive, at: now() };
1926
+ return archive;
1927
+ }
1928
+ const records = source.iterateRecords ? await responsive(collectRecords(source.iterateRecords()), controller.signal) : source.records;
1929
+ const previous = incremental;
1930
+ incremental = undefined;
1931
+ const append = previous && records.length >= previous.prefix.length && previous.prefix.every((record2, index) => records[index] === record2);
1932
+ const accumulator = append ? previous.accumulator : createModelAccumulator({ location });
1933
+ const checkpoint = scanScheduler(controller.signal);
1934
+ for (let index = append ? previous.prefix.length : 0;index < records.length; index += 256) {
1935
+ {
1936
+ const waiting = checkpoint();
1937
+ if (waiting)
1938
+ await waiting;
1939
+ }
1940
+ accumulator.append(records.slice(index, index + 256));
1941
+ }
1942
+ const analysis = await responsive(accumulator.steps(), controller.signal);
1943
+ controller.signal.throwIfAborted();
1944
+ if (records.length <= 1e5 && records.every((r) => Object.isFrozen(r) && (!r.performance || Object.isFrozen(r.performance))))
1945
+ incremental = { prefix: records.slice(), accumulator };
1446
1946
  const result = {
1447
1947
  at: now(),
1948
+ ...revision === undefined ? {} : { revision },
1448
1949
  source: {
1449
1950
  metadata: source.metadata,
1450
- records: source.records.filter((r) => r.type === "gap")
1951
+ records: records.filter((r) => r.type === "gap")
1451
1952
  },
1452
1953
  analysis
1453
1954
  };
@@ -1505,48 +2006,82 @@ function createAnalyticsService(path, location, enabled, options = {}) {
1505
2006
  signal.throwIfAborted();
1506
2007
  if (stopped)
1507
2008
  throw new Error("Analytics service stopped");
2009
+ signal = AbortSignal.any([signal, shutdown.signal]);
1508
2010
  for (const [id, item] of snapshots)
1509
2011
  if (item.expiresAt <= now())
1510
2012
  snapshots.delete(id);
1511
- let snapshot = input.snapshotId ? snapshots.get(input.snapshotId) : [...snapshots.values()].find((s) => s.key === queryKey(input) && now() - s.generatedAt < 30000);
2013
+ const current = options.revisioned && !input.snapshotId ? await load(signal) : undefined;
2014
+ let snapshot = input.snapshotId ? snapshots.get(input.snapshotId) : [...snapshots.values()].find((s) => s.key === queryKey(input) && (current ? s.revision === current.revision : now() - s.generatedAt < 30000));
1512
2015
  if (input.snapshotId && (!snapshot || snapshot.key !== queryKey(input)))
1513
2016
  throw new Error("Analytics snapshot expired; refresh Models");
1514
2017
  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);
2018
+ if (projecting >= 2)
2019
+ throw new Error("Analytics service busy; retry later");
2020
+ projecting++;
2021
+ try {
2022
+ const data = current ?? await load(signal);
2023
+ signal.throwIfAborted();
2024
+ let requests2 = data.analysis.requests.filter((r) => r.completedAt >= input.from && r.completedAt < input.to && (!input.agent || r.agent === input.agent));
2025
+ requests2 = await responsive(sortRequests(requests2), signal);
2026
+ const gaps = data.source.records.filter((r) => r.time >= input.from && r.time < input.to).map((r) => r.time);
2027
+ snapshot = {
2028
+ id: crypto.randomUUID(),
2029
+ key: queryKey(input),
2030
+ generatedAt: data.at,
2031
+ expiresAt: now() + 90000,
2032
+ requests: requests2,
2033
+ models: await responsive(summarizeModelRequestSteps(requests2), signal),
2034
+ coverage: data.analysis.coverage,
2035
+ metadata: data.source.metadata,
2036
+ gaps: gaps.slice(-128),
2037
+ omittedGaps: Math.max(0, gaps.length - 128),
2038
+ ...data.revision === undefined ? {} : { revision: data.revision },
2039
+ sorted: new Map,
2040
+ variants: new Map
2041
+ };
2042
+ signal.throwIfAborted();
2043
+ snapshots.set(snapshot.id, snapshot);
2044
+ if (snapshots.size > 2) {
2045
+ const oldest = snapshots.keys().next().value;
2046
+ if (oldest)
2047
+ snapshots.delete(oldest);
2048
+ }
2049
+ } finally {
2050
+ projecting--;
1537
2051
  }
1538
2052
  }
1539
2053
  const sort = input.sort ?? "requests";
1540
- const models = [...snapshot.models].sort((a, b) => {
2054
+ const models = snapshot.sorted.get(sort) ?? [...snapshot.models].sort((a, b) => {
1541
2055
  const key = sort === "tps" ? "averageTps" : sort === "ttft" ? "p95TtftMs" : "completedRequests";
1542
2056
  return (sort === "identity" ? 0 : (b[key] ?? -Infinity) - (a[key] ?? -Infinity)) || (a.model ?? "").localeCompare(b.model ?? "");
1543
2057
  });
2058
+ snapshot.sorted.set(sort, models);
1544
2059
  const modelOffset = pageOffset(input.modelCursor, snapshot, "models", sort);
1545
2060
  const selected = input.unknownModel ? models.find((m) => m.model === undefined) : input.model ? models.find((m) => m.model === input.model) : models[modelOffset];
1546
2061
  const selection = selected?.model ?? "";
1547
- const requests = selected ? snapshot.requests.filter((r) => r.model === selected.model) : [];
2062
+ const variantKey = JSON.stringify([
2063
+ selected?.model ?? null,
2064
+ !!selected,
2065
+ input.buckets
2066
+ ]);
2067
+ let variant = snapshot.variants.get(variantKey);
2068
+ if (!variant) {
2069
+ const requests2 = selected ? snapshot.requests.filter((r) => r.model === selected.model) : [];
2070
+ variant = {
2071
+ requests: requests2,
2072
+ buckets: selected ? bucketModelRequests(requests2, input.from, input.to, input.buckets) : []
2073
+ };
2074
+ }
2075
+ snapshot.variants.delete(variantKey);
2076
+ snapshot.variants.set(variantKey, variant);
2077
+ if (snapshot.variants.size > 8) {
2078
+ const oldest = snapshot.variants.keys().next().value;
2079
+ if (oldest !== undefined)
2080
+ snapshot.variants.delete(oldest);
2081
+ }
2082
+ const requests = variant.requests;
1548
2083
  const requestOffset = pageOffset(input.requestCursor, snapshot, "requests", selection);
1549
- const buckets = selected ? bucketModelRequests(requests, input.from, input.to, input.buckets) : [];
2084
+ const buckets = structuredClone(variant.buckets);
1550
2085
  const result = {
1551
2086
  schemaVersion: 1,
1552
2087
  generatedAt: snapshot.generatedAt,
@@ -1584,17 +2119,19 @@ function createAnalyticsService(path, location, enabled, options = {}) {
1584
2119
  },
1585
2120
  dispose() {
1586
2121
  stopped = true;
2122
+ shutdown.abort();
1587
2123
  scan?.controller.abort();
1588
2124
  snapshots.clear();
1589
2125
  archive = undefined;
2126
+ incremental = undefined;
1590
2127
  }
1591
2128
  };
1592
2129
  }
1593
- async function serveModelAnalytics(port, path, location, enabled) {
2130
+ async function serveModelAnalytics(port, path, location, enabled, options = {}) {
1594
2131
  if (!port)
1595
2132
  return () => {
1596
2133
  };
1597
- const service = createAnalyticsService(path, location, enabled);
2134
+ const service = createAnalyticsService(path, location, enabled, options);
1598
2135
  const registration = await port.register(analyticsRpc, {
1599
2136
  read: async (input, context) => {
1600
2137
  if (!isAnalyticsRead(input))
@@ -1847,10 +2384,10 @@ class SnapshotStore {
1847
2384
  const s = resident.get(id);
1848
2385
  if (!s)
1849
2386
  continue;
1850
- const performance = this.performanceSnapshot?.(id);
2387
+ const performance2 = this.performanceSnapshot?.(id);
1851
2388
  const next = freezeSnapshot({
1852
2389
  ...project(s, this.config, this.now()),
1853
- ...performance ? { performance } : {}
2390
+ ...performance2 ? { performance: performance2 } : {}
1854
2391
  });
1855
2392
  const old = this.rows.get(id);
1856
2393
  if (old && JSON.stringify({ ...old, revision: 0 }) === JSON.stringify(next))
@@ -1919,7 +2456,7 @@ function freezeSnapshot(row2) {
1919
2456
 
1920
2457
  // src/snapshotserver.ts
1921
2458
  init_rpc();
1922
- async function serveSnapshots(rpc, store, performance = false, prepareRead) {
2459
+ async function serveSnapshots(rpc, store, performance2 = false, prepareRead) {
1923
2460
  if (!rpc)
1924
2461
  return () => {
1925
2462
  };
@@ -1927,15 +2464,15 @@ async function serveSnapshots(rpc, store, performance = false, prepareRead) {
1927
2464
  ...batch,
1928
2465
  upserts: batch.upserts.map(({ performance: _performance, ...row2 }) => row2)
1929
2466
  });
1930
- const batchValid = performance ? isPerformanceBatch : isBatch;
1931
- const resultValid = performance ? isPerformanceResult : isResult;
1932
- const registration = await rpc.register(performance ? performanceRpc : monitorRpc, {
2467
+ const batchValid = performance2 ? isPerformanceBatch : isBatch;
2468
+ const resultValid = performance2 ? isPerformanceResult : isResult;
2469
+ const registration = await rpc.register(performance2 ? performanceRpc : monitorRpc, {
1933
2470
  read: async (input, context) => {
1934
2471
  if (!isRead(input))
1935
2472
  throw new Error("Invalid snapshot request");
1936
2473
  await prepareRead?.(input.sessionIds, context.signal);
1937
2474
  const raw = store.read(input);
1938
- const result = performance ? raw : stripPerformance(raw);
2475
+ const result = performance2 ? raw : stripPerformance(raw);
1939
2476
  if (!resultValid(result))
1940
2477
  throw new Error("Invalid snapshot projection");
1941
2478
  return result;
@@ -1966,7 +2503,7 @@ async function serveSnapshots(rpc, store, performance = false, prepareRead) {
1966
2503
  });
1967
2504
  }
1968
2505
  const unlisten = store.listen((raw) => {
1969
- const batch = performance ? raw : stripPerformance(raw);
2506
+ const batch = performance2 ? raw : stripPerformance(raw);
1970
2507
  if (!batchValid(batch))
1971
2508
  return;
1972
2509
  queue.push(batch);
@@ -2498,13 +3035,13 @@ function encodeRecovery(rows) {
2498
3035
  }
2499
3036
 
2500
3037
  // src/telemetry.ts
2501
- import { mkdir as mkdir2, open as open2, rename, stat as stat2 } from "fs/promises";
2502
- import { dirname, join as join2 } from "path";
3038
+ import { mkdir as mkdir2, open as open3, rename, stat as stat2 } from "fs/promises";
3039
+ import { dirname, join as join3 } from "path";
2503
3040
  import { homedir } from "os";
2504
3041
  import { createHash as createHash2 } from "crypto";
2505
3042
  function defaultPath(directory) {
2506
3043
  const key = createHash2("sha256").update(directory).digest("hex").slice(0, 16);
2507
- return join2(process.env.XDG_STATE_HOME || join2(homedir(), ".local/state"), "opencode-context-guard", `${key}.jsonl`);
3044
+ return join3(process.env.XDG_STATE_HOME || join3(homedir(), ".local/state"), "opencode-context-guard", `${key}.jsonl`);
2508
3045
  }
2509
3046
 
2510
3047
  class JsonlTelemetrySink {
@@ -2563,7 +3100,7 @@ class JsonlTelemetrySink {
2563
3100
  });
2564
3101
  this.bytes = 0;
2565
3102
  }
2566
- const file = await open2(this.path, "a", 384);
3103
+ const file = await open3(this.path, "a", 384);
2567
3104
  try {
2568
3105
  await file.writeFile(line);
2569
3106
  } finally {
@@ -2588,7 +3125,7 @@ async function readJournal(path, maxBytes = 16 * 1024 * 1024) {
2588
3125
  const size = await stat2(name).then((s) => s.size).catch(() => 0);
2589
3126
  if (!size)
2590
3127
  continue;
2591
- const file = await open2(name, "r");
3128
+ const file = await open3(name, "r");
2592
3129
  const buffer = Buffer.alloc(Math.min(size, maxBytes));
2593
3130
  try {
2594
3131
  await file.read(buffer, 0, buffer.length, Math.max(0, size - maxBytes));
@@ -2615,8 +3152,8 @@ async function setup(ctx, injectedSink) {
2615
3152
  const config = parseConfig(ctx.options);
2616
3153
  const disk = new JsonlTelemetrySink(config.telemetry.path ?? defaultPath(ctx.location.directory), config.telemetry);
2617
3154
  const diagnostics = injectedSink ?? disk;
2618
- const performance = new PerformanceTracker(config.safety.maxSessions, config.metrics.rollingWindowMs);
2619
- const store = new SnapshotStore(config, () => monitor?.sessions ?? new Map, Date.now, crypto.randomUUID(), config.metrics.enabled ? (id) => performance.snapshot(id) : undefined);
3155
+ const performance2 = new PerformanceTracker(config.safety.maxSessions, config.metrics.rollingWindowMs);
3156
+ const store = new SnapshotStore(config, () => monitor?.sessions ?? new Map, Date.now, crypto.randomUUID(), config.metrics.enabled ? (id) => performance2.snapshot(id) : undefined);
2620
3157
  let monitor;
2621
3158
  const sink = {
2622
3159
  emit(event) {
@@ -2662,15 +3199,17 @@ async function setup(ctx, injectedSink) {
2662
3199
  return () => {
2663
3200
  };
2664
3201
  });
3202
+ const archive = new PerformanceArchive(config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`);
3203
+ const archiveRead = (_path, filter) => archive.read(filter);
2665
3204
  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) => {
2666
3205
  const info = await ctx.session.get({ sessionID: sessionId }, { signal });
2667
3206
  return info.location.directory === ctx.location.directory;
2668
- }).catch(() => {
3207
+ }, archiveRead).catch(() => {
2669
3208
  diagnostics.emit({ time: Date.now(), type: "history-service-unavailable" });
2670
3209
  return () => {
2671
3210
  };
2672
3211
  });
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(() => {
3212
+ 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, { read: archiveRead, revisioned: true }).catch(() => {
2674
3213
  diagnostics.emit({
2675
3214
  time: Date.now(),
2676
3215
  type: "analytics-service-unavailable"
@@ -2679,8 +3218,13 @@ async function setup(ctx, injectedSink) {
2679
3218
  };
2680
3219
  });
2681
3220
  const stopSnapshots = async () => {
2682
- await stopAnalytics();
2683
- await stopHistory();
3221
+ const archives = await Promise.allSettled([stopAnalytics(), stopHistory()]);
3222
+ archive.dispose();
3223
+ if (archives.some((result) => result.status === "rejected"))
3224
+ diagnostics.emit({
3225
+ time: Date.now(),
3226
+ type: "registration-cleanup-failed"
3227
+ });
2684
3228
  await stopV2();
2685
3229
  await stopV1();
2686
3230
  };
@@ -2694,10 +3238,10 @@ async function setup(ctx, injectedSink) {
2694
3238
  const history = new PerformanceHistory(config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, {
2695
3239
  ...config.history,
2696
3240
  enabled: config.history.enabled && config.metrics.enabled
2697
- }, Date.now, (type) => diagnostics.emit({ time: Date.now(), type }));
3241
+ }, Date.now, (type) => diagnostics.emit({ time: Date.now(), type }), (name) => archive.invalidate(name));
2698
3242
  await history.initialize();
2699
3243
  if (config.metrics.enabled) {
2700
- performance.restore(await ctx.storage.get("performance.v1").catch(() => {
3244
+ performance2.restore(await ctx.storage.get("performance.v1").catch(() => {
2701
3245
  return;
2702
3246
  }));
2703
3247
  }
@@ -2705,7 +3249,7 @@ async function setup(ctx, injectedSink) {
2705
3249
  const recordPerformance = (id, type, event) => {
2706
3250
  if (!config.metrics.enabled)
2707
3251
  return;
2708
- const state = monitor?.sessions.get(id), snapshot = performance.snapshot(id);
3252
+ const state = monitor?.sessions.get(id), snapshot = performance2.snapshot(id);
2709
3253
  if (!state || !snapshot)
2710
3254
  return;
2711
3255
  history.emit({
@@ -2724,7 +3268,7 @@ async function setup(ctx, injectedSink) {
2724
3268
  const persistPerformance = async () => {
2725
3269
  if (!config.metrics.enabled)
2726
3270
  return;
2727
- await ctx.storage.set("performance.v1", performance.checkpoint().map((row2) => ({ ...row2 }))).catch(() => {
3271
+ await ctx.storage.set("performance.v1", performance2.checkpoint().map((row2) => ({ ...row2 }))).catch(() => {
2728
3272
  diagnostics.emit({
2729
3273
  time: Date.now(),
2730
3274
  type: "performance-persistence-unavailable"
@@ -2907,12 +3451,12 @@ async function setup(ctx, injectedSink) {
2907
3451
  recoveries.delete(sessionId);
2908
3452
  }
2909
3453
  if (config.metrics.enabled) {
2910
- if (!performance.snapshot(sessionId) && [...performance.ids()].length >= config.safety.maxSessions) {
2911
- const dormant = [...performance.ids()].find((id) => !performanceResidents.has(id));
3454
+ if (!performance2.snapshot(sessionId) && [...performance2.ids()].length >= config.safety.maxSessions) {
3455
+ const dormant = [...performance2.ids()].find((id) => !performanceResidents.has(id));
2912
3456
  if (dormant)
2913
- performance.remove(dormant);
3457
+ performance2.remove(dormant);
2914
3458
  }
2915
- performance.observe(sessionId, info.time.created, outputTokens(info.tokens));
3459
+ performance2.observe(sessionId, info.time.created, outputTokens(info.tokens));
2916
3460
  performanceResidents.add(sessionId);
2917
3461
  }
2918
3462
  s.usage.cumulative = info.tokens;
@@ -3015,7 +3559,7 @@ async function setup(ctx, injectedSink) {
3015
3559
  return;
3016
3560
  if (event.type === "session.text.delta" || event.type === "session.reasoning.delta") {
3017
3561
  if (config.metrics.enabled)
3018
- performance.delta(event.data.sessionID, event.data.assistantMessageID, event.id, event.created, event.data.delta.length);
3562
+ performance2.delta(event.data.sessionID, event.data.assistantMessageID, event.id, event.created, event.data.delta.length);
3019
3563
  return;
3020
3564
  }
3021
3565
  if (!relevant.has(event.type) || !("sessionID" in event.data))
@@ -3025,7 +3569,7 @@ async function setup(ctx, injectedSink) {
3025
3569
  return;
3026
3570
  if (event.type === "session.deleted") {
3027
3571
  recordPerformance(id, "terminal", "session.deleted");
3028
- performance.remove(id);
3572
+ performance2.remove(id);
3029
3573
  performanceResidents.delete(id);
3030
3574
  await persistPerformance();
3031
3575
  store.remove(id);
@@ -3041,11 +3585,11 @@ async function setup(ctx, injectedSink) {
3041
3585
  return;
3042
3586
  if ("durable" in event && event.durable && !activeMonitor.accept(s, event.durable.seq))
3043
3587
  return;
3044
- const perfAccepted = config.metrics.enabled && (!("durable" in event) || !event.durable || performance.accept(id, event.durable.seq));
3588
+ const perfAccepted = config.metrics.enabled && (!("durable" in event) || !event.durable || performance2.accept(id, event.durable.seq));
3045
3589
  switch (event.type) {
3046
3590
  case "session.step.started":
3047
3591
  if (perfAccepted)
3048
- performance.start(id, event.data.assistantMessageID, event.data.agent, `${event.data.model.providerID}/${event.data.model.id}`, event.data.started);
3592
+ performance2.start(id, event.data.assistantMessageID, event.data.agent, `${event.data.model.providerID}/${event.data.model.id}`, event.data.started);
3049
3593
  activeMonitor.touch({
3050
3594
  sessionId: id,
3051
3595
  agent: event.data.agent,
@@ -3060,12 +3604,12 @@ async function setup(ctx, injectedSink) {
3060
3604
  break;
3061
3605
  case "session.step.streamed":
3062
3606
  if (perfAccepted)
3063
- performance.streamed(id, event.data.assistantMessageID, event.created);
3607
+ performance2.streamed(id, event.data.assistantMessageID, event.created);
3064
3608
  break;
3065
3609
  case "session.step.ended":
3066
3610
  case "session.step.failed": {
3067
3611
  if (perfAccepted)
3068
- performance.settle(id, event.data.assistantMessageID, event.data.tokens ? outputTokens(event.data.tokens) : undefined);
3612
+ performance2.settle(id, event.data.assistantMessageID, event.data.tokens ? outputTokens(event.data.tokens) : undefined);
3069
3613
  const tokens = event.data.tokens && promptTokens(event.data.tokens);
3070
3614
  const request = requests.get(id);
3071
3615
  if (tokens !== undefined && request?.id === event.data.assistantMessageID && request.model === s.model)
@@ -3104,8 +3648,8 @@ async function setup(ctx, injectedSink) {
3104
3648
  case "session.execution.failed":
3105
3649
  case "session.execution.interrupted":
3106
3650
  s.active = false;
3107
- if (performance.snapshot(id)?.tokenSource === "estimated")
3108
- performance.gap(id);
3651
+ if (performance2.snapshot(id)?.tokenSource === "estimated")
3652
+ performance2.gap(id);
3109
3653
  s.lastExecutionEnd = event.created;
3110
3654
  break;
3111
3655
  case "session.tool.called":
@@ -3126,7 +3670,7 @@ async function setup(ctx, injectedSink) {
3126
3670
  lastPerformanceSample = now;
3127
3671
  for (const id of performanceResidents) {
3128
3672
  store.mark(id);
3129
- const snapshot = performance.snapshot(id);
3673
+ const snapshot = performance2.snapshot(id);
3130
3674
  if (now - lastHistorySample >= 1000 && snapshot && (snapshot.status === "waiting" || snapshot.status === "streaming"))
3131
3675
  recordPerformance(id, "sample");
3132
3676
  }
@@ -3144,7 +3688,7 @@ async function setup(ctx, injectedSink) {
3144
3688
  store.recover("capacity-exceeded");
3145
3689
  for (const id of performanceResidents)
3146
3690
  if (!activeMonitor.sessions.has(id)) {
3147
- performance.remove(id);
3691
+ performance2.remove(id);
3148
3692
  performanceResidents.delete(id);
3149
3693
  }
3150
3694
  for (const id of requests.keys())
@@ -3168,7 +3712,7 @@ async function setup(ctx, injectedSink) {
3168
3712
  } catch {
3169
3713
  }
3170
3714
  if (!stopped) {
3171
- performance.gap();
3715
+ performance2.gap();
3172
3716
  for (const id of performanceResidents) {
3173
3717
  store.mark(id);
3174
3718
  recordPerformance(id, "gap", "event-stream-gap");