@cesarus777/opencode-cgp 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,176 +1,34 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
3
29
 
4
30
  // src/rpc.ts
5
31
  import { Rpc } from "@opencode/plugin/rpc";
6
- var count = {
7
- type: "integer",
8
- minimum: 0,
9
- maximum: Number.MAX_SAFE_INTEGER
10
- };
11
- var text = { type: "string", minLength: 1, maxLength: 512 };
12
- var enumeration = (...values) => ({ type: "string", enum: values });
13
- var object = (properties, required = Object.keys(properties)) => ({
14
- type: "object",
15
- properties,
16
- required,
17
- additionalProperties: false
18
- });
19
- var array = (items, maxItems = 128) => ({
20
- type: "array",
21
- items,
22
- maxItems
23
- });
24
- var measurement = object({
25
- tokens: count,
26
- at: count,
27
- model: text,
28
- requestId: text,
29
- provenance: enumeration("provider-normalized", "serialized-estimate"),
30
- confidence: enumeration("reported", "approximate"),
31
- scope: enumeration("completed-request", "prepared-request")
32
- }, ["tokens", "at", "model", "provenance", "confidence", "scope"]);
33
- var row = object({
34
- sessionId: text,
35
- revision: count,
36
- observedAt: count,
37
- parentId: text,
38
- agent: text,
39
- model: text,
40
- measurement,
41
- limits: object({ context: count, input: count, output: count }, [
42
- "context",
43
- "output"
44
- ]),
45
- capacityTokens: count,
46
- outputReserveTokens: count,
47
- peakTokens: count,
48
- policy: object({
49
- id: text,
50
- configured: object({
51
- type: enumeration("tokens", "ratio", "remaining"),
52
- value: { type: "number", minimum: 0 }
53
- }),
54
- triggerTokens: { type: "number" },
55
- comparison: enumeration(">=", ">"),
56
- reason: enumeration("missing-usage", "model-changed", "invalid-usage", "stale-usage", "missing-model-budget", "incompatible-model-budget", "threshold-crossed", "below-threshold"),
57
- crossed: { type: "boolean" }
58
- }, ["id", "configured", "comparison", "reason", "crossed"]),
59
- compaction: object({
60
- phase: enumeration("idle", "requesting", "accepted", "running", "awaiting-measurement", "blocked", "uncertain"),
61
- count,
62
- armed: { type: "boolean" },
63
- blockedReason: enumeration("session-removed", "compaction-failed", "reclaim-unverifiable", "insufficient-reclaim", "completion-event-timeout", "admission-outcome-unknown", "restart-needs-lifecycle-evidence"),
64
- lastAttempt: count,
65
- lastCompleted: count,
66
- cooldownUntil: count
67
- }, ["phase", "armed"]),
68
- cumulative: object({
69
- input: count,
70
- output: count,
71
- reasoning: count,
72
- cache: object({ read: count, write: count })
73
- }),
74
- cumulativeScope: enumeration("session", "unknown"),
75
- lastObservedActive: { type: "boolean" }
76
- }, [
77
- "sessionId",
78
- "revision",
79
- "observedAt",
80
- "policy",
81
- "compaction",
82
- "cumulativeScope",
83
- "lastObservedActive"
84
- ]);
85
- var batchFields = {
86
- schemaVersion: { const: 1 },
87
- epoch: text,
88
- sequence: count,
89
- generatedAt: count,
90
- health: object({
91
- enabled: { type: "boolean" },
92
- mode: enumeration("observe", "enforce"),
93
- enforcement: enumeration("observing", "available", "suspended", "disabled"),
94
- reasons: array(enumeration("event-stream-gap", "persistence-unavailable", "capacity-exceeded", "model-metadata-unavailable"), 8),
95
- staleMs: count,
96
- maxSessions: count
97
- }),
98
- upserts: array(row),
99
- removed: array(object({
100
- sessionId: text,
101
- revision: count,
102
- reason: enumeration("deleted", "evicted")
103
- }))
104
- };
105
- var readSchema = object({
106
- schemaVersion: { const: 1 },
107
- sessionIds: { ...array(text), uniqueItems: true }
108
- });
109
- var batchSchema = object(batchFields);
110
- var resultSchema = object({
111
- ...batchFields,
112
- unavailable: array(object({ sessionId: text, reason: enumeration("not-monitored") }))
113
- });
114
- var monitorRpc = Rpc.define({
115
- id: "context-guard.monitor.v1",
116
- methods: { read: { input: readSchema, output: resultSchema } },
117
- events: { changed: { schema: batchSchema } }
118
- });
119
- var nonnegative = { type: "number", minimum: 0 };
120
- var performanceSchema = object({
121
- observedSince: count,
122
- at: count,
123
- sessionCreatedAt: count,
124
- sessionOutputTokens: count,
125
- status: enumeration("waiting", "streaming", "idle", "unavailable"),
126
- partial: { type: "boolean" },
127
- liveTps: nonnegative,
128
- averageModelTps: nonnegative,
129
- averageSessionTps: nonnegative,
130
- ttftMs: nonnegative,
131
- latestRequestTps: nonnegative,
132
- requestId: text,
133
- requestAgent: text,
134
- requestModel: text,
135
- requestOutputTokens: count,
136
- requestStartedAt: count,
137
- firstOutputAt: count,
138
- streamEndedAt: count,
139
- estimatedOutputTokens: nonnegative,
140
- modelOutputTokens: nonnegative,
141
- modelDurationMs: nonnegative,
142
- completedRequests: count,
143
- tokenSource: enumeration("estimated", "reported", "unavailable")
144
- }, [
145
- "sessionCreatedAt",
146
- "observedSince",
147
- "at",
148
- "status",
149
- "partial",
150
- "estimatedOutputTokens",
151
- "modelOutputTokens",
152
- "modelDurationMs",
153
- "completedRequests",
154
- "tokenSource"
155
- ]);
156
- var performanceRow = {
157
- ...row,
158
- properties: { ...row.properties, performance: performanceSchema }
159
- };
160
- var performanceBatchSchema = object({
161
- ...batchFields,
162
- upserts: array(performanceRow)
163
- });
164
- var performanceResultSchema = object({
165
- ...batchFields,
166
- upserts: array(performanceRow),
167
- unavailable: resultSchema.properties.unavailable ?? {}
168
- });
169
- var performanceRpc = Rpc.define({
170
- id: "context-guard.monitor.v2",
171
- methods: { read: { input: readSchema, output: performanceResultSchema } },
172
- events: { changed: { schema: performanceBatchSchema } }
173
- });
174
32
  function isPerformanceBatch(value) {
175
33
  return valid(value, performanceBatchSchema) && uniqueRows(value);
176
34
  }
@@ -180,84 +38,6 @@ function isPerformanceResult(value) {
180
38
  function isPerformanceSnapshot(value) {
181
39
  return valid(value, performanceSchema);
182
40
  }
183
- var chartPointSchema = object({
184
- time: count,
185
- agent: text,
186
- model: text,
187
- breakBefore: { type: "boolean" },
188
- partial: { type: "boolean" },
189
- liveTps: nonnegative,
190
- averageModelTps: nonnegative,
191
- averageSessionTps: nonnegative,
192
- sessionOutputTokens: count
193
- }, ["time", "breakBefore", "partial"]);
194
- var chartRequestSchema = object({
195
- time: count,
196
- requestId: text,
197
- started: count,
198
- agent: text,
199
- model: text,
200
- ttftMs: nonnegative,
201
- outputTokens: count,
202
- durationMs: nonnegative,
203
- tps: nonnegative,
204
- partial: { type: "boolean" }
205
- }, ["time", "requestId", "started", "partial"]);
206
- var historyReadSchema = object({
207
- schemaVersion: { const: 1 },
208
- sessionId: text,
209
- from: count,
210
- to: count,
211
- maxPoints: { type: "integer", minimum: 16, maximum: 500 }
212
- });
213
- var historyResultSchema = object({
214
- schemaVersion: { const: 1 },
215
- generatedAt: count,
216
- enabled: { type: "boolean" },
217
- truncated: { type: "boolean" },
218
- metadata: object({
219
- schema: { const: 1 },
220
- retainedFrom: count,
221
- retainedBytes: count,
222
- skippedBytes: count,
223
- malformedRecords: count,
224
- completeness: enumeration("bounded-retention")
225
- }, [
226
- "schema",
227
- "retainedBytes",
228
- "skippedBytes",
229
- "malformedRecords",
230
- "completeness"
231
- ]),
232
- data: object({
233
- schema: { const: 1 },
234
- omittedSessions: count,
235
- omittedGaps: count,
236
- maxPoints: count,
237
- gaps: array(object({ time: count, sessionId: text, reason: text }), 128),
238
- series: array(object({
239
- sessionId: text,
240
- parentId: text,
241
- agents: array(text, 1024),
242
- models: array(text, 1024),
243
- points: array(chartPointSchema, 500),
244
- requests: array(chartRequestSchema, 500),
245
- originalPoints: count
246
- }, [
247
- "sessionId",
248
- "agents",
249
- "models",
250
- "points",
251
- "requests",
252
- "originalPoints"
253
- ]), 1)
254
- }, ["schema", "omittedSessions", "maxPoints", "gaps", "series"])
255
- });
256
- var historyRpc = Rpc.define({
257
- id: "context-guard.history.v1",
258
- methods: { read: { input: historyReadSchema, output: historyResultSchema } },
259
- events: {}
260
- });
261
41
  function isHistoryRead(value) {
262
42
  return valid(value, historyReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from <= value.to && value.to - value.from <= 7 * 86400000;
263
43
  }
@@ -312,858 +92,3414 @@ function uniqueRows(value) {
312
92
  const ids = [...value.upserts, ...value.removed].map((v) => record(v) ? v.sessionId : undefined);
313
93
  return ids.length <= 128 && new Set(ids).size === ids.length;
314
94
  }
95
+ var count, text, enumeration = (...values) => ({ type: "string", enum: values }), object = (properties, required = Object.keys(properties)) => ({
96
+ type: "object",
97
+ properties,
98
+ required,
99
+ additionalProperties: false
100
+ }), array = (items, maxItems = 128) => ({
101
+ type: "array",
102
+ items,
103
+ maxItems
104
+ }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
105
+ var init_rpc = __esm(() => {
106
+ count = {
107
+ type: "integer",
108
+ minimum: 0,
109
+ maximum: Number.MAX_SAFE_INTEGER
110
+ };
111
+ text = { type: "string", minLength: 1, maxLength: 512 };
112
+ measurement = object({
113
+ tokens: count,
114
+ at: count,
115
+ model: text,
116
+ requestId: text,
117
+ provenance: enumeration("provider-normalized", "serialized-estimate"),
118
+ confidence: enumeration("reported", "approximate"),
119
+ scope: enumeration("completed-request", "prepared-request")
120
+ }, ["tokens", "at", "model", "provenance", "confidence", "scope"]);
121
+ row = object({
122
+ sessionId: text,
123
+ revision: count,
124
+ observedAt: count,
125
+ parentId: text,
126
+ agent: text,
127
+ model: text,
128
+ measurement,
129
+ limits: object({ context: count, input: count, output: count }, [
130
+ "context",
131
+ "output"
132
+ ]),
133
+ capacityTokens: count,
134
+ outputReserveTokens: count,
135
+ peakTokens: count,
136
+ policy: object({
137
+ id: text,
138
+ configured: object({
139
+ type: enumeration("tokens", "ratio", "remaining"),
140
+ value: { type: "number", minimum: 0 }
141
+ }),
142
+ triggerTokens: { type: "number" },
143
+ comparison: enumeration(">=", ">"),
144
+ reason: enumeration("missing-usage", "model-changed", "invalid-usage", "stale-usage", "missing-model-budget", "incompatible-model-budget", "threshold-crossed", "below-threshold"),
145
+ crossed: { type: "boolean" }
146
+ }, ["id", "configured", "comparison", "reason", "crossed"]),
147
+ compaction: object({
148
+ phase: enumeration("idle", "requesting", "accepted", "running", "awaiting-measurement", "blocked", "uncertain"),
149
+ count,
150
+ armed: { type: "boolean" },
151
+ blockedReason: enumeration("session-removed", "compaction-failed", "reclaim-unverifiable", "insufficient-reclaim", "completion-event-timeout", "admission-outcome-unknown", "restart-needs-lifecycle-evidence"),
152
+ lastAttempt: count,
153
+ lastCompleted: count,
154
+ cooldownUntil: count
155
+ }, ["phase", "armed"]),
156
+ cumulative: object({
157
+ input: count,
158
+ output: count,
159
+ reasoning: count,
160
+ cache: object({ read: count, write: count })
161
+ }),
162
+ cumulativeScope: enumeration("session", "unknown"),
163
+ lastObservedActive: { type: "boolean" }
164
+ }, [
165
+ "sessionId",
166
+ "revision",
167
+ "observedAt",
168
+ "policy",
169
+ "compaction",
170
+ "cumulativeScope",
171
+ "lastObservedActive"
172
+ ]);
173
+ batchFields = {
174
+ schemaVersion: { const: 1 },
175
+ epoch: text,
176
+ sequence: count,
177
+ generatedAt: count,
178
+ health: object({
179
+ enabled: { type: "boolean" },
180
+ mode: enumeration("observe", "enforce"),
181
+ enforcement: enumeration("observing", "available", "suspended", "disabled"),
182
+ reasons: array(enumeration("event-stream-gap", "persistence-unavailable", "capacity-exceeded", "model-metadata-unavailable"), 8),
183
+ staleMs: count,
184
+ maxSessions: count
185
+ }),
186
+ upserts: array(row),
187
+ removed: array(object({
188
+ sessionId: text,
189
+ revision: count,
190
+ reason: enumeration("deleted", "evicted")
191
+ }))
192
+ };
193
+ readSchema = object({
194
+ schemaVersion: { const: 1 },
195
+ sessionIds: { ...array(text), uniqueItems: true }
196
+ });
197
+ batchSchema = object(batchFields);
198
+ resultSchema = object({
199
+ ...batchFields,
200
+ unavailable: array(object({ sessionId: text, reason: enumeration("not-monitored") }))
201
+ });
202
+ monitorRpc = Rpc.define({
203
+ id: "context-guard.monitor.v1",
204
+ methods: { read: { input: readSchema, output: resultSchema } },
205
+ events: { changed: { schema: batchSchema } }
206
+ });
207
+ nonnegative = { type: "number", minimum: 0 };
208
+ performanceSchema = object({
209
+ observedSince: count,
210
+ at: count,
211
+ sessionCreatedAt: count,
212
+ sessionOutputTokens: count,
213
+ status: enumeration("waiting", "streaming", "idle", "unavailable"),
214
+ partial: { type: "boolean" },
215
+ liveTps: nonnegative,
216
+ averageModelTps: nonnegative,
217
+ averageSessionTps: nonnegative,
218
+ ttftMs: nonnegative,
219
+ latestRequestTps: nonnegative,
220
+ requestId: text,
221
+ requestAgent: text,
222
+ requestModel: text,
223
+ requestOutputTokens: count,
224
+ requestStartedAt: count,
225
+ firstOutputAt: count,
226
+ streamEndedAt: count,
227
+ estimatedOutputTokens: nonnegative,
228
+ modelOutputTokens: nonnegative,
229
+ modelDurationMs: nonnegative,
230
+ completedRequests: count,
231
+ tokenSource: enumeration("estimated", "reported", "unavailable")
232
+ }, [
233
+ "sessionCreatedAt",
234
+ "observedSince",
235
+ "at",
236
+ "status",
237
+ "partial",
238
+ "estimatedOutputTokens",
239
+ "modelOutputTokens",
240
+ "modelDurationMs",
241
+ "completedRequests",
242
+ "tokenSource"
243
+ ]);
244
+ performanceRow = {
245
+ ...row,
246
+ properties: { ...row.properties, performance: performanceSchema }
247
+ };
248
+ performanceBatchSchema = object({
249
+ ...batchFields,
250
+ upserts: array(performanceRow)
251
+ });
252
+ performanceResultSchema = object({
253
+ ...batchFields,
254
+ upserts: array(performanceRow),
255
+ unavailable: resultSchema.properties.unavailable ?? {}
256
+ });
257
+ performanceRpc = Rpc.define({
258
+ id: "context-guard.monitor.v2",
259
+ methods: { read: { input: readSchema, output: performanceResultSchema } },
260
+ events: { changed: { schema: performanceBatchSchema } }
261
+ });
262
+ chartPointSchema = object({
263
+ time: count,
264
+ agent: text,
265
+ model: text,
266
+ breakBefore: { type: "boolean" },
267
+ partial: { type: "boolean" },
268
+ liveTps: nonnegative,
269
+ averageModelTps: nonnegative,
270
+ averageSessionTps: nonnegative,
271
+ sessionOutputTokens: count
272
+ }, ["time", "breakBefore", "partial"]);
273
+ chartRequestSchema = object({
274
+ time: count,
275
+ requestId: text,
276
+ started: count,
277
+ agent: text,
278
+ model: text,
279
+ ttftMs: nonnegative,
280
+ outputTokens: count,
281
+ durationMs: nonnegative,
282
+ tps: nonnegative,
283
+ partial: { type: "boolean" }
284
+ }, ["time", "requestId", "started", "partial"]);
285
+ historyReadSchema = object({
286
+ schemaVersion: { const: 1 },
287
+ sessionId: text,
288
+ from: count,
289
+ to: count,
290
+ maxPoints: { type: "integer", minimum: 16, maximum: 500 }
291
+ });
292
+ historyResultSchema = object({
293
+ schemaVersion: { const: 1 },
294
+ generatedAt: count,
295
+ enabled: { type: "boolean" },
296
+ truncated: { type: "boolean" },
297
+ metadata: object({
298
+ schema: { const: 1 },
299
+ retainedFrom: count,
300
+ retainedBytes: count,
301
+ skippedBytes: count,
302
+ malformedRecords: count,
303
+ completeness: enumeration("bounded-retention")
304
+ }, [
305
+ "schema",
306
+ "retainedBytes",
307
+ "skippedBytes",
308
+ "malformedRecords",
309
+ "completeness"
310
+ ]),
311
+ data: object({
312
+ schema: { const: 1 },
313
+ omittedSessions: count,
314
+ omittedGaps: count,
315
+ maxPoints: count,
316
+ gaps: array(object({ time: count, sessionId: text, reason: text }), 128),
317
+ series: array(object({
318
+ sessionId: text,
319
+ parentId: text,
320
+ agents: array(text, 1024),
321
+ models: array(text, 1024),
322
+ points: array(chartPointSchema, 500),
323
+ requests: array(chartRequestSchema, 500),
324
+ originalPoints: count
325
+ }, [
326
+ "sessionId",
327
+ "agents",
328
+ "models",
329
+ "points",
330
+ "requests",
331
+ "originalPoints"
332
+ ]), 1)
333
+ }, ["schema", "omittedSessions", "maxPoints", "gaps", "series"])
334
+ });
335
+ historyRpc = Rpc.define({
336
+ id: "context-guard.history.v1",
337
+ methods: { read: { input: historyReadSchema, output: historyResultSchema } },
338
+ events: {}
339
+ });
340
+ });
315
341
 
316
- // src/cli.ts
317
- import { writeFile } from "fs/promises";
318
- import { resolve } from "path";
319
-
320
- // src/charts.ts
321
- var rateFields = [
322
- "liveTps",
323
- "averageModelTps",
324
- "averageSessionTps",
325
- "sessionOutputTokens"
326
- ];
327
- function downsamplePoints(points, limit) {
328
- if (!Number.isSafeInteger(limit) || limit < 16 || limit > 5000)
329
- throw new Error("maxPoints must be an integer from 16 to 5000");
330
- if (points.length <= limit)
331
- return [...points];
332
- const buckets = Math.max(1, Math.floor(limit / (2 + rateFields.length * 2)));
333
- const selected = new Set;
334
- for (let b = 0;b < buckets; b++) {
335
- const start = Math.floor(b * points.length / buckets), end = Math.floor((b + 1) * points.length / buckets);
336
- selected.add(start);
337
- selected.add(end - 1);
338
- for (const field of rateFields) {
339
- let low, high;
340
- for (let i = start;i < end; i++) {
341
- const value = points[i]?.[field];
342
- if (value === undefined)
343
- continue;
344
- if (low === undefined || value < (points[low]?.[field] ?? Infinity))
345
- low = i;
346
- if (high === undefined || value > (points[high]?.[field] ?? -Infinity))
347
- high = i;
348
- }
349
- if (low !== undefined)
350
- selected.add(low);
351
- if (high !== undefined)
352
- selected.add(high);
353
- }
354
- }
355
- let previous = -1;
356
- return [...selected].sort((a, b) => a - b).flatMap((index) => {
357
- const point = points[index];
358
- if (!point)
342
+ // src/history.ts
343
+ import { mkdir, readdir, stat, open, unlink } from "fs/promises";
344
+ import { join } from "path";
345
+ async function segments(directory) {
346
+ const names = await readdir(directory).catch((error) => {
347
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
359
348
  return [];
360
- const skippedGap = points.slice(previous + 1, index + 1).some((p) => p.breakBefore);
361
- previous = index;
362
- return [{ ...point, breakBefore: point.breakBefore || skippedGap }];
349
+ throw error;
363
350
  });
351
+ return (await Promise.all(names.filter((n) => filename.test(n)).map(async (name) => ({
352
+ name,
353
+ time: Number(filename.exec(name)?.[1]),
354
+ bytes: (await stat(join(directory, name))).size
355
+ })))).sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
364
356
  }
365
- function buildChartData(records, options = {}) {
366
- const maxPoints = options.maxPoints ?? 1000, maxSessions = options.maxSessions ?? 128;
367
- if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 1024)
368
- throw new Error("maxSessions must be from 1 to 1024");
369
- downsamplePoints([], maxPoints);
370
- const sorted = [...records].sort((a, b) => a.time - b.time || a.recordId.localeCompare(b.recordId));
371
- const lastSeen = new Map;
372
- for (const r of sorted)
373
- if (r.performance && (!options.sessionId || r.sessionId === options.sessionId))
374
- lastSeen.set(r.sessionId, r.time);
375
- const sessions = [...lastSeen].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
376
- const selected = new Set(sessions.slice(0, maxSessions).map(([id]) => id));
377
- const series = new Map, gaps = [];
378
- const seen = new Set, requestSeen = new Set;
379
- const pendingGaps = new Set;
380
- const observedSince = new Map;
381
- for (const r of sorted) {
382
- if (seen.has(r.recordId))
383
- continue;
384
- seen.add(r.recordId);
385
- if (r.type === "gap") {
386
- if (r.sessionId === "__history__" || selected.has(r.sessionId)) {
387
- gaps.push({
388
- time: r.time,
389
- sessionId: r.sessionId,
390
- reason: r.event ?? "observation-gap"
391
- });
392
- if (r.sessionId === "__history__")
393
- for (const id of selected)
394
- pendingGaps.add(id);
395
- else
396
- pendingGaps.add(r.sessionId);
397
- }
398
- continue;
357
+
358
+ class PerformanceHistory {
359
+ path;
360
+ config;
361
+ now;
362
+ report;
363
+ pending = [];
364
+ working;
365
+ active;
366
+ bytes = 0;
367
+ started = 0;
368
+ disabled = false;
369
+ dropped = 0;
370
+ droppedTotal = 0;
371
+ constructor(path, config, now = Date.now, report = () => {
372
+ }) {
373
+ this.path = path;
374
+ this.config = config;
375
+ this.now = now;
376
+ this.report = report;
377
+ }
378
+ emit(record2) {
379
+ if (!this.config.enabled || this.disabled)
380
+ return;
381
+ const line = JSON.stringify({
382
+ schema: 1,
383
+ recordId: record2.recordId,
384
+ time: record2.time,
385
+ type: record2.type,
386
+ sessionId: record2.sessionId,
387
+ ...record2.parentId ? { parentId: record2.parentId } : {},
388
+ ...record2.agent ? { agent: record2.agent } : {},
389
+ ...record2.model ? { model: record2.model } : {},
390
+ ...record2.event ? { event: record2.event } : {},
391
+ ...record2.droppedRecords === undefined ? {} : { droppedRecords: record2.droppedRecords },
392
+ ...record2.performance ? { performance: scalarPerformance(record2.performance) } : {}
393
+ }) + "\n";
394
+ if (this.pending.length >= 256 || Buffer.byteLength(line) > Math.min(4 * 1024 * 1024, this.config.maxBytes)) {
395
+ this.dropped++;
396
+ this.droppedTotal++;
397
+ if (this.dropped === 1)
398
+ this.report("history-records-dropped");
399
+ return;
399
400
  }
400
- const p = r.performance;
401
- if (!p || !selected.has(r.sessionId))
402
- continue;
403
- let s = series.get(r.sessionId);
404
- if (!s) {
405
- s = {
406
- sessionId: r.sessionId,
407
- ...r.parentId ? { parentId: r.parentId } : {},
408
- agents: [],
409
- models: [],
410
- points: [],
411
- requests: [],
412
- originalPoints: 0
413
- };
414
- series.set(r.sessionId, s);
401
+ this.pending.push(line);
402
+ this.kick();
403
+ }
404
+ kick() {
405
+ if (this.working || this.disabled)
406
+ return;
407
+ this.working = this.drain().catch(() => {
408
+ this.disabled = true;
409
+ this.pending = [];
410
+ this.report("history-unavailable");
411
+ }).finally(() => {
412
+ this.working = undefined;
413
+ if (this.pending.length)
414
+ this.kick();
415
+ });
416
+ }
417
+ async prune(incoming = 0) {
418
+ const files = await segments(this.path);
419
+ let total = files.reduce((n, f) => n + f.bytes, 0);
420
+ let truncated = false;
421
+ for (const file of files) {
422
+ if (file.time >= this.now() - this.config.retentionDays * 86400000 && total + incoming <= this.config.maxBytes)
423
+ break;
424
+ await unlink(join(this.path, file.name));
425
+ if (file.name === this.active) {
426
+ this.active = undefined;
427
+ this.bytes = 0;
428
+ }
429
+ total -= file.bytes;
430
+ truncated = true;
415
431
  }
416
- const agent = p.requestAgent ?? r.agent, model = p.requestModel ?? r.model;
417
- if (agent && !s.agents.includes(agent))
418
- s.agents.push(agent);
419
- if (model && !s.models.includes(model))
420
- s.models.push(model);
421
- const previous = s.points.at(-1);
422
- const restart = observedSince.has(r.sessionId) && observedSince.get(r.sessionId) !== p.observedSince;
423
- const reset = previous?.sessionOutputTokens !== undefined && p.sessionOutputTokens !== undefined && p.sessionOutputTokens < previous.sessionOutputTokens;
424
- 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;
425
- const point = {
426
- time: r.time,
427
- ...agent ? { agent } : {},
428
- ...model ? { model } : {},
429
- breakBefore: discontinuity,
430
- partial: p.partial,
431
- ...p.liveTps === undefined ? {} : { liveTps: p.liveTps },
432
- ...p.averageModelTps === undefined ? {} : { averageModelTps: p.averageModelTps },
433
- ...p.averageSessionTps === undefined ? {} : { averageSessionTps: p.averageSessionTps },
434
- ...p.sessionOutputTokens === undefined ? {} : { sessionOutputTokens: p.sessionOutputTokens }
435
- };
436
- if (previous?.time === point.time)
437
- s.points[s.points.length - 1] = {
438
- ...point,
439
- breakBefore: point.breakBefore || previous.breakBefore
440
- };
441
- else
442
- s.points.push(point);
443
- observedSince.set(r.sessionId, p.observedSince);
444
- pendingGaps.delete(r.sessionId);
445
- if (r.type === "request" && p.requestId !== undefined && p.requestStartedAt !== undefined) {
446
- const key = JSON.stringify([
447
- r.sessionId,
448
- p.requestId,
449
- p.requestStartedAt
450
- ]);
451
- if (requestSeen.has(key))
432
+ if (truncated)
433
+ this.report("history-truncated");
434
+ }
435
+ gapLine(event, droppedRecords) {
436
+ return JSON.stringify({
437
+ schema: 1,
438
+ recordId: crypto.randomUUID(),
439
+ time: this.now(),
440
+ type: "gap",
441
+ sessionId: "__history__",
442
+ event,
443
+ ...droppedRecords === undefined ? {} : { droppedRecords }
444
+ }) + "\n";
445
+ }
446
+ async drain() {
447
+ await mkdir(this.path, { recursive: true, mode: 448 });
448
+ await this.prune();
449
+ while (this.pending.length) {
450
+ let line = this.pending.shift();
451
+ if (!line)
452
452
  continue;
453
- requestSeen.add(key);
454
- const duration = p.streamEndedAt === undefined ? undefined : p.streamEndedAt - p.requestStartedAt;
455
- const validDuration = duration !== undefined && duration > 0;
456
- const output = p.tokenSource === "reported" ? p.requestOutputTokens : undefined;
457
- s.requests.push({
458
- time: r.time,
459
- requestId: p.requestId,
460
- started: p.requestStartedAt,
461
- ...agent ? { agent } : {},
462
- ...model ? { model } : {},
463
- partial: p.partial,
464
- ...p.ttftMs === undefined ? {} : { ttftMs: p.ttftMs },
465
- ...output === undefined ? {} : { outputTokens: output },
466
- ...validDuration ? { durationMs: duration } : {},
467
- ...output !== undefined && validDuration ? { tps: output * 1000 / duration } : {}
468
- });
453
+ if (this.dropped > 0) {
454
+ this.pending.unshift(line);
455
+ line = this.gapLine("history-records-dropped", this.dropped);
456
+ this.dropped = 0;
457
+ }
458
+ const size = Buffer.byteLength(line);
459
+ const now = this.now();
460
+ if (!this.active || this.bytes + size > Math.min(4 * 1024 * 1024, this.config.maxBytes) || now - this.started >= 3600000) {
461
+ this.active = undefined;
462
+ this.bytes = 0;
463
+ await this.prune(size);
464
+ this.started = now;
465
+ this.active = `${now}-${crypto.randomUUID()}.jsonl`;
466
+ } else
467
+ await this.prune(size);
468
+ if (!this.active) {
469
+ this.started = now;
470
+ this.active = `${now}-${crypto.randomUUID()}.jsonl`;
471
+ }
472
+ const file = await open(join(this.path, this.active), "a", 384);
473
+ try {
474
+ await file.writeFile(line);
475
+ } finally {
476
+ await file.close();
477
+ }
478
+ this.bytes += size;
469
479
  }
470
480
  }
481
+ health() {
482
+ return { unavailable: this.disabled, droppedRecords: this.droppedTotal };
483
+ }
484
+ async flush() {
485
+ while (this.working)
486
+ await this.working;
487
+ }
488
+ async initialize() {
489
+ if (!this.config.enabled)
490
+ return;
491
+ await mkdir(this.path, { recursive: true, mode: 448 }).then(() => this.prune()).catch(() => {
492
+ this.disabled = true;
493
+ this.report("history-unavailable");
494
+ });
495
+ }
496
+ }
497
+ function scalarPerformance(p) {
471
498
  return {
472
- schema: 1,
473
- series: [...series.values()].map((s) => ({
474
- ...s,
475
- originalPoints: s.points.length,
476
- points: downsamplePoints(s.points, maxPoints)
477
- })),
478
- gaps: gaps.slice(-1024),
479
- omittedGaps: Math.max(0, gaps.length - 1024),
480
- omittedSessions: Math.max(0, sessions.length - selected.size),
481
- maxPoints
499
+ observedSince: p.observedSince,
500
+ sessionCreatedAt: p.sessionCreatedAt,
501
+ at: p.at,
502
+ status: p.status,
503
+ partial: p.partial,
504
+ estimatedOutputTokens: p.estimatedOutputTokens,
505
+ modelOutputTokens: p.modelOutputTokens,
506
+ modelDurationMs: p.modelDurationMs,
507
+ completedRequests: p.completedRequests,
508
+ tokenSource: p.tokenSource,
509
+ ...Object.fromEntries([
510
+ "sessionOutputTokens",
511
+ "liveTps",
512
+ "averageModelTps",
513
+ "averageSessionTps",
514
+ "ttftMs",
515
+ "latestRequestTps",
516
+ "requestId",
517
+ "requestAgent",
518
+ "requestModel",
519
+ "requestOutputTokens",
520
+ "requestStartedAt",
521
+ "firstOutputAt",
522
+ "streamEndedAt"
523
+ ].flatMap((key) => {
524
+ const value = p[key];
525
+ return value === undefined ? [] : [[key, value]];
526
+ }))
482
527
  };
483
528
  }
484
-
485
- // src/chartreport.ts
486
- function renderChartReport(report) {
487
- const payload = JSON.stringify(report).replace(/[<>&\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
488
- return `<!doctype html>
489
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
490
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src blob: data:; connect-src 'none'">
491
- <title>Context Guard \xB7 Performance history</title>
492
- <style>
493
- :root{color-scheme:dark;font-family:system-ui,sans-serif;background:#0b1120;color:#e2e8f0}*{box-sizing:border-box}body{max-width:1600px;margin:auto;padding:28px}h1{font-size:28px;letter-spacing:-.03em;margin:8px 0}h2{font-size:16px;margin:0}p{color:#a8b5cb;line-height:1.6;margin:8px 0}.eyebrow{color:#7dd3fc;font-size:12px;letter-spacing:.13em;text-transform:uppercase}.toolbar,.stats,.charts{display:grid;gap:14px;margin-top:22px}.toolbar{grid-template-columns:2fr 1fr 1fr 1.4fr 1.4fr auto;align-items:end}label{font-size:12px;color:#a8b5cb;display:grid;gap:7px}select,input,button{background:#172338;border:1px solid #33435a;border-radius:7px;color:inherit;padding:10px;min-width:0;font:inherit}button{cursor:pointer;font-size:12px}button:hover,button:focus-visible{border-color:#7dd3fc}select:focus-visible,input:focus-visible{outline:2px solid #7dd3fc}.stats{grid-template-columns:repeat(4,1fr)}.stat,.chart{background:#111c2e;border:1px solid #26354e;border-radius:12px;padding:18px}.stat strong{font-size:24px;display:block;margin-top:7px}.stat span{font-size:12px;color:#a8b5cb}.charts{grid-template-columns:repeat(2,minmax(0,1fr))}.charthead{display:flex;justify-content:space-between;align-items:center;gap:10px}.chart p{font-size:12px;min-height:38px}.chart svg{display:block;width:100%;height:auto}.notice{padding:12px 15px;border-left:3px solid #fbbf24;background:#201f2d;font-size:13px;margin-top:15px}.legend{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:18px;font-size:12px}.legend span{overflow-wrap:anywhere}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:7px}.empty{padding:30px;text-align:center;color:#a8b5cb}.tooltip{position:fixed;z-index:5;pointer-events:none;background:#e2e8f0;color:#0b1120;border-radius:7px;padding:10px;max-width:360px;white-space:pre-wrap;font-size:12px;box-shadow:0 8px 30px #0008}footer{margin-top:25px;font-size:12px;color:#a8b5cb}svg circle:focus{outline:none;stroke:white;stroke-width:3}@media(max-width:1000px){.toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.charts{grid-template-columns:1fr}}@media(max-width:600px){body{padding:16px}.stats{grid-template-columns:repeat(2,1fr)}}@media print{button,.toolbar,.tooltip{display:none}body{padding:0}.chart{break-inside:avoid}.charts{grid-template-columns:1fr}}
494
- </style></head><body>
495
- <header><span class="eyebrow">Context Guard / Local analysis</span><h1>Performance history</h1><p>Compare agent instances over time. All timestamps are UTC. This report is an offline snapshot of retained metrics.</p></header>
496
- <section class="toolbar" aria-label="Chart filters">
497
- <label>Session instance<select id="session"></select></label><label>Agent<select id="agent"></select></label><label>Model<select id="model"></select></label>
498
- <label>From (UTC)<input id="from" type="datetime-local" step="1"></label><label>To (UTC)<input id="to" type="datetime-local" step="1"></label><button id="reset">Reset filters</button>
499
- </section>
500
- <div id="notice" class="notice" role="status"></div><section id="stats" class="stats" aria-label="Request statistics"></section>
501
- <div id="legend" class="legend"></div><main id="charts" class="charts"></main>
502
- <footer>Live TPS uses a character estimate; TTFT includes output publication delay. Dashed curves mark estimates or partial observations. Gaps and missing samples are never filled with zero. Request statistics use retained matching requests; average curves retain the session's original cumulative totals. Agent/model filters select observations, not recomputed lifetime averages. Exported SVGs reflect the current filters. Regenerate this report to include new history.</footer>
503
- <div id="tooltip" class="tooltip" hidden role="tooltip"></div>
504
- <script id="chart-data" type="application/json">${payload}</script>
505
- <script>(${chartClient.toString()})(JSON.parse(document.getElementById("chart-data").textContent));</script>
506
- </body></html>`;
507
- }
508
- function chartClient(report) {
509
- const { data, metadata } = report;
510
- const element = (id, kind) => {
511
- const node = document.getElementById(id);
512
- if (!(node instanceof kind))
513
- throw new Error(`Missing report element ${id}`);
514
- return node;
515
- };
516
- const session = element("session", HTMLSelectElement), agent = element("agent", HTMLSelectElement), model = element("model", HTMLSelectElement);
517
- const from = element("from", HTMLInputElement), to = element("to", HTMLInputElement);
518
- const charts = element("charts", HTMLElement), stats = element("stats", HTMLElement), legend = element("legend", HTMLElement);
519
- const notice = element("notice", HTMLElement), tooltip = element("tooltip", HTMLElement);
520
- const color = (index) => `hsl(${(index * 137.508 + 195) % 360}, 78%, 68%)`;
521
- const date = (value) => {
522
- const d = new Date(value);
523
- return Number.isFinite(d.getTime()) ? d.toISOString() : String(value);
524
- };
525
- const number = (n, digits = 1) => n === undefined ? "Unavailable" : n.toLocaleString("en-US", { maximumFractionDigits: digits });
526
- function options(select, values) {
527
- for (const v of values) {
528
- const o = document.createElement("option");
529
- o.value = v.value;
530
- o.textContent = v.label;
531
- select.append(o);
529
+ async function readPerformanceHistory(directory, filter = {}) {
530
+ const records = [];
531
+ const files = await segments(directory);
532
+ const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
533
+ let bytes = 0, skippedBytes = 0, malformedRecords = 0;
534
+ const selected = [];
535
+ for (const file of files.slice().reverse()) {
536
+ if (bytes + file.bytes > maxBytes) {
537
+ skippedBytes += file.bytes;
538
+ continue;
532
539
  }
540
+ bytes += file.bytes;
541
+ selected.unshift(file);
533
542
  }
534
- options(session, [
535
- { value: "*", label: "Compare sessions (up to 12)" },
536
- ...data.series.map((s) => ({
537
- value: s.sessionId,
538
- label: `${s.agents.join(", ") || "Agent"} \xB7 ${s.sessionId}`
539
- }))
540
- ]);
541
- options(agent, [
542
- { value: "*", label: "All agents" },
543
- ...[...new Set(data.series.flatMap((s) => s.agents))].sort().map((v) => ({ value: v, label: v }))
544
- ]);
545
- options(model, [
546
- { value: "*", label: "All models" },
547
- ...[...new Set(data.series.flatMap((s) => s.models))].sort().map((v) => ({ value: v, label: v }))
548
- ]);
549
- function reset() {
550
- session.value = data.series[0]?.sessionId ?? "*";
551
- agent.value = report.agent ?? "*";
552
- model.value = report.model ?? "*";
553
- from.value = report.from === undefined ? "" : date(report.from).slice(0, 19);
554
- to.value = report.to === undefined ? "" : date(report.to).slice(0, 19);
555
- }
556
- const timestamp = (value) => value ? Date.parse(`${value}Z`) : undefined;
557
- function matches2(p, start, end) {
558
- return (agent.value === "*" || p.agent === agent.value) && (model.value === "*" || p.model === model.value) && (start === undefined || p.time >= start) && (end === undefined || p.time <= end);
543
+ for (const file of selected) {
544
+ const handle = await open(join(directory, file.name), "r");
545
+ let source;
546
+ try {
547
+ const buffer = Buffer.alloc(file.bytes);
548
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
549
+ source = buffer.subarray(0, bytesRead).toString("utf8");
550
+ } finally {
551
+ await handle.close();
552
+ }
553
+ for (const line of source.split("\n")) {
554
+ if (!line)
555
+ continue;
556
+ try {
557
+ const value = JSON.parse(line);
558
+ if (!isHistoryRecord(value)) {
559
+ malformedRecords++;
560
+ continue;
561
+ }
562
+ 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)
563
+ continue;
564
+ records.push(value);
565
+ } catch {
566
+ malformedRecords++;
567
+ }
568
+ }
559
569
  }
560
- const ns = "http://www.w3.org/2000/svg";
561
- function svgNode(name, attrs = {}, text2) {
562
- const n = document.createElementNS(ns, name);
563
- for (const [key, value] of Object.entries(attrs))
564
- n.setAttribute(key, String(value));
565
- if (text2 !== undefined)
566
- n.textContent = text2;
567
- return n;
570
+ const metadata = {
571
+ schema: 1,
572
+ ...files[0] ? { retainedFrom: files[0].time } : {},
573
+ retainedBytes: files.reduce((n, f) => n + f.bytes, 0),
574
+ skippedBytes,
575
+ malformedRecords,
576
+ completeness: "bounded-retention"
577
+ };
578
+ return { metadata, records };
579
+ }
580
+ function isRecord(value) {
581
+ return value !== null && typeof value === "object" && !Array.isArray(value);
582
+ }
583
+ function isHistoryRecord(value) {
584
+ if (!isRecord(value))
585
+ return false;
586
+ const r = value;
587
+ const allowed = new Set([
588
+ "schema",
589
+ "recordId",
590
+ "time",
591
+ "type",
592
+ "sessionId",
593
+ "parentId",
594
+ "agent",
595
+ "model",
596
+ "event",
597
+ "performance",
598
+ "droppedRecords"
599
+ ]);
600
+ 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);
601
+ }
602
+ var filename;
603
+ var init_history = __esm(() => {
604
+ init_rpc();
605
+ filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
606
+ });
607
+
608
+ // src/dashboard/command.ts
609
+ var exports_command = {};
610
+ __export(exports_command, {
611
+ parseDashboardArgs: () => parseDashboardArgs,
612
+ loadTerminalRenderer: () => loadTerminalRenderer,
613
+ dashboardHelp: () => dashboardHelp,
614
+ checkTerminalRuntime: () => checkTerminalRuntime
615
+ });
616
+ import { isAbsolute, resolve } from "path";
617
+ function parseDashboardArgs(args, cwd, env) {
618
+ if (args[0] !== "tui") {
619
+ if (args.length === 1 && args[0] === "--help")
620
+ return "help";
621
+ throw new Error("Use context-guard dashboard tui; only tui is supported");
568
622
  }
569
- function showTooltip(text2, x, y) {
570
- tooltip.textContent = text2;
571
- tooltip.hidden = false;
572
- tooltip.style.left = `${Math.max(8, Math.min(x + 12, window.innerWidth - 370))}px`;
573
- tooltip.style.top = `${Math.max(8, Math.min(y + 12, window.innerHeight - tooltip.offsetHeight - 10))}px`;
623
+ let server, directory;
624
+ const seen = new Set;
625
+ for (let i = 1;i < args.length; i++) {
626
+ const flag = args[i];
627
+ if (flag === "--help" && args.length === 2)
628
+ return "help";
629
+ if (flag !== "--server" && flag !== "--directory")
630
+ throw new Error("Unknown dashboard option; use dashboard tui --help");
631
+ if (seen.has(flag))
632
+ throw new Error("Duplicate dashboard option");
633
+ seen.add(flag);
634
+ const value = args[++i];
635
+ if (!value || value.startsWith("--"))
636
+ throw new Error(`Missing value for ${flag}`);
637
+ if (flag === "--server")
638
+ server = value;
639
+ else
640
+ directory = value;
574
641
  }
575
- const definitions = [
576
- {
577
- key: "liveTps",
578
- title: "Live TPS",
579
- unit: "tokens / second",
580
- description: "Estimated text + reasoning throughput in the rolling collection window.",
581
- requests: false,
582
- estimated: true
583
- },
584
- {
585
- key: "averageModelTps",
586
- title: "Average model throughput",
587
- unit: "tokens / second",
588
- description: "Session output / matched model-request time, including TTFT and excluding subsequent tool time.",
589
- requests: false,
590
- estimated: false
591
- },
592
- {
593
- key: "averageSessionTps",
594
- title: "Average session throughput",
595
- unit: "tokens / second",
596
- description: "Session output / elapsed session lifetime, including tools and idle time.",
597
- requests: false,
598
- estimated: false
599
- },
600
- {
601
- key: "ttftMs",
602
- title: "Time to first output",
603
- unit: "seconds",
604
- description: "One point per request with observed text/reasoning output. Includes publication delay.",
605
- requests: true,
606
- estimated: true
607
- },
608
- {
609
- key: "tps",
610
- title: "Completed request throughput",
611
- unit: "tokens / second",
612
- description: "Reported output + reasoning / request duration. Requests are never joined into a continuous curve.",
613
- requests: true,
614
- estimated: false
615
- },
616
- {
617
- key: "sessionOutputTokens",
618
- title: "Cumulative output",
619
- unit: "tokens",
620
- description: "Authoritative session output + reasoning totals. Lifecycle and sample records are snapshots, not increments.",
621
- requests: false,
622
- estimated: false
642
+ if (server !== undefined && (!directory || !isAbsolute(directory)))
643
+ throw new Error("Explicit servers require --directory with an absolute server path");
644
+ return {
645
+ directory: server === undefined ? resolve(cwd, directory ?? ".") : directory ?? cwd,
646
+ ...server === undefined ? {} : {
647
+ server,
648
+ ...env.CONTEXT_GUARD_PASSWORD === undefined ? {} : {
649
+ password: env.CONTEXT_GUARD_PASSWORD,
650
+ username: env.CONTEXT_GUARD_USERNAME ?? "opencode"
651
+ }
623
652
  }
624
- ];
625
- function render() {
626
- tooltip.hidden = true;
627
- charts.replaceChildren();
628
- stats.replaceChildren();
629
- legend.replaceChildren();
630
- const start = timestamp(from.value), end = timestamp(to.value);
631
- if (start !== undefined && !Number.isFinite(start) || end !== undefined && !Number.isFinite(end) || start !== undefined && end !== undefined && start > end) {
632
- notice.textContent = "Choose a valid time range with From before To.";
653
+ };
654
+ }
655
+ function checkTerminalRuntime(version, interactive) {
656
+ const [major = 0, minor = 0] = version.split(".").map(Number);
657
+ if (major < 1 || major === 1 && minor < 3 || !Number.isFinite(major + minor))
658
+ throw new Error("dashboard tui requires Bun >=1.3.0; other commands support Bun >=1.1.34");
659
+ if (!interactive)
660
+ throw new Error("dashboard tui requires an interactive input and output terminal; run it directly in a terminal");
661
+ }
662
+ async function loadTerminalRenderer() {
663
+ try {
664
+ return await import("@opentui/core");
665
+ } catch {
666
+ throw new Error("Terminal renderer unavailable; reinstall @cesarus777/opencode-cgp with optional dependencies enabled for its platform renderer, using Bun >=1.3.0");
667
+ }
668
+ }
669
+ var dashboardHelp = `Usage: context-guard dashboard tui [options]
670
+ Open a read-only live terminal dashboard for an existing OpenCode server.
671
+
672
+ Start here
673
+ 1. Keep an OpenCode server running with Context Guard registered.
674
+ 2. Close OpenCode's native TUI before opening the standalone dashboard.
675
+ 3. In the owning project directory, run: context-guard dashboard tui
676
+
677
+ Options
678
+ --directory PATH Owning project directory (local default: current directory)
679
+ --server URL Connect to an explicit server; requires --directory with
680
+ an absolute server-side project path. Use HTTPS except
681
+ on loopback. Credentials in URLs are rejected.
682
+ -h, --help Show this help
683
+
684
+ Examples
685
+ context-guard dashboard tui
686
+ context-guard dashboard tui --directory /absolute/project
687
+ context-guard dashboard tui --server https://server.example --directory /server/project
688
+
689
+ Requirements
690
+ Interactive terminal and Bun >=1.3.0. The OpenTUI renderer is installed
691
+ automatically with CGP. This command never starts a server.
692
+
693
+ Authentication (explicit servers)
694
+ CONTEXT_GUARD_PASSWORD Server password
695
+ CONTEXT_GUARD_USERNAME Server username (default: opencode)
696
+
697
+ Keys
698
+ 1/2/3 overview/detail/history; arrows or j/k move; / search; f filters;
699
+ l location; w history window; r refresh; ? help; q or Ctrl+C quit.`;
700
+ var init_command = () => {
701
+ };
702
+
703
+ // src/family.ts
704
+ function metric(snapshot, now, staleMs) {
705
+ const m = snapshot?.measurement;
706
+ const freshness = !m ? "missing" : m.model !== snapshot?.model || m.at > now ? "invalid" : now - m.at > staleMs ? "stale" : "fresh";
707
+ const t = snapshot?.policy.triggerTokens;
708
+ const valid2 = freshness === "fresh" && m !== undefined;
709
+ const displayable = valid2 || freshness === "stale" && m !== undefined;
710
+ const pressureRatio = valid2 && t !== undefined && t > 0 ? m.tokens / t : undefined;
711
+ const crossed = valid2 && t !== undefined && t > 0 && (snapshot?.policy.comparison === ">" ? m.tokens > t : m.tokens >= t);
712
+ return {
713
+ freshness,
714
+ pressure: pressureRatio === undefined ? "unknown" : crossed ? "crossed" : pressureRatio >= 0.9 ? "near" : "normal",
715
+ ...pressureRatio === undefined ? {} : { pressureRatio },
716
+ ...displayable && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
717
+ ...valid2 && t !== undefined ? { triggerDeltaTokens: t - m.tokens } : {}
718
+ };
719
+ }
720
+ function familyRows(nodes, snapshots, current, now, staleMs, expanded = new Set, limit = 1024, timing, stableOrder = []) {
721
+ const byId = new Map(nodes.map((n) => [n.sessionId, n]));
722
+ const children = new Map;
723
+ for (const n of byId.values()) {
724
+ const parent = n.parentId && byId.has(n.parentId) ? n.parentId : "";
725
+ const list = children.get(parent) ?? [];
726
+ list.push(n);
727
+ children.set(parent, list);
728
+ }
729
+ const metrics = new Map([...byId.keys()].map((id) => [
730
+ id,
731
+ metric(snapshots.get(id), timing?.(id).now ?? now, timing?.(id).staleMs ?? staleMs)
732
+ ]));
733
+ const protectedIds = new Set;
734
+ let ancestor = byId.get(current);
735
+ while (ancestor && !protectedIds.has(ancestor.sessionId)) {
736
+ protectedIds.add(ancestor.sessionId);
737
+ ancestor = ancestor.parentId ? byId.get(ancestor.parentId) : undefined;
738
+ }
739
+ const scores = new Map, sensitive = new Map, sizes = new Map;
740
+ const calculate = (n, seen) => {
741
+ if (seen.has(n.sessionId))
742
+ return -1;
743
+ if (scores.has(n.sessionId))
744
+ return scores.get(n.sessionId) ?? -1;
745
+ const path = new Set(seen).add(n.sessionId), m = metrics.get(n.sessionId);
746
+ let score = m?.pressureRatio ?? -1, size = 1;
747
+ let keep = protectedIds.has(n.sessionId) || n.status === "running" || m?.pressure === "crossed" || ["blocked", "uncertain"].includes(snapshots.get(n.sessionId)?.compaction.phase ?? "");
748
+ for (const child of children.get(n.sessionId) ?? []) {
749
+ score = Math.max(score, calculate(child, path));
750
+ size += sizes.get(child.sessionId) ?? 0;
751
+ keep ||= sensitive.get(child.sessionId) ?? false;
752
+ }
753
+ scores.set(n.sessionId, score);
754
+ sizes.set(n.sessionId, size);
755
+ sensitive.set(n.sessionId, keep);
756
+ return score;
757
+ };
758
+ for (const n of byId.values())
759
+ calculate(n, new Set);
760
+ const visited = new Set, rows = [];
761
+ const visit = (n, depth) => {
762
+ if (visited.has(n.sessionId) || rows.length >= limit || depth > 64)
633
763
  return;
764
+ visited.add(n.sessionId);
765
+ const snapshot = snapshots.get(n.sessionId), m = metrics.get(n.sessionId) ?? metric(undefined, now, staleMs);
766
+ const collapsed = n.status === "completed" && !sensitive.get(n.sessionId) && !expanded.has(n.sessionId);
767
+ rows.push({
768
+ node: n,
769
+ ...snapshot ? { snapshot } : {},
770
+ depth,
771
+ current: n.sessionId === current,
772
+ ...m,
773
+ hidden: collapsed ? (sizes.get(n.sessionId) ?? 1) - 1 : 0
774
+ });
775
+ if (collapsed) {
776
+ const pending = [...children.get(n.sessionId) ?? []];
777
+ while (pending.length) {
778
+ const child = pending.pop();
779
+ if (!child || visited.has(child.sessionId))
780
+ continue;
781
+ visited.add(child.sessionId);
782
+ pending.push(...children.get(child.sessionId) ?? []);
783
+ }
634
784
  }
635
- const selected = data.series.filter((s) => (session.value === "*" || s.sessionId === session.value) && s.points.some((p) => matches2(p, start, end)) || (session.value === "*" || s.sessionId === session.value) && s.requests.some((p) => matches2(p, start, end)));
636
- const visible = selected.slice(0, 12);
637
- const requests = visible.flatMap((s) => s.requests.filter((r) => matches2(r, start, end)));
638
- const matched = requests.filter((r) => r.outputTokens !== undefined && r.durationMs !== undefined && r.durationMs > 0);
639
- const duration = matched.reduce((sum, r) => sum + (r.durationMs ?? 0), 0), tokens = matched.reduce((sum, r) => sum + (r.outputTokens ?? 0), 0);
640
- const ttfts = requests.flatMap((r) => r.ttftMs === undefined ? [] : [r.ttftMs]).sort((a, b) => a - b);
641
- const percentile = (q) => ttfts.length ? (ttfts[Math.max(0, Math.ceil(ttfts.length * q) - 1)] ?? 0) / 1000 : undefined;
642
- for (const [label, value] of [
643
- ["Matched / observed requests", `${matched.length} / ${requests.length}`],
644
- [
645
- "Retained request throughput",
646
- duration ? `${number(tokens * 1000 / duration)} t/s` : "Unavailable"
647
- ],
648
- [
649
- "Median observed TTFT",
650
- ttfts.length ? `${number(percentile(0.5), 2)}s` : "Unavailable"
651
- ],
652
- [
653
- "95th percentile TTFT",
654
- ttfts.length ? `${number(percentile(0.95), 2)}s` : "Unavailable"
655
- ]
656
- ]) {
657
- const box = document.createElement("div");
658
- box.className = "stat";
659
- const name = document.createElement("span"), valueNode = document.createElement("strong");
660
- name.textContent = label ?? "";
661
- valueNode.textContent = value ?? "";
662
- box.append(name, valueNode);
663
- stats.append(box);
785
+ if (!collapsed)
786
+ for (const child of sorted(children.get(n.sessionId) ?? []))
787
+ visit(child, depth + 1);
788
+ };
789
+ const ranks = new Map(stableOrder.map((id, i) => [id, i]));
790
+ const sorted = (items) => [...items].sort((a, b) => (stableOrder.length ? (ranks.get(a.sessionId) ?? Infinity) - (ranks.get(b.sessionId) ?? Infinity) : 0) || (scores.get(b.sessionId) ?? -1) - (scores.get(a.sessionId) ?? -1) || a.sessionId.localeCompare(b.sessionId));
791
+ for (const n of sorted(children.get("") ?? []))
792
+ visit(n, 0);
793
+ for (const n of byId.values())
794
+ if (!visited.has(n.sessionId) && rows.length < limit)
795
+ visit(n, 0);
796
+ return rows;
797
+ }
798
+ function executionStatus(known, nativeRunning, evidence) {
799
+ if (nativeRunning || evidence.startedAt !== undefined && evidence.startedAt > (evidence.terminalAt ?? -1))
800
+ return "running";
801
+ if (evidence.outcome && (evidence.terminalAt ?? -1) >= (evidence.startedAt ?? -1))
802
+ return evidence.outcome === "succeeded" ? "completed" : evidence.outcome === "failed" ? "failed" : "unknown";
803
+ return known ? "idle" : "unknown";
804
+ }
805
+
806
+ // src/tui/transport.ts
807
+ class SnapshotClient {
808
+ transport;
809
+ changed;
810
+ elapsed;
811
+ rows = new Map;
812
+ health;
813
+ connection = "loading";
814
+ epoch;
815
+ sequence = -1;
816
+ revisions = new Map;
817
+ baselines = new Map;
818
+ controller;
819
+ generation = 0;
820
+ ids = [];
821
+ location = "";
822
+ refreshTimer;
823
+ reading;
824
+ buffered = [];
825
+ overflow = false;
826
+ retryTimer;
827
+ failures = 0;
828
+ serverAt = 0;
829
+ receivedAt = 0;
830
+ refreshAgain = false;
831
+ subscriptionActive = false;
832
+ constructor(transport, changed, elapsed = () => performance.now()) {
833
+ this.transport = transport;
834
+ this.changed = changed;
835
+ this.elapsed = elapsed;
836
+ }
837
+ now() {
838
+ return this.serverAt + Math.max(0, this.elapsed() - this.receivedAt);
839
+ }
840
+ setIds(ids) {
841
+ const next = [...new Set(ids)].slice(0, 2048);
842
+ if (JSON.stringify(next) === JSON.stringify(this.ids))
843
+ return;
844
+ this.ids = next;
845
+ const selected = new Set(next);
846
+ for (const id of new Set([
847
+ ...this.revisions.keys(),
848
+ ...this.baselines.keys()
849
+ ]))
850
+ if (!selected.has(id)) {
851
+ this.revisions.delete(id);
852
+ this.baselines.delete(id);
853
+ this.rows.delete(id);
854
+ }
855
+ if (this.reading)
856
+ this.refreshAgain = true;
857
+ else
858
+ this.refresh();
859
+ }
860
+ start(location, ids) {
861
+ this.stop();
862
+ this.location = location;
863
+ this.ids = [...new Set(ids)].slice(0, 2048);
864
+ this.rows.clear();
865
+ this.revisions.clear();
866
+ this.baselines.clear();
867
+ this.health = undefined;
868
+ this.epoch = undefined;
869
+ this.sequence = -1;
870
+ this.failures = 0;
871
+ this.connection = "loading";
872
+ const controller = new AbortController;
873
+ this.controller = controller;
874
+ const generation = ++this.generation;
875
+ this.supervise(generation, controller.signal);
876
+ this.refreshTimer = setInterval(() => {
877
+ this.refresh();
878
+ }, 15000);
879
+ this.refresh();
880
+ }
881
+ merge(batch, baseline = false, requested = []) {
882
+ if (this.epoch !== batch.epoch) {
883
+ if (!baseline) {
884
+ this.connection = "reconnecting";
885
+ this.refreshAgain = true;
886
+ return;
887
+ }
888
+ this.rows.clear();
889
+ this.revisions.clear();
890
+ this.baselines.clear();
891
+ this.epoch = batch.epoch;
892
+ this.sequence = -1;
664
893
  }
665
- const gaps = data.gaps.filter((g) => (g.sessionId === "__history__" || visible.some((s) => s.sessionId === g.sessionId)) && (start === undefined || g.time >= start) && (end === undefined || g.time <= end));
666
- const notes = [
667
- `Bounded retained history: ${metadata.retainedFrom === undefined ? "empty archive" : `segments since ${date(metadata.retainedFrom)}`}.`,
668
- `${gaps.length} recorded gap markers.`
669
- ];
670
- if (metadata.skippedBytes)
671
- notes.push(`${number(metadata.skippedBytes, 0)} bytes excluded by the read limit.`);
672
- if (metadata.malformedRecords)
673
- notes.push(`${metadata.malformedRecords} malformed/partial records skipped.`);
674
- if (data.omittedGaps)
675
- notes.push(`${data.omittedGaps} older gap markers omitted; curve discontinuities remain preserved.`);
676
- if (data.omittedSessions)
677
- notes.push(`${data.omittedSessions} sessions omitted from the report limit; regenerate with --session to inspect one.`);
678
- if (selected.length > 12)
679
- notes.push(`Showing 12 of ${selected.length} matching sessions; select a session for detail.`);
680
- if (visible.some((s) => s.originalPoints > s.points.length))
681
- notes.push(`Curves downsampled to at most ${data.maxPoints} observations per session, preserving extrema and gaps.`);
682
- if (visible.some((s) => s.points.some((p) => p.partial && matches2(p, start, end))))
683
- notes.push("Partial timing coverage is present.");
684
- if (!visible.length)
685
- notes.push("No observations match these filters.");
686
- notice.textContent = notes.join(" ");
687
- visible.forEach((s, index) => {
688
- const item = document.createElement("span"), dot = document.createElement("i");
689
- dot.className = "dot";
690
- dot.style.backgroundColor = color(index);
691
- item.append(dot, document.createTextNode(`${s.agents.join(", ") || "Agent"} \xB7 ${s.sessionId}${s.parentId ? ` (child of ${s.parentId})` : ""}`));
692
- legend.append(item);
894
+ if (!baseline && batch.sequence > this.sequence + 1) {
895
+ this.connection = "reconnecting";
896
+ this.refreshAgain = true;
897
+ }
898
+ if (batch.sequence >= this.sequence) {
899
+ this.sequence = batch.sequence;
900
+ this.health = batch.health;
901
+ this.serverAt = batch.generatedAt;
902
+ this.receivedAt = this.elapsed();
903
+ }
904
+ const selected = new Set(baseline ? requested.filter((id) => this.ids.includes(id)) : this.ids);
905
+ for (const row2 of batch.upserts)
906
+ if (selected.has(row2.sessionId) && (baseline || batch.sequence > (this.baselines.get(row2.sessionId) ?? -1)) && row2.revision > (this.revisions.get(row2.sessionId) ?? -1)) {
907
+ this.rows.set(row2.sessionId, row2);
908
+ this.revisions.set(row2.sessionId, row2.revision);
909
+ }
910
+ for (const row2 of batch.removed)
911
+ if (selected.has(row2.sessionId) && (baseline || batch.sequence > (this.baselines.get(row2.sessionId) ?? -1)) && row2.revision > (this.revisions.get(row2.sessionId) ?? -1)) {
912
+ this.rows.delete(row2.sessionId);
913
+ this.revisions.set(row2.sessionId, row2.revision);
914
+ }
915
+ this.changed();
916
+ }
917
+ refresh() {
918
+ if (this.reading)
919
+ return this.reading;
920
+ const controller = this.controller, generation = this.generation, location = this.location;
921
+ if (!controller)
922
+ return Promise.resolve();
923
+ clearTimeout(this.retryTimer);
924
+ const requestedIds = [...this.ids];
925
+ this.reading = (async () => {
926
+ const signal = AbortSignal.any([
927
+ controller.signal,
928
+ AbortSignal.timeout(5000)
929
+ ]);
930
+ const chunks = [];
931
+ for (let i = 0;i < requestedIds.length; i += 128)
932
+ chunks.push(requestedIds.slice(i, i + 128));
933
+ if (!chunks.length)
934
+ chunks.push([]);
935
+ const results = [];
936
+ for (let i = 0;i < chunks.length; i += 2) {
937
+ const reads = await Promise.all(chunks.slice(i, i + 2).map(async (ids) => {
938
+ const value = await abortable(this.transport.read(location, ids, signal), signal);
939
+ if (generation !== this.generation)
940
+ throw new Error("Obsolete read");
941
+ if (!isPerformanceResult(value)) {
942
+ this.connection = "incompatible";
943
+ throw new Error("Incompatible snapshot");
944
+ }
945
+ const answered = [
946
+ ...value.upserts.map((row2) => row2.sessionId),
947
+ ...value.unavailable.map((row2) => row2.sessionId)
948
+ ];
949
+ if (value.removed.length || answered.length !== ids.length || new Set(answered).size !== ids.length || answered.some((id) => !ids.includes(id)))
950
+ throw new Error("Incomplete snapshot read");
951
+ return { ids, result: value };
952
+ }));
953
+ results.push(...reads);
954
+ }
955
+ if (generation !== this.generation || signal.aborted)
956
+ return;
957
+ if (new Set(results.map((r) => r.result.epoch)).size !== 1)
958
+ throw new Error("Epoch changed during read");
959
+ for (const { ids, result } of results) {
960
+ this.merge(result, true, ids);
961
+ for (const missing of result.unavailable)
962
+ if (this.ids.includes(missing.sessionId))
963
+ this.rows.delete(missing.sessionId);
964
+ for (const id of ids)
965
+ if (this.ids.includes(id))
966
+ this.baselines.set(id, result.sequence);
967
+ }
968
+ const buffered = this.buffered;
969
+ this.buffered = [];
970
+ if (buffered.some((b) => b.epoch !== this.epoch) || this.overflow) {
971
+ this.overflow = false;
972
+ throw new Error("Stream changed during read");
973
+ }
974
+ for (const batch of buffered)
975
+ this.merge(batch);
976
+ this.failures = 0;
977
+ this.connection = this.refreshAgain || !this.subscriptionActive ? "reconnecting" : "live";
978
+ this.changed();
979
+ })().catch(() => {
980
+ if (generation === this.generation && !controller.signal.aborted) {
981
+ this.buffered = [];
982
+ this.overflow = false;
983
+ if (this.connection !== "incompatible")
984
+ this.connection = this.rows.size ? "reconnecting" : "unavailable";
985
+ this.changed();
986
+ this.retryTimer = setTimeout(() => {
987
+ this.refresh();
988
+ }, Math.min(30000, 1000 * 2 ** Math.min(this.failures++, 5)));
989
+ }
990
+ }).finally(() => {
991
+ if (generation === this.generation) {
992
+ this.reading = undefined;
993
+ if (this.refreshAgain) {
994
+ this.refreshAgain = false;
995
+ this.refresh();
996
+ }
997
+ }
693
998
  });
694
- const times = visible.flatMap((s) => [...s.points, ...s.requests].filter((p) => matches2(p, start, end)).map((p) => p.time));
695
- let minTime = start ?? times.reduce((a, t) => Math.min(a, t), Infinity), maxTime = end ?? times.reduce((a, t) => Math.max(a, t), -Infinity);
696
- if (!Number.isFinite(minTime))
697
- minTime = 0;
698
- if (!Number.isFinite(maxTime))
699
- maxTime = minTime + 1000;
700
- if (maxTime <= minTime)
701
- maxTime = minTime + 1000;
702
- for (const def of definitions) {
703
- const card = document.createElement("section");
704
- card.className = "chart";
705
- const heading = document.createElement("div");
706
- heading.className = "charthead";
707
- const title = document.createElement("h2");
708
- title.textContent = def.title;
709
- const download = document.createElement("button");
710
- download.textContent = "Save SVG";
711
- download.setAttribute("aria-label", `Save ${def.title} as SVG`);
712
- heading.append(title, download);
713
- const desc = document.createElement("p");
714
- desc.textContent = def.description;
715
- card.append(heading, desc);
716
- charts.append(card);
717
- const plots = visible.map((s) => {
718
- if (def.requests)
719
- return s.requests.filter((p) => matches2(p, start, end)).map((p) => {
720
- const raw = def.key === "ttftMs" ? p.ttftMs : p.tps;
721
- return {
722
- ...p,
723
- breakBefore: true,
724
- ...raw === undefined ? {} : { value: def.key === "ttftMs" ? raw / 1000 : raw }
725
- };
999
+ return this.reading;
1000
+ }
1001
+ async supervise(generation, signal) {
1002
+ let retry = 0;
1003
+ while (!signal.aborted && generation === this.generation) {
1004
+ try {
1005
+ this.subscriptionActive = true;
1006
+ if (retry)
1007
+ queueMicrotask(() => {
1008
+ if (generation === this.generation && !signal.aborted)
1009
+ this.refresh();
726
1010
  });
727
- let skipped = false;
728
- return s.points.flatMap((p) => {
729
- if (!matches2(p, start, end)) {
730
- skipped = true;
731
- return [];
1011
+ for await (const value of this.transport.subscribe(this.location, signal)) {
1012
+ if (signal.aborted || generation !== this.generation)
1013
+ return;
1014
+ if (!isPerformanceBatch(value)) {
1015
+ this.connection = "incompatible";
1016
+ this.changed();
1017
+ continue;
732
1018
  }
733
- const key = def.key;
734
- const value = key === "liveTps" ? p.liveTps : key === "averageModelTps" ? p.averageModelTps : key === "averageSessionTps" ? p.averageSessionTps : p.sessionOutputTokens;
735
- const point = {
736
- ...p,
737
- breakBefore: p.breakBefore || skipped,
738
- ...value === undefined ? {} : { value }
739
- };
740
- skipped = false;
741
- return [point];
742
- });
743
- });
744
- const drawn = plots.map((points) => {
745
- if (!def.requests || points.length <= data.maxPoints)
746
- return points;
747
- const bins = Math.max(1, Math.floor(data.maxPoints / 4)), chosen = new Set;
748
- for (let b = 0;b < bins; b++) {
749
- const first = Math.floor(b * points.length / bins), last = Math.floor((b + 1) * points.length / bins) - 1;
750
- chosen.add(first);
751
- chosen.add(last);
752
- let low = first, high = first;
753
- for (let i = first;i <= last; i++) {
754
- if ((points[i]?.value ?? Infinity) < (points[low]?.value ?? Infinity))
755
- low = i;
756
- if ((points[i]?.value ?? -Infinity) > (points[high]?.value ?? -Infinity))
757
- high = i;
1019
+ if (this.reading || !this.epoch) {
1020
+ if (this.buffered.length < 256)
1021
+ this.buffered.push(value);
1022
+ else
1023
+ this.overflow = true;
1024
+ } else {
1025
+ this.merge(value);
1026
+ if (this.refreshAgain) {
1027
+ this.refreshAgain = false;
1028
+ this.refresh();
1029
+ }
758
1030
  }
759
- chosen.add(low);
760
- chosen.add(high);
1031
+ retry = 0;
761
1032
  }
762
- return [...chosen].sort((a, b) => a - b).flatMap((i) => points[i] ? [points[i]] : []);
763
- });
764
- const maxValue = drawn.reduce((maximum, points) => points.reduce((n, p) => Math.max(n, p.value ?? 0), maximum), 1) * 1.08;
765
- const w = 720, h = 300, left = 64, right = 20, top = 24, bottom = 47;
766
- const x = (t) => left + (t - minTime) / (maxTime - minTime) * (w - left - right);
767
- const y = (v) => h - bottom - v / maxValue * (h - top - bottom);
768
- const svg = svgNode("svg", {
769
- viewBox: `0 0 ${w} ${h}`,
770
- role: "img",
771
- "aria-label": `${def.title} over UTC timestamps`,
772
- xmlns: ns
773
- });
774
- svg.append(svgNode("rect", { width: w, height: h, fill: "#111c2e" }), svgNode("title", {}, `${def.title}. ${def.description}`));
775
- svg.append(svgNode("text", {
776
- x: left,
777
- y: 14,
778
- fill: "#a8b5cb",
779
- "font-size": 11,
780
- "font-family": "system-ui,sans-serif"
781
- }, def.unit));
782
- for (let tick = 0;tick <= 4; tick++) {
783
- const v = maxValue * tick / 4;
784
- svg.append(svgNode("line", {
785
- x1: left,
786
- x2: w - right,
787
- y1: y(v),
788
- y2: y(v),
789
- stroke: "#2a3950",
790
- "stroke-width": 1
791
- }));
792
- svg.append(svgNode("text", {
793
- x: left - 8,
794
- y: y(v) + 4,
795
- fill: "#a8b5cb",
796
- "text-anchor": "end",
797
- "font-size": 11,
798
- "font-family": "system-ui,sans-serif"
799
- }, v >= 1e4 ? `${number(v / 1000)}k` : number(v)));
800
- const t = minTime + (maxTime - minTime) * tick / 4;
801
- const label = maxTime - minTime > 86400000 ? date(t).slice(5, 16).replace("T", " ") : date(t).slice(11, 19);
802
- svg.append(svgNode("text", {
803
- x: x(t),
804
- y: h - 22,
805
- fill: "#a8b5cb",
806
- "text-anchor": tick === 0 ? "start" : tick === 4 ? "end" : "middle",
807
- "font-size": 11,
808
- "font-family": "system-ui,sans-serif"
809
- }, label));
1033
+ } catch {
810
1034
  }
811
- for (const gap of gaps)
812
- if (gap.time >= minTime && gap.time <= maxTime) {
813
- const line = svgNode("line", {
814
- x1: x(gap.time),
815
- x2: x(gap.time),
816
- y1: top,
817
- y2: h - bottom,
818
- stroke: "#fb7185",
819
- "stroke-dasharray": "3 5",
820
- opacity: 0.65
821
- });
822
- line.append(svgNode("title", {}, `${date(gap.time)} \xB7 ${gap.reason}`));
823
- svg.append(line);
824
- }
825
- let count2 = 0;
826
- drawn.forEach((points, index) => {
827
- let run = [];
828
- const flush = () => {
829
- if (run.length > 1)
830
- svg.append(svgNode("polyline", {
831
- points: run.map((p) => `${x(p.time)},${y(p.value ?? 0)}`).join(" "),
832
- fill: "none",
833
- stroke: color(index),
834
- "stroke-width": 1.8,
835
- ...def.estimated || run.some((p) => p.partial) ? { "stroke-dasharray": "5 4" } : {}
836
- }));
837
- run = [];
1035
+ if (signal.aborted || generation !== this.generation)
1036
+ return;
1037
+ this.subscriptionActive = false;
1038
+ this.connection = "reconnecting";
1039
+ this.changed();
1040
+ await this.refresh();
1041
+ await new Promise((resolve2) => {
1042
+ const finish = () => {
1043
+ clearTimeout(timer);
1044
+ signal.removeEventListener("abort", finish);
1045
+ resolve2();
838
1046
  };
839
- for (const point of points) {
840
- if (point.breakBefore || point.value === undefined)
841
- flush();
842
- if (point.value === undefined)
843
- continue;
844
- count2++;
845
- if (!def.requests)
846
- run.push(point);
847
- const label = `${date(point.time)}\n${visible[index]?.sessionId ?? ""}${point.agent ? ` \xB7 ${point.agent}` : ""}${point.model ? `\n${point.model}` : ""}\n${def.title}: ${def.estimated ? "~" : ""}${number(point.value, 3)} ${def.unit}${point.requestId ? `\nRequest: ${point.requestId}` : ""}${point.partial ? "\nPartial observation coverage" : ""}`;
848
- const dot = svgNode("circle", {
849
- cx: x(point.time),
850
- cy: y(point.value),
851
- r: def.requests ? 3 : 2.5,
852
- fill: color(index),
853
- tabindex: 0,
854
- "aria-label": label
855
- });
856
- dot.append(svgNode("title", {}, label));
857
- dot.addEventListener("mouseenter", (e) => showTooltip(label, e.clientX, e.clientY));
858
- dot.addEventListener("mouseleave", () => {
859
- tooltip.hidden = true;
860
- });
861
- dot.addEventListener("focus", () => {
862
- const box = dot.getBoundingClientRect();
863
- showTooltip(label, box.x, box.y);
1047
+ const timer = setTimeout(finish, Math.min(30000, 1000 * 2 ** Math.min(retry++, 5)));
1048
+ signal.addEventListener("abort", finish, { once: true });
1049
+ if (signal.aborted)
1050
+ finish();
1051
+ });
1052
+ }
1053
+ }
1054
+ stop() {
1055
+ this.generation++;
1056
+ this.controller?.abort();
1057
+ this.controller = undefined;
1058
+ clearInterval(this.refreshTimer);
1059
+ clearTimeout(this.retryTimer);
1060
+ this.reading = undefined;
1061
+ this.buffered = [];
1062
+ this.overflow = false;
1063
+ this.refreshAgain = false;
1064
+ }
1065
+ }
1066
+ function abortable(promise, signal) {
1067
+ return new Promise((resolve2, reject) => {
1068
+ const abort = () => reject(new Error("Snapshot request cancelled"));
1069
+ if (signal.aborted) {
1070
+ abort();
1071
+ return;
1072
+ }
1073
+ signal.addEventListener("abort", abort, { once: true });
1074
+ promise.then(resolve2, reject).finally(() => signal.removeEventListener("abort", abort));
1075
+ });
1076
+ }
1077
+ var init_transport = __esm(() => {
1078
+ init_rpc();
1079
+ });
1080
+
1081
+ // src/dashboard/inventory.ts
1082
+ function validateInventoryLimits(limits) {
1083
+ if (!Number.isInteger(limits.maxNodes) || limits.maxNodes < 1 || limits.maxNodes > 2048 || !Number.isInteger(limits.maxPages) || limits.maxPages < 1 || limits.maxPages > 100 || !Number.isInteger(limits.maxDepth) || limits.maxDepth < 0 || limits.maxDepth > 64)
1084
+ throw new Error("Invalid dashboard inventory limits");
1085
+ }
1086
+ async function discoverInventory(port, signal, limits = inventoryLimits) {
1087
+ validateInventoryLimits(limits);
1088
+ const sessions = new Map;
1089
+ const notices = new Set;
1090
+ const queue = [{ parent: null, depth: 0, cursors: [] }];
1091
+ let pages = 0;
1092
+ while (queue.length && pages < limits.maxPages) {
1093
+ signal.throwIfAborted();
1094
+ const branch = queue.shift();
1095
+ if (!branch)
1096
+ break;
1097
+ try {
1098
+ pages++;
1099
+ const page = await port.listSessions({
1100
+ parentID: branch.parent,
1101
+ limit: 100,
1102
+ ...branch.cursor ? { cursor: branch.cursor } : {}
1103
+ }, signal);
1104
+ signal.throwIfAborted();
1105
+ for (const session of page.data) {
1106
+ if (session.directory !== port.location.directory) {
1107
+ notices.add("foreign-location");
1108
+ continue;
1109
+ }
1110
+ if ((session.parentId ?? null) !== branch.parent || sessions.has(session.sessionId)) {
1111
+ notices.add("invalid-hierarchy");
1112
+ continue;
1113
+ }
1114
+ if (sessions.size >= limits.maxNodes) {
1115
+ notices.add("node-limit");
1116
+ return {
1117
+ sessions: [...sessions.values()],
1118
+ complete: false,
1119
+ pages,
1120
+ notices: [...notices]
1121
+ };
1122
+ }
1123
+ sessions.set(session.sessionId, session);
1124
+ if (branch.depth < limits.maxDepth)
1125
+ queue.push({
1126
+ parent: session.sessionId,
1127
+ depth: branch.depth + 1,
1128
+ cursors: []
864
1129
  });
865
- dot.addEventListener("blur", () => {
866
- tooltip.hidden = true;
1130
+ else
1131
+ notices.add("depth-limit");
1132
+ }
1133
+ const cursor = page.cursor?.next;
1134
+ if (cursor) {
1135
+ if (cursor === branch.cursor || branch.cursors.includes(cursor))
1136
+ notices.add("repeated-cursor");
1137
+ else
1138
+ queue.unshift({
1139
+ ...branch,
1140
+ cursor,
1141
+ cursors: [...branch.cursors, cursor]
867
1142
  });
868
- svg.append(dot);
869
- }
870
- flush();
871
- });
872
- if (!count2)
873
- svg.append(svgNode("text", {
874
- x: w / 2,
875
- y: h / 2,
876
- fill: "#a8b5cb",
877
- "text-anchor": "middle",
878
- "font-size": 14,
879
- "font-family": "system-ui,sans-serif"
880
- }, "No measurements in this selection"));
881
- card.append(svg);
882
- download.onclick = () => {
883
- const blob = new Blob([new XMLSerializer().serializeToString(svg)], {
884
- type: "image/svg+xml"
885
- });
886
- const url = URL.createObjectURL(blob);
887
- const link = document.createElement("a");
888
- link.href = url;
889
- link.download = `context-guard-${def.key}.svg`;
890
- link.click();
891
- setTimeout(() => URL.revokeObjectURL(url), 1000);
892
- };
1143
+ }
1144
+ } catch {
1145
+ signal.throwIfAborted();
1146
+ notices.add("inventory-unavailable");
893
1147
  }
894
1148
  }
895
- for (const control of [session, agent, model, from, to])
896
- control.addEventListener("change", render);
897
- element("reset", HTMLButtonElement).onclick = () => {
898
- reset();
899
- render();
1149
+ if (queue.length)
1150
+ notices.add("page-limit");
1151
+ return {
1152
+ sessions: [...sessions.values()],
1153
+ complete: !notices.size,
1154
+ pages,
1155
+ notices: [...notices]
900
1156
  };
901
- reset();
902
- render();
903
1157
  }
1158
+ var inventoryLimits;
1159
+ var init_inventory = __esm(() => {
1160
+ inventoryLimits = Object.freeze({
1161
+ maxNodes: 1024,
1162
+ maxPages: 100,
1163
+ maxDepth: 64
1164
+ });
1165
+ });
904
1166
 
905
- // src/history.ts
906
- import { mkdir, readdir, stat, open, unlink } from "fs/promises";
907
- import { join } from "path";
908
- var filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
909
- async function segments(directory) {
910
- const names = await readdir(directory).catch((error) => {
911
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
912
- return [];
913
- throw error;
1167
+ // src/dashboard/view.ts
1168
+ function immutableView(value) {
1169
+ const copy = structuredClone(value);
1170
+ const freeze = (item) => {
1171
+ if (item === null || typeof item !== "object")
1172
+ return;
1173
+ for (const child of Object.values(item))
1174
+ freeze(child);
1175
+ Object.freeze(item);
1176
+ };
1177
+ freeze(copy);
1178
+ return copy;
1179
+ }
1180
+ function projectHistory(result, filters) {
1181
+ const match = (point) => (!filters.agent || point.agent === filters.agent) && (!filters.model || point.model === filters.model);
1182
+ const series = result.data.series.map((series2) => {
1183
+ let gap = false;
1184
+ const points = series2.points.flatMap((point) => {
1185
+ gap ||= point.breakBefore;
1186
+ if (!match(point))
1187
+ return [];
1188
+ const next = { ...point, breakBefore: gap };
1189
+ gap = false;
1190
+ return [next];
1191
+ });
1192
+ return { ...series2, points, requests: series2.requests.filter(match) };
914
1193
  });
915
- return (await Promise.all(names.filter((n) => filename.test(n)).map(async (name) => ({
916
- name,
917
- time: Number(filename.exec(name)?.[1]),
918
- bytes: (await stat(join(directory, name))).size
919
- })))).sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
1194
+ const requests = series.flatMap((series2) => series2.requests);
1195
+ const matched = requests.filter((request) => request.outputTokens !== undefined && request.durationMs !== undefined && request.durationMs > 0);
1196
+ const output = matched.reduce((sum, request) => sum + (request.outputTokens ?? 0), 0);
1197
+ const duration = matched.reduce((sum, request) => sum + (request.durationMs ?? 0), 0);
1198
+ const partial = result.truncated || result.metadata.skippedBytes > 0 || result.metadata.malformedRecords > 0 || result.data.omittedSessions > 0 || requests.some((request) => request.partial) || matched.length !== requests.length || result.data.gaps.length > 0 || (result.data.omittedGaps ?? 0) > 0;
1199
+ const notices = [
1200
+ "bounded-retention",
1201
+ "maximum-seven-days",
1202
+ "maximum-500-points-and-requests"
1203
+ ];
1204
+ if (!result.enabled)
1205
+ notices.push("history-disabled");
1206
+ if (result.truncated || result.metadata.skippedBytes || result.metadata.malformedRecords || result.data.omittedSessions)
1207
+ notices.push("partial-history");
1208
+ if (result.data.gaps.length || result.data.omittedGaps || result.data.series.some((series2) => series2.points.some((point) => point.breakBefore)))
1209
+ notices.push("history-gaps");
1210
+ if (result.data.series.some((series2) => series2.originalPoints > series2.points.length))
1211
+ notices.push("downsampled-history");
1212
+ if (result.data.series.some((series2) => series2.points.some((point) => point.partial)) || requests.some((request) => request.partial))
1213
+ notices.push("partial-measurements");
1214
+ return {
1215
+ result: { ...result, data: { ...result.data, series } },
1216
+ summary: {
1217
+ source: "matched-request-records",
1218
+ observedRequests: requests.length,
1219
+ matchedRequests: matched.length,
1220
+ partial,
1221
+ ...matched.length ? {
1222
+ outputTokens: output,
1223
+ durationMs: duration,
1224
+ averageModelTps: output * 1000 / duration
1225
+ } : {}
1226
+ },
1227
+ notices
1228
+ };
920
1229
  }
1230
+ var historyWindows;
1231
+ var init_view = __esm(() => {
1232
+ historyWindows = [300000, 3600000, 86400000, 604800000];
1233
+ });
921
1234
 
922
- class PerformanceHistory {
923
- path;
924
- config;
1235
+ // src/dashboard/controller.ts
1236
+ class DashboardController {
1237
+ locations = new Map;
1238
+ cache = new Map;
1239
+ listeners = new Set;
1240
+ limits;
925
1241
  now;
926
- report;
927
- pending = [];
928
- working;
929
- active;
930
- bytes = 0;
931
- started = 0;
932
- disabled = false;
933
- dropped = 0;
934
- droppedTotal = 0;
935
- constructor(path, config, now = Date.now, report = () => {
936
- }) {
937
- this.path = path;
938
- this.config = config;
939
- this.now = now;
940
- this.report = report;
1242
+ elapsed;
1243
+ refreshIntervalMs;
1244
+ state;
1245
+ scope;
1246
+ running = false;
1247
+ screen = "overview";
1248
+ filters = { search: "", agent: "", model: "" };
1249
+ windowMs = 300000;
1250
+ generation = 0;
1251
+ historyGeneration = 0;
1252
+ pendingHistory;
1253
+ historyAbort;
1254
+ historyBusy = false;
1255
+ historyStatus = "idle";
1256
+ refreshTimer;
1257
+ ageTimer;
1258
+ view;
1259
+ constructor(port, options = {}) {
1260
+ this.limits = { ...options.limits ?? inventoryLimits };
1261
+ validateInventoryLimits(this.limits);
1262
+ this.now = options.now ?? Date.now;
1263
+ this.elapsed = options.elapsed ?? (() => performance.now());
1264
+ this.refreshIntervalMs = options.refreshIntervalMs ?? 30000;
1265
+ if (!Number.isSafeInteger(this.refreshIntervalMs) || this.refreshIntervalMs < 0)
1266
+ throw new Error("Invalid dashboard refresh interval");
1267
+ this.state = this.createLocation(port);
1268
+ this.locations.set(port.location.directory, this.state);
1269
+ this.view = this.buildView();
941
1270
  }
942
- emit(record2) {
943
- if (!this.config.enabled || this.disabled)
1271
+ createLocation(port) {
1272
+ return {
1273
+ port,
1274
+ nodes: new Map,
1275
+ rows: new Map,
1276
+ connection: "loading",
1277
+ complete: false,
1278
+ stale: true,
1279
+ pages: 0,
1280
+ notices: []
1281
+ };
1282
+ }
1283
+ getView() {
1284
+ return this.view;
1285
+ }
1286
+ subscribe(listener) {
1287
+ this.listeners.add(listener);
1288
+ listener(this.view);
1289
+ return () => {
1290
+ this.listeners.delete(listener);
1291
+ };
1292
+ }
1293
+ publish() {
1294
+ this.view = this.buildView();
1295
+ for (const listener of this.listeners) {
1296
+ try {
1297
+ listener(this.view);
1298
+ } catch {
1299
+ }
1300
+ }
1301
+ }
1302
+ async start() {
1303
+ if (this.running)
944
1304
  return;
945
- const line = JSON.stringify({
946
- schema: 1,
947
- recordId: record2.recordId,
948
- time: record2.time,
949
- type: record2.type,
950
- sessionId: record2.sessionId,
951
- ...record2.parentId ? { parentId: record2.parentId } : {},
952
- ...record2.agent ? { agent: record2.agent } : {},
953
- ...record2.model ? { model: record2.model } : {},
954
- ...record2.event ? { event: record2.event } : {},
955
- ...record2.droppedRecords === undefined ? {} : { droppedRecords: record2.droppedRecords },
956
- ...record2.performance ? { performance: scalarPerformance(record2.performance) } : {}
957
- }) + "\n";
958
- if (this.pending.length >= 256 || Buffer.byteLength(line) > Math.min(4 * 1024 * 1024, this.config.maxBytes)) {
959
- this.dropped++;
960
- this.droppedTotal++;
961
- if (this.dropped === 1)
962
- this.report("history-records-dropped");
1305
+ this.running = true;
1306
+ this.scope = new AbortController;
1307
+ if (this.refreshIntervalMs) {
1308
+ this.refreshTimer = setInterval(() => {
1309
+ this.refresh();
1310
+ }, this.refreshIntervalMs);
1311
+ this.ageTimer = setInterval(() => {
1312
+ this.publish();
1313
+ }, 1000);
1314
+ }
1315
+ await this.refresh();
1316
+ }
1317
+ stop() {
1318
+ this.running = false;
1319
+ this.generation++;
1320
+ this.scope?.abort();
1321
+ this.scope = undefined;
1322
+ this.cancelHistory();
1323
+ clearInterval(this.refreshTimer);
1324
+ clearInterval(this.ageTimer);
1325
+ for (const state of this.locations.values()) {
1326
+ state.client?.stop();
1327
+ delete state.client;
1328
+ delete state.scanning;
1329
+ delete state.probing;
1330
+ state.stale = true;
1331
+ state.connection = "unavailable";
1332
+ }
1333
+ this.publish();
1334
+ }
1335
+ async selectLocation(directory) {
1336
+ if (directory === this.state.port.location.directory)
963
1337
  return;
1338
+ const port = this.state.port.atLocation(directory);
1339
+ this.scope?.abort();
1340
+ this.state.client?.stop();
1341
+ delete this.state.client;
1342
+ delete this.state.scanning;
1343
+ delete this.state.probing;
1344
+ this.state.stale = true;
1345
+ this.state.connection = "reconnecting";
1346
+ this.cancelHistory();
1347
+ this.generation++;
1348
+ const next = this.locations.get(directory) ?? this.createLocation(port);
1349
+ this.locations.delete(directory);
1350
+ this.locations.set(directory, next);
1351
+ this.state = next;
1352
+ if (this.locations.size > 4) {
1353
+ const oldest = this.locations.keys().next().value;
1354
+ if (oldest !== undefined)
1355
+ this.locations.delete(oldest);
964
1356
  }
965
- this.pending.push(line);
966
- this.kick();
1357
+ this.scope = this.running ? new AbortController : undefined;
1358
+ this.historyStatus = "idle";
1359
+ this.publish();
1360
+ if (this.running)
1361
+ await this.refresh();
967
1362
  }
968
- kick() {
969
- if (this.working || this.disabled)
1363
+ selectSession(id) {
1364
+ if (!this.state.nodes.has(id))
1365
+ throw new Error("Unknown dashboard session");
1366
+ if (id === this.state.selected)
970
1367
  return;
971
- this.working = this.drain().catch(() => {
972
- this.disabled = true;
973
- this.pending = [];
974
- this.report("history-unavailable");
975
- }).finally(() => {
976
- this.working = undefined;
977
- if (this.pending.length)
978
- this.kick();
979
- });
1368
+ this.state.selected = id;
1369
+ this.queueHistory();
1370
+ this.publish();
980
1371
  }
981
- async prune(incoming = 0) {
982
- const files = await segments(this.path);
983
- let total = files.reduce((n, f) => n + f.bytes, 0);
984
- let truncated = false;
985
- for (const file of files) {
986
- if (file.time >= this.now() - this.config.retentionDays * 86400000 && total + incoming <= this.config.maxBytes)
987
- break;
988
- await unlink(join(this.path, file.name));
989
- if (file.name === this.active) {
990
- this.active = undefined;
991
- this.bytes = 0;
992
- }
993
- total -= file.bytes;
994
- truncated = true;
995
- }
996
- if (truncated)
997
- this.report("history-truncated");
1372
+ setScreen(screen) {
1373
+ if (!["overview", "detail", "history"].includes(screen))
1374
+ throw new Error("Invalid dashboard screen");
1375
+ if (screen === this.screen)
1376
+ return;
1377
+ this.screen = screen;
1378
+ this.queueHistory();
1379
+ this.publish();
998
1380
  }
999
- gapLine(event, droppedRecords) {
1000
- return JSON.stringify({
1001
- schema: 1,
1002
- recordId: crypto.randomUUID(),
1003
- time: this.now(),
1004
- type: "gap",
1005
- sessionId: "__history__",
1006
- event,
1007
- ...droppedRecords === undefined ? {} : { droppedRecords }
1008
- }) + "\n";
1381
+ setWindow(windowMs) {
1382
+ if (!historyWindows.includes(windowMs))
1383
+ throw new Error("Invalid dashboard history window");
1384
+ if (windowMs === this.windowMs)
1385
+ return;
1386
+ this.windowMs = windowMs;
1387
+ this.queueHistory();
1388
+ this.publish();
1009
1389
  }
1010
- async drain() {
1011
- await mkdir(this.path, { recursive: true, mode: 448 });
1012
- await this.prune();
1013
- while (this.pending.length) {
1014
- let line = this.pending.shift();
1015
- if (!line)
1016
- continue;
1017
- if (this.dropped > 0) {
1018
- this.pending.unshift(line);
1019
- line = this.gapLine("history-records-dropped", this.dropped);
1020
- this.dropped = 0;
1021
- }
1022
- const size = Buffer.byteLength(line);
1023
- const now = this.now();
1024
- if (!this.active || this.bytes + size > Math.min(4 * 1024 * 1024, this.config.maxBytes) || now - this.started >= 3600000) {
1025
- this.active = undefined;
1026
- this.bytes = 0;
1027
- await this.prune(size);
1028
- this.started = now;
1029
- this.active = `${now}-${crypto.randomUUID()}.jsonl`;
1030
- } else
1031
- await this.prune(size);
1032
- if (!this.active) {
1033
- this.started = now;
1034
- this.active = `${now}-${crypto.randomUUID()}.jsonl`;
1390
+ setFilters(filters) {
1391
+ for (const value of Object.values(filters))
1392
+ if (typeof value !== "string" || value.length > 512)
1393
+ throw new Error("Invalid dashboard filter");
1394
+ this.filters = {
1395
+ search: filters.search ?? this.filters.search,
1396
+ agent: filters.agent ?? this.filters.agent,
1397
+ model: filters.model ?? this.filters.model
1398
+ };
1399
+ this.publish();
1400
+ }
1401
+ async refresh() {
1402
+ if (!this.running || !this.scope)
1403
+ return;
1404
+ for (const cached of this.cache.values())
1405
+ if (cached.directory === this.state.port.location.directory)
1406
+ cached.valid = false;
1407
+ this.queueHistory();
1408
+ const state = this.state, generation = this.generation, signal = this.scope.signal;
1409
+ await Promise.all([
1410
+ this.scan(state, generation, signal),
1411
+ this.monitor(state, generation, signal)
1412
+ ]);
1413
+ if (this.current(state, generation, signal))
1414
+ await state.client?.refresh();
1415
+ }
1416
+ current(state, generation, signal) {
1417
+ return this.running && this.state === state && generation === this.generation && !signal.aborted;
1418
+ }
1419
+ scan(state, generation, signal) {
1420
+ if (state.scanning)
1421
+ return state.scanning;
1422
+ state.scanning = (async () => {
1423
+ try {
1424
+ const scanSignal = AbortSignal.any([
1425
+ signal,
1426
+ AbortSignal.timeout(15000)
1427
+ ]);
1428
+ const result = await discoverInventory(state.port, scanSignal, this.limits);
1429
+ let active;
1430
+ try {
1431
+ active = new Set(await state.port.activeSessions(result.sessions.map((session) => session.sessionId), scanSignal));
1432
+ } catch {
1433
+ }
1434
+ if (!this.current(state, generation, signal))
1435
+ return;
1436
+ const nodes = new Map;
1437
+ for (const session of result.sessions)
1438
+ nodes.set(session.sessionId, {
1439
+ ...session,
1440
+ status: active ? executionStatus(true, active.has(session.sessionId), {
1441
+ ...session.outcome ? { outcome: session.outcome } : {},
1442
+ ...session.terminalAt === undefined ? {} : { terminalAt: session.terminalAt }
1443
+ }) : state.nodes.get(session.sessionId)?.status ?? "unknown",
1444
+ inventoryStale: active === undefined
1445
+ });
1446
+ if (!result.complete) {
1447
+ for (const [id, node] of state.nodes)
1448
+ if (!nodes.has(id) && nodes.size < this.limits.maxNodes)
1449
+ nodes.set(id, { ...node, inventoryStale: true });
1450
+ }
1451
+ state.nodes = nodes;
1452
+ state.complete = result.complete && active !== undefined;
1453
+ state.stale = !state.complete;
1454
+ state.pages = result.pages;
1455
+ state.notices = active ? result.notices : [...new Set([...result.notices, "inventory-unavailable"])];
1456
+ if (!state.selected || !nodes.has(state.selected)) {
1457
+ const previous = state.selected;
1458
+ const first = nodes.keys().next().value;
1459
+ if (first === undefined)
1460
+ delete state.selected;
1461
+ else
1462
+ state.selected = first;
1463
+ if (previous !== state.selected)
1464
+ this.queueHistory();
1465
+ }
1466
+ state.client?.setIds([...nodes.keys()]);
1467
+ } catch {
1468
+ if (!this.current(state, generation, signal))
1469
+ return;
1470
+ state.complete = false;
1471
+ state.stale = true;
1472
+ state.nodes = new Map([...state.nodes].map(([id, node]) => [
1473
+ id,
1474
+ { ...node, inventoryStale: true }
1475
+ ]));
1476
+ state.notices = ["inventory-unavailable"];
1477
+ } finally {
1478
+ if (this.current(state, generation, signal)) {
1479
+ delete state.scanning;
1480
+ this.publish();
1481
+ }
1035
1482
  }
1036
- const file = await open(join(this.path, this.active), "a", 384);
1483
+ })();
1484
+ this.publish();
1485
+ return state.scanning;
1486
+ }
1487
+ monitor(state, generation, signal) {
1488
+ if (state.client)
1489
+ return Promise.resolve();
1490
+ if (state.probing)
1491
+ return state.probing;
1492
+ state.probing = (async () => {
1037
1493
  try {
1038
- await file.writeFile(line);
1494
+ const monitor = await state.port.monitor(signal);
1495
+ if (!this.current(state, generation, signal))
1496
+ return;
1497
+ state.version = monitor.version;
1498
+ const client = new SnapshotClient(monitor.transport, () => {
1499
+ if (!this.current(state, generation, signal))
1500
+ return;
1501
+ state.connection = client.connection;
1502
+ if (client.health) {
1503
+ state.rows = new Map(client.rows);
1504
+ state.health = client.health;
1505
+ state.serverAt = client.now();
1506
+ state.receivedAt = this.elapsed();
1507
+ }
1508
+ this.publish();
1509
+ }, this.elapsed);
1510
+ state.client = client;
1511
+ client.start(state.port.location.directory, [...state.nodes.keys()]);
1512
+ } catch {
1513
+ if (this.current(state, generation, signal)) {
1514
+ state.connection = state.rows.size ? "reconnecting" : "unavailable";
1515
+ this.publish();
1516
+ }
1039
1517
  } finally {
1040
- await file.close();
1518
+ if (this.current(state, generation, signal))
1519
+ delete state.probing;
1041
1520
  }
1042
- this.bytes += size;
1043
- }
1521
+ })();
1522
+ return state.probing;
1044
1523
  }
1045
- health() {
1046
- return { unavailable: this.disabled, droppedRecords: this.droppedTotal };
1524
+ cacheKey() {
1525
+ return JSON.stringify([
1526
+ this.state.port.location.directory,
1527
+ this.state.selected,
1528
+ this.windowMs
1529
+ ]);
1047
1530
  }
1048
- async flush() {
1049
- while (this.working)
1050
- await this.working;
1531
+ cancelHistory() {
1532
+ this.historyGeneration++;
1533
+ this.pendingHistory = undefined;
1534
+ this.historyAbort?.abort();
1051
1535
  }
1052
- async initialize() {
1053
- if (!this.config.enabled)
1536
+ queueHistory() {
1537
+ this.cancelHistory();
1538
+ const sessionId = this.state.selected;
1539
+ if (!this.running || !this.scope || this.screen !== "history" || !sessionId) {
1540
+ this.historyStatus = "idle";
1054
1541
  return;
1055
- await mkdir(this.path, { recursive: true, mode: 448 }).then(() => this.prune()).catch(() => {
1056
- this.disabled = true;
1057
- this.report("history-unavailable");
1542
+ }
1543
+ const key = this.cacheKey(), cached = this.cache.get(key);
1544
+ if (cached?.valid) {
1545
+ this.cache.delete(key);
1546
+ this.cache.set(key, cached);
1547
+ this.historyStatus = cached.result.enabled ? "ready" : "disabled";
1548
+ return;
1549
+ }
1550
+ this.historyStatus = "loading";
1551
+ this.pendingHistory = {
1552
+ key,
1553
+ state: this.state,
1554
+ sessionId,
1555
+ windowMs: this.windowMs,
1556
+ generation: this.historyGeneration,
1557
+ scope: this.scope.signal
1558
+ };
1559
+ this.pumpHistory();
1560
+ }
1561
+ async pumpHistory() {
1562
+ if (this.historyBusy || !this.pendingHistory)
1563
+ return;
1564
+ const task = this.pendingHistory;
1565
+ this.pendingHistory = undefined;
1566
+ this.historyBusy = true;
1567
+ const controller = new AbortController;
1568
+ this.historyAbort = controller;
1569
+ const signal = AbortSignal.any([
1570
+ controller.signal,
1571
+ task.scope,
1572
+ AbortSignal.timeout(5000)
1573
+ ]);
1574
+ const to = Math.max(0, Math.floor(this.serverNow()));
1575
+ const from = Math.max(0, to - task.windowMs);
1576
+ try {
1577
+ const result = await task.state.port.history({
1578
+ schemaVersion: 1,
1579
+ sessionId: task.sessionId,
1580
+ from,
1581
+ to,
1582
+ maxPoints: 500
1583
+ }, signal);
1584
+ if (signal.aborted || task.generation !== this.historyGeneration)
1585
+ return;
1586
+ if (!isHistoryResult(result) || result.data.series.some((series) => series.sessionId !== task.sessionId))
1587
+ throw new Error("Invalid history response");
1588
+ this.cache.set(task.key, {
1589
+ directory: task.state.port.location.directory,
1590
+ valid: true,
1591
+ result: structuredClone(result),
1592
+ from,
1593
+ to
1594
+ });
1595
+ if (this.cache.size > 16) {
1596
+ const oldest = this.cache.keys().next().value;
1597
+ if (oldest !== undefined)
1598
+ this.cache.delete(oldest);
1599
+ }
1600
+ this.historyStatus = result.enabled ? "ready" : "disabled";
1601
+ } catch {
1602
+ if (task.generation === this.historyGeneration && !task.scope.aborted)
1603
+ this.historyStatus = "unavailable";
1604
+ } finally {
1605
+ if (task.generation === this.historyGeneration && signal.aborted && !task.scope.aborted)
1606
+ this.historyStatus = "unavailable";
1607
+ this.historyBusy = false;
1608
+ if (this.historyAbort === controller)
1609
+ this.historyAbort = undefined;
1610
+ if (this.running)
1611
+ this.publish();
1612
+ this.pumpHistory();
1613
+ }
1614
+ }
1615
+ serverNow() {
1616
+ if (this.state.client?.health)
1617
+ return this.state.client.now();
1618
+ if (this.state.serverAt !== undefined && this.state.receivedAt !== undefined)
1619
+ return this.state.serverAt + Math.max(0, this.elapsed() - this.state.receivedAt);
1620
+ return this.now();
1621
+ }
1622
+ buildView() {
1623
+ const state = this.state, now = this.serverNow();
1624
+ const nodes = [...state.nodes.values()].map((node) => {
1625
+ const snapshot = state.rows.get(node.sessionId);
1626
+ const agent = snapshot?.agent ?? node.agent;
1627
+ const model = snapshot?.model ?? node.model;
1628
+ return {
1629
+ sessionId: node.sessionId,
1630
+ status: node.status,
1631
+ ...node.parentId ? { parentId: node.parentId } : {},
1632
+ ...agent ? { agent } : {},
1633
+ ...model ? { model } : {},
1634
+ ...node.outcome === "interrupted" ? { interrupted: true } : {}
1635
+ };
1636
+ });
1637
+ const rows = familyRows(nodes, state.rows, state.selected ?? "", now, state.health?.staleMs ?? 300000, new Set(state.nodes.keys()), this.limits.maxNodes).map((row2) => {
1638
+ const liveStale = !this.running || state.connection !== "live";
1639
+ const measurement2 = row2.snapshot?.measurement;
1640
+ let snapshot = row2.snapshot;
1641
+ if (snapshot && state.version === 1) {
1642
+ const { performance: _, ...context } = snapshot;
1643
+ snapshot = context;
1644
+ }
1645
+ const stale = liveStale && row2.freshness === "fresh";
1646
+ const p = snapshot?.performance;
1647
+ const { pressureRatio, triggerDeltaTokens, ...retained } = row2;
1648
+ return {
1649
+ ...stale ? { ...retained, freshness: "stale", pressure: "unknown" } : row2,
1650
+ ...snapshot ? { snapshot } : {},
1651
+ inventoryStale: !this.running || state.stale || (state.nodes.get(row2.node.sessionId)?.inventoryStale ?? true),
1652
+ liveStale,
1653
+ ...measurement2 && now >= measurement2.at ? { measurementAgeMs: now - measurement2.at } : {},
1654
+ performanceAvailable: state.version === 2 && snapshot?.performance !== undefined,
1655
+ performanceFreshness: !p ? "missing" : p.at > now ? "invalid" : liveStale || now - p.at > (state.health?.staleMs ?? 300000) ? "stale" : "fresh"
1656
+ };
1657
+ });
1658
+ const search = this.filters.search.toLowerCase();
1659
+ const visible = rows.filter((row2) => (!search || [row2.node.sessionId, row2.node.agent, row2.node.model].some((value) => value?.toLowerCase().includes(search))) && (!this.filters.agent || row2.node.agent === this.filters.agent) && (!this.filters.model || row2.node.model === this.filters.model));
1660
+ const cached = this.cache.get(this.cacheKey());
1661
+ const projected = cached ? projectHistory(cached.result, this.filters) : undefined;
1662
+ const selected = rows.find((row2) => row2.node.sessionId === state.selected);
1663
+ return immutableView({
1664
+ schemaVersion: 1,
1665
+ generatedAt: now,
1666
+ running: this.running,
1667
+ location: state.port.location.directory,
1668
+ locations: [...this.locations.keys()],
1669
+ screen: this.screen,
1670
+ filters: this.filters,
1671
+ connection: state.connection,
1672
+ ...state.version === undefined ? {} : { monitorVersion: state.version },
1673
+ ...state.health ? { health: state.health } : {},
1674
+ inventory: {
1675
+ refreshing: state.scanning !== undefined,
1676
+ complete: state.complete,
1677
+ stale: state.stale,
1678
+ pages: state.pages,
1679
+ total: rows.length,
1680
+ visible: visible.length,
1681
+ monitored: rows.filter((row2) => row2.snapshot).length,
1682
+ active: rows.filter((row2) => row2.node.status === "running").length,
1683
+ ...this.limits,
1684
+ notices: state.notices
1685
+ },
1686
+ rows: visible,
1687
+ ...state.selected ? { selectedSessionId: state.selected } : {},
1688
+ ...selected ? { selected } : {},
1689
+ history: {
1690
+ status: this.historyStatus,
1691
+ windowMs: this.windowMs,
1692
+ maxWindowMs: 604800000,
1693
+ maxPoints: 500,
1694
+ stale: this.historyStatus === "loading" || this.historyStatus === "unavailable" || !this.running,
1695
+ ...state.selected ? { sessionId: state.selected } : {},
1696
+ ...cached ? { from: cached.from, to: cached.to } : {},
1697
+ ...projected ? { result: projected.result, summary: projected.summary } : {},
1698
+ notices: projected?.notices ?? [
1699
+ "bounded-retention",
1700
+ "maximum-seven-days",
1701
+ "maximum-500-points-and-requests"
1702
+ ]
1703
+ },
1704
+ notices: [
1705
+ ...!state.complete ? ["partial-inventory"] : [],
1706
+ ...state.connection !== "live" ? ["live-data-stale"] : [],
1707
+ ...state.version === 1 ? ["performance-unavailable-v1"] : []
1708
+ ]
1058
1709
  });
1059
1710
  }
1060
1711
  }
1061
- function scalarPerformance(p) {
1712
+ var init_controller = __esm(() => {
1713
+ init_rpc();
1714
+ init_transport();
1715
+ init_inventory();
1716
+ init_view();
1717
+ });
1718
+
1719
+ // src/dashboard/connection.ts
1720
+ import { OpenCode } from "@opencode/client";
1721
+ import { Service } from "@opencode/client/service";
1722
+ import { isAbsolute as isAbsolute2 } from "path";
1723
+ function serverUrl(value) {
1724
+ try {
1725
+ const url = new URL(value);
1726
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
1727
+ throw new Error;
1728
+ return url.href;
1729
+ } catch {
1730
+ throw new Error("Use an HTTPS server URL or loopback HTTP URL without credentials, query or fragment");
1731
+ }
1732
+ }
1733
+ async function connectDashboard(options, discovery = Service) {
1734
+ if (!isAbsolute2(options.directory))
1735
+ throw new Error("Dashboard directory must be an absolute path on the server");
1736
+ if (!options.server && (options.password !== undefined || options.username !== undefined))
1737
+ throw new Error("Explicit credentials require an explicit server URL");
1738
+ let endpoint;
1739
+ if (options.server !== undefined) {
1740
+ endpoint = {
1741
+ url: serverUrl(options.server),
1742
+ ...options.password === undefined ? {} : {
1743
+ auth: {
1744
+ type: "basic",
1745
+ username: options.username ?? "opencode",
1746
+ password: options.password
1747
+ }
1748
+ }
1749
+ };
1750
+ } else {
1751
+ let found;
1752
+ try {
1753
+ found = await discovery.discover(options.serviceFile === undefined ? {} : { file: options.serviceFile });
1754
+ } catch {
1755
+ throw new Error("Local OpenCode service discovery failed");
1756
+ }
1757
+ if (!found)
1758
+ throw new Error("No compatible local OpenCode service found; start OpenCode or supply --server");
1759
+ endpoint = {
1760
+ url: serverUrl(found.url),
1761
+ ...found.auth ? { auth: found.auth } : {}
1762
+ };
1763
+ }
1764
+ const headers = discovery.headers(endpoint);
1765
+ const request = discovery.fetch ?? globalThis.fetch;
1766
+ const client = OpenCode.make({
1767
+ baseUrl: endpoint.url,
1768
+ ...headers ? { headers } : {},
1769
+ fetch: Object.assign((input, init) => request(input, { ...init, redirect: "error" }), { preconnect: request.preconnect })
1770
+ });
1771
+ const deadline = (signal) => AbortSignal.any([signal, AbortSignal.timeout(5000)]);
1772
+ function atLocation(directory) {
1773
+ if (!isAbsolute2(directory))
1774
+ throw new Error("Dashboard directory must be an absolute path on the server");
1775
+ const location = Object.freeze({ directory });
1776
+ return {
1777
+ location,
1778
+ atLocation,
1779
+ async listSessions(input, signal) {
1780
+ if (input.limit !== undefined && (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100))
1781
+ throw new Error("Session page limit must be between 1 and 100");
1782
+ try {
1783
+ const page = await client.session.list({
1784
+ ...input,
1785
+ directory: location.directory,
1786
+ order: "desc",
1787
+ limit: input.limit ?? 100
1788
+ }, { signal: deadline(signal) });
1789
+ return {
1790
+ data: page.data.map((session) => ({
1791
+ sessionId: session.id,
1792
+ directory: session.location.directory,
1793
+ ...session.parentID ? { parentId: session.parentID } : {},
1794
+ ...session.agent ? { agent: session.agent } : {},
1795
+ ...session.model ? { model: `${session.model.providerID}/${session.model.id}` } : {},
1796
+ ...session.outcome ? { outcome: session.outcome } : {},
1797
+ ...session.time.idle === undefined ? {} : { terminalAt: session.time.idle }
1798
+ })),
1799
+ ...page.cursor?.next === undefined ? {} : { cursor: { next: page.cursor.next } }
1800
+ };
1801
+ } catch {
1802
+ throw new Error("Dashboard session inventory unavailable");
1803
+ }
1804
+ },
1805
+ async activeSessions(ids, signal) {
1806
+ try {
1807
+ const active = await client.session.active({
1808
+ signal: deadline(signal)
1809
+ });
1810
+ return ids.filter((id) => active[id]?.type === "running");
1811
+ } catch {
1812
+ throw new Error("Dashboard execution state unavailable");
1813
+ }
1814
+ },
1815
+ async monitor(signal) {
1816
+ let source = client.rpc(performanceRpc);
1817
+ let version = 2;
1818
+ try {
1819
+ const result = await source.read({ schemaVersion: 1, sessionIds: [] }, { location, signal: deadline(signal) });
1820
+ if (!isPerformanceResult(result))
1821
+ throw new Error;
1822
+ } catch {
1823
+ if (signal.aborted)
1824
+ throw new Error("Dashboard monitor probe cancelled");
1825
+ source = client.rpc(monitorRpc);
1826
+ version = 1;
1827
+ try {
1828
+ const result = await source.read({ schemaVersion: 1, sessionIds: [] }, { location, signal: deadline(signal) });
1829
+ if (!isResult(result))
1830
+ throw new Error;
1831
+ } catch {
1832
+ throw new Error("Context Guard monitor unavailable or incompatible in this location");
1833
+ }
1834
+ }
1835
+ const rpc = source;
1836
+ const checkLocation = (directory2) => {
1837
+ if (directory2 !== location.directory)
1838
+ throw new Error("Dashboard location mismatch");
1839
+ };
1840
+ return {
1841
+ version,
1842
+ transport: {
1843
+ async read(directory2, ids, requestSignal) {
1844
+ checkLocation(directory2);
1845
+ try {
1846
+ return await rpc.read({ schemaVersion: 1, sessionIds: [...ids] }, { location, signal: deadline(requestSignal) });
1847
+ } catch {
1848
+ throw new Error("Dashboard monitor read failed");
1849
+ }
1850
+ },
1851
+ async* subscribe(directory2, requestSignal) {
1852
+ checkLocation(directory2);
1853
+ try {
1854
+ for await (const event of rpc.events.subscribe("changed", {
1855
+ signal: requestSignal
1856
+ })) {
1857
+ if (event.location?.directory === location.directory)
1858
+ yield event.data;
1859
+ }
1860
+ } catch {
1861
+ if (!requestSignal.aborted)
1862
+ throw new Error("Dashboard monitor stream interrupted");
1863
+ }
1864
+ }
1865
+ }
1866
+ };
1867
+ },
1868
+ async history(input, signal) {
1869
+ if (!isHistoryRead(input))
1870
+ throw new Error("History requires one session, at most seven days and 16\u2013500 points");
1871
+ try {
1872
+ const result = await client.rpc(historyRpc).read(input, { location, signal: deadline(signal) });
1873
+ if (!isHistoryResult(result))
1874
+ throw new Error;
1875
+ return result;
1876
+ } catch {
1877
+ throw new Error("Context Guard history unavailable or incompatible in this location");
1878
+ }
1879
+ }
1880
+ };
1881
+ }
1062
1882
  return {
1063
- observedSince: p.observedSince,
1064
- sessionCreatedAt: p.sessionCreatedAt,
1065
- at: p.at,
1066
- status: p.status,
1067
- partial: p.partial,
1068
- estimatedOutputTokens: p.estimatedOutputTokens,
1069
- modelOutputTokens: p.modelOutputTokens,
1070
- modelDurationMs: p.modelDurationMs,
1071
- completedRequests: p.completedRequests,
1072
- tokenSource: p.tokenSource,
1073
- ...Object.fromEntries([
1074
- "sessionOutputTokens",
1075
- "liveTps",
1076
- "averageModelTps",
1077
- "averageSessionTps",
1078
- "ttftMs",
1079
- "latestRequestTps",
1080
- "requestId",
1081
- "requestAgent",
1082
- "requestModel",
1083
- "requestOutputTokens",
1084
- "requestStartedAt",
1085
- "firstOutputAt",
1086
- "streamEndedAt"
1087
- ].flatMap((key) => {
1088
- const value = p[key];
1089
- return value === undefined ? [] : [[key, value]];
1090
- }))
1883
+ ...atLocation(options.directory),
1884
+ async info(signal) {
1885
+ try {
1886
+ return await client.server.info({ signal: deadline(signal) });
1887
+ } catch {
1888
+ throw new Error("Dashboard server connection failed");
1889
+ }
1890
+ }
1091
1891
  };
1092
1892
  }
1093
- async function readPerformanceHistory(directory, filter = {}) {
1094
- const records = [];
1095
- const files = await segments(directory);
1096
- const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
1097
- let bytes = 0, skippedBytes = 0, malformedRecords = 0;
1098
- const selected = [];
1099
- for (const file of files.slice().reverse()) {
1100
- if (bytes + file.bytes > maxBytes) {
1101
- skippedBytes += file.bytes;
1102
- continue;
1893
+ var init_connection = __esm(() => {
1894
+ init_rpc();
1895
+ });
1896
+
1897
+ // src/tui/format.ts
1898
+ function quantity(n) {
1899
+ return n === undefined || !Number.isFinite(n) ? "?" : Math.abs(n) >= 1e6 ? `${(n / 1e6).toFixed(1)}m` : Math.abs(n) >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
1900
+ }
1901
+ function cells(value) {
1902
+ return [...segmenter.segment(value)].reduce((n, { segment }) => n + graphemeWidth(segment), 0);
1903
+ }
1904
+ function graphemeWidth(s) {
1905
+ const cp = s.codePointAt(0) ?? 0;
1906
+ if (/^[\p{Mark}\u200d\ufe0f]+$/u.test(s))
1907
+ return 0;
1908
+ return /[\p{Extended_Pictographic}\p{Regional_Indicator}\u20e3]/u.test(s) || cp >= 4352 && (cp <= 4447 || cp >= 9001 && cp <= 9002 || cp >= 11904 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65040 && cp <= 65135 || cp >= 65281 && cp <= 65376 || cp >= 131072) ? 2 : 1;
1909
+ }
1910
+ function clip(value, width) {
1911
+ const clean = sanitize(value);
1912
+ if (cells(clean) <= width)
1913
+ return clean;
1914
+ let result = "", used = 0;
1915
+ for (const { segment } of segmenter.segment(clean)) {
1916
+ const size = graphemeWidth(segment);
1917
+ if (used + size > Math.max(0, width - 3))
1918
+ break;
1919
+ result += segment;
1920
+ used += size;
1921
+ }
1922
+ return width < 3 ? ".".repeat(Math.max(0, width)) : result + "...";
1923
+ }
1924
+ function rowLines(row2, width) {
1925
+ const s = row2.snapshot, m = s?.measurement;
1926
+ const prefix = row2.current ? "* " : row2.depth > 5 ? ".../" : " ".repeat(Math.min(row2.depth, 5)) + "|-";
1927
+ const percent = row2.utilizationPercent === undefined ? "?" : `${row2.utilizationPercent.toFixed(1)}%`;
1928
+ const warning = row2.freshness === "stale" ? " STALE" : row2.pressure === "crossed" ? " !! crossed" : row2.pressure === "near" ? " ! near" : "";
1929
+ const suffix = ` ${statuses[row2.node.status]} ${m?.confidence === "approximate" ? "~" : ""}${percent}${warning}`;
1930
+ const label = clip(`${prefix}${row2.node.agent ?? row2.node.sessionId}`, Math.max(1, width - cells(suffix)));
1931
+ return [
1932
+ clip(`${label}${suffix}`, width),
1933
+ ...performanceLines(row2, width),
1934
+ ...wrap(`${quantity(m?.tokens)}/${quantity(s?.capacityTokens)} input; to ${quantity(row2.triggerDeltaTokens)}`, width),
1935
+ ...s && s.compaction.phase !== "idle" ? wrap(`compact: ${s.compaction.phase}`, width) : [],
1936
+ ...row2.node.interrupted ? [clip("interrupted", width)] : [],
1937
+ ...row2.hidden ? wrap(`+ ${row2.hidden} completed descendants`, width) : []
1938
+ ];
1939
+ }
1940
+ function detailLines(row2, width, expanded = false, ageMs = 0) {
1941
+ const s = row2?.snapshot, m = s?.measurement;
1942
+ if (!row2)
1943
+ return wrap("Loading session family...", width);
1944
+ if (!s)
1945
+ return [
1946
+ ...wrap(`${row2.node.agent ?? row2.node.sessionId} / ${row2.node.model ?? "?"}`, width),
1947
+ ...wrap("Guard: not monitored / unavailable", width)
1948
+ ];
1949
+ const threshold = s.policy.configured;
1950
+ const headroom = row2.triggerDeltaTokens;
1951
+ const lines = [
1952
+ `${s.agent ?? "?"} / ${s.model ?? "?"}`,
1953
+ ...performanceLines(row2, width),
1954
+ `${m?.confidence === "approximate" ? "~" : ""}${quantity(m?.tokens)} / ${quantity(s.capacityTokens)} input`,
1955
+ `${row2.utilizationPercent?.toFixed(1) ?? "?"}% ${row2.pressure === "crossed" ? "!! crossed" : row2.pressure === "near" ? "! near" : row2.freshness.toUpperCase()}`,
1956
+ `Trigger ${threshold.type === "ratio" ? `${threshold.value * 100}%` : `${threshold.value} ${threshold.type}`} (${s.policy.comparison})`,
1957
+ `Resolved trigger ${s.policy.triggerTokens ?? "?"}`,
1958
+ `Until trigger ${quantity(headroom)}`,
1959
+ `Compact: ${s.compaction.phase}`,
1960
+ `${s.compaction.armed ? "armed" : "disarmed"}${s.compaction.blockedReason ? ` / ${s.compaction.blockedReason}` : ""}`,
1961
+ m ? `${m.confidence} ${m.scope}` : "measurement unknown",
1962
+ m ? `${Math.floor(Math.max(0, ageMs) / 1000)}s ago; ${row2.freshness}` : ""
1963
+ ];
1964
+ if (expanded)
1965
+ lines.push(`Policy ${s.policy.id}: ${s.policy.reason}`, `Limits in/context/out ${quantity(s.limits?.input)}/${quantity(s.limits?.context)}/${quantity(s.limits?.output)}`, `Output reserve ${quantity(s.outputReserveTokens)}`, `Provenance ${m?.provenance ?? "?"}`, `Cooldown until ${s.compaction.cooldownUntil ?? "?"}`, `Cumulative (${s.cumulativeScope})`, `Input ${quantity(s.cumulative?.input)} output ${quantity(s.cumulative?.output)}`, `Reasoning ${quantity(s.cumulative?.reasoning)}`, `Cache read ${quantity(s.cumulative?.cache.read)} write ${quantity(s.cumulative?.cache.write)}`);
1966
+ return lines.filter(Boolean).flatMap((line) => wrap(line, width));
1967
+ }
1968
+ function sanitize(value) {
1969
+ return value.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, "");
1970
+ }
1971
+ function wrap(value, width) {
1972
+ const result = [];
1973
+ let part = "";
1974
+ for (const word of sanitize(value).split(" ")) {
1975
+ if (part && cells(part + " " + word) > width) {
1976
+ result.push(part);
1977
+ part = "";
1978
+ }
1979
+ for (const { segment } of segmenter.segment((part ? " " : "") + word)) {
1980
+ if (part && cells(part + segment) > width) {
1981
+ result.push(part);
1982
+ part = "";
1983
+ }
1984
+ part += cells(segment) <= width ? segment : "?";
1103
1985
  }
1104
- bytes += file.bytes;
1105
- selected.unshift(file);
1106
1986
  }
1107
- for (const file of selected) {
1108
- const handle = await open(join(directory, file.name), "r");
1109
- let source;
1110
- try {
1111
- const buffer = Buffer.alloc(file.bytes);
1112
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
1113
- source = buffer.subarray(0, bytesRead).toString("utf8");
1114
- } finally {
1115
- await handle.close();
1987
+ if (part)
1988
+ result.push(part);
1989
+ return result;
1990
+ }
1991
+ function performanceLines(row2, width) {
1992
+ const p = row2.snapshot?.performance;
1993
+ if (!p)
1994
+ return [];
1995
+ const value = (n) => n === undefined ? "?" : n.toFixed(1);
1996
+ return [
1997
+ ...wrap(`TPS ${p.status === "waiting" ? "waiting" : `${p.status === "streaming" ? "~" : ""}${value(p.liveTps)}`} \xB7 ${p.status}${p.partial ? " \xB7 partial" : ""}`, width),
1998
+ ...wrap(`Avg model ${value(p.averageModelTps)} \xB7 Avg session ${value(p.averageSessionTps)}`, width),
1999
+ ...wrap(`TTFT ${p.ttftMs === undefined ? "?" : `~${(p.ttftMs / 1000).toFixed(2)}s`}${p.latestRequestTps === undefined ? "" : ` \xB7 Last ${value(p.latestRequestTps)} t/s`}`, width)
2000
+ ];
2001
+ }
2002
+ var segmenter, statuses;
2003
+ var init_format = __esm(() => {
2004
+ segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
2005
+ statuses = {
2006
+ running: "RUN",
2007
+ idle: "IDLE",
2008
+ completed: "DONE",
2009
+ failed: "FAIL",
2010
+ unknown: "?"
2011
+ };
2012
+ });
2013
+
2014
+ // src/tui/chartformat.ts
2015
+ function columns(points, from, to, width, gaps = []) {
2016
+ const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
2017
+ const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
2018
+ let min = Infinity, max = -Infinity;
2019
+ for (const point of points) {
2020
+ if (point.time < from || point.time > to || point.value === undefined || !Number.isFinite(point.value) || point.value < 0)
2021
+ continue;
2022
+ min = Math.min(min, point.value);
2023
+ max = Math.max(max, point.value);
2024
+ const i = column(point.time);
2025
+ values[i] = Math.max(values[i] ?? -Infinity, point.value);
2026
+ }
2027
+ for (const time of gaps)
2028
+ if (time >= from && time <= to)
2029
+ values[column(time)] = undefined;
2030
+ return { values, column, min, max };
2031
+ }
2032
+ function sparkline(points, from, to, width, gaps = []) {
2033
+ const { values, column, min, max } = columns(points, from, to, width, gaps);
2034
+ const bars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2035
+ const chars = values.map((v) => v === undefined ? " " : bars[Math.min(7, Math.floor(v / Math.max(1, max) * 7))] ?? "\u2581");
2036
+ for (const time of gaps)
2037
+ if (time >= from && time <= to)
2038
+ chars[column(time)] = "\u2506";
2039
+ return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
2040
+ }
2041
+ function metricPlot(points, from, to, width, gaps = []) {
2042
+ const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
2043
+ const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
2044
+ const scale = Math.max(1, peak);
2045
+ const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
2046
+ const { values, column } = columns(points, from, to, Math.max(1, width - gutter - 2), gaps);
2047
+ const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
2048
+ const height = 6;
2049
+ const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2050
+ const lines = Array.from({ length: height }, (_, row2) => {
2051
+ const axis = gutter ? (row2 === 0 ? label(scale) : row2 === height - 1 ? "0.0" : "").padStart(gutter) : "";
2052
+ return `${axis}\u2502${values.map((value, i) => {
2053
+ if (gapColumns.has(i))
2054
+ return "\u2506";
2055
+ if (value === undefined)
2056
+ return " ";
2057
+ const fill = Math.max(0, Math.min(8, Math.ceil((value / scale * height - (height - row2 - 1)) * 8)));
2058
+ return value === 0 && row2 === height - 1 ? "\u2581" : blocks[fill];
2059
+ }).join("")}\u2502`;
2060
+ });
2061
+ lines.push(`${" ".repeat(gutter)}\u2514${"\u2500".repeat(values.length)}\u2518`);
2062
+ const start = new Date(from).toISOString().slice(11, 16), end = new Date(to).toISOString().slice(11, 16);
2063
+ const axisWidth = values.length + 2;
2064
+ if (axisWidth >= 11)
2065
+ lines.push(`${" ".repeat(gutter)}${start}${" ".repeat(axisWidth - 10)}${end}`);
2066
+ return lines;
2067
+ }
2068
+ function historyChartLines(result, width, from, to, metricIndex = 0) {
2069
+ if (!result.enabled)
2070
+ return wrap("History recording is disabled on this server.", width);
2071
+ const series = result.data.series[0];
2072
+ if (!series)
2073
+ return wrap("No retained measurements in this window.", width);
2074
+ const gaps = result.data.gaps.map((g) => g.time);
2075
+ const observations = (key) => series.points.map((p) => ({
2076
+ time: p.time,
2077
+ ...p[key] === undefined ? {} : { value: p[key] }
2078
+ }));
2079
+ const requests = (key) => series.requests.map((p) => ({
2080
+ time: p.time,
2081
+ ...p[key] === undefined ? {} : { value: key === "ttftMs" ? p[key] / 1000 : p[key] }
2082
+ }));
2083
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2084
+ const points = metric2.source === "points" ? observations(metric2.key) : requests(metric2.key);
2085
+ const plotted = sparkline(points, from, to, width - 2, gaps);
2086
+ return [
2087
+ ...wrap(`\u2039 ${metric2.label} \u203A \xB7 ${metricIndex + 1}/${chartMetrics.length}`, width),
2088
+ ...wrap(plotted.min === undefined ? "No measurements for this metric." : `${plotted.min.toFixed(1)}\u2013${plotted.max?.toFixed(1)} ${metric2.unit} \xB7 observed range`, width),
2089
+ "",
2090
+ ...metricPlot(points, from, to, width, gaps),
2091
+ "",
2092
+ ...wrap("UTC time \u2192 \xB7 peaks per column", width),
2093
+ ...wrap("blank = no sample \xB7 \u2506 = gap", width),
2094
+ ...wrap(`${new Date(from).toISOString()} \u2192 ${new Date(to).toISOString()}`, width),
2095
+ ...series.points.some((p) => p.partial) ? wrap("Partial timing coverage; averages include only matched model durations.", width) : [],
2096
+ ...result.truncated ? wrap("Downsampled/bounded history; some older requests or gap markers may be omitted.", width) : [],
2097
+ ...result.metadata.malformedRecords ? wrap(`${result.metadata.malformedRecords} malformed/partial records skipped.`, width) : []
2098
+ ];
2099
+ }
2100
+ var chartMetrics;
2101
+ var init_chartformat = __esm(() => {
2102
+ init_format();
2103
+ chartMetrics = [
2104
+ { label: "Live TPS ~", unit: "t/s", key: "liveTps", source: "points" },
2105
+ { label: "Avg model", unit: "t/s", key: "averageModelTps", source: "points" },
2106
+ {
2107
+ label: "Avg session",
2108
+ unit: "t/s",
2109
+ key: "averageSessionTps",
2110
+ source: "points"
2111
+ },
2112
+ { label: "TTFT ~", unit: "s", key: "ttftMs", source: "requests" },
2113
+ { label: "Request throughput", unit: "t/s", key: "tps", source: "requests" },
2114
+ {
2115
+ label: "Cumulative output",
2116
+ unit: "tokens",
2117
+ key: "sessionOutputTokens",
2118
+ source: "points"
1116
2119
  }
1117
- for (const line of source.split("\n")) {
1118
- if (!line)
1119
- continue;
1120
- try {
1121
- const value = JSON.parse(line);
1122
- if (!isHistoryRecord(value)) {
1123
- malformedRecords++;
2120
+ ];
2121
+ });
2122
+
2123
+ // src/dashboard/terminalview.ts
2124
+ function overviewRow(row2) {
2125
+ const s = row2.snapshot;
2126
+ const valid2 = row2.freshness === "fresh" || row2.freshness === "stale";
2127
+ return `${" ".repeat(Math.min(row2.depth, 12))}${row2.depth ? "\u2514 " : ""}${row2.node.agent ?? "unknown agent"} / ${row2.node.model ?? "unknown model"} [${row2.node.sessionId}] ${row2.node.status} | prompt ${quantity(valid2 ? s?.measurement?.tokens : undefined)}/${quantity(s?.capacityTokens)} | occupancy ${row2.utilizationPercent?.toFixed(1) ?? "?"}% | trigger ${quantity(s?.policy.triggerTokens)} headroom ${quantity(row2.triggerDeltaTokens)} | pressure ${row2.pressure} | compact ${s?.compaction.phase ?? "unavailable"} | performance ${s?.performance?.status ?? "unavailable"} | live TPS ${quantity(row2.performanceAvailable && row2.performanceFreshness !== "invalid" ? s?.performance?.liveTps : undefined)} | age ${row2.measurementAgeMs === undefined ? "unknown" : `${Math.floor(row2.measurementAgeMs / 1000)}s`} | inventory ${row2.inventoryStale ? "stale" : "fresh"}, live ${row2.liveStale ? "stale" : "fresh"}, usage ${row2.freshness}, performance ${row2.performanceFreshness}${s?.measurement?.confidence === "approximate" ? " estimated" : ""}${s?.performance?.tokenSource === "estimated" ? " estimated TPS" : ""}${s?.performance?.partial ? " partial" : ""}${row2.hidden ? ` | ${row2.hidden} hidden descendants` : ""}`;
2128
+ }
2129
+ function pathValue(object2, path) {
2130
+ let value = object2;
2131
+ for (const key of path.split(".")) {
2132
+ if (value === null || typeof value !== "object")
2133
+ return;
2134
+ value = Reflect.get(value, key);
2135
+ }
2136
+ return value;
2137
+ }
2138
+ function terminalDetail(row2) {
2139
+ if (!row2)
2140
+ return ["No selected session; choose one in overview."];
2141
+ return [
2142
+ overviewRow(row2),
2143
+ `Session ID: ${row2.node.sessionId}`,
2144
+ `Parent ID: ${row2.node.parentId ?? "unknown/unavailable"}`,
2145
+ "All snapshot fields (timestamps are Unix milliseconds):",
2146
+ ...snapshotFields.map((path) => `${path}: ${scalar(pathValue(row2.snapshot, path))}`),
2147
+ "Compaction count records completed lifecycle events; admission is not completion."
2148
+ ];
2149
+ }
2150
+ function historyObservations(view, metricIndex) {
2151
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2152
+ return view.history.result?.data.series.flatMap((series) => metric2.source === "points" ? series.points.map((point) => ({
2153
+ time: point.time,
2154
+ value: point[metric2.key],
2155
+ details: {
2156
+ sessionId: series.sessionId,
2157
+ ...point,
2158
+ tokenSource: "per-sample source not retained; live TPS estimated; cumulative output reported; rates use matched timing"
2159
+ }
2160
+ })) : series.requests.map((request) => ({
2161
+ time: request.time,
2162
+ value: metric2.key === "ttftMs" ? request.ttftMs === undefined ? undefined : request.ttftMs / 1000 : request.tps,
2163
+ details: {
2164
+ sessionId: series.sessionId,
2165
+ ...request,
2166
+ tokenSource: "reported output; observed timing"
2167
+ }
2168
+ }))).sort((a, b) => a.time - b.time) ?? [];
2169
+ }
2170
+ function terminalHistory(view, width, metricIndex, sample) {
2171
+ const h = view.history;
2172
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2173
+ const observations = historyObservations(view, metricIndex);
2174
+ const gaps = [
2175
+ ...h.result?.data.gaps.map((gap) => gap.time) ?? [],
2176
+ ...h.result?.data.series.flatMap((series) => series.points.filter((p) => p.breakBefore).map((p) => p.time)) ?? []
2177
+ ];
2178
+ const selected = observations[Math.min(sample, Math.max(0, observations.length - 1))];
2179
+ return [
2180
+ `History: ${h.status}${h.stale ? " \xB7 stale cached results" : ""} \xB7 ${h.windowMs / 60000} minutes \xB7 ${metric2.label} (${metric2.unit})`,
2181
+ "Left/Right metric \xB7 w window \xB7 [/] exact sample/request \xB7 Up/Down scroll",
2182
+ "Retention bounded to seven days; maximum 500 points and 500 requests per session.",
2183
+ ...h.notices,
2184
+ ...h.summary ? [
2185
+ `Matched requests ${h.summary.matchedRequests}/${h.summary.observedRequests}; output ${scalar(h.summary.outputTokens)}; duration ms ${scalar(h.summary.durationMs)}; average model TPS ${scalar(h.summary.averageModelTps)}${h.summary.partial ? " \xB7 partial" : ""}`
2186
+ ] : [],
2187
+ ...h.from === undefined || h.to === undefined ? [] : [
2188
+ `${new Date(h.from).toISOString()} \u2192 ${new Date(h.to).toISOString()}`,
2189
+ ...metricPlot(observations.map((p) => ({
2190
+ time: p.time,
2191
+ ...p.value === undefined ? {} : { value: p.value }
2192
+ })), h.from, h.to, width, gaps),
2193
+ "blank = no sample; \u2506 = gap; observed column peaks; no interpolation"
2194
+ ],
2195
+ ...selected ? [
2196
+ `Exact ${metric2.source === "points" ? "sample" : "request"} ${Math.min(sample + 1, observations.length)}/${observations.length}:`,
2197
+ ...Object.entries(selected.details).map(([key, value]) => `${key}: ${scalar(value)}`),
2198
+ `Selected ${metric2.label}: ${scalar(selected.value)} ${metric2.unit}`
2199
+ ] : [
2200
+ "No retained measurements for this metric; values unknown/unavailable."
2201
+ ]
2202
+ ];
2203
+ }
2204
+ function wrapped(lines, width) {
2205
+ return lines.flatMap((line) => line ? wrap(line, Math.max(1, width)) : [""]);
2206
+ }
2207
+ function headerLines(view, width) {
2208
+ return [
2209
+ clip(`Context Guard \xB7 ${view.screen} \xB7 ${view.connection} \xB7 enforcement ${view.health?.enforcement ?? "unknown"}`, width),
2210
+ clip(`Known ${view.inventory.total} \xB7 monitored ${view.inventory.monitored} \xB7 active ${view.inventory.active} \xB7 inventory ${view.inventory.stale ? "stale" : "fresh"}${view.inventory.complete ? "" : " / partial"}`, width),
2211
+ clip(`Location: ${view.location}`, width)
2212
+ ];
2213
+ }
2214
+ var snapshotFields, scalar = (value) => value === undefined || value === null ? "unknown/unavailable" : typeof value === "number" && !Number.isFinite(value) ? "invalid" : String(value), terminalHelp;
2215
+ var init_terminalview = __esm(() => {
2216
+ init_chartformat();
2217
+ init_format();
2218
+ snapshotFields = [
2219
+ "sessionId",
2220
+ "parentId",
2221
+ "agent",
2222
+ "model",
2223
+ "revision",
2224
+ "observedAt",
2225
+ "measurement.tokens",
2226
+ "measurement.at",
2227
+ "measurement.model",
2228
+ "measurement.provenance",
2229
+ "measurement.scope",
2230
+ "measurement.confidence",
2231
+ "measurement.requestId",
2232
+ "limits.input",
2233
+ "limits.context",
2234
+ "limits.output",
2235
+ "capacityTokens",
2236
+ "outputReserveTokens",
2237
+ "peakTokens",
2238
+ "policy.id",
2239
+ "policy.configured.type",
2240
+ "policy.configured.value",
2241
+ "policy.triggerTokens",
2242
+ "policy.comparison",
2243
+ "policy.reason",
2244
+ "policy.crossed",
2245
+ "compaction.phase",
2246
+ "compaction.count",
2247
+ "compaction.armed",
2248
+ "compaction.blockedReason",
2249
+ "compaction.lastAttempt",
2250
+ "compaction.lastCompleted",
2251
+ "compaction.cooldownUntil",
2252
+ "cumulativeScope",
2253
+ "cumulative.input",
2254
+ "cumulative.output",
2255
+ "cumulative.reasoning",
2256
+ "cumulative.cache.read",
2257
+ "cumulative.cache.write",
2258
+ "lastObservedActive",
2259
+ "performance.sessionCreatedAt",
2260
+ "performance.sessionOutputTokens",
2261
+ "performance.observedSince",
2262
+ "performance.at",
2263
+ "performance.status",
2264
+ "performance.partial",
2265
+ "performance.liveTps",
2266
+ "performance.averageModelTps",
2267
+ "performance.averageSessionTps",
2268
+ "performance.ttftMs",
2269
+ "performance.latestRequestTps",
2270
+ "performance.requestId",
2271
+ "performance.requestAgent",
2272
+ "performance.requestModel",
2273
+ "performance.requestOutputTokens",
2274
+ "performance.requestStartedAt",
2275
+ "performance.firstOutputAt",
2276
+ "performance.streamEndedAt",
2277
+ "performance.estimatedOutputTokens",
2278
+ "performance.modelOutputTokens",
2279
+ "performance.modelDurationMs",
2280
+ "performance.completedRequests",
2281
+ "performance.tokenSource"
2282
+ ];
2283
+ terminalHelp = [
2284
+ "Context Guard standalone dashboard \u2014 keyboard help",
2285
+ "1 overview \xB7 2 detail \xB7 3 history \xB7 Enter detail",
2286
+ "Up/Down or j/k: select session in overview; scroll other screens",
2287
+ "PageUp/PageDown: move by a page \xB7 Home/End: first/last",
2288
+ "Left/Right: history metric (six metrics) \xB7 w: cycle 5m/1h/24h/7d",
2289
+ "[ / ]: previous/next exact history sample or request",
2290
+ "/: search session identity \xB7 f: agent filter, then model filter (exact matches)",
2291
+ "l: owning absolute location directory; Tab cycles up to four remembered locations",
2292
+ "Remembered locations are not complete server-wide discovery.",
2293
+ "Editors: type/paste text; Backspace deletes; Enter applies; Escape cancels",
2294
+ "r: refresh \xB7 ?: help \xB7 Escape: overview \xB7 q / Ctrl+C: quit",
2295
+ "Detail scrolls through full identifiers and every exposed snapshot field.",
2296
+ "Unknown metrics are unavailable, not zero. Stale occupancy is retained; pressure is unknown."
2297
+ ];
2298
+ });
2299
+
2300
+ // src/dashboard/terminal.ts
2301
+ var exports_terminal = {};
2302
+ __export(exports_terminal, {
2303
+ runTerminal: () => runTerminal,
2304
+ mountTerminal: () => mountTerminal,
2305
+ TerminalShell: () => TerminalShell
2306
+ });
2307
+ import { isAbsolute as isAbsolute3 } from "path";
2308
+
2309
+ class TerminalShell {
2310
+ controller;
2311
+ quit;
2312
+ scroll = 0;
2313
+ metric = 0;
2314
+ sample = 0;
2315
+ help = false;
2316
+ editor;
2317
+ input = "";
2318
+ notice = "";
2319
+ rowOffset = 0;
2320
+ historyIdentity = "";
2321
+ constructor(controller, quit) {
2322
+ this.controller = controller;
2323
+ this.quit = quit;
2324
+ }
2325
+ task(task) {
2326
+ task.catch(() => {
2327
+ this.notice = "Dashboard action unavailable; refresh to retry.";
2328
+ });
2329
+ }
2330
+ paste(text2) {
2331
+ if (this.editor)
2332
+ this.input = (this.input + clip(text2, 2048)).slice(0, this.editor === "location" ? 4096 : 512);
2333
+ }
2334
+ key(key, page = 10) {
2335
+ const view = this.controller.getView();
2336
+ const name = key.name;
2337
+ if (key.ctrl && name === "c") {
2338
+ this.quit();
2339
+ return;
2340
+ }
2341
+ if (this.editor) {
2342
+ if (name === "escape") {
2343
+ this.editor = undefined;
2344
+ this.notice = "";
2345
+ } else if (name === "backspace")
2346
+ this.input = Array.from(this.input).slice(0, -1).join("");
2347
+ else if (name === "tab" && this.editor === "location") {
2348
+ const i = view.locations.indexOf(this.input);
2349
+ this.input = view.locations[(i + 1) % view.locations.length] ?? view.location;
2350
+ } else if (name === "return" || name === "enter") {
2351
+ const editor = this.editor;
2352
+ if (editor === "location") {
2353
+ if (!isAbsolute3(this.input)) {
2354
+ this.notice = "Location must be an owning absolute server directory.";
2355
+ return;
2356
+ }
2357
+ this.task(this.controller.selectLocation(this.input));
2358
+ } else
2359
+ this.controller.setFilters({ [editor]: this.input });
2360
+ this.editor = editor === "agent" ? "model" : undefined;
2361
+ this.input = this.editor === "model" ? view.filters.model : "";
2362
+ this.scroll = 0;
2363
+ this.notice = "";
2364
+ } else if (!key.ctrl && !key.meta && key.sequence && !/[\x00-\x1f\x7f-\x9f]/.test(key.sequence))
2365
+ this.paste(key.sequence);
2366
+ return;
2367
+ }
2368
+ if (name === "q") {
2369
+ this.quit();
2370
+ return;
2371
+ }
2372
+ if (name === "?" || key.sequence === "?") {
2373
+ this.help = !this.help;
2374
+ this.scroll = 0;
2375
+ return;
2376
+ }
2377
+ if (name === "escape") {
2378
+ this.help = false;
2379
+ this.scroll = 0;
2380
+ this.controller.setScreen("overview");
2381
+ return;
2382
+ }
2383
+ if (name === "/" || key.sequence === "/") {
2384
+ this.editor = "search";
2385
+ this.input = view.filters.search;
2386
+ return;
2387
+ }
2388
+ if (name === "f") {
2389
+ this.editor = "agent";
2390
+ this.input = view.filters.agent;
2391
+ return;
2392
+ }
2393
+ if (name === "l") {
2394
+ this.editor = "location";
2395
+ this.input = view.location;
2396
+ return;
2397
+ }
2398
+ if (name === "r") {
2399
+ this.task(this.controller.refresh());
2400
+ return;
2401
+ }
2402
+ if (["1", "2", "3", "return", "enter"].includes(name)) {
2403
+ this.help = false;
2404
+ this.scroll = 0;
2405
+ this.controller.setScreen(name === "1" ? "overview" : name === "3" ? "history" : "detail");
2406
+ return;
2407
+ }
2408
+ if (view.screen === "history" && !this.help) {
2409
+ if (name === "left" || name === "right") {
2410
+ this.metric = (this.metric + (name === "left" ? 5 : 1)) % 6;
2411
+ this.sample = 0;
2412
+ this.scroll = 0;
2413
+ return;
2414
+ }
2415
+ if (name === "w") {
2416
+ this.controller.setWindow(historyWindows[(historyWindows.indexOf(view.history.windowMs) + 1) % historyWindows.length] ?? historyWindows[0]);
2417
+ this.sample = 0;
2418
+ return;
2419
+ }
2420
+ if (name === "[" || name === "]") {
2421
+ this.sample = Math.max(0, Math.min(historyObservations(view, this.metric).length - 1, this.sample + (name === "[" ? -1 : 1)));
2422
+ return;
2423
+ }
2424
+ }
2425
+ const delta = name === "down" || name === "j" ? 1 : name === "up" || name === "k" ? -1 : name === "pagedown" ? page : name === "pageup" ? -page : 0;
2426
+ if (view.screen === "overview" && !this.help) {
2427
+ const index = view.rows.findIndex((row3) => row3.node.sessionId === view.selectedSessionId);
2428
+ const next = name === "home" ? 0 : name === "end" ? view.rows.length - 1 : Math.max(0, Math.min(view.rows.length - 1, index + delta));
2429
+ const row2 = view.rows[next];
2430
+ if (row2 && (delta || name === "home" || name === "end"))
2431
+ this.controller.selectSession(row2.node.sessionId);
2432
+ } else
2433
+ this.scroll = name === "home" ? 0 : name === "end" ? Number.MAX_SAFE_INTEGER : Math.max(0, this.scroll + delta);
2434
+ }
2435
+ frame(width, height) {
2436
+ width = Math.max(1, width);
2437
+ height = Math.max(1, height);
2438
+ const view = this.controller.getView();
2439
+ const identity = `${view.location}\0${view.selectedSessionId}\0${view.history.windowMs}`;
2440
+ if (identity !== this.historyIdentity) {
2441
+ this.sample = 0;
2442
+ this.scroll = 0;
2443
+ this.historyIdentity = identity;
2444
+ }
2445
+ const footer = this.editor ? `${this.editor}> ${this.input} [Enter apply; Esc cancel${this.editor === "location" ? "; Tab remembered" : ""}]` : "? help q quit \xB7 1 overview 2 detail 3 history \xB7 / search f filters l location r refresh";
2446
+ if (height < 5)
2447
+ return [
2448
+ clip("Context Guard \xB7 ? help \xB7 q quit", width),
2449
+ ...wrapped([footer], width)
2450
+ ].slice(0, height).join("\n");
2451
+ const header = headerLines(view, width);
2452
+ const available = height - header.length - 1;
2453
+ const notices = [
2454
+ ...view.notices,
2455
+ ...view.inventory.notices,
2456
+ ...view.health?.reasons ?? [],
2457
+ ...this.notice ? [this.notice] : [],
2458
+ ...Object.values(view.filters).some(Boolean) ? [
2459
+ `Filters: search=${view.filters.search} agent=${view.filters.agent} model=${view.filters.model}`
2460
+ ] : []
2461
+ ];
2462
+ let body;
2463
+ if (this.help)
2464
+ body = wrapped(terminalHelp, width);
2465
+ else if (view.screen === "overview") {
2466
+ const rows = view.rows.map((row2) => wrapped([
2467
+ `${row2.node.sessionId === view.selectedSessionId ? "> " : " "}${overviewRow(row2)}`
2468
+ ], width));
2469
+ const selected = view.rows.findIndex((row2) => row2.node.sessionId === view.selectedSessionId);
2470
+ const start = rows.slice(0, Math.max(0, selected)).reduce((n, row2) => n + row2.length, 0);
2471
+ const end = start + (rows[selected]?.length ?? 0);
2472
+ const status = wrapped(notices, width);
2473
+ const budget = Math.max(1, available - Math.min(status.length, Math.floor(available / 3)));
2474
+ if (start < this.rowOffset)
2475
+ this.rowOffset = start;
2476
+ if (end > this.rowOffset + budget)
2477
+ this.rowOffset = Math.max(start, end - budget);
2478
+ const flat = rows.flat();
2479
+ this.rowOffset = Math.max(0, Math.min(this.rowOffset, Math.max(0, flat.length - budget)));
2480
+ body = [
2481
+ ...status.slice(0, available - budget),
2482
+ ...flat.length ? flat.slice(this.rowOffset, this.rowOffset + budget) : ["No matching sessions; / search, f filters, r refresh."]
2483
+ ];
2484
+ return [
2485
+ ...header,
2486
+ ...body,
2487
+ ...Array.from({ length: Math.max(0, available - body.length) }, () => ""),
2488
+ clip(footer, width)
2489
+ ].join("\n");
2490
+ } else
2491
+ body = wrapped([
2492
+ ...notices,
2493
+ ...view.screen === "detail" ? terminalDetail(view.selected) : terminalHistory(view, width, this.metric, this.sample)
2494
+ ], width);
2495
+ this.scroll = Math.max(0, Math.min(this.scroll, Math.max(0, body.length - available)));
2496
+ return [
2497
+ ...header,
2498
+ ...body.slice(this.scroll, this.scroll + available),
2499
+ ...Array.from({ length: Math.max(0, available - body.length) }, () => ""),
2500
+ clip(footer, width)
2501
+ ].join("\n");
2502
+ }
2503
+ }
2504
+ function mountTerminal(controller, renderer, text2, finished) {
2505
+ let closed = false;
2506
+ let unsubscribe = () => {
2507
+ };
2508
+ const shutdown = (failed = false) => {
2509
+ if (closed)
2510
+ return;
2511
+ closed = true;
2512
+ unsubscribe();
2513
+ controller.stop();
2514
+ renderer.keyInput.off("keypress", key);
2515
+ renderer.keyInput.off("paste", paste);
2516
+ renderer.off("resize", draw);
2517
+ renderer.off("render:error", fail);
2518
+ renderer.off("handler:error", fail);
2519
+ renderer.off("destroy", destroyed);
2520
+ for (const signal of signals)
2521
+ process.off(signal, onSignal);
2522
+ process.off("uncaughtException", fail);
2523
+ process.off("unhandledRejection", fail);
2524
+ try {
2525
+ renderer.destroy();
2526
+ } finally {
2527
+ finished(failed);
2528
+ }
2529
+ };
2530
+ const shell = new TerminalShell(controller, () => shutdown());
2531
+ const fail = () => shutdown(true);
2532
+ const destroyed = () => shutdown();
2533
+ const onSignal = () => shutdown();
2534
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
2535
+ const draw = () => {
2536
+ if (closed)
2537
+ return;
2538
+ try {
2539
+ text2.content = shell.frame(renderer.width, renderer.height);
2540
+ } catch {
2541
+ fail();
2542
+ }
2543
+ };
2544
+ const key = (event) => {
2545
+ try {
2546
+ shell.key(event, Math.max(1, renderer.height - 4));
2547
+ draw();
2548
+ } catch {
2549
+ fail();
2550
+ }
2551
+ };
2552
+ const paste = (event) => {
2553
+ shell.paste(new TextDecoder().decode(event.bytes));
2554
+ draw();
2555
+ };
2556
+ try {
2557
+ renderer.root.add(text2);
2558
+ renderer.keyInput.on("keypress", key);
2559
+ renderer.keyInput.on("paste", paste);
2560
+ renderer.on("resize", draw);
2561
+ renderer.on("render:error", fail);
2562
+ renderer.on("handler:error", fail);
2563
+ renderer.on("destroy", destroyed);
2564
+ for (const signal of signals)
2565
+ process.on(signal, onSignal);
2566
+ process.on("uncaughtException", fail);
2567
+ process.on("unhandledRejection", fail);
2568
+ unsubscribe = controller.subscribe(draw);
2569
+ if (closed) {
2570
+ unsubscribe();
2571
+ return { shell, shutdown };
2572
+ }
2573
+ controller.start().catch(fail);
2574
+ } catch {
2575
+ shutdown(true);
2576
+ }
2577
+ return { shell, shutdown };
2578
+ }
2579
+ async function runTerminal(options) {
2580
+ const { createCliRenderer, TextRenderable } = await loadTerminalRenderer();
2581
+ const connection = await connectDashboard(options);
2582
+ await connection.info(AbortSignal.timeout(15000));
2583
+ const controller = new DashboardController(connection);
2584
+ const renderer = await createCliRenderer({
2585
+ exitOnCtrlC: false,
2586
+ exitSignals: [],
2587
+ useMouse: false,
2588
+ screenMode: "alternate-screen",
2589
+ consoleMode: "disabled",
2590
+ openConsoleOnError: false
2591
+ });
2592
+ let failed = false;
2593
+ let app;
2594
+ try {
2595
+ await new Promise((done) => {
2596
+ const text2 = new TextRenderable(renderer, {
2597
+ id: "dashboard",
2598
+ width: "100%",
2599
+ height: "100%",
2600
+ content: "",
2601
+ wrapMode: "none"
2602
+ });
2603
+ app = mountTerminal(controller, renderer, text2, (error) => {
2604
+ failed = error;
2605
+ done();
2606
+ });
2607
+ });
2608
+ } finally {
2609
+ if (app)
2610
+ app.shutdown();
2611
+ else {
2612
+ controller.stop();
2613
+ renderer.destroy();
2614
+ }
2615
+ }
2616
+ if (failed)
2617
+ throw new Error("Terminal dashboard stopped after a runtime failure; terminal restored");
2618
+ }
2619
+ var init_terminal = __esm(() => {
2620
+ init_controller();
2621
+ init_connection();
2622
+ init_command();
2623
+ init_view();
2624
+ init_format();
2625
+ init_terminalview();
2626
+ });
2627
+
2628
+ // src/modelanalytics.ts
2629
+ function requestFact(record2) {
2630
+ const p = record2.performance;
2631
+ if (!p?.requestId || !record2.sessionId || !nonnegative2(p.requestStartedAt))
2632
+ return;
2633
+ const model = p.requestModel || record2.model;
2634
+ const agent = p.requestAgent || record2.agent;
2635
+ const ttft = p.ttftMs ?? (p.firstOutputAt === undefined ? undefined : p.firstOutputAt - p.requestStartedAt);
2636
+ return {
2637
+ location: "",
2638
+ sessionId: record2.sessionId,
2639
+ requestId: p.requestId,
2640
+ startedAt: p.requestStartedAt,
2641
+ ...p.streamEndedAt === undefined ? {} : { completedAt: p.streamEndedAt },
2642
+ ...model ? { model } : {},
2643
+ ...agent ? { agent } : {},
2644
+ attribution: p.requestModel ? "request" : model ? "legacy-record" : "unknown",
2645
+ partial: p.partial,
2646
+ ...p.tokenSource === "reported" && nonnegative2(p.requestOutputTokens) ? { outputTokens: p.requestOutputTokens } : {},
2647
+ ...ttft === undefined ? {} : { ttftMs: ttft }
2648
+ };
2649
+ }
2650
+ function reconcile(a, b) {
2651
+ for (const field of [
2652
+ "completedAt",
2653
+ "outputTokens",
2654
+ "ttftMs",
2655
+ "agent"
2656
+ ])
2657
+ if (a[field] !== undefined && b[field] !== undefined && a[field] !== b[field])
2658
+ return;
2659
+ if (a.model && b.model && a.model !== b.model)
2660
+ return;
2661
+ const rank = { unknown: 0, "legacy-record": 1, request: 2 };
2662
+ const preferred = rank[b.attribution] > rank[a.attribution] ? b : a;
2663
+ return {
2664
+ ...a,
2665
+ ...b,
2666
+ ...preferred.model ? { model: preferred.model } : {},
2667
+ attribution: preferred.attribution,
2668
+ partial: a.partial || b.partial
2669
+ };
2670
+ }
2671
+ function aggregateModels(records, options) {
2672
+ const maxRequests = options.maxRequests ?? 1e5;
2673
+ if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
2674
+ throw new Error("maxRequests must be an integer from 1 to 1000000");
2675
+ if (!options.location)
2676
+ throw new Error("Analytics requires a location identity");
2677
+ for (const value of [options.from, options.to])
2678
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
2679
+ throw new Error("Invalid analytics timestamp");
2680
+ if (options.from !== undefined && options.to !== undefined && options.from > options.to)
2681
+ throw new Error("--from must precede --to");
2682
+ const coverage = {
2683
+ source: "retained-request-records",
2684
+ scannedRecords: 0,
2685
+ unscannedRecords: 0,
2686
+ duplicateRecords: 0,
2687
+ conflictingRequests: 0,
2688
+ missingIdentityRecords: 0,
2689
+ incompleteRequests: 0,
2690
+ invalidTimingRequests: 0,
2691
+ gapRecords: 0,
2692
+ maxRequests,
2693
+ partial: false
2694
+ };
2695
+ const requests = new Map;
2696
+ for (const record2 of records) {
2697
+ coverage.scannedRecords++;
2698
+ if (record2.type === "gap") {
2699
+ coverage.gapRecords++;
2700
+ continue;
2701
+ }
2702
+ if (record2.type !== "request")
2703
+ continue;
2704
+ const fact = requestFact(record2);
2705
+ if (!fact) {
2706
+ coverage.missingIdentityRecords++;
2707
+ continue;
2708
+ }
2709
+ fact.location = options.location;
2710
+ const key = JSON.stringify([
2711
+ fact.sessionId,
2712
+ fact.requestId,
2713
+ fact.startedAt
2714
+ ]);
2715
+ const previous = requests.get(key);
2716
+ if (previous) {
2717
+ coverage.duplicateRecords++;
2718
+ const merged = reconcile(previous.fact, fact);
2719
+ if (!merged)
2720
+ previous.conflict = true;
2721
+ else
2722
+ previous.fact = merged;
2723
+ } else {
2724
+ if (requests.size === maxRequests) {
2725
+ coverage.scannedRecords--;
2726
+ coverage.unscannedRecords = records.length - coverage.scannedRecords;
2727
+ break;
2728
+ }
2729
+ requests.set(key, { fact, conflict: false });
2730
+ }
2731
+ }
2732
+ const groups = new Map;
2733
+ for (const { fact: r, conflict } of requests.values()) {
2734
+ if (conflict) {
2735
+ coverage.conflictingRequests++;
2736
+ continue;
2737
+ }
2738
+ if (r.completedAt === undefined) {
2739
+ coverage.incompleteRequests++;
2740
+ continue;
2741
+ }
2742
+ const duration = r.completedAt - r.startedAt;
2743
+ if (!nonnegative2(r.completedAt) || !Number.isFinite(duration) || duration <= 0) {
2744
+ coverage.invalidTimingRequests++;
2745
+ continue;
2746
+ }
2747
+ 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)
2748
+ continue;
2749
+ let group = groups.get(r.model);
2750
+ if (!group) {
2751
+ group = {
2752
+ summary: {
2753
+ ...r.model ? { model: r.model } : {},
2754
+ sessions: 0,
2755
+ completedRequests: 0,
2756
+ partialRequests: 0,
2757
+ legacyAttributionRequests: 0,
2758
+ tpsRequests: 0,
2759
+ ttftRequests: 0,
2760
+ excludedTpsRequests: 0,
2761
+ excludedTtftRequests: 0,
2762
+ outputTokens: 0,
2763
+ durationMs: 0
2764
+ },
2765
+ sessions: new Set,
2766
+ ttfts: []
2767
+ };
2768
+ groups.set(r.model, group);
2769
+ }
2770
+ const s = group.summary;
2771
+ group.sessions.add(r.sessionId);
2772
+ s.completedRequests++;
2773
+ if (r.partial)
2774
+ s.partialRequests++;
2775
+ if (r.attribution === "legacy-record")
2776
+ s.legacyAttributionRequests++;
2777
+ if (r.outputTokens !== undefined) {
2778
+ s.tpsRequests++;
2779
+ s.outputTokens += r.outputTokens;
2780
+ s.durationMs += duration;
2781
+ } else
2782
+ s.excludedTpsRequests++;
2783
+ if (nonnegative2(r.ttftMs) && r.ttftMs <= duration)
2784
+ group.ttfts.push(r.ttftMs);
2785
+ else
2786
+ s.excludedTtftRequests++;
2787
+ }
2788
+ const models = [...groups.values()].map(({ summary: s, sessions, ttfts }) => {
2789
+ s.sessions = sessions.size;
2790
+ s.ttftRequests = ttfts.length;
2791
+ if (s.tpsRequests)
2792
+ s.averageTps = s.outputTokens * 1000 / s.durationMs;
2793
+ ttfts.sort((a, b) => a - b);
2794
+ if (ttfts.length) {
2795
+ s.meanTtftMs = ttfts.reduce((sum, n) => sum + n, 0) / ttfts.length;
2796
+ s.medianTtftMs = ((ttfts[Math.floor((ttfts.length - 1) / 2)] ?? 0) + (ttfts[Math.floor(ttfts.length / 2)] ?? 0)) / 2;
2797
+ const p95 = ttfts[Math.ceil(ttfts.length * 0.95) - 1];
2798
+ if (p95 !== undefined)
2799
+ s.p95TtftMs = p95;
2800
+ }
2801
+ return s;
2802
+ }).sort((a, b) => (a.model ?? "").localeCompare(b.model ?? ""));
2803
+ 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);
2804
+ return { schemaVersion: 1, location: options.location, models, coverage };
2805
+ }
2806
+ var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
2807
+
2808
+ // src/modelcli.ts
2809
+ var exports_modelcli = {};
2810
+ __export(exports_modelcli, {
2811
+ inspectModels: () => inspectModels
2812
+ });
2813
+ import { isAbsolute as isAbsolute4, resolve as resolve2 } from "path";
2814
+ async function inspectModels(args) {
2815
+ const directory = args[0];
2816
+ if (!directory || !isAbsolute4(directory))
2817
+ throw new Error("Use context-guard models --help for usage and examples; the history directory must be an absolute path");
2818
+ const values = new Map;
2819
+ let csv = false, json = false;
2820
+ for (let i = 1;i < args.length; i++) {
2821
+ const name = args[i];
2822
+ if (name === "--csv" || name === "--json") {
2823
+ if (name === "--csv" && csv || name === "--json" && json)
2824
+ throw new Error(`Duplicate option ${name}`);
2825
+ if (name === "--csv")
2826
+ csv = true;
2827
+ else
2828
+ json = true;
2829
+ } else if (name && ["--model", "--agent", "--from", "--to"].includes(name)) {
2830
+ if (values.has(name))
2831
+ throw new Error(`Duplicate option ${name}`);
2832
+ const value = args[++i];
2833
+ if (!value || value.startsWith("--"))
2834
+ throw new Error(`Missing value for ${name}`);
2835
+ values.set(name, value);
2836
+ } else
2837
+ throw new Error(`Unknown models option ${name}`);
2838
+ }
2839
+ if (csv && json)
2840
+ throw new Error("Choose JSON or CSV export");
2841
+ const timestamp = (value) => {
2842
+ if (value === undefined)
2843
+ return;
2844
+ const n = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
2845
+ if (!Number.isSafeInteger(n) || n < 0)
2846
+ throw new Error("Invalid timestamp");
2847
+ return n;
2848
+ };
2849
+ const from = timestamp(values.get("--from")), to = timestamp(values.get("--to"));
2850
+ if (from !== undefined && to !== undefined && from > to)
2851
+ throw new Error("--from must precede --to");
2852
+ const model = values.get("--model"), agent = values.get("--agent");
2853
+ const history = await readPerformanceHistory(directory);
2854
+ const result = aggregateModels(history.records, {
2855
+ location: resolve2(directory),
2856
+ ...from === undefined ? {} : { from },
2857
+ ...to === undefined ? {} : { to },
2858
+ ...model === undefined ? {} : { model },
2859
+ ...agent === undefined ? {} : { agent }
2860
+ });
2861
+ const metadata = {
2862
+ ...history.metadata,
2863
+ ...result.coverage,
2864
+ partial: result.coverage.partial || history.metadata.skippedBytes > 0 || history.metadata.malformedRecords > 0,
2865
+ requested: { from, to, model, agent },
2866
+ timeBasis: "stream-completion-[from,to)",
2867
+ tpsBasis: "reported-output-plus-reasoning-over-summed-request-duration",
2868
+ ttftBasis: "observed-approximate-timing",
2869
+ percentileMethod: "nearest-rank-p95-midpoint-median",
2870
+ notices: [
2871
+ "bounded-retention",
2872
+ "scan-limit-64-MiB",
2873
+ "no-pre-observation-reconstruction"
2874
+ ]
2875
+ };
2876
+ if (!csv) {
2877
+ console.log(JSON.stringify({
2878
+ schemaVersion: 1,
2879
+ location: result.location,
2880
+ metadata,
2881
+ models: result.models
2882
+ }, null, 2));
2883
+ return;
2884
+ }
2885
+ console.error(JSON.stringify(metadata));
2886
+ const fields = [
2887
+ "model",
2888
+ "sessions",
2889
+ "completedRequests",
2890
+ "partialRequests",
2891
+ "legacyAttributionRequests",
2892
+ "tpsRequests",
2893
+ "ttftRequests",
2894
+ "excludedTpsRequests",
2895
+ "excludedTtftRequests",
2896
+ "outputTokens",
2897
+ "durationMs",
2898
+ "averageTps",
2899
+ "meanTtftMs",
2900
+ "medianTtftMs",
2901
+ "p95TtftMs"
2902
+ ];
2903
+ console.log(fields.join(","));
2904
+ for (const row2 of result.models)
2905
+ console.log(fields.map((field) => `"${String(row2[field] ?? "").replaceAll('"', '""')}"`).join(","));
2906
+ }
2907
+ var init_modelcli = __esm(() => {
2908
+ init_history();
2909
+ });
2910
+
2911
+ // src/cli.ts
2912
+ import { writeFile } from "fs/promises";
2913
+ import { resolve as resolve3 } from "path";
2914
+
2915
+ // src/charts.ts
2916
+ var rateFields = [
2917
+ "liveTps",
2918
+ "averageModelTps",
2919
+ "averageSessionTps",
2920
+ "sessionOutputTokens"
2921
+ ];
2922
+ function downsamplePoints(points, limit) {
2923
+ if (!Number.isSafeInteger(limit) || limit < 16 || limit > 5000)
2924
+ throw new Error("maxPoints must be an integer from 16 to 5000");
2925
+ if (points.length <= limit)
2926
+ return [...points];
2927
+ const buckets = Math.max(1, Math.floor(limit / (2 + rateFields.length * 2)));
2928
+ const selected = new Set;
2929
+ for (let b = 0;b < buckets; b++) {
2930
+ const start = Math.floor(b * points.length / buckets), end = Math.floor((b + 1) * points.length / buckets);
2931
+ selected.add(start);
2932
+ selected.add(end - 1);
2933
+ for (const field of rateFields) {
2934
+ let low, high;
2935
+ for (let i = start;i < end; i++) {
2936
+ const value = points[i]?.[field];
2937
+ if (value === undefined)
1124
2938
  continue;
2939
+ if (low === undefined || value < (points[low]?.[field] ?? Infinity))
2940
+ low = i;
2941
+ if (high === undefined || value > (points[high]?.[field] ?? -Infinity))
2942
+ high = i;
2943
+ }
2944
+ if (low !== undefined)
2945
+ selected.add(low);
2946
+ if (high !== undefined)
2947
+ selected.add(high);
2948
+ }
2949
+ }
2950
+ let previous = -1;
2951
+ return [...selected].sort((a, b) => a - b).flatMap((index) => {
2952
+ const point = points[index];
2953
+ if (!point)
2954
+ return [];
2955
+ const skippedGap = points.slice(previous + 1, index + 1).some((p) => p.breakBefore);
2956
+ previous = index;
2957
+ return [{ ...point, breakBefore: point.breakBefore || skippedGap }];
2958
+ });
2959
+ }
2960
+ function buildChartData(records, options = {}) {
2961
+ const maxPoints = options.maxPoints ?? 1000, maxSessions = options.maxSessions ?? 128;
2962
+ if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 1024)
2963
+ throw new Error("maxSessions must be from 1 to 1024");
2964
+ downsamplePoints([], maxPoints);
2965
+ const sorted = [...records].sort((a, b) => a.time - b.time || a.recordId.localeCompare(b.recordId));
2966
+ const lastSeen = new Map;
2967
+ for (const r of sorted)
2968
+ if (r.performance && (!options.sessionId || r.sessionId === options.sessionId))
2969
+ lastSeen.set(r.sessionId, r.time);
2970
+ const sessions = [...lastSeen].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
2971
+ const selected = new Set(sessions.slice(0, maxSessions).map(([id]) => id));
2972
+ const series = new Map, gaps = [];
2973
+ const seen = new Set, requestSeen = new Set;
2974
+ const pendingGaps = new Set;
2975
+ const observedSince = new Map;
2976
+ for (const r of sorted) {
2977
+ if (seen.has(r.recordId))
2978
+ continue;
2979
+ seen.add(r.recordId);
2980
+ if (r.type === "gap") {
2981
+ if (r.sessionId === "__history__" || selected.has(r.sessionId)) {
2982
+ gaps.push({
2983
+ time: r.time,
2984
+ sessionId: r.sessionId,
2985
+ reason: r.event ?? "observation-gap"
2986
+ });
2987
+ if (r.sessionId === "__history__")
2988
+ for (const id of selected)
2989
+ pendingGaps.add(id);
2990
+ else
2991
+ pendingGaps.add(r.sessionId);
2992
+ }
2993
+ continue;
2994
+ }
2995
+ const p = r.performance;
2996
+ if (!p || !selected.has(r.sessionId))
2997
+ continue;
2998
+ let s = series.get(r.sessionId);
2999
+ if (!s) {
3000
+ s = {
3001
+ sessionId: r.sessionId,
3002
+ ...r.parentId ? { parentId: r.parentId } : {},
3003
+ agents: [],
3004
+ models: [],
3005
+ points: [],
3006
+ requests: [],
3007
+ originalPoints: 0
3008
+ };
3009
+ series.set(r.sessionId, s);
3010
+ }
3011
+ const agent = p.requestAgent ?? r.agent, model = p.requestModel ?? r.model;
3012
+ if (agent && !s.agents.includes(agent))
3013
+ s.agents.push(agent);
3014
+ if (model && !s.models.includes(model))
3015
+ s.models.push(model);
3016
+ const previous = s.points.at(-1);
3017
+ const restart = observedSince.has(r.sessionId) && observedSince.get(r.sessionId) !== p.observedSince;
3018
+ const reset = previous?.sessionOutputTokens !== undefined && p.sessionOutputTokens !== undefined && p.sessionOutputTokens < previous.sessionOutputTokens;
3019
+ 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;
3020
+ const point = {
3021
+ time: r.time,
3022
+ ...agent ? { agent } : {},
3023
+ ...model ? { model } : {},
3024
+ breakBefore: discontinuity,
3025
+ partial: p.partial,
3026
+ ...p.liveTps === undefined ? {} : { liveTps: p.liveTps },
3027
+ ...p.averageModelTps === undefined ? {} : { averageModelTps: p.averageModelTps },
3028
+ ...p.averageSessionTps === undefined ? {} : { averageSessionTps: p.averageSessionTps },
3029
+ ...p.sessionOutputTokens === undefined ? {} : { sessionOutputTokens: p.sessionOutputTokens }
3030
+ };
3031
+ if (previous?.time === point.time)
3032
+ s.points[s.points.length - 1] = {
3033
+ ...point,
3034
+ breakBefore: point.breakBefore || previous.breakBefore
3035
+ };
3036
+ else
3037
+ s.points.push(point);
3038
+ observedSince.set(r.sessionId, p.observedSince);
3039
+ pendingGaps.delete(r.sessionId);
3040
+ if (r.type === "request" && p.requestId !== undefined && p.requestStartedAt !== undefined) {
3041
+ const key = JSON.stringify([
3042
+ r.sessionId,
3043
+ p.requestId,
3044
+ p.requestStartedAt
3045
+ ]);
3046
+ if (requestSeen.has(key))
3047
+ continue;
3048
+ requestSeen.add(key);
3049
+ const duration = p.streamEndedAt === undefined ? undefined : p.streamEndedAt - p.requestStartedAt;
3050
+ const validDuration = duration !== undefined && duration > 0;
3051
+ const output = p.tokenSource === "reported" ? p.requestOutputTokens : undefined;
3052
+ s.requests.push({
3053
+ time: r.time,
3054
+ requestId: p.requestId,
3055
+ started: p.requestStartedAt,
3056
+ ...agent ? { agent } : {},
3057
+ ...model ? { model } : {},
3058
+ partial: p.partial,
3059
+ ...p.ttftMs === undefined ? {} : { ttftMs: p.ttftMs },
3060
+ ...output === undefined ? {} : { outputTokens: output },
3061
+ ...validDuration ? { durationMs: duration } : {},
3062
+ ...output !== undefined && validDuration ? { tps: output * 1000 / duration } : {}
3063
+ });
3064
+ }
3065
+ }
3066
+ return {
3067
+ schema: 1,
3068
+ series: [...series.values()].map((s) => ({
3069
+ ...s,
3070
+ originalPoints: s.points.length,
3071
+ points: downsamplePoints(s.points, maxPoints)
3072
+ })),
3073
+ gaps: gaps.slice(-1024),
3074
+ omittedGaps: Math.max(0, gaps.length - 1024),
3075
+ omittedSessions: Math.max(0, sessions.length - selected.size),
3076
+ maxPoints
3077
+ };
3078
+ }
3079
+
3080
+ // src/chartreport.ts
3081
+ function renderChartReport(report) {
3082
+ const payload = JSON.stringify(report).replace(/[<>&\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
3083
+ return `<!doctype html>
3084
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3085
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src blob: data:; connect-src 'none'">
3086
+ <title>Context Guard \xB7 Performance history</title>
3087
+ <style>
3088
+ :root{color-scheme:dark;font-family:system-ui,sans-serif;background:#0b1120;color:#e2e8f0}*{box-sizing:border-box}body{max-width:1600px;margin:auto;padding:28px}h1{font-size:28px;letter-spacing:-.03em;margin:8px 0}h2{font-size:16px;margin:0}p{color:#a8b5cb;line-height:1.6;margin:8px 0}.eyebrow{color:#7dd3fc;font-size:12px;letter-spacing:.13em;text-transform:uppercase}.toolbar,.stats,.charts{display:grid;gap:14px;margin-top:22px}.toolbar{grid-template-columns:2fr 1fr 1fr 1.4fr 1.4fr auto;align-items:end}label{font-size:12px;color:#a8b5cb;display:grid;gap:7px}select,input,button{background:#172338;border:1px solid #33435a;border-radius:7px;color:inherit;padding:10px;min-width:0;font:inherit}button{cursor:pointer;font-size:12px}button:hover,button:focus-visible{border-color:#7dd3fc}select:focus-visible,input:focus-visible{outline:2px solid #7dd3fc}.stats{grid-template-columns:repeat(4,1fr)}.stat,.chart{background:#111c2e;border:1px solid #26354e;border-radius:12px;padding:18px}.stat strong{font-size:24px;display:block;margin-top:7px}.stat span{font-size:12px;color:#a8b5cb}.charts{grid-template-columns:repeat(2,minmax(0,1fr))}.charthead{display:flex;justify-content:space-between;align-items:center;gap:10px}.chart p{font-size:12px;min-height:38px}.chart svg{display:block;width:100%;height:auto}.notice{padding:12px 15px;border-left:3px solid #fbbf24;background:#201f2d;font-size:13px;margin-top:15px}.legend{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:18px;font-size:12px}.legend span{overflow-wrap:anywhere}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:7px}.empty{padding:30px;text-align:center;color:#a8b5cb}.tooltip{position:fixed;z-index:5;pointer-events:none;background:#e2e8f0;color:#0b1120;border-radius:7px;padding:10px;max-width:360px;white-space:pre-wrap;font-size:12px;box-shadow:0 8px 30px #0008}footer{margin-top:25px;font-size:12px;color:#a8b5cb}svg circle:focus{outline:none;stroke:white;stroke-width:3}@media(max-width:1000px){.toolbar{grid-template-columns:repeat(2,minmax(0,1fr))}.charts{grid-template-columns:1fr}}@media(max-width:600px){body{padding:16px}.stats{grid-template-columns:repeat(2,1fr)}}@media print{button,.toolbar,.tooltip{display:none}body{padding:0}.chart{break-inside:avoid}.charts{grid-template-columns:1fr}}
3089
+ </style></head><body>
3090
+ <header><span class="eyebrow">Context Guard / Local analysis</span><h1>Performance history</h1><p>Compare agent instances over time. All timestamps are UTC. This report is an offline snapshot of retained metrics.</p></header>
3091
+ <section class="toolbar" aria-label="Chart filters">
3092
+ <label>Session instance<select id="session"></select></label><label>Agent<select id="agent"></select></label><label>Model<select id="model"></select></label>
3093
+ <label>From (UTC)<input id="from" type="datetime-local" step="1"></label><label>To (UTC)<input id="to" type="datetime-local" step="1"></label><button id="reset">Reset filters</button>
3094
+ </section>
3095
+ <div id="notice" class="notice" role="status"></div><section id="stats" class="stats" aria-label="Request statistics"></section>
3096
+ <div id="legend" class="legend"></div><main id="charts" class="charts"></main>
3097
+ <footer>Live TPS uses a character estimate; TTFT includes output publication delay. Dashed curves mark estimates or partial observations. Gaps and missing samples are never filled with zero. Request statistics use retained matching requests; average curves retain the session's original cumulative totals. Agent/model filters select observations, not recomputed lifetime averages. Exported SVGs reflect the current filters. Regenerate this report to include new history.</footer>
3098
+ <div id="tooltip" class="tooltip" hidden role="tooltip"></div>
3099
+ <script id="chart-data" type="application/json">${payload}</script>
3100
+ <script>(${chartClient.toString()})(JSON.parse(document.getElementById("chart-data").textContent));</script>
3101
+ </body></html>`;
3102
+ }
3103
+ function chartClient(report) {
3104
+ const { data, metadata } = report;
3105
+ const element = (id, kind) => {
3106
+ const node = document.getElementById(id);
3107
+ if (!(node instanceof kind))
3108
+ throw new Error(`Missing report element ${id}`);
3109
+ return node;
3110
+ };
3111
+ const session = element("session", HTMLSelectElement), agent = element("agent", HTMLSelectElement), model = element("model", HTMLSelectElement);
3112
+ const from = element("from", HTMLInputElement), to = element("to", HTMLInputElement);
3113
+ const charts = element("charts", HTMLElement), stats = element("stats", HTMLElement), legend = element("legend", HTMLElement);
3114
+ const notice = element("notice", HTMLElement), tooltip = element("tooltip", HTMLElement);
3115
+ const color = (index) => `hsl(${(index * 137.508 + 195) % 360}, 78%, 68%)`;
3116
+ const date = (value) => {
3117
+ const d = new Date(value);
3118
+ return Number.isFinite(d.getTime()) ? d.toISOString() : String(value);
3119
+ };
3120
+ const number = (n, digits = 1) => n === undefined ? "Unavailable" : n.toLocaleString("en-US", { maximumFractionDigits: digits });
3121
+ function options(select, values) {
3122
+ for (const v of values) {
3123
+ const o = document.createElement("option");
3124
+ o.value = v.value;
3125
+ o.textContent = v.label;
3126
+ select.append(o);
3127
+ }
3128
+ }
3129
+ options(session, [
3130
+ { value: "*", label: "Compare sessions (up to 12)" },
3131
+ ...data.series.map((s) => ({
3132
+ value: s.sessionId,
3133
+ label: `${s.agents.join(", ") || "Agent"} \xB7 ${s.sessionId}`
3134
+ }))
3135
+ ]);
3136
+ options(agent, [
3137
+ { value: "*", label: "All agents" },
3138
+ ...[...new Set(data.series.flatMap((s) => s.agents))].sort().map((v) => ({ value: v, label: v }))
3139
+ ]);
3140
+ options(model, [
3141
+ { value: "*", label: "All models" },
3142
+ ...[...new Set(data.series.flatMap((s) => s.models))].sort().map((v) => ({ value: v, label: v }))
3143
+ ]);
3144
+ function reset() {
3145
+ session.value = data.series[0]?.sessionId ?? "*";
3146
+ agent.value = report.agent ?? "*";
3147
+ model.value = report.model ?? "*";
3148
+ from.value = report.from === undefined ? "" : date(report.from).slice(0, 19);
3149
+ to.value = report.to === undefined ? "" : date(report.to).slice(0, 19);
3150
+ }
3151
+ const timestamp = (value) => value ? Date.parse(`${value}Z`) : undefined;
3152
+ function matches2(p, start, end) {
3153
+ return (agent.value === "*" || p.agent === agent.value) && (model.value === "*" || p.model === model.value) && (start === undefined || p.time >= start) && (end === undefined || p.time <= end);
3154
+ }
3155
+ const ns = "http://www.w3.org/2000/svg";
3156
+ function svgNode(name, attrs = {}, text2) {
3157
+ const n = document.createElementNS(ns, name);
3158
+ for (const [key, value] of Object.entries(attrs))
3159
+ n.setAttribute(key, String(value));
3160
+ if (text2 !== undefined)
3161
+ n.textContent = text2;
3162
+ return n;
3163
+ }
3164
+ function showTooltip(text2, x, y) {
3165
+ tooltip.textContent = text2;
3166
+ tooltip.hidden = false;
3167
+ tooltip.style.left = `${Math.max(8, Math.min(x + 12, window.innerWidth - 370))}px`;
3168
+ tooltip.style.top = `${Math.max(8, Math.min(y + 12, window.innerHeight - tooltip.offsetHeight - 10))}px`;
3169
+ }
3170
+ const definitions = [
3171
+ {
3172
+ key: "liveTps",
3173
+ title: "Live TPS",
3174
+ unit: "tokens / second",
3175
+ description: "Estimated text + reasoning throughput in the rolling collection window.",
3176
+ requests: false,
3177
+ estimated: true
3178
+ },
3179
+ {
3180
+ key: "averageModelTps",
3181
+ title: "Average model throughput",
3182
+ unit: "tokens / second",
3183
+ description: "Session output / matched model-request time, including TTFT and excluding subsequent tool time.",
3184
+ requests: false,
3185
+ estimated: false
3186
+ },
3187
+ {
3188
+ key: "averageSessionTps",
3189
+ title: "Average session throughput",
3190
+ unit: "tokens / second",
3191
+ description: "Session output / elapsed session lifetime, including tools and idle time.",
3192
+ requests: false,
3193
+ estimated: false
3194
+ },
3195
+ {
3196
+ key: "ttftMs",
3197
+ title: "Time to first output",
3198
+ unit: "seconds",
3199
+ description: "One point per request with observed text/reasoning output. Includes publication delay.",
3200
+ requests: true,
3201
+ estimated: true
3202
+ },
3203
+ {
3204
+ key: "tps",
3205
+ title: "Completed request throughput",
3206
+ unit: "tokens / second",
3207
+ description: "Reported output + reasoning / request duration. Requests are never joined into a continuous curve.",
3208
+ requests: true,
3209
+ estimated: false
3210
+ },
3211
+ {
3212
+ key: "sessionOutputTokens",
3213
+ title: "Cumulative output",
3214
+ unit: "tokens",
3215
+ description: "Authoritative session output + reasoning totals. Lifecycle and sample records are snapshots, not increments.",
3216
+ requests: false,
3217
+ estimated: false
3218
+ }
3219
+ ];
3220
+ function render() {
3221
+ tooltip.hidden = true;
3222
+ charts.replaceChildren();
3223
+ stats.replaceChildren();
3224
+ legend.replaceChildren();
3225
+ const start = timestamp(from.value), end = timestamp(to.value);
3226
+ if (start !== undefined && !Number.isFinite(start) || end !== undefined && !Number.isFinite(end) || start !== undefined && end !== undefined && start > end) {
3227
+ notice.textContent = "Choose a valid time range with From before To.";
3228
+ return;
3229
+ }
3230
+ const selected = data.series.filter((s) => (session.value === "*" || s.sessionId === session.value) && s.points.some((p) => matches2(p, start, end)) || (session.value === "*" || s.sessionId === session.value) && s.requests.some((p) => matches2(p, start, end)));
3231
+ const visible = selected.slice(0, 12);
3232
+ const requests = visible.flatMap((s) => s.requests.filter((r) => matches2(r, start, end)));
3233
+ const matched = requests.filter((r) => r.outputTokens !== undefined && r.durationMs !== undefined && r.durationMs > 0);
3234
+ const duration = matched.reduce((sum, r) => sum + (r.durationMs ?? 0), 0), tokens = matched.reduce((sum, r) => sum + (r.outputTokens ?? 0), 0);
3235
+ const ttfts = requests.flatMap((r) => r.ttftMs === undefined ? [] : [r.ttftMs]).sort((a, b) => a - b);
3236
+ const percentile = (q) => ttfts.length ? (ttfts[Math.max(0, Math.ceil(ttfts.length * q) - 1)] ?? 0) / 1000 : undefined;
3237
+ for (const [label, value] of [
3238
+ ["Matched / observed requests", `${matched.length} / ${requests.length}`],
3239
+ [
3240
+ "Retained request throughput",
3241
+ duration ? `${number(tokens * 1000 / duration)} t/s` : "Unavailable"
3242
+ ],
3243
+ [
3244
+ "Median observed TTFT",
3245
+ ttfts.length ? `${number(percentile(0.5), 2)}s` : "Unavailable"
3246
+ ],
3247
+ [
3248
+ "95th percentile TTFT",
3249
+ ttfts.length ? `${number(percentile(0.95), 2)}s` : "Unavailable"
3250
+ ]
3251
+ ]) {
3252
+ const box = document.createElement("div");
3253
+ box.className = "stat";
3254
+ const name = document.createElement("span"), valueNode = document.createElement("strong");
3255
+ name.textContent = label ?? "";
3256
+ valueNode.textContent = value ?? "";
3257
+ box.append(name, valueNode);
3258
+ stats.append(box);
3259
+ }
3260
+ const gaps = data.gaps.filter((g) => (g.sessionId === "__history__" || visible.some((s) => s.sessionId === g.sessionId)) && (start === undefined || g.time >= start) && (end === undefined || g.time <= end));
3261
+ const notes = [
3262
+ `Bounded retained history: ${metadata.retainedFrom === undefined ? "empty archive" : `segments since ${date(metadata.retainedFrom)}`}.`,
3263
+ `${gaps.length} recorded gap markers.`
3264
+ ];
3265
+ if (metadata.skippedBytes)
3266
+ notes.push(`${number(metadata.skippedBytes, 0)} bytes excluded by the read limit.`);
3267
+ if (metadata.malformedRecords)
3268
+ notes.push(`${metadata.malformedRecords} malformed/partial records skipped.`);
3269
+ if (data.omittedGaps)
3270
+ notes.push(`${data.omittedGaps} older gap markers omitted; curve discontinuities remain preserved.`);
3271
+ if (data.omittedSessions)
3272
+ notes.push(`${data.omittedSessions} sessions omitted from the report limit; regenerate with --session to inspect one.`);
3273
+ if (selected.length > 12)
3274
+ notes.push(`Showing 12 of ${selected.length} matching sessions; select a session for detail.`);
3275
+ if (visible.some((s) => s.originalPoints > s.points.length))
3276
+ notes.push(`Curves downsampled to at most ${data.maxPoints} observations per session, preserving extrema and gaps.`);
3277
+ if (visible.some((s) => s.points.some((p) => p.partial && matches2(p, start, end))))
3278
+ notes.push("Partial timing coverage is present.");
3279
+ if (!visible.length)
3280
+ notes.push("No observations match these filters.");
3281
+ notice.textContent = notes.join(" ");
3282
+ visible.forEach((s, index) => {
3283
+ const item = document.createElement("span"), dot = document.createElement("i");
3284
+ dot.className = "dot";
3285
+ dot.style.backgroundColor = color(index);
3286
+ item.append(dot, document.createTextNode(`${s.agents.join(", ") || "Agent"} \xB7 ${s.sessionId}${s.parentId ? ` (child of ${s.parentId})` : ""}`));
3287
+ legend.append(item);
3288
+ });
3289
+ const times = visible.flatMap((s) => [...s.points, ...s.requests].filter((p) => matches2(p, start, end)).map((p) => p.time));
3290
+ let minTime = start ?? times.reduce((a, t) => Math.min(a, t), Infinity), maxTime = end ?? times.reduce((a, t) => Math.max(a, t), -Infinity);
3291
+ if (!Number.isFinite(minTime))
3292
+ minTime = 0;
3293
+ if (!Number.isFinite(maxTime))
3294
+ maxTime = minTime + 1000;
3295
+ if (maxTime <= minTime)
3296
+ maxTime = minTime + 1000;
3297
+ for (const def of definitions) {
3298
+ const card = document.createElement("section");
3299
+ card.className = "chart";
3300
+ const heading = document.createElement("div");
3301
+ heading.className = "charthead";
3302
+ const title = document.createElement("h2");
3303
+ title.textContent = def.title;
3304
+ const download = document.createElement("button");
3305
+ download.textContent = "Save SVG";
3306
+ download.setAttribute("aria-label", `Save ${def.title} as SVG`);
3307
+ heading.append(title, download);
3308
+ const desc = document.createElement("p");
3309
+ desc.textContent = def.description;
3310
+ card.append(heading, desc);
3311
+ charts.append(card);
3312
+ const plots = visible.map((s) => {
3313
+ if (def.requests)
3314
+ return s.requests.filter((p) => matches2(p, start, end)).map((p) => {
3315
+ const raw = def.key === "ttftMs" ? p.ttftMs : p.tps;
3316
+ return {
3317
+ ...p,
3318
+ breakBefore: true,
3319
+ ...raw === undefined ? {} : { value: def.key === "ttftMs" ? raw / 1000 : raw }
3320
+ };
3321
+ });
3322
+ let skipped = false;
3323
+ return s.points.flatMap((p) => {
3324
+ if (!matches2(p, start, end)) {
3325
+ skipped = true;
3326
+ return [];
3327
+ }
3328
+ const key = def.key;
3329
+ const value = key === "liveTps" ? p.liveTps : key === "averageModelTps" ? p.averageModelTps : key === "averageSessionTps" ? p.averageSessionTps : p.sessionOutputTokens;
3330
+ const point = {
3331
+ ...p,
3332
+ breakBefore: p.breakBefore || skipped,
3333
+ ...value === undefined ? {} : { value }
3334
+ };
3335
+ skipped = false;
3336
+ return [point];
3337
+ });
3338
+ });
3339
+ const drawn = plots.map((points) => {
3340
+ if (!def.requests || points.length <= data.maxPoints)
3341
+ return points;
3342
+ const bins = Math.max(1, Math.floor(data.maxPoints / 4)), chosen = new Set;
3343
+ for (let b = 0;b < bins; b++) {
3344
+ const first = Math.floor(b * points.length / bins), last = Math.floor((b + 1) * points.length / bins) - 1;
3345
+ chosen.add(first);
3346
+ chosen.add(last);
3347
+ let low = first, high = first;
3348
+ for (let i = first;i <= last; i++) {
3349
+ if ((points[i]?.value ?? Infinity) < (points[low]?.value ?? Infinity))
3350
+ low = i;
3351
+ if ((points[i]?.value ?? -Infinity) > (points[high]?.value ?? -Infinity))
3352
+ high = i;
3353
+ }
3354
+ chosen.add(low);
3355
+ chosen.add(high);
1125
3356
  }
1126
- 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)
1127
- continue;
1128
- records.push(value);
1129
- } catch {
1130
- malformedRecords++;
3357
+ return [...chosen].sort((a, b) => a - b).flatMap((i) => points[i] ? [points[i]] : []);
3358
+ });
3359
+ const maxValue = drawn.reduce((maximum, points) => points.reduce((n, p) => Math.max(n, p.value ?? 0), maximum), 1) * 1.08;
3360
+ const w = 720, h = 300, left = 64, right = 20, top = 24, bottom = 47;
3361
+ const x = (t) => left + (t - minTime) / (maxTime - minTime) * (w - left - right);
3362
+ const y = (v) => h - bottom - v / maxValue * (h - top - bottom);
3363
+ const svg = svgNode("svg", {
3364
+ viewBox: `0 0 ${w} ${h}`,
3365
+ role: "img",
3366
+ "aria-label": `${def.title} over UTC timestamps`,
3367
+ xmlns: ns
3368
+ });
3369
+ svg.append(svgNode("rect", { width: w, height: h, fill: "#111c2e" }), svgNode("title", {}, `${def.title}. ${def.description}`));
3370
+ svg.append(svgNode("text", {
3371
+ x: left,
3372
+ y: 14,
3373
+ fill: "#a8b5cb",
3374
+ "font-size": 11,
3375
+ "font-family": "system-ui,sans-serif"
3376
+ }, def.unit));
3377
+ for (let tick = 0;tick <= 4; tick++) {
3378
+ const v = maxValue * tick / 4;
3379
+ svg.append(svgNode("line", {
3380
+ x1: left,
3381
+ x2: w - right,
3382
+ y1: y(v),
3383
+ y2: y(v),
3384
+ stroke: "#2a3950",
3385
+ "stroke-width": 1
3386
+ }));
3387
+ svg.append(svgNode("text", {
3388
+ x: left - 8,
3389
+ y: y(v) + 4,
3390
+ fill: "#a8b5cb",
3391
+ "text-anchor": "end",
3392
+ "font-size": 11,
3393
+ "font-family": "system-ui,sans-serif"
3394
+ }, v >= 1e4 ? `${number(v / 1000)}k` : number(v)));
3395
+ const t = minTime + (maxTime - minTime) * tick / 4;
3396
+ const label = maxTime - minTime > 86400000 ? date(t).slice(5, 16).replace("T", " ") : date(t).slice(11, 19);
3397
+ svg.append(svgNode("text", {
3398
+ x: x(t),
3399
+ y: h - 22,
3400
+ fill: "#a8b5cb",
3401
+ "text-anchor": tick === 0 ? "start" : tick === 4 ? "end" : "middle",
3402
+ "font-size": 11,
3403
+ "font-family": "system-ui,sans-serif"
3404
+ }, label));
1131
3405
  }
3406
+ for (const gap of gaps)
3407
+ if (gap.time >= minTime && gap.time <= maxTime) {
3408
+ const line = svgNode("line", {
3409
+ x1: x(gap.time),
3410
+ x2: x(gap.time),
3411
+ y1: top,
3412
+ y2: h - bottom,
3413
+ stroke: "#fb7185",
3414
+ "stroke-dasharray": "3 5",
3415
+ opacity: 0.65
3416
+ });
3417
+ line.append(svgNode("title", {}, `${date(gap.time)} \xB7 ${gap.reason}`));
3418
+ svg.append(line);
3419
+ }
3420
+ let count2 = 0;
3421
+ drawn.forEach((points, index) => {
3422
+ let run = [];
3423
+ const flush = () => {
3424
+ if (run.length > 1)
3425
+ svg.append(svgNode("polyline", {
3426
+ points: run.map((p) => `${x(p.time)},${y(p.value ?? 0)}`).join(" "),
3427
+ fill: "none",
3428
+ stroke: color(index),
3429
+ "stroke-width": 1.8,
3430
+ ...def.estimated || run.some((p) => p.partial) ? { "stroke-dasharray": "5 4" } : {}
3431
+ }));
3432
+ run = [];
3433
+ };
3434
+ for (const point of points) {
3435
+ if (point.breakBefore || point.value === undefined)
3436
+ flush();
3437
+ if (point.value === undefined)
3438
+ continue;
3439
+ count2++;
3440
+ if (!def.requests)
3441
+ run.push(point);
3442
+ const label = `${date(point.time)}\n${visible[index]?.sessionId ?? ""}${point.agent ? ` \xB7 ${point.agent}` : ""}${point.model ? `\n${point.model}` : ""}\n${def.title}: ${def.estimated ? "~" : ""}${number(point.value, 3)} ${def.unit}${point.requestId ? `\nRequest: ${point.requestId}` : ""}${point.partial ? "\nPartial observation coverage" : ""}`;
3443
+ const dot = svgNode("circle", {
3444
+ cx: x(point.time),
3445
+ cy: y(point.value),
3446
+ r: def.requests ? 3 : 2.5,
3447
+ fill: color(index),
3448
+ tabindex: 0,
3449
+ "aria-label": label
3450
+ });
3451
+ dot.append(svgNode("title", {}, label));
3452
+ dot.addEventListener("mouseenter", (e) => showTooltip(label, e.clientX, e.clientY));
3453
+ dot.addEventListener("mouseleave", () => {
3454
+ tooltip.hidden = true;
3455
+ });
3456
+ dot.addEventListener("focus", () => {
3457
+ const box = dot.getBoundingClientRect();
3458
+ showTooltip(label, box.x, box.y);
3459
+ });
3460
+ dot.addEventListener("blur", () => {
3461
+ tooltip.hidden = true;
3462
+ });
3463
+ svg.append(dot);
3464
+ }
3465
+ flush();
3466
+ });
3467
+ if (!count2)
3468
+ svg.append(svgNode("text", {
3469
+ x: w / 2,
3470
+ y: h / 2,
3471
+ fill: "#a8b5cb",
3472
+ "text-anchor": "middle",
3473
+ "font-size": 14,
3474
+ "font-family": "system-ui,sans-serif"
3475
+ }, "No measurements in this selection"));
3476
+ card.append(svg);
3477
+ download.onclick = () => {
3478
+ const blob = new Blob([new XMLSerializer().serializeToString(svg)], {
3479
+ type: "image/svg+xml"
3480
+ });
3481
+ const url = URL.createObjectURL(blob);
3482
+ const link = document.createElement("a");
3483
+ link.href = url;
3484
+ link.download = `context-guard-${def.key}.svg`;
3485
+ link.click();
3486
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
3487
+ };
1132
3488
  }
1133
3489
  }
1134
- const metadata = {
1135
- schema: 1,
1136
- ...files[0] ? { retainedFrom: files[0].time } : {},
1137
- retainedBytes: files.reduce((n, f) => n + f.bytes, 0),
1138
- skippedBytes,
1139
- malformedRecords,
1140
- completeness: "bounded-retention"
3490
+ for (const control of [session, agent, model, from, to])
3491
+ control.addEventListener("change", render);
3492
+ element("reset", HTMLButtonElement).onclick = () => {
3493
+ reset();
3494
+ render();
1141
3495
  };
1142
- return { metadata, records };
1143
- }
1144
- function isRecord(value) {
1145
- return value !== null && typeof value === "object" && !Array.isArray(value);
1146
- }
1147
- function isHistoryRecord(value) {
1148
- if (!isRecord(value))
1149
- return false;
1150
- const r = value;
1151
- const allowed = new Set([
1152
- "schema",
1153
- "recordId",
1154
- "time",
1155
- "type",
1156
- "sessionId",
1157
- "parentId",
1158
- "agent",
1159
- "model",
1160
- "event",
1161
- "performance",
1162
- "droppedRecords"
1163
- ]);
1164
- 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);
3496
+ reset();
3497
+ render();
1165
3498
  }
1166
3499
 
3500
+ // src/cli.ts
3501
+ init_history();
3502
+
1167
3503
  // src/telemetry.ts
1168
3504
  import { mkdir as mkdir2, open as open2, rename, stat as stat2 } from "fs/promises";
1169
3505
  import { dirname, join as join2 } from "path";
@@ -1277,14 +3613,147 @@ async function readJournal(path, maxBytes = 16 * 1024 * 1024) {
1277
3613
  return records;
1278
3614
  }
1279
3615
 
3616
+ // src/clihelp.ts
3617
+ var mainHelp = `Context Guard \u2014 context monitoring for OpenCode V2
3618
+
3619
+ Start here
3620
+ In your project, run: opencode
3621
+ Inside OpenCode, enter: /context-guard
3622
+ The plugin loads with OpenCode; observe mode is the default.
3623
+
3624
+ Usage: context-guard <command> [options]
3625
+ context-guard <journal.jsonl> [--history | --json]
3626
+
3627
+ Commands
3628
+ dashboard tui Open the standalone live terminal dashboard
3629
+ models Compare models across retained history
3630
+ metrics Export retained performance records as JSON or CSV
3631
+ charts Generate an interactive HTML report
3632
+ <journal.jsonl> Inspect diagnostic session records
3633
+
3634
+ Examples
3635
+ context-guard dashboard tui
3636
+ context-guard models /absolute/history-directory
3637
+ context-guard charts /absolute/history-directory --output report.html
3638
+ context-guard /absolute/events.jsonl --history
3639
+
3640
+ Help
3641
+ context-guard <command> --help Show options and examples
3642
+ context-guard help journal Explain diagnostic inspection
3643
+ -h, --help Show help
3644
+
3645
+ Requires Bun >=1.1.34; the standalone dashboard requires Bun >=1.3.0.
3646
+ From a source checkout, replace context-guard with bun run inspect.`;
3647
+ var filterOptions = ` --agent NAME Filter by exact agent name
3648
+ --model PROVIDER/MODEL
3649
+ Filter by exact model ID
3650
+ --from ISO-or-ms Start time: ISO timestamp or Unix milliseconds
3651
+ --to ISO-or-ms End time: ISO timestamp or Unix milliseconds`;
3652
+ var historyOptions = ` --session ID Filter by session
3653
+ ${filterOptions}`;
3654
+ var historyPath = `Use the owning location's history.path directory, not its diagnostic JSONL
3655
+ file. By default it is a location-hashed directory under
3656
+ \$XDG_STATE_HOME/opencode-context-guard (or ~/.local/state/opencode-context-guard).
3657
+ A custom telemetry.path does not change the history directory.`;
3658
+ var commandHelp = {
3659
+ models: `Usage: context-guard models <history-directory> [options]
3660
+ Compare model throughput and time to first token across retained sessions.
3661
+ The history directory must be an absolute path. Output defaults to JSON.
3662
+
3663
+ Options
3664
+ ${filterOptions}
3665
+ --csv Export CSV; coverage metadata goes to stderr
3666
+ --json Export JSON (default); cannot combine with --csv
3667
+ -h, --help Show this help
3668
+
3669
+ Example
3670
+ context-guard models /absolute/history-directory --agent build --csv
3671
+
3672
+ ${historyPath}`,
3673
+ metrics: `Usage: context-guard metrics <history-directory> [options]
3674
+ Read retained performance records. Output defaults to JSON.
3675
+
3676
+ Options
3677
+ ${historyOptions}
3678
+ --csv Export CSV; coverage metadata goes to stderr
3679
+ --chart FILE.html Generate an HTML report instead (see charts --help)
3680
+ -h, --help Show this help
3681
+
3682
+ Example
3683
+ context-guard metrics /absolute/history-directory --csv > metrics.csv
3684
+
3685
+ ${historyPath}`,
3686
+ charts: `Usage: context-guard charts <history-directory> [options]
3687
+ Generate a self-contained interactive HTML report, then open it in a browser.
3688
+
3689
+ Options
3690
+ ${historyOptions}
3691
+ --output FILE.html Write the report (default: context-guard-charts.html)
3692
+ --max-points N Points per series: 16\u20135000 (default: 1000)
3693
+ --max-sessions N Session instances: 1\u20131024 (default: 128)
3694
+ --force Overwrite an existing report
3695
+ -h, --help Show this help
3696
+
3697
+ Example
3698
+ context-guard charts /absolute/history-directory --output report.html
3699
+
3700
+ ${historyPath}`,
3701
+ journal: `Usage: context-guard <journal.jsonl> [options]
3702
+ Inspect the diagnostic journal reported in OpenCode's Context Guard startup log.
3703
+ By default, print the latest retained record for each session, one JSON per line.
3704
+
3705
+ Options
3706
+ --json Print latest session records as a JSON array
3707
+ --history Print all retained journal records as a JSON array
3708
+ -h, --help Show this help
3709
+
3710
+ Example
3711
+ context-guard /absolute/path/from/startup.jsonl --history
3712
+
3713
+ This reads a diagnostic file, not a performance history directory.`
3714
+ };
3715
+
1280
3716
  // src/cli.ts
1281
3717
  var args = process.argv.slice(2);
1282
- if (!args[0] || args.includes("--help")) {
1283
- console.log("Usage: bun run inspect /absolute/path/to/journal.jsonl [--history] [--json]\nThe ready log reports the location-specific journal path.\nMetrics: inspect metrics /absolute/history-directory [--session ID] [--from ISO-or-ms] [--to ISO-or-ms] [--csv]\nCharts: inspect charts /absolute/history-directory --output report.html [--session ID] [--agent NAME] [--model provider/model] [--from ISO-or-ms] [--to ISO-or-ms] [--max-points 1000] [--force]");
3718
+ var helpRequested = args.some((arg) => arg === "--help" || arg === "-h");
3719
+ var helpTopic = args[0] === "help" ? args[1] : args[0];
3720
+ if (!args[0] || args[0] === "help" || helpRequested || args.length === 1 && (args[0] === "dashboard" || Object.hasOwn(commandHelp, args[0]))) {
3721
+ if (helpTopic === "dashboard") {
3722
+ const { dashboardHelp: dashboardHelp2 } = await Promise.resolve().then(() => (init_command(), exports_command));
3723
+ console.log(dashboardHelp2);
3724
+ } else if (helpTopic && Object.hasOwn(commandHelp, helpTopic)) {
3725
+ console.log(commandHelp[helpTopic]);
3726
+ } else if (args[0] === "help" && helpTopic) {
3727
+ console.error(`Unknown help topic. Use context-guard --help for commands.`);
3728
+ process.exitCode = 1;
3729
+ } else if (helpRequested && helpTopic && !helpTopic.startsWith("-")) {
3730
+ console.log(commandHelp.journal);
3731
+ } else
3732
+ console.log(mainHelp);
3733
+ } else if (args[0] === "dashboard") {
3734
+ const { parseDashboardArgs: parseDashboardArgs2, dashboardHelp: dashboardHelp2, checkTerminalRuntime: checkTerminalRuntime2 } = await Promise.resolve().then(() => (init_command(), exports_command));
3735
+ try {
3736
+ const options = parseDashboardArgs2(args.slice(1), process.cwd(), process.env);
3737
+ if (options === "help")
3738
+ console.log(dashboardHelp2);
3739
+ else {
3740
+ checkTerminalRuntime2(Bun.version, Boolean(process.stdin.isTTY && process.stdout.isTTY));
3741
+ const { runTerminal: runTerminal2 } = await Promise.resolve().then(() => (init_terminal(), exports_terminal));
3742
+ await runTerminal2(options);
3743
+ }
3744
+ } catch (error) {
3745
+ const message = error instanceof Error ? error.message : "";
3746
+ const safe = /^(Use context-guard|Unknown dashboard|Duplicate dashboard|Missing value for --|Explicit servers require|dashboard tui requires|Terminal renderer unavailable|Terminal dashboard stopped|Dashboard directory must|Explicit credentials require|Use an HTTPS|No running|Dashboard server connection failed)/.test(message);
3747
+ console.error(safe ? message : "Dashboard startup failed; check server, owning directory, authentication and renderer installation");
3748
+ process.exitCode = 1;
3749
+ }
3750
+ } else if (args[0] === "models") {
3751
+ const { inspectModels: inspectModels2 } = await Promise.resolve().then(() => (init_modelcli(), exports_modelcli));
3752
+ await inspectModels2(args.slice(1));
1284
3753
  } else if (args[0] === "metrics" || args[0] === "charts") {
1285
3754
  const directory = args[1];
1286
3755
  if (!directory || directory.startsWith("--"))
1287
- throw new Error("Usage: inspect metrics /absolute/history-directory [--session ID] [--from ISO-or-ms] [--to ISO-or-ms] [--csv]");
3756
+ throw new Error(`Use context-guard ${args[0]} --help for usage and examples`);
1288
3757
  const option = (name) => {
1289
3758
  const index = args.indexOf(name);
1290
3759
  if (index < 0)
@@ -1324,7 +3793,7 @@ if (!args[0] || args.includes("--help")) {
1324
3793
  ...to === undefined ? {} : { to }
1325
3794
  });
1326
3795
  if (chart) {
1327
- const output = resolve(option("--chart") ?? option("--output") ?? "context-guard-charts.html");
3796
+ const output = resolve3(option("--chart") ?? option("--output") ?? "context-guard-charts.html");
1328
3797
  if (!output.toLowerCase().endsWith(".html"))
1329
3798
  throw new Error("Chart output must use an .html extension");
1330
3799
  const data = buildChartData(result.records, { maxPoints, maxSessions });