@cesarus777/opencode-cgp 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,174 +1,33 @@
1
1
  // @bun
2
- // src/rpc.ts
3
- import { Rpc } from "@opencode/plugin/rpc";
4
- var count = {
5
- type: "integer",
6
- minimum: 0,
7
- maximum: Number.MAX_SAFE_INTEGER
8
- };
9
- var text = { type: "string", minLength: 1, maxLength: 512 };
10
- var enumeration = (...values) => ({ type: "string", enum: values });
11
- var object = (properties, required = Object.keys(properties)) => ({
12
- type: "object",
13
- properties,
14
- required,
15
- additionalProperties: false
16
- });
17
- var array = (items, maxItems = 128) => ({
18
- type: "array",
19
- items,
20
- maxItems
21
- });
22
- var measurement = object({
23
- tokens: count,
24
- at: count,
25
- model: text,
26
- requestId: text,
27
- provenance: enumeration("provider-normalized", "serialized-estimate"),
28
- confidence: enumeration("reported", "approximate"),
29
- scope: enumeration("completed-request", "prepared-request")
30
- }, ["tokens", "at", "model", "provenance", "confidence", "scope"]);
31
- var row = object({
32
- sessionId: text,
33
- revision: count,
34
- observedAt: count,
35
- parentId: text,
36
- agent: text,
37
- model: text,
38
- measurement,
39
- limits: object({ context: count, input: count, output: count }, [
40
- "context",
41
- "output"
42
- ]),
43
- capacityTokens: count,
44
- outputReserveTokens: count,
45
- peakTokens: count,
46
- policy: object({
47
- id: text,
48
- configured: object({
49
- type: enumeration("tokens", "ratio", "remaining"),
50
- value: { type: "number", minimum: 0 }
51
- }),
52
- triggerTokens: { type: "number" },
53
- comparison: enumeration(">=", ">"),
54
- reason: enumeration("missing-usage", "model-changed", "invalid-usage", "stale-usage", "missing-model-budget", "incompatible-model-budget", "threshold-crossed", "below-threshold"),
55
- crossed: { type: "boolean" }
56
- }, ["id", "configured", "comparison", "reason", "crossed"]),
57
- compaction: object({
58
- phase: enumeration("idle", "requesting", "accepted", "running", "awaiting-measurement", "blocked", "uncertain"),
59
- count,
60
- armed: { type: "boolean" },
61
- blockedReason: enumeration("session-removed", "compaction-failed", "reclaim-unverifiable", "insufficient-reclaim", "completion-event-timeout", "admission-outcome-unknown", "restart-needs-lifecycle-evidence"),
62
- lastAttempt: count,
63
- lastCompleted: count,
64
- cooldownUntil: count
65
- }, ["phase", "armed"]),
66
- cumulative: object({
67
- input: count,
68
- output: count,
69
- reasoning: count,
70
- cache: object({ read: count, write: count })
71
- }),
72
- cumulativeScope: enumeration("session", "unknown"),
73
- lastObservedActive: { type: "boolean" }
74
- }, [
75
- "sessionId",
76
- "revision",
77
- "observedAt",
78
- "policy",
79
- "compaction",
80
- "cumulativeScope",
81
- "lastObservedActive"
82
- ]);
83
- var batchFields = {
84
- schemaVersion: { const: 1 },
85
- epoch: text,
86
- sequence: count,
87
- generatedAt: count,
88
- health: object({
89
- enabled: { type: "boolean" },
90
- mode: enumeration("observe", "enforce"),
91
- enforcement: enumeration("observing", "available", "suspended", "disabled"),
92
- reasons: array(enumeration("event-stream-gap", "persistence-unavailable", "capacity-exceeded", "model-metadata-unavailable"), 8),
93
- staleMs: count,
94
- maxSessions: count
95
- }),
96
- upserts: array(row),
97
- removed: array(object({
98
- sessionId: text,
99
- revision: count,
100
- reason: enumeration("deleted", "evicted")
101
- }))
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
102
17
  };
103
- var readSchema = object({
104
- schemaVersion: { const: 1 },
105
- sessionIds: { ...array(text), uniqueItems: true }
106
- });
107
- var batchSchema = object(batchFields);
108
- var resultSchema = object({
109
- ...batchFields,
110
- unavailable: array(object({ sessionId: text, reason: enumeration("not-monitored") }))
111
- });
112
- var monitorRpc = Rpc.define({
113
- id: "context-guard.monitor.v1",
114
- methods: { read: { input: readSchema, output: resultSchema } },
115
- events: { changed: { schema: batchSchema } }
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 }
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, {
21
+ get: all[name],
22
+ enumerable: true,
23
+ configurable: true,
24
+ set: (newValue) => all[name] = () => newValue
25
+ });
157
26
  };
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
- });
27
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
28
+
29
+ // src/rpc.ts
30
+ import { Rpc } from "@opencode/plugin/rpc";
172
31
  function isPerformanceBatch(value) {
173
32
  return valid(value, performanceBatchSchema) && uniqueRows(value);
174
33
  }
@@ -178,84 +37,6 @@ function isPerformanceResult(value) {
178
37
  function isPerformanceSnapshot(value) {
179
38
  return valid(value, performanceSchema);
180
39
  }
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
40
  function isHistoryRead(value) {
260
41
  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
42
  }
@@ -310,656 +91,908 @@ function uniqueRows(value) {
310
91
  const ids = [...value.upserts, ...value.removed].map((v) => record(v) ? v.sessionId : undefined);
311
92
  return ids.length <= 128 && new Set(ids).size === ids.length;
312
93
  }
94
+ var count, text, enumeration = (...values) => ({ type: "string", enum: values }), object = (properties, required = Object.keys(properties)) => ({
95
+ type: "object",
96
+ properties,
97
+ required,
98
+ additionalProperties: false
99
+ }), array = (items, maxItems = 128) => ({
100
+ type: "array",
101
+ items,
102
+ maxItems
103
+ }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
104
+ var init_rpc = __esm(() => {
105
+ count = {
106
+ type: "integer",
107
+ minimum: 0,
108
+ maximum: Number.MAX_SAFE_INTEGER
109
+ };
110
+ text = { type: "string", minLength: 1, maxLength: 512 };
111
+ measurement = object({
112
+ tokens: count,
113
+ at: count,
114
+ model: text,
115
+ requestId: text,
116
+ provenance: enumeration("provider-normalized", "serialized-estimate"),
117
+ confidence: enumeration("reported", "approximate"),
118
+ scope: enumeration("completed-request", "prepared-request")
119
+ }, ["tokens", "at", "model", "provenance", "confidence", "scope"]);
120
+ row = object({
121
+ sessionId: text,
122
+ revision: count,
123
+ observedAt: count,
124
+ parentId: text,
125
+ agent: text,
126
+ model: text,
127
+ measurement,
128
+ limits: object({ context: count, input: count, output: count }, [
129
+ "context",
130
+ "output"
131
+ ]),
132
+ capacityTokens: count,
133
+ outputReserveTokens: count,
134
+ peakTokens: count,
135
+ policy: object({
136
+ id: text,
137
+ configured: object({
138
+ type: enumeration("tokens", "ratio", "remaining"),
139
+ value: { type: "number", minimum: 0 }
140
+ }),
141
+ triggerTokens: { type: "number" },
142
+ comparison: enumeration(">=", ">"),
143
+ reason: enumeration("missing-usage", "model-changed", "invalid-usage", "stale-usage", "missing-model-budget", "incompatible-model-budget", "threshold-crossed", "below-threshold"),
144
+ crossed: { type: "boolean" }
145
+ }, ["id", "configured", "comparison", "reason", "crossed"]),
146
+ compaction: object({
147
+ phase: enumeration("idle", "requesting", "accepted", "running", "awaiting-measurement", "blocked", "uncertain"),
148
+ count,
149
+ armed: { type: "boolean" },
150
+ blockedReason: enumeration("session-removed", "compaction-failed", "reclaim-unverifiable", "insufficient-reclaim", "completion-event-timeout", "admission-outcome-unknown", "restart-needs-lifecycle-evidence"),
151
+ lastAttempt: count,
152
+ lastCompleted: count,
153
+ cooldownUntil: count
154
+ }, ["phase", "armed"]),
155
+ cumulative: object({
156
+ input: count,
157
+ output: count,
158
+ reasoning: count,
159
+ cache: object({ read: count, write: count })
160
+ }),
161
+ cumulativeScope: enumeration("session", "unknown"),
162
+ lastObservedActive: { type: "boolean" }
163
+ }, [
164
+ "sessionId",
165
+ "revision",
166
+ "observedAt",
167
+ "policy",
168
+ "compaction",
169
+ "cumulativeScope",
170
+ "lastObservedActive"
171
+ ]);
172
+ batchFields = {
173
+ schemaVersion: { const: 1 },
174
+ epoch: text,
175
+ sequence: count,
176
+ generatedAt: count,
177
+ health: object({
178
+ enabled: { type: "boolean" },
179
+ mode: enumeration("observe", "enforce"),
180
+ enforcement: enumeration("observing", "available", "suspended", "disabled"),
181
+ reasons: array(enumeration("event-stream-gap", "persistence-unavailable", "capacity-exceeded", "model-metadata-unavailable"), 8),
182
+ staleMs: count,
183
+ maxSessions: count
184
+ }),
185
+ upserts: array(row),
186
+ removed: array(object({
187
+ sessionId: text,
188
+ revision: count,
189
+ reason: enumeration("deleted", "evicted")
190
+ }))
191
+ };
192
+ readSchema = object({
193
+ schemaVersion: { const: 1 },
194
+ sessionIds: { ...array(text), uniqueItems: true }
195
+ });
196
+ batchSchema = object(batchFields);
197
+ resultSchema = object({
198
+ ...batchFields,
199
+ unavailable: array(object({ sessionId: text, reason: enumeration("not-monitored") }))
200
+ });
201
+ monitorRpc = Rpc.define({
202
+ id: "context-guard.monitor.v1",
203
+ methods: { read: { input: readSchema, output: resultSchema } },
204
+ events: { changed: { schema: batchSchema } }
205
+ });
206
+ nonnegative = { type: "number", minimum: 0 };
207
+ performanceSchema = object({
208
+ observedSince: count,
209
+ at: count,
210
+ sessionCreatedAt: count,
211
+ sessionOutputTokens: count,
212
+ status: enumeration("waiting", "streaming", "idle", "unavailable"),
213
+ partial: { type: "boolean" },
214
+ liveTps: nonnegative,
215
+ averageModelTps: nonnegative,
216
+ averageSessionTps: nonnegative,
217
+ ttftMs: nonnegative,
218
+ latestRequestTps: nonnegative,
219
+ requestId: text,
220
+ requestAgent: text,
221
+ requestModel: text,
222
+ requestOutputTokens: count,
223
+ requestStartedAt: count,
224
+ firstOutputAt: count,
225
+ streamEndedAt: count,
226
+ estimatedOutputTokens: nonnegative,
227
+ modelOutputTokens: nonnegative,
228
+ modelDurationMs: nonnegative,
229
+ completedRequests: count,
230
+ tokenSource: enumeration("estimated", "reported", "unavailable")
231
+ }, [
232
+ "sessionCreatedAt",
233
+ "observedSince",
234
+ "at",
235
+ "status",
236
+ "partial",
237
+ "estimatedOutputTokens",
238
+ "modelOutputTokens",
239
+ "modelDurationMs",
240
+ "completedRequests",
241
+ "tokenSource"
242
+ ]);
243
+ performanceRow = {
244
+ ...row,
245
+ properties: { ...row.properties, performance: performanceSchema }
246
+ };
247
+ performanceBatchSchema = object({
248
+ ...batchFields,
249
+ upserts: array(performanceRow)
250
+ });
251
+ performanceResultSchema = object({
252
+ ...batchFields,
253
+ upserts: array(performanceRow),
254
+ unavailable: resultSchema.properties.unavailable ?? {}
255
+ });
256
+ performanceRpc = Rpc.define({
257
+ id: "context-guard.monitor.v2",
258
+ methods: { read: { input: readSchema, output: performanceResultSchema } },
259
+ events: { changed: { schema: performanceBatchSchema } }
260
+ });
261
+ chartPointSchema = object({
262
+ time: count,
263
+ agent: text,
264
+ model: text,
265
+ breakBefore: { type: "boolean" },
266
+ partial: { type: "boolean" },
267
+ liveTps: nonnegative,
268
+ averageModelTps: nonnegative,
269
+ averageSessionTps: nonnegative,
270
+ sessionOutputTokens: count
271
+ }, ["time", "breakBefore", "partial"]);
272
+ chartRequestSchema = object({
273
+ time: count,
274
+ requestId: text,
275
+ started: count,
276
+ agent: text,
277
+ model: text,
278
+ ttftMs: nonnegative,
279
+ outputTokens: count,
280
+ durationMs: nonnegative,
281
+ tps: nonnegative,
282
+ partial: { type: "boolean" }
283
+ }, ["time", "requestId", "started", "partial"]);
284
+ historyReadSchema = object({
285
+ schemaVersion: { const: 1 },
286
+ sessionId: text,
287
+ from: count,
288
+ to: count,
289
+ maxPoints: { type: "integer", minimum: 16, maximum: 500 }
290
+ });
291
+ historyResultSchema = object({
292
+ schemaVersion: { const: 1 },
293
+ generatedAt: count,
294
+ enabled: { type: "boolean" },
295
+ truncated: { type: "boolean" },
296
+ metadata: object({
297
+ schema: { const: 1 },
298
+ retainedFrom: count,
299
+ retainedBytes: count,
300
+ skippedBytes: count,
301
+ malformedRecords: count,
302
+ completeness: enumeration("bounded-retention")
303
+ }, [
304
+ "schema",
305
+ "retainedBytes",
306
+ "skippedBytes",
307
+ "malformedRecords",
308
+ "completeness"
309
+ ]),
310
+ data: object({
311
+ schema: { const: 1 },
312
+ omittedSessions: count,
313
+ omittedGaps: count,
314
+ maxPoints: count,
315
+ gaps: array(object({ time: count, sessionId: text, reason: text }), 128),
316
+ series: array(object({
317
+ sessionId: text,
318
+ parentId: text,
319
+ agents: array(text, 1024),
320
+ models: array(text, 1024),
321
+ points: array(chartPointSchema, 500),
322
+ requests: array(chartRequestSchema, 500),
323
+ originalPoints: count
324
+ }, [
325
+ "sessionId",
326
+ "agents",
327
+ "models",
328
+ "points",
329
+ "requests",
330
+ "originalPoints"
331
+ ]), 1)
332
+ }, ["schema", "omittedSessions", "maxPoints", "gaps", "series"])
333
+ });
334
+ historyRpc = Rpc.define({
335
+ id: "context-guard.history.v1",
336
+ methods: { read: { input: historyReadSchema, output: historyResultSchema } },
337
+ events: {}
338
+ });
339
+ });
313
340
 
314
- // src/index.ts
315
- import { Plugin } from "@opencode/plugin";
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;
341
+ // src/history.ts
342
+ import { mkdir, readdir, stat, open, unlink } from "fs/promises";
343
+ import { join } from "path";
344
+ async function segments(directory) {
345
+ const names = await readdir(directory).catch((error) => {
346
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
347
+ return [];
348
+ throw error;
349
+ });
350
+ return (await Promise.all(names.filter((n) => filename.test(n)).map(async (name) => ({
351
+ name,
352
+ time: Number(filename.exec(name)?.[1]),
353
+ bytes: (await stat(join(directory, name))).size
354
+ })))).sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
323
355
  }
324
356
 
325
- class PerformanceTracker {
326
- maxSessions;
327
- windowMs;
357
+ class PerformanceHistory {
358
+ path;
359
+ config;
328
360
  now;
329
- rows = new Map;
330
- constructor(maxSessions, windowMs = 2000, now = Date.now) {
331
- this.maxSessions = maxSessions;
332
- this.windowMs = windowMs;
361
+ report;
362
+ pending = [];
363
+ working;
364
+ active;
365
+ bytes = 0;
366
+ started = 0;
367
+ disabled = false;
368
+ dropped = 0;
369
+ droppedTotal = 0;
370
+ constructor(path, config, now = Date.now, report = () => {
371
+ }) {
372
+ this.path = path;
373
+ this.config = config;
333
374
  this.now = now;
375
+ this.report = report;
334
376
  }
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)
377
+ emit(record2) {
378
+ if (!this.config.enabled || this.disabled)
369
379
  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))
380
+ const line = JSON.stringify({
381
+ schema: 1,
382
+ recordId: record2.recordId,
383
+ time: record2.time,
384
+ type: record2.type,
385
+ sessionId: record2.sessionId,
386
+ ...record2.parentId ? { parentId: record2.parentId } : {},
387
+ ...record2.agent ? { agent: record2.agent } : {},
388
+ ...record2.model ? { model: record2.model } : {},
389
+ ...record2.event ? { event: record2.event } : {},
390
+ ...record2.droppedRecords === undefined ? {} : { droppedRecords: record2.droppedRecords },
391
+ ...record2.performance ? { performance: scalarPerformance(record2.performance) } : {}
392
+ }) + "\n";
393
+ if (this.pending.length >= 256 || Buffer.byteLength(line) > Math.min(4 * 1024 * 1024, this.config.maxBytes)) {
394
+ this.dropped++;
395
+ this.droppedTotal++;
396
+ if (this.dropped === 1)
397
+ this.report("history-records-dropped");
386
398
  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
399
  }
400
+ this.pending.push(line);
401
+ this.kick();
398
402
  }
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;
403
+ kick() {
404
+ if (this.working || this.disabled)
405
405
  return;
406
- }
407
- r.ended = at;
406
+ this.working = this.drain().catch(() => {
407
+ this.disabled = true;
408
+ this.pending = [];
409
+ this.report("history-unavailable");
410
+ }).finally(() => {
411
+ this.working = undefined;
412
+ if (this.pending.length)
413
+ this.kick();
414
+ });
408
415
  }
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
+ async prune(incoming = 0) {
417
+ const files = await segments(this.path);
418
+ let total = files.reduce((n, f) => n + f.bytes, 0);
419
+ let truncated = false;
420
+ for (const file of files) {
421
+ if (file.time >= this.now() - this.config.retentionDays * 86400000 && total + incoming <= this.config.maxBytes)
422
+ break;
423
+ await unlink(join(this.path, file.name));
424
+ if (file.name === this.active) {
425
+ this.active = undefined;
426
+ this.bytes = 0;
427
+ }
428
+ total -= file.bytes;
429
+ truncated = true;
416
430
  }
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();
431
+ if (truncated)
432
+ this.report("history-truncated");
439
433
  }
440
- gap(id) {
441
- for (const [key, row2] of this.rows) {
442
- if (id !== undefined && key !== id)
434
+ gapLine(event, droppedRecords) {
435
+ return JSON.stringify({
436
+ schema: 1,
437
+ recordId: crypto.randomUUID(),
438
+ time: this.now(),
439
+ type: "gap",
440
+ sessionId: "__history__",
441
+ event,
442
+ ...droppedRecords === undefined ? {} : { droppedRecords }
443
+ }) + "\n";
444
+ }
445
+ async drain() {
446
+ await mkdir(this.path, { recursive: true, mode: 448 });
447
+ await this.prune();
448
+ while (this.pending.length) {
449
+ let line = this.pending.shift();
450
+ if (!line)
443
451
  continue;
444
- row2.partial = true;
445
- delete row2.request;
452
+ if (this.dropped > 0) {
453
+ this.pending.unshift(line);
454
+ line = this.gapLine("history-records-dropped", this.dropped);
455
+ this.dropped = 0;
456
+ }
457
+ const size = Buffer.byteLength(line);
458
+ const now = this.now();
459
+ if (!this.active || this.bytes + size > Math.min(4 * 1024 * 1024, this.config.maxBytes) || now - this.started >= 3600000) {
460
+ this.active = undefined;
461
+ this.bytes = 0;
462
+ await this.prune(size);
463
+ this.started = now;
464
+ this.active = `${now}-${crypto.randomUUID()}.jsonl`;
465
+ } else
466
+ await this.prune(size);
467
+ if (!this.active) {
468
+ this.started = now;
469
+ this.active = `${now}-${crypto.randomUUID()}.jsonl`;
470
+ }
471
+ const file = await open(join(this.path, this.active), "a", 384);
472
+ try {
473
+ await file.writeFile(line);
474
+ } finally {
475
+ await file.close();
476
+ }
477
+ this.bytes += size;
446
478
  }
447
479
  }
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
- };
480
+ health() {
481
+ return { unavailable: this.disabled, droppedRecords: this.droppedTotal };
492
482
  }
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
- }));
483
+ async flush() {
484
+ while (this.working)
485
+ await this.working;
503
486
  }
504
- restore(value) {
505
- if (!Array.isArray(value))
487
+ async initialize() {
488
+ if (!this.config.enabled)
506
489
  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();
490
+ await mkdir(this.path, { recursive: true, mode: 448 }).then(() => this.prune()).catch(() => {
491
+ this.disabled = true;
492
+ this.report("history-unavailable");
493
+ });
532
494
  }
533
495
  }
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)
496
+ function scalarPerformance(p) {
497
+ return {
498
+ observedSince: p.observedSince,
499
+ sessionCreatedAt: p.sessionCreatedAt,
500
+ at: p.at,
501
+ status: p.status,
502
+ partial: p.partial,
503
+ estimatedOutputTokens: p.estimatedOutputTokens,
504
+ modelOutputTokens: p.modelOutputTokens,
505
+ modelDurationMs: p.modelDurationMs,
506
+ completedRequests: p.completedRequests,
507
+ tokenSource: p.tokenSource,
508
+ ...Object.fromEntries([
509
+ "sessionOutputTokens",
510
+ "liveTps",
511
+ "averageModelTps",
512
+ "averageSessionTps",
513
+ "ttftMs",
514
+ "latestRequestTps",
515
+ "requestId",
516
+ "requestAgent",
517
+ "requestModel",
518
+ "requestOutputTokens",
519
+ "requestStartedAt",
520
+ "firstOutputAt",
521
+ "streamEndedAt"
522
+ ].flatMap((key) => {
523
+ const value = p[key];
524
+ return value === undefined ? [] : [[key, value]];
525
+ }))
526
+ };
527
+ }
528
+ async function readPerformanceHistory(directory, filter = {}) {
529
+ const records = [];
530
+ const files = await segments(directory);
531
+ const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
532
+ let bytes = 0, skippedBytes = 0, malformedRecords = 0;
533
+ const selected = [];
534
+ for (const file of files.slice().reverse()) {
535
+ if (bytes + file.bytes > maxBytes) {
536
+ skippedBytes += file.bytes;
537
+ continue;
538
+ }
539
+ bytes += file.bytes;
540
+ selected.unshift(file);
541
+ }
542
+ for (const file of selected) {
543
+ const handle = await open(join(directory, file.name), "r");
544
+ let source;
545
+ try {
546
+ const buffer = Buffer.alloc(file.bytes);
547
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
548
+ source = buffer.subarray(0, bytesRead).toString("utf8");
549
+ } finally {
550
+ await handle.close();
551
+ }
552
+ for (const line of source.split("\n")) {
553
+ if (!line)
554
+ continue;
555
+ try {
556
+ const value = JSON.parse(line);
557
+ if (!isHistoryRecord(value)) {
558
+ malformedRecords++;
558
559
  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;
560
+ }
561
+ 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)
562
+ continue;
563
+ records.push(value);
564
+ } catch {
565
+ malformedRecords++;
563
566
  }
564
- if (low !== undefined)
565
- selected.add(low);
566
- if (high !== undefined)
567
- selected.add(high);
568
567
  }
569
568
  }
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 {
569
+ const metadata = {
687
570
  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
571
+ ...files[0] ? { retainedFrom: files[0].time } : {},
572
+ retainedBytes: files.reduce((n, f) => n + f.bytes, 0),
573
+ skippedBytes,
574
+ malformedRecords,
575
+ completeness: "bounded-retention"
697
576
  };
577
+ return { metadata, records };
578
+ }
579
+ function isRecord(value) {
580
+ return value !== null && typeof value === "object" && !Array.isArray(value);
581
+ }
582
+ function isHistoryRecord(value) {
583
+ if (!isRecord(value))
584
+ return false;
585
+ const r = value;
586
+ const allowed = new Set([
587
+ "schema",
588
+ "recordId",
589
+ "time",
590
+ "type",
591
+ "sessionId",
592
+ "parentId",
593
+ "agent",
594
+ "model",
595
+ "event",
596
+ "performance",
597
+ "droppedRecords"
598
+ ]);
599
+ 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);
698
600
  }
601
+ var filename;
602
+ var init_history = __esm(() => {
603
+ init_rpc();
604
+ filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
605
+ });
699
606
 
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));
607
+ // src/index.ts
608
+ import { Plugin } from "@opencode/plugin";
609
+
610
+ // src/performance.ts
611
+ var valid2 = (n) => Number.isFinite(n) && n >= 0;
612
+ var rate = (tokens, ms) => valid2(tokens) && ms > 0 ? tokens * 1000 / ms : undefined;
613
+ function outputTokens(tokens) {
614
+ const n = tokens.output + tokens.reasoning;
615
+ return Number.isSafeInteger(tokens.output) && tokens.output >= 0 && Number.isSafeInteger(tokens.reasoning) && tokens.reasoning >= 0 && Number.isSafeInteger(n) ? n : undefined;
715
616
  }
716
617
 
717
- class PerformanceHistory {
718
- path;
719
- config;
618
+ class PerformanceTracker {
619
+ maxSessions;
620
+ windowMs;
720
621
  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;
622
+ rows = new Map;
623
+ constructor(maxSessions, windowMs = 2000, now = Date.now) {
624
+ this.maxSessions = maxSessions;
625
+ this.windowMs = windowMs;
734
626
  this.now = now;
735
- this.report = report;
736
627
  }
737
- emit(record2) {
738
- if (!this.config.enabled || this.disabled)
628
+ observe(id, created, cumulative) {
629
+ let row2 = this.rows.get(id);
630
+ if (!row2) {
631
+ if (this.rows.size >= this.maxSessions)
632
+ return;
633
+ row2 = {
634
+ observedSince: this.now(),
635
+ created,
636
+ partial: cumulative !== 0,
637
+ output: 0,
638
+ duration: 0,
639
+ completed: 0,
640
+ sequence: -1
641
+ };
642
+ this.rows.set(id, row2);
643
+ }
644
+ if (valid2(created))
645
+ row2.created = created;
646
+ if (cumulative !== undefined && valid2(cumulative))
647
+ row2.cumulative = cumulative;
648
+ }
649
+ accept(id, sequence) {
650
+ const row2 = this.rows.get(id);
651
+ if (!row2 || sequence <= row2.sequence)
652
+ return false;
653
+ row2.sequence = sequence;
654
+ return true;
655
+ }
656
+ start(id, requestId, agent, model, started) {
657
+ const row2 = this.rows.get(id);
658
+ if (!row2 || !valid2(started))
739
659
  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");
660
+ const old = row2.request;
661
+ if (old?.id === requestId && old.started === started)
662
+ return;
663
+ if (old && !old.settled)
664
+ row2.partial = true;
665
+ row2.request = {
666
+ id: requestId,
667
+ agent,
668
+ model,
669
+ started,
670
+ chars: 0,
671
+ settled: false,
672
+ events: new Set,
673
+ buckets: new Map
674
+ };
675
+ }
676
+ delta(id, requestId, eventId, at, characters) {
677
+ const row2 = this.rows.get(id), r = row2?.request;
678
+ if (!row2 || !r || r.id !== requestId || r.settled || r.ended !== undefined || !valid2(at) || at < r.started || !valid2(characters) || !characters || r.events.has(eventId))
758
679
  return;
680
+ if (r.events.size >= 256)
681
+ r.events.delete(r.events.values().next().value ?? "");
682
+ r.events.add(eventId);
683
+ r.first = r.first === undefined ? at : Math.min(r.first, at);
684
+ r.chars += characters;
685
+ const bucket = Math.floor(at / 100) * 100;
686
+ r.buckets.set(bucket, (r.buckets.get(bucket) ?? 0) + characters);
687
+ this.prune(r, Math.max(at, this.now()));
688
+ while (r.buckets.size > Math.ceil(this.windowMs / 100) + 2) {
689
+ r.buckets.delete(Math.min(...r.buckets.keys()));
759
690
  }
760
- this.pending.push(line);
761
- this.kick();
762
691
  }
763
- kick() {
764
- if (this.working || this.disabled)
692
+ streamed(id, requestId, at) {
693
+ const row2 = this.rows.get(id), r = row2?.request;
694
+ if (!row2 || !r || r.id !== requestId || r.settled || r.ended !== undefined)
765
695
  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
- });
696
+ if (!valid2(at) || at <= r.started || r.first !== undefined && at < r.first) {
697
+ row2.partial = true;
698
+ return;
699
+ }
700
+ r.ended = at;
775
701
  }
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;
702
+ settle(id, requestId, tokens) {
703
+ const row2 = this.rows.get(id), r = row2?.request;
704
+ if (!row2)
705
+ return;
706
+ if (!r || r.id !== requestId) {
707
+ row2.partial = true;
708
+ return;
790
709
  }
791
- if (truncated)
792
- this.report("history-truncated");
710
+ if (r.settled)
711
+ return;
712
+ r.settled = true;
713
+ if (tokens !== undefined && valid2(tokens))
714
+ r.output = tokens;
715
+ if (tokens !== undefined && valid2(tokens) && r.ended !== undefined) {
716
+ row2.output += tokens;
717
+ row2.duration += r.ended - r.started;
718
+ row2.completed++;
719
+ } else
720
+ row2.partial = true;
721
+ const snapshot = this.snapshot(id);
722
+ if (snapshot) {
723
+ const { latestRequestTps: _previousRate, ...current } = snapshot;
724
+ row2.latest = {
725
+ ...current,
726
+ tokenSource: tokens === undefined ? "unavailable" : "reported",
727
+ ...tokens !== undefined && r.ended !== undefined ? { latestRequestTps: tokens * 1000 / (r.ended - r.started) } : {}
728
+ };
729
+ }
730
+ r.events.clear();
731
+ r.buckets.clear();
793
732
  }
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";
733
+ gap(id) {
734
+ for (const [key, row2] of this.rows) {
735
+ if (id !== undefined && key !== id)
736
+ continue;
737
+ row2.partial = true;
738
+ delete row2.request;
739
+ }
804
740
  }
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)
741
+ prune(r, at) {
742
+ for (const time of r.buckets.keys())
743
+ if (time < at - this.windowMs)
744
+ r.buckets.delete(time);
745
+ }
746
+ snapshot(id) {
747
+ const row2 = this.rows.get(id);
748
+ if (!row2)
749
+ return;
750
+ const at = this.now(), r = row2.request;
751
+ const active = r && !r.settled && r.ended === undefined;
752
+ if (r)
753
+ this.prune(r, at);
754
+ const live = active ? rate([...r.buckets.values()].reduce((a, b) => a + b, 0) / 4, Math.min(this.windowMs, at - r.started)) : 0;
755
+ const averageModelTps = rate(row2.output, row2.duration);
756
+ const averageSessionTps = row2.cumulative === undefined ? undefined : rate(row2.cumulative, at - row2.created);
757
+ return {
758
+ observedSince: row2.observedSince,
759
+ at,
760
+ sessionCreatedAt: row2.created,
761
+ ...row2.cumulative === undefined ? {} : { sessionOutputTokens: row2.cumulative },
762
+ status: active ? r.first === undefined ? "waiting" : "streaming" : !r && row2.partial ? "unavailable" : "idle",
763
+ partial: row2.partial,
764
+ ...live === undefined || !r && row2.partial ? {} : { liveTps: live },
765
+ ...averageModelTps === undefined ? {} : { averageModelTps },
766
+ ...averageSessionTps === undefined ? {} : { averageSessionTps },
767
+ ...r ? {
768
+ requestId: r.id,
769
+ requestAgent: r.agent,
770
+ requestModel: r.model,
771
+ requestStartedAt: r.started,
772
+ ...r.output === undefined ? {} : { requestOutputTokens: r.output },
773
+ ...r.first === undefined ? {} : { firstOutputAt: r.first, ttftMs: r.first - r.started },
774
+ ...r.ended === undefined ? {} : { streamEndedAt: r.ended }
775
+ } : row2.latest ? {
776
+ ...row2.latest.ttftMs === undefined ? {} : { ttftMs: row2.latest.ttftMs }
777
+ } : {},
778
+ ...row2.latest?.latestRequestTps === undefined ? {} : { latestRequestTps: row2.latest.latestRequestTps },
779
+ estimatedOutputTokens: r ? r.chars / 4 : 0,
780
+ modelOutputTokens: row2.output,
781
+ modelDurationMs: row2.duration,
782
+ completedRequests: row2.completed,
783
+ tokenSource: r && !r.settled ? "estimated" : row2.latest?.tokenSource ?? "unavailable"
784
+ };
785
+ }
786
+ checkpoint() {
787
+ return [...this.rows].map(([sessionId, r]) => ({
788
+ sessionId,
789
+ observedSince: r.observedSince,
790
+ created: r.created,
791
+ output: r.output,
792
+ duration: r.duration,
793
+ completed: r.completed,
794
+ sequence: r.sequence
795
+ }));
796
+ }
797
+ restore(value) {
798
+ if (!Array.isArray(value))
799
+ return;
800
+ for (const item of value.slice(0, this.maxSessions)) {
801
+ if (!item || typeof item !== "object")
811
802
  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();
803
+ const r = item;
804
+ 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)
805
+ continue;
806
+ const { observedSince, created, output, duration, completed, sequence } = r;
807
+ if (typeof observedSince !== "number" || typeof created !== "number" || typeof output !== "number" || typeof duration !== "number" || typeof completed !== "number")
808
+ continue;
809
+ this.rows.set(r.sessionId, {
810
+ observedSince,
811
+ created,
812
+ output,
813
+ duration,
814
+ completed,
815
+ sequence,
816
+ partial: true
817
+ });
818
+ }
819
+ }
820
+ remove(id) {
821
+ this.rows.delete(id);
822
+ }
823
+ ids() {
824
+ return this.rows.keys();
825
+ }
826
+ }
827
+
828
+ // src/charts.ts
829
+ var rateFields = [
830
+ "liveTps",
831
+ "averageModelTps",
832
+ "averageSessionTps",
833
+ "sessionOutputTokens"
834
+ ];
835
+ function downsamplePoints(points, limit) {
836
+ if (!Number.isSafeInteger(limit) || limit < 16 || limit > 5000)
837
+ throw new Error("maxPoints must be an integer from 16 to 5000");
838
+ if (points.length <= limit)
839
+ return [...points];
840
+ const buckets = Math.max(1, Math.floor(limit / (2 + rateFields.length * 2)));
841
+ const selected = new Set;
842
+ for (let b = 0;b < buckets; b++) {
843
+ const start = Math.floor(b * points.length / buckets), end = Math.floor((b + 1) * points.length / buckets);
844
+ selected.add(start);
845
+ selected.add(end - 1);
846
+ for (const field of rateFields) {
847
+ let low, high;
848
+ for (let i = start;i < end; i++) {
849
+ const value = points[i]?.[field];
850
+ if (value === undefined)
851
+ continue;
852
+ if (low === undefined || value < (points[low]?.[field] ?? Infinity))
853
+ low = i;
854
+ if (high === undefined || value > (points[high]?.[field] ?? -Infinity))
855
+ high = i;
836
856
  }
837
- this.bytes += size;
857
+ if (low !== undefined)
858
+ selected.add(low);
859
+ if (high !== undefined)
860
+ selected.add(high);
838
861
  }
839
862
  }
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
- };
863
+ let previous = -1;
864
+ return [...selected].sort((a, b) => a - b).flatMap((index) => {
865
+ const point = points[index];
866
+ if (!point)
867
+ return [];
868
+ const skippedGap = points.slice(previous + 1, index + 1).some((p) => p.breakBefore);
869
+ previous = index;
870
+ return [{ ...point, breakBefore: point.breakBefore || skippedGap }];
871
+ });
887
872
  }
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;
873
+ function buildChartData(records, options = {}) {
874
+ const maxPoints = options.maxPoints ?? 1000, maxSessions = options.maxSessions ?? 128;
875
+ if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 1024)
876
+ throw new Error("maxSessions must be from 1 to 1024");
877
+ downsamplePoints([], maxPoints);
878
+ const sorted = [...records].sort((a, b) => a.time - b.time || a.recordId.localeCompare(b.recordId));
879
+ const lastSeen = new Map;
880
+ for (const r of sorted)
881
+ if (r.performance && (!options.sessionId || r.sessionId === options.sessionId))
882
+ lastSeen.set(r.sessionId, r.time);
883
+ const sessions = [...lastSeen].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
884
+ const selected = new Set(sessions.slice(0, maxSessions).map(([id]) => id));
885
+ const series = new Map, gaps = [];
886
+ const seen = new Set, requestSeen = new Set;
887
+ const pendingGaps = new Set;
888
+ const observedSince = new Map;
889
+ for (const r of sorted) {
890
+ if (seen.has(r.recordId))
891
+ continue;
892
+ seen.add(r.recordId);
893
+ if (r.type === "gap") {
894
+ if (r.sessionId === "__history__" || selected.has(r.sessionId)) {
895
+ gaps.push({
896
+ time: r.time,
897
+ sessionId: r.sessionId,
898
+ reason: r.event ?? "observation-gap"
899
+ });
900
+ if (r.sessionId === "__history__")
901
+ for (const id of selected)
902
+ pendingGaps.add(id);
903
+ else
904
+ pendingGaps.add(r.sessionId);
905
+ }
897
906
  continue;
898
907
  }
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();
908
+ const p = r.performance;
909
+ if (!p || !selected.has(r.sessionId))
910
+ continue;
911
+ let s = series.get(r.sessionId);
912
+ if (!s) {
913
+ s = {
914
+ sessionId: r.sessionId,
915
+ ...r.parentId ? { parentId: r.parentId } : {},
916
+ agents: [],
917
+ models: [],
918
+ points: [],
919
+ requests: [],
920
+ originalPoints: 0
921
+ };
922
+ series.set(r.sessionId, s);
911
923
  }
912
- for (const line of source.split("\n")) {
913
- if (!line)
924
+ const agent = p.requestAgent ?? r.agent, model = p.requestModel ?? r.model;
925
+ if (agent && !s.agents.includes(agent))
926
+ s.agents.push(agent);
927
+ if (model && !s.models.includes(model))
928
+ s.models.push(model);
929
+ const previous = s.points.at(-1);
930
+ const restart = observedSince.has(r.sessionId) && observedSince.get(r.sessionId) !== p.observedSince;
931
+ const reset = previous?.sessionOutputTokens !== undefined && p.sessionOutputTokens !== undefined && p.sessionOutputTokens < previous.sessionOutputTokens;
932
+ 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;
933
+ const point = {
934
+ time: r.time,
935
+ ...agent ? { agent } : {},
936
+ ...model ? { model } : {},
937
+ breakBefore: discontinuity,
938
+ partial: p.partial,
939
+ ...p.liveTps === undefined ? {} : { liveTps: p.liveTps },
940
+ ...p.averageModelTps === undefined ? {} : { averageModelTps: p.averageModelTps },
941
+ ...p.averageSessionTps === undefined ? {} : { averageSessionTps: p.averageSessionTps },
942
+ ...p.sessionOutputTokens === undefined ? {} : { sessionOutputTokens: p.sessionOutputTokens }
943
+ };
944
+ if (previous?.time === point.time)
945
+ s.points[s.points.length - 1] = {
946
+ ...point,
947
+ breakBefore: point.breakBefore || previous.breakBefore
948
+ };
949
+ else
950
+ s.points.push(point);
951
+ observedSince.set(r.sessionId, p.observedSince);
952
+ pendingGaps.delete(r.sessionId);
953
+ if (r.type === "request" && p.requestId !== undefined && p.requestStartedAt !== undefined) {
954
+ const key = JSON.stringify([
955
+ r.sessionId,
956
+ p.requestId,
957
+ p.requestStartedAt
958
+ ]);
959
+ if (requestSeen.has(key))
914
960
  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
- }
961
+ requestSeen.add(key);
962
+ const duration = p.streamEndedAt === undefined ? undefined : p.streamEndedAt - p.requestStartedAt;
963
+ const validDuration = duration !== undefined && duration > 0;
964
+ const output = p.tokenSource === "reported" ? p.requestOutputTokens : undefined;
965
+ s.requests.push({
966
+ time: r.time,
967
+ requestId: p.requestId,
968
+ started: p.requestStartedAt,
969
+ ...agent ? { agent } : {},
970
+ ...model ? { model } : {},
971
+ partial: p.partial,
972
+ ...p.ttftMs === undefined ? {} : { ttftMs: p.ttftMs },
973
+ ...output === undefined ? {} : { outputTokens: output },
974
+ ...validDuration ? { durationMs: duration } : {},
975
+ ...output !== undefined && validDuration ? { tps: output * 1000 / duration } : {}
976
+ });
927
977
  }
928
978
  }
929
- const metadata = {
979
+ return {
930
980
  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"
981
+ series: [...series.values()].map((s) => ({
982
+ ...s,
983
+ originalPoints: s.points.length,
984
+ points: downsamplePoints(s.points, maxPoints)
985
+ })),
986
+ gaps: gaps.slice(-1024),
987
+ omittedGaps: Math.max(0, gaps.length - 1024),
988
+ omittedSessions: Math.max(0, sessions.length - selected.size),
989
+ maxPoints
936
990
  };
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
991
  }
961
992
 
962
993
  // src/historyserver.ts
994
+ init_history();
995
+ init_rpc();
963
996
  async function serveHistory(rpc, path, enabled, now = Date.now, authorize = async () => true) {
964
997
  if (!rpc)
965
998
  return () => {
@@ -1023,6 +1056,9 @@ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = asyn
1023
1056
  };
1024
1057
  }
1025
1058
 
1059
+ // src/adapter.ts
1060
+ init_history();
1061
+
1026
1062
  // src/policy.ts
1027
1063
  function glob(pattern, text2) {
1028
1064
  let p = 0, t = 0, star = -1, retry = 0;
@@ -1330,6 +1366,7 @@ function freezeSnapshot(row2) {
1330
1366
  }
1331
1367
 
1332
1368
  // src/snapshotserver.ts
1369
+ init_rpc();
1333
1370
  async function serveSnapshots(rpc, store, performance = false, prepareRead) {
1334
1371
  if (!rpc)
1335
1372
  return () => {