@cesarus777/opencode-cgp 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,3393 @@ 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; install @opentui/core@0.5.17 beside @cesarus777/opencode-cgp using Bun >=1.3.0");
667
+ }
668
+ }
669
+ var dashboardHelp = `Usage: context-guard dashboard tui [--server URL] [--directory PATH]
670
+ Read-only standalone terminal dashboard; never starts an OpenCode server.
671
+ Requires an interactive terminal, Bun >=1.3.0 and @opentui/core@0.5.17.
672
+ Install the renderer beside this package: npm install @opentui/core@0.5.17
673
+ Local discovery defaults to the current directory.
674
+ An explicit server requires --directory with an owning absolute server path.
675
+ Remote authentication: CONTEXT_GUARD_PASSWORD and optional CONTEXT_GUARD_USERNAME
676
+ (default opencode). Credentials must never be supplied in the URL.
677
+ Keys: 1 overview, 2 detail, 3 history, arrows/j/k move, / search, f filters,
678
+ l location, w history window, r refresh, ? help, Escape back, q/Ctrl+C quit.`;
679
+ var init_command = () => {
680
+ };
681
+
682
+ // src/family.ts
683
+ function metric(snapshot, now, staleMs) {
684
+ const m = snapshot?.measurement;
685
+ const freshness = !m ? "missing" : m.model !== snapshot?.model || m.at > now ? "invalid" : now - m.at > staleMs ? "stale" : "fresh";
686
+ const t = snapshot?.policy.triggerTokens;
687
+ const valid2 = freshness === "fresh" && m !== undefined;
688
+ const displayable = valid2 || freshness === "stale" && m !== undefined;
689
+ const pressureRatio = valid2 && t !== undefined && t > 0 ? m.tokens / t : undefined;
690
+ const crossed = valid2 && t !== undefined && t > 0 && (snapshot?.policy.comparison === ">" ? m.tokens > t : m.tokens >= t);
691
+ return {
692
+ freshness,
693
+ pressure: pressureRatio === undefined ? "unknown" : crossed ? "crossed" : pressureRatio >= 0.9 ? "near" : "normal",
694
+ ...pressureRatio === undefined ? {} : { pressureRatio },
695
+ ...displayable && snapshot?.capacityTokens && snapshot.capacityTokens > 0 ? { utilizationPercent: m.tokens / snapshot.capacityTokens * 100 } : {},
696
+ ...valid2 && t !== undefined ? { triggerDeltaTokens: t - m.tokens } : {}
697
+ };
698
+ }
699
+ function familyRows(nodes, snapshots, current, now, staleMs, expanded = new Set, limit = 1024, timing, stableOrder = []) {
700
+ const byId = new Map(nodes.map((n) => [n.sessionId, n]));
701
+ const children = new Map;
702
+ for (const n of byId.values()) {
703
+ const parent = n.parentId && byId.has(n.parentId) ? n.parentId : "";
704
+ const list = children.get(parent) ?? [];
705
+ list.push(n);
706
+ children.set(parent, list);
707
+ }
708
+ const metrics = new Map([...byId.keys()].map((id) => [
709
+ id,
710
+ metric(snapshots.get(id), timing?.(id).now ?? now, timing?.(id).staleMs ?? staleMs)
711
+ ]));
712
+ const protectedIds = new Set;
713
+ let ancestor = byId.get(current);
714
+ while (ancestor && !protectedIds.has(ancestor.sessionId)) {
715
+ protectedIds.add(ancestor.sessionId);
716
+ ancestor = ancestor.parentId ? byId.get(ancestor.parentId) : undefined;
717
+ }
718
+ const scores = new Map, sensitive = new Map, sizes = new Map;
719
+ const calculate = (n, seen) => {
720
+ if (seen.has(n.sessionId))
721
+ return -1;
722
+ if (scores.has(n.sessionId))
723
+ return scores.get(n.sessionId) ?? -1;
724
+ const path = new Set(seen).add(n.sessionId), m = metrics.get(n.sessionId);
725
+ let score = m?.pressureRatio ?? -1, size = 1;
726
+ let keep = protectedIds.has(n.sessionId) || n.status === "running" || m?.pressure === "crossed" || ["blocked", "uncertain"].includes(snapshots.get(n.sessionId)?.compaction.phase ?? "");
727
+ for (const child of children.get(n.sessionId) ?? []) {
728
+ score = Math.max(score, calculate(child, path));
729
+ size += sizes.get(child.sessionId) ?? 0;
730
+ keep ||= sensitive.get(child.sessionId) ?? false;
731
+ }
732
+ scores.set(n.sessionId, score);
733
+ sizes.set(n.sessionId, size);
734
+ sensitive.set(n.sessionId, keep);
735
+ return score;
736
+ };
737
+ for (const n of byId.values())
738
+ calculate(n, new Set);
739
+ const visited = new Set, rows = [];
740
+ const visit = (n, depth) => {
741
+ if (visited.has(n.sessionId) || rows.length >= limit || depth > 64)
633
742
  return;
743
+ visited.add(n.sessionId);
744
+ const snapshot = snapshots.get(n.sessionId), m = metrics.get(n.sessionId) ?? metric(undefined, now, staleMs);
745
+ const collapsed = n.status === "completed" && !sensitive.get(n.sessionId) && !expanded.has(n.sessionId);
746
+ rows.push({
747
+ node: n,
748
+ ...snapshot ? { snapshot } : {},
749
+ depth,
750
+ current: n.sessionId === current,
751
+ ...m,
752
+ hidden: collapsed ? (sizes.get(n.sessionId) ?? 1) - 1 : 0
753
+ });
754
+ if (collapsed) {
755
+ const pending = [...children.get(n.sessionId) ?? []];
756
+ while (pending.length) {
757
+ const child = pending.pop();
758
+ if (!child || visited.has(child.sessionId))
759
+ continue;
760
+ visited.add(child.sessionId);
761
+ pending.push(...children.get(child.sessionId) ?? []);
762
+ }
634
763
  }
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);
764
+ if (!collapsed)
765
+ for (const child of sorted(children.get(n.sessionId) ?? []))
766
+ visit(child, depth + 1);
767
+ };
768
+ const ranks = new Map(stableOrder.map((id, i) => [id, i]));
769
+ 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));
770
+ for (const n of sorted(children.get("") ?? []))
771
+ visit(n, 0);
772
+ for (const n of byId.values())
773
+ if (!visited.has(n.sessionId) && rows.length < limit)
774
+ visit(n, 0);
775
+ return rows;
776
+ }
777
+ function executionStatus(known, nativeRunning, evidence) {
778
+ if (nativeRunning || evidence.startedAt !== undefined && evidence.startedAt > (evidence.terminalAt ?? -1))
779
+ return "running";
780
+ if (evidence.outcome && (evidence.terminalAt ?? -1) >= (evidence.startedAt ?? -1))
781
+ return evidence.outcome === "succeeded" ? "completed" : evidence.outcome === "failed" ? "failed" : "unknown";
782
+ return known ? "idle" : "unknown";
783
+ }
784
+
785
+ // src/tui/transport.ts
786
+ class SnapshotClient {
787
+ transport;
788
+ changed;
789
+ elapsed;
790
+ rows = new Map;
791
+ health;
792
+ connection = "loading";
793
+ epoch;
794
+ sequence = -1;
795
+ revisions = new Map;
796
+ baselines = new Map;
797
+ controller;
798
+ generation = 0;
799
+ ids = [];
800
+ location = "";
801
+ refreshTimer;
802
+ reading;
803
+ buffered = [];
804
+ overflow = false;
805
+ retryTimer;
806
+ failures = 0;
807
+ serverAt = 0;
808
+ receivedAt = 0;
809
+ refreshAgain = false;
810
+ subscriptionActive = false;
811
+ constructor(transport, changed, elapsed = () => performance.now()) {
812
+ this.transport = transport;
813
+ this.changed = changed;
814
+ this.elapsed = elapsed;
815
+ }
816
+ now() {
817
+ return this.serverAt + Math.max(0, this.elapsed() - this.receivedAt);
818
+ }
819
+ setIds(ids) {
820
+ const next = [...new Set(ids)].slice(0, 2048);
821
+ if (JSON.stringify(next) === JSON.stringify(this.ids))
822
+ return;
823
+ this.ids = next;
824
+ const selected = new Set(next);
825
+ for (const id of new Set([
826
+ ...this.revisions.keys(),
827
+ ...this.baselines.keys()
828
+ ]))
829
+ if (!selected.has(id)) {
830
+ this.revisions.delete(id);
831
+ this.baselines.delete(id);
832
+ this.rows.delete(id);
833
+ }
834
+ if (this.reading)
835
+ this.refreshAgain = true;
836
+ else
837
+ this.refresh();
838
+ }
839
+ start(location, ids) {
840
+ this.stop();
841
+ this.location = location;
842
+ this.ids = [...new Set(ids)].slice(0, 2048);
843
+ this.rows.clear();
844
+ this.revisions.clear();
845
+ this.baselines.clear();
846
+ this.health = undefined;
847
+ this.epoch = undefined;
848
+ this.sequence = -1;
849
+ this.failures = 0;
850
+ this.connection = "loading";
851
+ const controller = new AbortController;
852
+ this.controller = controller;
853
+ const generation = ++this.generation;
854
+ this.supervise(generation, controller.signal);
855
+ this.refreshTimer = setInterval(() => {
856
+ this.refresh();
857
+ }, 15000);
858
+ this.refresh();
859
+ }
860
+ merge(batch, baseline = false, requested = []) {
861
+ if (this.epoch !== batch.epoch) {
862
+ if (!baseline) {
863
+ this.connection = "reconnecting";
864
+ this.refreshAgain = true;
865
+ return;
866
+ }
867
+ this.rows.clear();
868
+ this.revisions.clear();
869
+ this.baselines.clear();
870
+ this.epoch = batch.epoch;
871
+ this.sequence = -1;
664
872
  }
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);
873
+ if (!baseline && batch.sequence > this.sequence + 1) {
874
+ this.connection = "reconnecting";
875
+ this.refreshAgain = true;
876
+ }
877
+ if (batch.sequence >= this.sequence) {
878
+ this.sequence = batch.sequence;
879
+ this.health = batch.health;
880
+ this.serverAt = batch.generatedAt;
881
+ this.receivedAt = this.elapsed();
882
+ }
883
+ const selected = new Set(baseline ? requested.filter((id) => this.ids.includes(id)) : this.ids);
884
+ for (const row2 of batch.upserts)
885
+ if (selected.has(row2.sessionId) && (baseline || batch.sequence > (this.baselines.get(row2.sessionId) ?? -1)) && row2.revision > (this.revisions.get(row2.sessionId) ?? -1)) {
886
+ this.rows.set(row2.sessionId, row2);
887
+ this.revisions.set(row2.sessionId, row2.revision);
888
+ }
889
+ for (const row2 of batch.removed)
890
+ if (selected.has(row2.sessionId) && (baseline || batch.sequence > (this.baselines.get(row2.sessionId) ?? -1)) && row2.revision > (this.revisions.get(row2.sessionId) ?? -1)) {
891
+ this.rows.delete(row2.sessionId);
892
+ this.revisions.set(row2.sessionId, row2.revision);
893
+ }
894
+ this.changed();
895
+ }
896
+ refresh() {
897
+ if (this.reading)
898
+ return this.reading;
899
+ const controller = this.controller, generation = this.generation, location = this.location;
900
+ if (!controller)
901
+ return Promise.resolve();
902
+ clearTimeout(this.retryTimer);
903
+ const requestedIds = [...this.ids];
904
+ this.reading = (async () => {
905
+ const signal = AbortSignal.any([
906
+ controller.signal,
907
+ AbortSignal.timeout(5000)
908
+ ]);
909
+ const chunks = [];
910
+ for (let i = 0;i < requestedIds.length; i += 128)
911
+ chunks.push(requestedIds.slice(i, i + 128));
912
+ if (!chunks.length)
913
+ chunks.push([]);
914
+ const results = [];
915
+ for (let i = 0;i < chunks.length; i += 2) {
916
+ const reads = await Promise.all(chunks.slice(i, i + 2).map(async (ids) => {
917
+ const value = await abortable(this.transport.read(location, ids, signal), signal);
918
+ if (generation !== this.generation)
919
+ throw new Error("Obsolete read");
920
+ if (!isPerformanceResult(value)) {
921
+ this.connection = "incompatible";
922
+ throw new Error("Incompatible snapshot");
923
+ }
924
+ const answered = [
925
+ ...value.upserts.map((row2) => row2.sessionId),
926
+ ...value.unavailable.map((row2) => row2.sessionId)
927
+ ];
928
+ if (value.removed.length || answered.length !== ids.length || new Set(answered).size !== ids.length || answered.some((id) => !ids.includes(id)))
929
+ throw new Error("Incomplete snapshot read");
930
+ return { ids, result: value };
931
+ }));
932
+ results.push(...reads);
933
+ }
934
+ if (generation !== this.generation || signal.aborted)
935
+ return;
936
+ if (new Set(results.map((r) => r.result.epoch)).size !== 1)
937
+ throw new Error("Epoch changed during read");
938
+ for (const { ids, result } of results) {
939
+ this.merge(result, true, ids);
940
+ for (const missing of result.unavailable)
941
+ if (this.ids.includes(missing.sessionId))
942
+ this.rows.delete(missing.sessionId);
943
+ for (const id of ids)
944
+ if (this.ids.includes(id))
945
+ this.baselines.set(id, result.sequence);
946
+ }
947
+ const buffered = this.buffered;
948
+ this.buffered = [];
949
+ if (buffered.some((b) => b.epoch !== this.epoch) || this.overflow) {
950
+ this.overflow = false;
951
+ throw new Error("Stream changed during read");
952
+ }
953
+ for (const batch of buffered)
954
+ this.merge(batch);
955
+ this.failures = 0;
956
+ this.connection = this.refreshAgain || !this.subscriptionActive ? "reconnecting" : "live";
957
+ this.changed();
958
+ })().catch(() => {
959
+ if (generation === this.generation && !controller.signal.aborted) {
960
+ this.buffered = [];
961
+ this.overflow = false;
962
+ if (this.connection !== "incompatible")
963
+ this.connection = this.rows.size ? "reconnecting" : "unavailable";
964
+ this.changed();
965
+ this.retryTimer = setTimeout(() => {
966
+ this.refresh();
967
+ }, Math.min(30000, 1000 * 2 ** Math.min(this.failures++, 5)));
968
+ }
969
+ }).finally(() => {
970
+ if (generation === this.generation) {
971
+ this.reading = undefined;
972
+ if (this.refreshAgain) {
973
+ this.refreshAgain = false;
974
+ this.refresh();
975
+ }
976
+ }
693
977
  });
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
- };
978
+ return this.reading;
979
+ }
980
+ async supervise(generation, signal) {
981
+ let retry = 0;
982
+ while (!signal.aborted && generation === this.generation) {
983
+ try {
984
+ this.subscriptionActive = true;
985
+ if (retry)
986
+ queueMicrotask(() => {
987
+ if (generation === this.generation && !signal.aborted)
988
+ this.refresh();
726
989
  });
727
- let skipped = false;
728
- return s.points.flatMap((p) => {
729
- if (!matches2(p, start, end)) {
730
- skipped = true;
731
- return [];
990
+ for await (const value of this.transport.subscribe(this.location, signal)) {
991
+ if (signal.aborted || generation !== this.generation)
992
+ return;
993
+ if (!isPerformanceBatch(value)) {
994
+ this.connection = "incompatible";
995
+ this.changed();
996
+ continue;
732
997
  }
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;
998
+ if (this.reading || !this.epoch) {
999
+ if (this.buffered.length < 256)
1000
+ this.buffered.push(value);
1001
+ else
1002
+ this.overflow = true;
1003
+ } else {
1004
+ this.merge(value);
1005
+ if (this.refreshAgain) {
1006
+ this.refreshAgain = false;
1007
+ this.refresh();
1008
+ }
758
1009
  }
759
- chosen.add(low);
760
- chosen.add(high);
1010
+ retry = 0;
761
1011
  }
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));
1012
+ } catch {
810
1013
  }
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 = [];
1014
+ if (signal.aborted || generation !== this.generation)
1015
+ return;
1016
+ this.subscriptionActive = false;
1017
+ this.connection = "reconnecting";
1018
+ this.changed();
1019
+ await this.refresh();
1020
+ await new Promise((resolve2) => {
1021
+ const finish = () => {
1022
+ clearTimeout(timer);
1023
+ signal.removeEventListener("abort", finish);
1024
+ resolve2();
838
1025
  };
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);
1026
+ const timer = setTimeout(finish, Math.min(30000, 1000 * 2 ** Math.min(retry++, 5)));
1027
+ signal.addEventListener("abort", finish, { once: true });
1028
+ if (signal.aborted)
1029
+ finish();
1030
+ });
1031
+ }
1032
+ }
1033
+ stop() {
1034
+ this.generation++;
1035
+ this.controller?.abort();
1036
+ this.controller = undefined;
1037
+ clearInterval(this.refreshTimer);
1038
+ clearTimeout(this.retryTimer);
1039
+ this.reading = undefined;
1040
+ this.buffered = [];
1041
+ this.overflow = false;
1042
+ this.refreshAgain = false;
1043
+ }
1044
+ }
1045
+ function abortable(promise, signal) {
1046
+ return new Promise((resolve2, reject) => {
1047
+ const abort = () => reject(new Error("Snapshot request cancelled"));
1048
+ if (signal.aborted) {
1049
+ abort();
1050
+ return;
1051
+ }
1052
+ signal.addEventListener("abort", abort, { once: true });
1053
+ promise.then(resolve2, reject).finally(() => signal.removeEventListener("abort", abort));
1054
+ });
1055
+ }
1056
+ var init_transport = __esm(() => {
1057
+ init_rpc();
1058
+ });
1059
+
1060
+ // src/dashboard/inventory.ts
1061
+ function validateInventoryLimits(limits) {
1062
+ 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)
1063
+ throw new Error("Invalid dashboard inventory limits");
1064
+ }
1065
+ async function discoverInventory(port, signal, limits = inventoryLimits) {
1066
+ validateInventoryLimits(limits);
1067
+ const sessions = new Map;
1068
+ const notices = new Set;
1069
+ const queue = [{ parent: null, depth: 0, cursors: [] }];
1070
+ let pages = 0;
1071
+ while (queue.length && pages < limits.maxPages) {
1072
+ signal.throwIfAborted();
1073
+ const branch = queue.shift();
1074
+ if (!branch)
1075
+ break;
1076
+ try {
1077
+ pages++;
1078
+ const page = await port.listSessions({
1079
+ parentID: branch.parent,
1080
+ limit: 100,
1081
+ ...branch.cursor ? { cursor: branch.cursor } : {}
1082
+ }, signal);
1083
+ signal.throwIfAborted();
1084
+ for (const session of page.data) {
1085
+ if (session.directory !== port.location.directory) {
1086
+ notices.add("foreign-location");
1087
+ continue;
1088
+ }
1089
+ if ((session.parentId ?? null) !== branch.parent || sessions.has(session.sessionId)) {
1090
+ notices.add("invalid-hierarchy");
1091
+ continue;
1092
+ }
1093
+ if (sessions.size >= limits.maxNodes) {
1094
+ notices.add("node-limit");
1095
+ return {
1096
+ sessions: [...sessions.values()],
1097
+ complete: false,
1098
+ pages,
1099
+ notices: [...notices]
1100
+ };
1101
+ }
1102
+ sessions.set(session.sessionId, session);
1103
+ if (branch.depth < limits.maxDepth)
1104
+ queue.push({
1105
+ parent: session.sessionId,
1106
+ depth: branch.depth + 1,
1107
+ cursors: []
864
1108
  });
865
- dot.addEventListener("blur", () => {
866
- tooltip.hidden = true;
1109
+ else
1110
+ notices.add("depth-limit");
1111
+ }
1112
+ const cursor = page.cursor?.next;
1113
+ if (cursor) {
1114
+ if (cursor === branch.cursor || branch.cursors.includes(cursor))
1115
+ notices.add("repeated-cursor");
1116
+ else
1117
+ queue.unshift({
1118
+ ...branch,
1119
+ cursor,
1120
+ cursors: [...branch.cursors, cursor]
867
1121
  });
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
- };
1122
+ }
1123
+ } catch {
1124
+ signal.throwIfAborted();
1125
+ notices.add("inventory-unavailable");
893
1126
  }
894
1127
  }
895
- for (const control of [session, agent, model, from, to])
896
- control.addEventListener("change", render);
897
- element("reset", HTMLButtonElement).onclick = () => {
898
- reset();
899
- render();
1128
+ if (queue.length)
1129
+ notices.add("page-limit");
1130
+ return {
1131
+ sessions: [...sessions.values()],
1132
+ complete: !notices.size,
1133
+ pages,
1134
+ notices: [...notices]
900
1135
  };
901
- reset();
902
- render();
903
1136
  }
1137
+ var inventoryLimits;
1138
+ var init_inventory = __esm(() => {
1139
+ inventoryLimits = Object.freeze({
1140
+ maxNodes: 1024,
1141
+ maxPages: 100,
1142
+ maxDepth: 64
1143
+ });
1144
+ });
904
1145
 
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;
1146
+ // src/dashboard/view.ts
1147
+ function immutableView(value) {
1148
+ const copy = structuredClone(value);
1149
+ const freeze = (item) => {
1150
+ if (item === null || typeof item !== "object")
1151
+ return;
1152
+ for (const child of Object.values(item))
1153
+ freeze(child);
1154
+ Object.freeze(item);
1155
+ };
1156
+ freeze(copy);
1157
+ return copy;
1158
+ }
1159
+ function projectHistory(result, filters) {
1160
+ const match = (point) => (!filters.agent || point.agent === filters.agent) && (!filters.model || point.model === filters.model);
1161
+ const series = result.data.series.map((series2) => {
1162
+ let gap = false;
1163
+ const points = series2.points.flatMap((point) => {
1164
+ gap ||= point.breakBefore;
1165
+ if (!match(point))
1166
+ return [];
1167
+ const next = { ...point, breakBefore: gap };
1168
+ gap = false;
1169
+ return [next];
1170
+ });
1171
+ return { ...series2, points, requests: series2.requests.filter(match) };
914
1172
  });
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));
1173
+ const requests = series.flatMap((series2) => series2.requests);
1174
+ const matched = requests.filter((request) => request.outputTokens !== undefined && request.durationMs !== undefined && request.durationMs > 0);
1175
+ const output = matched.reduce((sum, request) => sum + (request.outputTokens ?? 0), 0);
1176
+ const duration = matched.reduce((sum, request) => sum + (request.durationMs ?? 0), 0);
1177
+ 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;
1178
+ const notices = [
1179
+ "bounded-retention",
1180
+ "maximum-seven-days",
1181
+ "maximum-500-points-and-requests"
1182
+ ];
1183
+ if (!result.enabled)
1184
+ notices.push("history-disabled");
1185
+ if (result.truncated || result.metadata.skippedBytes || result.metadata.malformedRecords || result.data.omittedSessions)
1186
+ notices.push("partial-history");
1187
+ if (result.data.gaps.length || result.data.omittedGaps || result.data.series.some((series2) => series2.points.some((point) => point.breakBefore)))
1188
+ notices.push("history-gaps");
1189
+ if (result.data.series.some((series2) => series2.originalPoints > series2.points.length))
1190
+ notices.push("downsampled-history");
1191
+ if (result.data.series.some((series2) => series2.points.some((point) => point.partial)) || requests.some((request) => request.partial))
1192
+ notices.push("partial-measurements");
1193
+ return {
1194
+ result: { ...result, data: { ...result.data, series } },
1195
+ summary: {
1196
+ source: "matched-request-records",
1197
+ observedRequests: requests.length,
1198
+ matchedRequests: matched.length,
1199
+ partial,
1200
+ ...matched.length ? {
1201
+ outputTokens: output,
1202
+ durationMs: duration,
1203
+ averageModelTps: output * 1000 / duration
1204
+ } : {}
1205
+ },
1206
+ notices
1207
+ };
920
1208
  }
1209
+ var historyWindows;
1210
+ var init_view = __esm(() => {
1211
+ historyWindows = [300000, 3600000, 86400000, 604800000];
1212
+ });
921
1213
 
922
- class PerformanceHistory {
923
- path;
924
- config;
1214
+ // src/dashboard/controller.ts
1215
+ class DashboardController {
1216
+ locations = new Map;
1217
+ cache = new Map;
1218
+ listeners = new Set;
1219
+ limits;
925
1220
  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;
1221
+ elapsed;
1222
+ refreshIntervalMs;
1223
+ state;
1224
+ scope;
1225
+ running = false;
1226
+ screen = "overview";
1227
+ filters = { search: "", agent: "", model: "" };
1228
+ windowMs = 300000;
1229
+ generation = 0;
1230
+ historyGeneration = 0;
1231
+ pendingHistory;
1232
+ historyAbort;
1233
+ historyBusy = false;
1234
+ historyStatus = "idle";
1235
+ refreshTimer;
1236
+ ageTimer;
1237
+ view;
1238
+ constructor(port, options = {}) {
1239
+ this.limits = { ...options.limits ?? inventoryLimits };
1240
+ validateInventoryLimits(this.limits);
1241
+ this.now = options.now ?? Date.now;
1242
+ this.elapsed = options.elapsed ?? (() => performance.now());
1243
+ this.refreshIntervalMs = options.refreshIntervalMs ?? 30000;
1244
+ if (!Number.isSafeInteger(this.refreshIntervalMs) || this.refreshIntervalMs < 0)
1245
+ throw new Error("Invalid dashboard refresh interval");
1246
+ this.state = this.createLocation(port);
1247
+ this.locations.set(port.location.directory, this.state);
1248
+ this.view = this.buildView();
941
1249
  }
942
- emit(record2) {
943
- if (!this.config.enabled || this.disabled)
944
- 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");
1250
+ createLocation(port) {
1251
+ return {
1252
+ port,
1253
+ nodes: new Map,
1254
+ rows: new Map,
1255
+ connection: "loading",
1256
+ complete: false,
1257
+ stale: true,
1258
+ pages: 0,
1259
+ notices: []
1260
+ };
1261
+ }
1262
+ getView() {
1263
+ return this.view;
1264
+ }
1265
+ subscribe(listener) {
1266
+ this.listeners.add(listener);
1267
+ listener(this.view);
1268
+ return () => {
1269
+ this.listeners.delete(listener);
1270
+ };
1271
+ }
1272
+ publish() {
1273
+ this.view = this.buildView();
1274
+ for (const listener of this.listeners) {
1275
+ try {
1276
+ listener(this.view);
1277
+ } catch {
1278
+ }
1279
+ }
1280
+ }
1281
+ async start() {
1282
+ if (this.running)
963
1283
  return;
1284
+ this.running = true;
1285
+ this.scope = new AbortController;
1286
+ if (this.refreshIntervalMs) {
1287
+ this.refreshTimer = setInterval(() => {
1288
+ this.refresh();
1289
+ }, this.refreshIntervalMs);
1290
+ this.ageTimer = setInterval(() => {
1291
+ this.publish();
1292
+ }, 1000);
964
1293
  }
965
- this.pending.push(line);
966
- this.kick();
1294
+ await this.refresh();
967
1295
  }
968
- kick() {
969
- if (this.working || this.disabled)
970
- 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
- });
1296
+ stop() {
1297
+ this.running = false;
1298
+ this.generation++;
1299
+ this.scope?.abort();
1300
+ this.scope = undefined;
1301
+ this.cancelHistory();
1302
+ clearInterval(this.refreshTimer);
1303
+ clearInterval(this.ageTimer);
1304
+ for (const state of this.locations.values()) {
1305
+ state.client?.stop();
1306
+ delete state.client;
1307
+ delete state.scanning;
1308
+ delete state.probing;
1309
+ state.stale = true;
1310
+ state.connection = "unavailable";
1311
+ }
1312
+ this.publish();
980
1313
  }
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;
1314
+ async selectLocation(directory) {
1315
+ if (directory === this.state.port.location.directory)
1316
+ return;
1317
+ const port = this.state.port.atLocation(directory);
1318
+ this.scope?.abort();
1319
+ this.state.client?.stop();
1320
+ delete this.state.client;
1321
+ delete this.state.scanning;
1322
+ delete this.state.probing;
1323
+ this.state.stale = true;
1324
+ this.state.connection = "reconnecting";
1325
+ this.cancelHistory();
1326
+ this.generation++;
1327
+ const next = this.locations.get(directory) ?? this.createLocation(port);
1328
+ this.locations.delete(directory);
1329
+ this.locations.set(directory, next);
1330
+ this.state = next;
1331
+ if (this.locations.size > 4) {
1332
+ const oldest = this.locations.keys().next().value;
1333
+ if (oldest !== undefined)
1334
+ this.locations.delete(oldest);
995
1335
  }
996
- if (truncated)
997
- this.report("history-truncated");
1336
+ this.scope = this.running ? new AbortController : undefined;
1337
+ this.historyStatus = "idle";
1338
+ this.publish();
1339
+ if (this.running)
1340
+ await this.refresh();
998
1341
  }
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";
1342
+ selectSession(id) {
1343
+ if (!this.state.nodes.has(id))
1344
+ throw new Error("Unknown dashboard session");
1345
+ if (id === this.state.selected)
1346
+ return;
1347
+ this.state.selected = id;
1348
+ this.queueHistory();
1349
+ this.publish();
1009
1350
  }
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`;
1351
+ setScreen(screen) {
1352
+ if (!["overview", "detail", "history"].includes(screen))
1353
+ throw new Error("Invalid dashboard screen");
1354
+ if (screen === this.screen)
1355
+ return;
1356
+ this.screen = screen;
1357
+ this.queueHistory();
1358
+ this.publish();
1359
+ }
1360
+ setWindow(windowMs) {
1361
+ if (!historyWindows.includes(windowMs))
1362
+ throw new Error("Invalid dashboard history window");
1363
+ if (windowMs === this.windowMs)
1364
+ return;
1365
+ this.windowMs = windowMs;
1366
+ this.queueHistory();
1367
+ this.publish();
1368
+ }
1369
+ setFilters(filters) {
1370
+ for (const value of Object.values(filters))
1371
+ if (typeof value !== "string" || value.length > 512)
1372
+ throw new Error("Invalid dashboard filter");
1373
+ this.filters = {
1374
+ search: filters.search ?? this.filters.search,
1375
+ agent: filters.agent ?? this.filters.agent,
1376
+ model: filters.model ?? this.filters.model
1377
+ };
1378
+ this.publish();
1379
+ }
1380
+ async refresh() {
1381
+ if (!this.running || !this.scope)
1382
+ return;
1383
+ for (const cached of this.cache.values())
1384
+ if (cached.directory === this.state.port.location.directory)
1385
+ cached.valid = false;
1386
+ this.queueHistory();
1387
+ const state = this.state, generation = this.generation, signal = this.scope.signal;
1388
+ await Promise.all([
1389
+ this.scan(state, generation, signal),
1390
+ this.monitor(state, generation, signal)
1391
+ ]);
1392
+ if (this.current(state, generation, signal))
1393
+ await state.client?.refresh();
1394
+ }
1395
+ current(state, generation, signal) {
1396
+ return this.running && this.state === state && generation === this.generation && !signal.aborted;
1397
+ }
1398
+ scan(state, generation, signal) {
1399
+ if (state.scanning)
1400
+ return state.scanning;
1401
+ state.scanning = (async () => {
1402
+ try {
1403
+ const scanSignal = AbortSignal.any([
1404
+ signal,
1405
+ AbortSignal.timeout(15000)
1406
+ ]);
1407
+ const result = await discoverInventory(state.port, scanSignal, this.limits);
1408
+ let active;
1409
+ try {
1410
+ active = new Set(await state.port.activeSessions(result.sessions.map((session) => session.sessionId), scanSignal));
1411
+ } catch {
1412
+ }
1413
+ if (!this.current(state, generation, signal))
1414
+ return;
1415
+ const nodes = new Map;
1416
+ for (const session of result.sessions)
1417
+ nodes.set(session.sessionId, {
1418
+ ...session,
1419
+ status: active ? executionStatus(true, active.has(session.sessionId), {
1420
+ ...session.outcome ? { outcome: session.outcome } : {},
1421
+ ...session.terminalAt === undefined ? {} : { terminalAt: session.terminalAt }
1422
+ }) : state.nodes.get(session.sessionId)?.status ?? "unknown",
1423
+ inventoryStale: active === undefined
1424
+ });
1425
+ if (!result.complete) {
1426
+ for (const [id, node] of state.nodes)
1427
+ if (!nodes.has(id) && nodes.size < this.limits.maxNodes)
1428
+ nodes.set(id, { ...node, inventoryStale: true });
1429
+ }
1430
+ state.nodes = nodes;
1431
+ state.complete = result.complete && active !== undefined;
1432
+ state.stale = !state.complete;
1433
+ state.pages = result.pages;
1434
+ state.notices = active ? result.notices : [...new Set([...result.notices, "inventory-unavailable"])];
1435
+ if (!state.selected || !nodes.has(state.selected)) {
1436
+ const previous = state.selected;
1437
+ const first = nodes.keys().next().value;
1438
+ if (first === undefined)
1439
+ delete state.selected;
1440
+ else
1441
+ state.selected = first;
1442
+ if (previous !== state.selected)
1443
+ this.queueHistory();
1444
+ }
1445
+ state.client?.setIds([...nodes.keys()]);
1446
+ } catch {
1447
+ if (!this.current(state, generation, signal))
1448
+ return;
1449
+ state.complete = false;
1450
+ state.stale = true;
1451
+ state.nodes = new Map([...state.nodes].map(([id, node]) => [
1452
+ id,
1453
+ { ...node, inventoryStale: true }
1454
+ ]));
1455
+ state.notices = ["inventory-unavailable"];
1456
+ } finally {
1457
+ if (this.current(state, generation, signal)) {
1458
+ delete state.scanning;
1459
+ this.publish();
1460
+ }
1035
1461
  }
1036
- const file = await open(join(this.path, this.active), "a", 384);
1462
+ })();
1463
+ this.publish();
1464
+ return state.scanning;
1465
+ }
1466
+ monitor(state, generation, signal) {
1467
+ if (state.client)
1468
+ return Promise.resolve();
1469
+ if (state.probing)
1470
+ return state.probing;
1471
+ state.probing = (async () => {
1037
1472
  try {
1038
- await file.writeFile(line);
1473
+ const monitor = await state.port.monitor(signal);
1474
+ if (!this.current(state, generation, signal))
1475
+ return;
1476
+ state.version = monitor.version;
1477
+ const client = new SnapshotClient(monitor.transport, () => {
1478
+ if (!this.current(state, generation, signal))
1479
+ return;
1480
+ state.connection = client.connection;
1481
+ if (client.health) {
1482
+ state.rows = new Map(client.rows);
1483
+ state.health = client.health;
1484
+ state.serverAt = client.now();
1485
+ state.receivedAt = this.elapsed();
1486
+ }
1487
+ this.publish();
1488
+ }, this.elapsed);
1489
+ state.client = client;
1490
+ client.start(state.port.location.directory, [...state.nodes.keys()]);
1491
+ } catch {
1492
+ if (this.current(state, generation, signal)) {
1493
+ state.connection = state.rows.size ? "reconnecting" : "unavailable";
1494
+ this.publish();
1495
+ }
1039
1496
  } finally {
1040
- await file.close();
1497
+ if (this.current(state, generation, signal))
1498
+ delete state.probing;
1041
1499
  }
1042
- this.bytes += size;
1043
- }
1500
+ })();
1501
+ return state.probing;
1044
1502
  }
1045
- health() {
1046
- return { unavailable: this.disabled, droppedRecords: this.droppedTotal };
1503
+ cacheKey() {
1504
+ return JSON.stringify([
1505
+ this.state.port.location.directory,
1506
+ this.state.selected,
1507
+ this.windowMs
1508
+ ]);
1047
1509
  }
1048
- async flush() {
1049
- while (this.working)
1050
- await this.working;
1510
+ cancelHistory() {
1511
+ this.historyGeneration++;
1512
+ this.pendingHistory = undefined;
1513
+ this.historyAbort?.abort();
1051
1514
  }
1052
- async initialize() {
1053
- if (!this.config.enabled)
1515
+ queueHistory() {
1516
+ this.cancelHistory();
1517
+ const sessionId = this.state.selected;
1518
+ if (!this.running || !this.scope || this.screen !== "history" || !sessionId) {
1519
+ this.historyStatus = "idle";
1054
1520
  return;
1055
- await mkdir(this.path, { recursive: true, mode: 448 }).then(() => this.prune()).catch(() => {
1056
- this.disabled = true;
1057
- this.report("history-unavailable");
1521
+ }
1522
+ const key = this.cacheKey(), cached = this.cache.get(key);
1523
+ if (cached?.valid) {
1524
+ this.cache.delete(key);
1525
+ this.cache.set(key, cached);
1526
+ this.historyStatus = cached.result.enabled ? "ready" : "disabled";
1527
+ return;
1528
+ }
1529
+ this.historyStatus = "loading";
1530
+ this.pendingHistory = {
1531
+ key,
1532
+ state: this.state,
1533
+ sessionId,
1534
+ windowMs: this.windowMs,
1535
+ generation: this.historyGeneration,
1536
+ scope: this.scope.signal
1537
+ };
1538
+ this.pumpHistory();
1539
+ }
1540
+ async pumpHistory() {
1541
+ if (this.historyBusy || !this.pendingHistory)
1542
+ return;
1543
+ const task = this.pendingHistory;
1544
+ this.pendingHistory = undefined;
1545
+ this.historyBusy = true;
1546
+ const controller = new AbortController;
1547
+ this.historyAbort = controller;
1548
+ const signal = AbortSignal.any([
1549
+ controller.signal,
1550
+ task.scope,
1551
+ AbortSignal.timeout(5000)
1552
+ ]);
1553
+ const to = Math.max(0, Math.floor(this.serverNow()));
1554
+ const from = Math.max(0, to - task.windowMs);
1555
+ try {
1556
+ const result = await task.state.port.history({
1557
+ schemaVersion: 1,
1558
+ sessionId: task.sessionId,
1559
+ from,
1560
+ to,
1561
+ maxPoints: 500
1562
+ }, signal);
1563
+ if (signal.aborted || task.generation !== this.historyGeneration)
1564
+ return;
1565
+ if (!isHistoryResult(result) || result.data.series.some((series) => series.sessionId !== task.sessionId))
1566
+ throw new Error("Invalid history response");
1567
+ this.cache.set(task.key, {
1568
+ directory: task.state.port.location.directory,
1569
+ valid: true,
1570
+ result: structuredClone(result),
1571
+ from,
1572
+ to
1573
+ });
1574
+ if (this.cache.size > 16) {
1575
+ const oldest = this.cache.keys().next().value;
1576
+ if (oldest !== undefined)
1577
+ this.cache.delete(oldest);
1578
+ }
1579
+ this.historyStatus = result.enabled ? "ready" : "disabled";
1580
+ } catch {
1581
+ if (task.generation === this.historyGeneration && !task.scope.aborted)
1582
+ this.historyStatus = "unavailable";
1583
+ } finally {
1584
+ if (task.generation === this.historyGeneration && signal.aborted && !task.scope.aborted)
1585
+ this.historyStatus = "unavailable";
1586
+ this.historyBusy = false;
1587
+ if (this.historyAbort === controller)
1588
+ this.historyAbort = undefined;
1589
+ if (this.running)
1590
+ this.publish();
1591
+ this.pumpHistory();
1592
+ }
1593
+ }
1594
+ serverNow() {
1595
+ if (this.state.client?.health)
1596
+ return this.state.client.now();
1597
+ if (this.state.serverAt !== undefined && this.state.receivedAt !== undefined)
1598
+ return this.state.serverAt + Math.max(0, this.elapsed() - this.state.receivedAt);
1599
+ return this.now();
1600
+ }
1601
+ buildView() {
1602
+ const state = this.state, now = this.serverNow();
1603
+ const nodes = [...state.nodes.values()].map((node) => {
1604
+ const snapshot = state.rows.get(node.sessionId);
1605
+ const agent = snapshot?.agent ?? node.agent;
1606
+ const model = snapshot?.model ?? node.model;
1607
+ return {
1608
+ sessionId: node.sessionId,
1609
+ status: node.status,
1610
+ ...node.parentId ? { parentId: node.parentId } : {},
1611
+ ...agent ? { agent } : {},
1612
+ ...model ? { model } : {},
1613
+ ...node.outcome === "interrupted" ? { interrupted: true } : {}
1614
+ };
1615
+ });
1616
+ const rows = familyRows(nodes, state.rows, state.selected ?? "", now, state.health?.staleMs ?? 300000, new Set(state.nodes.keys()), this.limits.maxNodes).map((row2) => {
1617
+ const liveStale = !this.running || state.connection !== "live";
1618
+ const measurement2 = row2.snapshot?.measurement;
1619
+ let snapshot = row2.snapshot;
1620
+ if (snapshot && state.version === 1) {
1621
+ const { performance: _, ...context } = snapshot;
1622
+ snapshot = context;
1623
+ }
1624
+ const stale = liveStale && row2.freshness === "fresh";
1625
+ const p = snapshot?.performance;
1626
+ const { pressureRatio, triggerDeltaTokens, ...retained } = row2;
1627
+ return {
1628
+ ...stale ? { ...retained, freshness: "stale", pressure: "unknown" } : row2,
1629
+ ...snapshot ? { snapshot } : {},
1630
+ inventoryStale: !this.running || state.stale || (state.nodes.get(row2.node.sessionId)?.inventoryStale ?? true),
1631
+ liveStale,
1632
+ ...measurement2 && now >= measurement2.at ? { measurementAgeMs: now - measurement2.at } : {},
1633
+ performanceAvailable: state.version === 2 && snapshot?.performance !== undefined,
1634
+ performanceFreshness: !p ? "missing" : p.at > now ? "invalid" : liveStale || now - p.at > (state.health?.staleMs ?? 300000) ? "stale" : "fresh"
1635
+ };
1636
+ });
1637
+ const search = this.filters.search.toLowerCase();
1638
+ 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));
1639
+ const cached = this.cache.get(this.cacheKey());
1640
+ const projected = cached ? projectHistory(cached.result, this.filters) : undefined;
1641
+ const selected = rows.find((row2) => row2.node.sessionId === state.selected);
1642
+ return immutableView({
1643
+ schemaVersion: 1,
1644
+ generatedAt: now,
1645
+ running: this.running,
1646
+ location: state.port.location.directory,
1647
+ locations: [...this.locations.keys()],
1648
+ screen: this.screen,
1649
+ filters: this.filters,
1650
+ connection: state.connection,
1651
+ ...state.version === undefined ? {} : { monitorVersion: state.version },
1652
+ ...state.health ? { health: state.health } : {},
1653
+ inventory: {
1654
+ refreshing: state.scanning !== undefined,
1655
+ complete: state.complete,
1656
+ stale: state.stale,
1657
+ pages: state.pages,
1658
+ total: rows.length,
1659
+ visible: visible.length,
1660
+ monitored: rows.filter((row2) => row2.snapshot).length,
1661
+ active: rows.filter((row2) => row2.node.status === "running").length,
1662
+ ...this.limits,
1663
+ notices: state.notices
1664
+ },
1665
+ rows: visible,
1666
+ ...state.selected ? { selectedSessionId: state.selected } : {},
1667
+ ...selected ? { selected } : {},
1668
+ history: {
1669
+ status: this.historyStatus,
1670
+ windowMs: this.windowMs,
1671
+ maxWindowMs: 604800000,
1672
+ maxPoints: 500,
1673
+ stale: this.historyStatus === "loading" || this.historyStatus === "unavailable" || !this.running,
1674
+ ...state.selected ? { sessionId: state.selected } : {},
1675
+ ...cached ? { from: cached.from, to: cached.to } : {},
1676
+ ...projected ? { result: projected.result, summary: projected.summary } : {},
1677
+ notices: projected?.notices ?? [
1678
+ "bounded-retention",
1679
+ "maximum-seven-days",
1680
+ "maximum-500-points-and-requests"
1681
+ ]
1682
+ },
1683
+ notices: [
1684
+ ...!state.complete ? ["partial-inventory"] : [],
1685
+ ...state.connection !== "live" ? ["live-data-stale"] : [],
1686
+ ...state.version === 1 ? ["performance-unavailable-v1"] : []
1687
+ ]
1058
1688
  });
1059
1689
  }
1060
1690
  }
1061
- function scalarPerformance(p) {
1691
+ var init_controller = __esm(() => {
1692
+ init_rpc();
1693
+ init_transport();
1694
+ init_inventory();
1695
+ init_view();
1696
+ });
1697
+
1698
+ // src/dashboard/connection.ts
1699
+ import { OpenCode } from "@opencode/client";
1700
+ import { Service } from "@opencode/client/service";
1701
+ import { isAbsolute as isAbsolute2 } from "path";
1702
+ function serverUrl(value) {
1703
+ try {
1704
+ const url = new URL(value);
1705
+ 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))
1706
+ throw new Error;
1707
+ return url.href;
1708
+ } catch {
1709
+ throw new Error("Use an HTTPS server URL or loopback HTTP URL without credentials, query or fragment");
1710
+ }
1711
+ }
1712
+ async function connectDashboard(options, discovery = Service) {
1713
+ if (!isAbsolute2(options.directory))
1714
+ throw new Error("Dashboard directory must be an absolute path on the server");
1715
+ if (!options.server && (options.password !== undefined || options.username !== undefined))
1716
+ throw new Error("Explicit credentials require an explicit server URL");
1717
+ let endpoint;
1718
+ if (options.server !== undefined) {
1719
+ endpoint = {
1720
+ url: serverUrl(options.server),
1721
+ ...options.password === undefined ? {} : {
1722
+ auth: {
1723
+ type: "basic",
1724
+ username: options.username ?? "opencode",
1725
+ password: options.password
1726
+ }
1727
+ }
1728
+ };
1729
+ } else {
1730
+ let found;
1731
+ try {
1732
+ found = await discovery.discover(options.serviceFile === undefined ? {} : { file: options.serviceFile });
1733
+ } catch {
1734
+ throw new Error("Local OpenCode service discovery failed");
1735
+ }
1736
+ if (!found)
1737
+ throw new Error("No compatible local OpenCode service found; start OpenCode or supply --server");
1738
+ endpoint = {
1739
+ url: serverUrl(found.url),
1740
+ ...found.auth ? { auth: found.auth } : {}
1741
+ };
1742
+ }
1743
+ const headers = discovery.headers(endpoint);
1744
+ const request = discovery.fetch ?? globalThis.fetch;
1745
+ const client = OpenCode.make({
1746
+ baseUrl: endpoint.url,
1747
+ ...headers ? { headers } : {},
1748
+ fetch: Object.assign((input, init) => request(input, { ...init, redirect: "error" }), { preconnect: request.preconnect })
1749
+ });
1750
+ const deadline = (signal) => AbortSignal.any([signal, AbortSignal.timeout(5000)]);
1751
+ function atLocation(directory) {
1752
+ if (!isAbsolute2(directory))
1753
+ throw new Error("Dashboard directory must be an absolute path on the server");
1754
+ const location = Object.freeze({ directory });
1755
+ return {
1756
+ location,
1757
+ atLocation,
1758
+ async listSessions(input, signal) {
1759
+ if (input.limit !== undefined && (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100))
1760
+ throw new Error("Session page limit must be between 1 and 100");
1761
+ try {
1762
+ const page = await client.session.list({
1763
+ ...input,
1764
+ directory: location.directory,
1765
+ order: "desc",
1766
+ limit: input.limit ?? 100
1767
+ }, { signal: deadline(signal) });
1768
+ return {
1769
+ data: page.data.map((session) => ({
1770
+ sessionId: session.id,
1771
+ directory: session.location.directory,
1772
+ ...session.parentID ? { parentId: session.parentID } : {},
1773
+ ...session.agent ? { agent: session.agent } : {},
1774
+ ...session.model ? { model: `${session.model.providerID}/${session.model.id}` } : {},
1775
+ ...session.outcome ? { outcome: session.outcome } : {},
1776
+ ...session.time.idle === undefined ? {} : { terminalAt: session.time.idle }
1777
+ })),
1778
+ ...page.cursor?.next === undefined ? {} : { cursor: { next: page.cursor.next } }
1779
+ };
1780
+ } catch {
1781
+ throw new Error("Dashboard session inventory unavailable");
1782
+ }
1783
+ },
1784
+ async activeSessions(ids, signal) {
1785
+ try {
1786
+ const active = await client.session.active({
1787
+ signal: deadline(signal)
1788
+ });
1789
+ return ids.filter((id) => active[id]?.type === "running");
1790
+ } catch {
1791
+ throw new Error("Dashboard execution state unavailable");
1792
+ }
1793
+ },
1794
+ async monitor(signal) {
1795
+ let source = client.rpc(performanceRpc);
1796
+ let version = 2;
1797
+ try {
1798
+ const result = await source.read({ schemaVersion: 1, sessionIds: [] }, { location, signal: deadline(signal) });
1799
+ if (!isPerformanceResult(result))
1800
+ throw new Error;
1801
+ } catch {
1802
+ if (signal.aborted)
1803
+ throw new Error("Dashboard monitor probe cancelled");
1804
+ source = client.rpc(monitorRpc);
1805
+ version = 1;
1806
+ try {
1807
+ const result = await source.read({ schemaVersion: 1, sessionIds: [] }, { location, signal: deadline(signal) });
1808
+ if (!isResult(result))
1809
+ throw new Error;
1810
+ } catch {
1811
+ throw new Error("Context Guard monitor unavailable or incompatible in this location");
1812
+ }
1813
+ }
1814
+ const rpc = source;
1815
+ const checkLocation = (directory2) => {
1816
+ if (directory2 !== location.directory)
1817
+ throw new Error("Dashboard location mismatch");
1818
+ };
1819
+ return {
1820
+ version,
1821
+ transport: {
1822
+ async read(directory2, ids, requestSignal) {
1823
+ checkLocation(directory2);
1824
+ try {
1825
+ return await rpc.read({ schemaVersion: 1, sessionIds: [...ids] }, { location, signal: deadline(requestSignal) });
1826
+ } catch {
1827
+ throw new Error("Dashboard monitor read failed");
1828
+ }
1829
+ },
1830
+ async* subscribe(directory2, requestSignal) {
1831
+ checkLocation(directory2);
1832
+ try {
1833
+ for await (const event of rpc.events.subscribe("changed", {
1834
+ signal: requestSignal
1835
+ })) {
1836
+ if (event.location?.directory === location.directory)
1837
+ yield event.data;
1838
+ }
1839
+ } catch {
1840
+ if (!requestSignal.aborted)
1841
+ throw new Error("Dashboard monitor stream interrupted");
1842
+ }
1843
+ }
1844
+ }
1845
+ };
1846
+ },
1847
+ async history(input, signal) {
1848
+ if (!isHistoryRead(input))
1849
+ throw new Error("History requires one session, at most seven days and 16\u2013500 points");
1850
+ try {
1851
+ const result = await client.rpc(historyRpc).read(input, { location, signal: deadline(signal) });
1852
+ if (!isHistoryResult(result))
1853
+ throw new Error;
1854
+ return result;
1855
+ } catch {
1856
+ throw new Error("Context Guard history unavailable or incompatible in this location");
1857
+ }
1858
+ }
1859
+ };
1860
+ }
1062
1861
  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
- }))
1862
+ ...atLocation(options.directory),
1863
+ async info(signal) {
1864
+ try {
1865
+ return await client.server.info({ signal: deadline(signal) });
1866
+ } catch {
1867
+ throw new Error("Dashboard server connection failed");
1868
+ }
1869
+ }
1091
1870
  };
1092
1871
  }
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;
1872
+ var init_connection = __esm(() => {
1873
+ init_rpc();
1874
+ });
1875
+
1876
+ // src/tui/format.ts
1877
+ function quantity(n) {
1878
+ 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);
1879
+ }
1880
+ function cells(value) {
1881
+ return [...segmenter.segment(value)].reduce((n, { segment }) => n + graphemeWidth(segment), 0);
1882
+ }
1883
+ function graphemeWidth(s) {
1884
+ const cp = s.codePointAt(0) ?? 0;
1885
+ if (/^[\p{Mark}\u200d\ufe0f]+$/u.test(s))
1886
+ return 0;
1887
+ 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;
1888
+ }
1889
+ function clip(value, width) {
1890
+ const clean = sanitize(value);
1891
+ if (cells(clean) <= width)
1892
+ return clean;
1893
+ let result = "", used = 0;
1894
+ for (const { segment } of segmenter.segment(clean)) {
1895
+ const size = graphemeWidth(segment);
1896
+ if (used + size > Math.max(0, width - 3))
1897
+ break;
1898
+ result += segment;
1899
+ used += size;
1900
+ }
1901
+ return width < 3 ? ".".repeat(Math.max(0, width)) : result + "...";
1902
+ }
1903
+ function rowLines(row2, width) {
1904
+ const s = row2.snapshot, m = s?.measurement;
1905
+ const prefix = row2.current ? "* " : row2.depth > 5 ? ".../" : " ".repeat(Math.min(row2.depth, 5)) + "|-";
1906
+ const percent = row2.utilizationPercent === undefined ? "?" : `${row2.utilizationPercent.toFixed(1)}%`;
1907
+ const warning = row2.freshness === "stale" ? " STALE" : row2.pressure === "crossed" ? " !! crossed" : row2.pressure === "near" ? " ! near" : "";
1908
+ const suffix = ` ${statuses[row2.node.status]} ${m?.confidence === "approximate" ? "~" : ""}${percent}${warning}`;
1909
+ const label = clip(`${prefix}${row2.node.agent ?? row2.node.sessionId}`, Math.max(1, width - cells(suffix)));
1910
+ return [
1911
+ clip(`${label}${suffix}`, width),
1912
+ ...performanceLines(row2, width),
1913
+ ...wrap(`${quantity(m?.tokens)}/${quantity(s?.capacityTokens)} input; to ${quantity(row2.triggerDeltaTokens)}`, width),
1914
+ ...s && s.compaction.phase !== "idle" ? wrap(`compact: ${s.compaction.phase}`, width) : [],
1915
+ ...row2.node.interrupted ? [clip("interrupted", width)] : [],
1916
+ ...row2.hidden ? wrap(`+ ${row2.hidden} completed descendants`, width) : []
1917
+ ];
1918
+ }
1919
+ function detailLines(row2, width, expanded = false, ageMs = 0) {
1920
+ const s = row2?.snapshot, m = s?.measurement;
1921
+ if (!row2)
1922
+ return wrap("Loading session family...", width);
1923
+ if (!s)
1924
+ return [
1925
+ ...wrap(`${row2.node.agent ?? row2.node.sessionId} / ${row2.node.model ?? "?"}`, width),
1926
+ ...wrap("Guard: not monitored / unavailable", width)
1927
+ ];
1928
+ const threshold = s.policy.configured;
1929
+ const headroom = row2.triggerDeltaTokens;
1930
+ const lines = [
1931
+ `${s.agent ?? "?"} / ${s.model ?? "?"}`,
1932
+ ...performanceLines(row2, width),
1933
+ `${m?.confidence === "approximate" ? "~" : ""}${quantity(m?.tokens)} / ${quantity(s.capacityTokens)} input`,
1934
+ `${row2.utilizationPercent?.toFixed(1) ?? "?"}% ${row2.pressure === "crossed" ? "!! crossed" : row2.pressure === "near" ? "! near" : row2.freshness.toUpperCase()}`,
1935
+ `Trigger ${threshold.type === "ratio" ? `${threshold.value * 100}%` : `${threshold.value} ${threshold.type}`} (${s.policy.comparison})`,
1936
+ `Resolved trigger ${s.policy.triggerTokens ?? "?"}`,
1937
+ `Until trigger ${quantity(headroom)}`,
1938
+ `Compact: ${s.compaction.phase}`,
1939
+ `${s.compaction.armed ? "armed" : "disarmed"}${s.compaction.blockedReason ? ` / ${s.compaction.blockedReason}` : ""}`,
1940
+ m ? `${m.confidence} ${m.scope}` : "measurement unknown",
1941
+ m ? `${Math.floor(Math.max(0, ageMs) / 1000)}s ago; ${row2.freshness}` : ""
1942
+ ];
1943
+ if (expanded)
1944
+ 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)}`);
1945
+ return lines.filter(Boolean).flatMap((line) => wrap(line, width));
1946
+ }
1947
+ function sanitize(value) {
1948
+ return value.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, "");
1949
+ }
1950
+ function wrap(value, width) {
1951
+ const result = [];
1952
+ let part = "";
1953
+ for (const word of sanitize(value).split(" ")) {
1954
+ if (part && cells(part + " " + word) > width) {
1955
+ result.push(part);
1956
+ part = "";
1957
+ }
1958
+ for (const { segment } of segmenter.segment((part ? " " : "") + word)) {
1959
+ if (part && cells(part + segment) > width) {
1960
+ result.push(part);
1961
+ part = "";
1962
+ }
1963
+ part += cells(segment) <= width ? segment : "?";
1964
+ }
1965
+ }
1966
+ if (part)
1967
+ result.push(part);
1968
+ return result;
1969
+ }
1970
+ function performanceLines(row2, width) {
1971
+ const p = row2.snapshot?.performance;
1972
+ if (!p)
1973
+ return [];
1974
+ const value = (n) => n === undefined ? "?" : n.toFixed(1);
1975
+ return [
1976
+ ...wrap(`TPS ${p.status === "waiting" ? "waiting" : `${p.status === "streaming" ? "~" : ""}${value(p.liveTps)}`} \xB7 ${p.status}${p.partial ? " \xB7 partial" : ""}`, width),
1977
+ ...wrap(`Avg model ${value(p.averageModelTps)} \xB7 Avg session ${value(p.averageSessionTps)}`, width),
1978
+ ...wrap(`TTFT ${p.ttftMs === undefined ? "?" : `~${(p.ttftMs / 1000).toFixed(2)}s`}${p.latestRequestTps === undefined ? "" : ` \xB7 Last ${value(p.latestRequestTps)} t/s`}`, width)
1979
+ ];
1980
+ }
1981
+ var segmenter, statuses;
1982
+ var init_format = __esm(() => {
1983
+ segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
1984
+ statuses = {
1985
+ running: "RUN",
1986
+ idle: "IDLE",
1987
+ completed: "DONE",
1988
+ failed: "FAIL",
1989
+ unknown: "?"
1990
+ };
1991
+ });
1992
+
1993
+ // src/tui/chartformat.ts
1994
+ function columns(points, from, to, width, gaps = []) {
1995
+ const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
1996
+ const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
1997
+ let min = Infinity, max = -Infinity;
1998
+ for (const point of points) {
1999
+ if (point.time < from || point.time > to || point.value === undefined || !Number.isFinite(point.value) || point.value < 0)
1102
2000
  continue;
2001
+ min = Math.min(min, point.value);
2002
+ max = Math.max(max, point.value);
2003
+ const i = column(point.time);
2004
+ values[i] = Math.max(values[i] ?? -Infinity, point.value);
2005
+ }
2006
+ for (const time of gaps)
2007
+ if (time >= from && time <= to)
2008
+ values[column(time)] = undefined;
2009
+ return { values, column, min, max };
2010
+ }
2011
+ function sparkline(points, from, to, width, gaps = []) {
2012
+ const { values, column, min, max } = columns(points, from, to, width, gaps);
2013
+ const bars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2014
+ const chars = values.map((v) => v === undefined ? " " : bars[Math.min(7, Math.floor(v / Math.max(1, max) * 7))] ?? "\u2581");
2015
+ for (const time of gaps)
2016
+ if (time >= from && time <= to)
2017
+ chars[column(time)] = "\u2506";
2018
+ return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
2019
+ }
2020
+ function metricPlot(points, from, to, width, gaps = []) {
2021
+ const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
2022
+ const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
2023
+ const scale = Math.max(1, peak);
2024
+ const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
2025
+ const { values, column } = columns(points, from, to, Math.max(1, width - gutter - 2), gaps);
2026
+ const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
2027
+ const height = 6;
2028
+ const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2029
+ const lines = Array.from({ length: height }, (_, row2) => {
2030
+ const axis = gutter ? (row2 === 0 ? label(scale) : row2 === height - 1 ? "0.0" : "").padStart(gutter) : "";
2031
+ return `${axis}\u2502${values.map((value, i) => {
2032
+ if (gapColumns.has(i))
2033
+ return "\u2506";
2034
+ if (value === undefined)
2035
+ return " ";
2036
+ const fill = Math.max(0, Math.min(8, Math.ceil((value / scale * height - (height - row2 - 1)) * 8)));
2037
+ return value === 0 && row2 === height - 1 ? "\u2581" : blocks[fill];
2038
+ }).join("")}\u2502`;
2039
+ });
2040
+ lines.push(`${" ".repeat(gutter)}\u2514${"\u2500".repeat(values.length)}\u2518`);
2041
+ const start = new Date(from).toISOString().slice(11, 16), end = new Date(to).toISOString().slice(11, 16);
2042
+ const axisWidth = values.length + 2;
2043
+ if (axisWidth >= 11)
2044
+ lines.push(`${" ".repeat(gutter)}${start}${" ".repeat(axisWidth - 10)}${end}`);
2045
+ return lines;
2046
+ }
2047
+ function historyChartLines(result, width, from, to, metricIndex = 0) {
2048
+ if (!result.enabled)
2049
+ return wrap("History recording is disabled on this server.", width);
2050
+ const series = result.data.series[0];
2051
+ if (!series)
2052
+ return wrap("No retained measurements in this window.", width);
2053
+ const gaps = result.data.gaps.map((g) => g.time);
2054
+ const observations = (key) => series.points.map((p) => ({
2055
+ time: p.time,
2056
+ ...p[key] === undefined ? {} : { value: p[key] }
2057
+ }));
2058
+ const requests = (key) => series.requests.map((p) => ({
2059
+ time: p.time,
2060
+ ...p[key] === undefined ? {} : { value: key === "ttftMs" ? p[key] / 1000 : p[key] }
2061
+ }));
2062
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2063
+ const points = metric2.source === "points" ? observations(metric2.key) : requests(metric2.key);
2064
+ const plotted = sparkline(points, from, to, width - 2, gaps);
2065
+ return [
2066
+ ...wrap(`\u2039 ${metric2.label} \u203A \xB7 ${metricIndex + 1}/${chartMetrics.length}`, width),
2067
+ ...wrap(plotted.min === undefined ? "No measurements for this metric." : `${plotted.min.toFixed(1)}\u2013${plotted.max?.toFixed(1)} ${metric2.unit} \xB7 observed range`, width),
2068
+ "",
2069
+ ...metricPlot(points, from, to, width, gaps),
2070
+ "",
2071
+ ...wrap("UTC time \u2192 \xB7 peaks per column", width),
2072
+ ...wrap("blank = no sample \xB7 \u2506 = gap", width),
2073
+ ...wrap(`${new Date(from).toISOString()} \u2192 ${new Date(to).toISOString()}`, width),
2074
+ ...series.points.some((p) => p.partial) ? wrap("Partial timing coverage; averages include only matched model durations.", width) : [],
2075
+ ...result.truncated ? wrap("Downsampled/bounded history; some older requests or gap markers may be omitted.", width) : [],
2076
+ ...result.metadata.malformedRecords ? wrap(`${result.metadata.malformedRecords} malformed/partial records skipped.`, width) : []
2077
+ ];
2078
+ }
2079
+ var chartMetrics;
2080
+ var init_chartformat = __esm(() => {
2081
+ init_format();
2082
+ chartMetrics = [
2083
+ { label: "Live TPS ~", unit: "t/s", key: "liveTps", source: "points" },
2084
+ { label: "Avg model", unit: "t/s", key: "averageModelTps", source: "points" },
2085
+ {
2086
+ label: "Avg session",
2087
+ unit: "t/s",
2088
+ key: "averageSessionTps",
2089
+ source: "points"
2090
+ },
2091
+ { label: "TTFT ~", unit: "s", key: "ttftMs", source: "requests" },
2092
+ { label: "Request throughput", unit: "t/s", key: "tps", source: "requests" },
2093
+ {
2094
+ label: "Cumulative output",
2095
+ unit: "tokens",
2096
+ key: "sessionOutputTokens",
2097
+ source: "points"
1103
2098
  }
1104
- bytes += file.bytes;
1105
- selected.unshift(file);
2099
+ ];
2100
+ });
2101
+
2102
+ // src/dashboard/terminalview.ts
2103
+ function overviewRow(row2) {
2104
+ const s = row2.snapshot;
2105
+ const valid2 = row2.freshness === "fresh" || row2.freshness === "stale";
2106
+ 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` : ""}`;
2107
+ }
2108
+ function pathValue(object2, path) {
2109
+ let value = object2;
2110
+ for (const key of path.split(".")) {
2111
+ if (value === null || typeof value !== "object")
2112
+ return;
2113
+ value = Reflect.get(value, key);
1106
2114
  }
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();
2115
+ return value;
2116
+ }
2117
+ function terminalDetail(row2) {
2118
+ if (!row2)
2119
+ return ["No selected session; choose one in overview."];
2120
+ return [
2121
+ overviewRow(row2),
2122
+ `Session ID: ${row2.node.sessionId}`,
2123
+ `Parent ID: ${row2.node.parentId ?? "unknown/unavailable"}`,
2124
+ "All snapshot fields (timestamps are Unix milliseconds):",
2125
+ ...snapshotFields.map((path) => `${path}: ${scalar(pathValue(row2.snapshot, path))}`),
2126
+ "Compaction count records completed lifecycle events; admission is not completion."
2127
+ ];
2128
+ }
2129
+ function historyObservations(view, metricIndex) {
2130
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2131
+ return view.history.result?.data.series.flatMap((series) => metric2.source === "points" ? series.points.map((point) => ({
2132
+ time: point.time,
2133
+ value: point[metric2.key],
2134
+ details: {
2135
+ sessionId: series.sessionId,
2136
+ ...point,
2137
+ tokenSource: "per-sample source not retained; live TPS estimated; cumulative output reported; rates use matched timing"
1116
2138
  }
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++;
2139
+ })) : series.requests.map((request) => ({
2140
+ time: request.time,
2141
+ value: metric2.key === "ttftMs" ? request.ttftMs === undefined ? undefined : request.ttftMs / 1000 : request.tps,
2142
+ details: {
2143
+ sessionId: series.sessionId,
2144
+ ...request,
2145
+ tokenSource: "reported output; observed timing"
2146
+ }
2147
+ }))).sort((a, b) => a.time - b.time) ?? [];
2148
+ }
2149
+ function terminalHistory(view, width, metricIndex, sample) {
2150
+ const h = view.history;
2151
+ const metric2 = chartMetrics[metricIndex] ?? chartMetrics[0];
2152
+ const observations = historyObservations(view, metricIndex);
2153
+ const gaps = [
2154
+ ...h.result?.data.gaps.map((gap) => gap.time) ?? [],
2155
+ ...h.result?.data.series.flatMap((series) => series.points.filter((p) => p.breakBefore).map((p) => p.time)) ?? []
2156
+ ];
2157
+ const selected = observations[Math.min(sample, Math.max(0, observations.length - 1))];
2158
+ return [
2159
+ `History: ${h.status}${h.stale ? " \xB7 stale cached results" : ""} \xB7 ${h.windowMs / 60000} minutes \xB7 ${metric2.label} (${metric2.unit})`,
2160
+ "Left/Right metric \xB7 w window \xB7 [/] exact sample/request \xB7 Up/Down scroll",
2161
+ "Retention bounded to seven days; maximum 500 points and 500 requests per session.",
2162
+ ...h.notices,
2163
+ ...h.summary ? [
2164
+ `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" : ""}`
2165
+ ] : [],
2166
+ ...h.from === undefined || h.to === undefined ? [] : [
2167
+ `${new Date(h.from).toISOString()} \u2192 ${new Date(h.to).toISOString()}`,
2168
+ ...metricPlot(observations.map((p) => ({
2169
+ time: p.time,
2170
+ ...p.value === undefined ? {} : { value: p.value }
2171
+ })), h.from, h.to, width, gaps),
2172
+ "blank = no sample; \u2506 = gap; observed column peaks; no interpolation"
2173
+ ],
2174
+ ...selected ? [
2175
+ `Exact ${metric2.source === "points" ? "sample" : "request"} ${Math.min(sample + 1, observations.length)}/${observations.length}:`,
2176
+ ...Object.entries(selected.details).map(([key, value]) => `${key}: ${scalar(value)}`),
2177
+ `Selected ${metric2.label}: ${scalar(selected.value)} ${metric2.unit}`
2178
+ ] : [
2179
+ "No retained measurements for this metric; values unknown/unavailable."
2180
+ ]
2181
+ ];
2182
+ }
2183
+ function wrapped(lines, width) {
2184
+ return lines.flatMap((line) => line ? wrap(line, Math.max(1, width)) : [""]);
2185
+ }
2186
+ function headerLines(view, width) {
2187
+ return [
2188
+ clip(`Context Guard \xB7 ${view.screen} \xB7 ${view.connection} \xB7 enforcement ${view.health?.enforcement ?? "unknown"}`, width),
2189
+ 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),
2190
+ clip(`Location: ${view.location}`, width)
2191
+ ];
2192
+ }
2193
+ var snapshotFields, scalar = (value) => value === undefined || value === null ? "unknown/unavailable" : typeof value === "number" && !Number.isFinite(value) ? "invalid" : String(value), terminalHelp;
2194
+ var init_terminalview = __esm(() => {
2195
+ init_chartformat();
2196
+ init_format();
2197
+ snapshotFields = [
2198
+ "sessionId",
2199
+ "parentId",
2200
+ "agent",
2201
+ "model",
2202
+ "revision",
2203
+ "observedAt",
2204
+ "measurement.tokens",
2205
+ "measurement.at",
2206
+ "measurement.model",
2207
+ "measurement.provenance",
2208
+ "measurement.scope",
2209
+ "measurement.confidence",
2210
+ "measurement.requestId",
2211
+ "limits.input",
2212
+ "limits.context",
2213
+ "limits.output",
2214
+ "capacityTokens",
2215
+ "outputReserveTokens",
2216
+ "peakTokens",
2217
+ "policy.id",
2218
+ "policy.configured.type",
2219
+ "policy.configured.value",
2220
+ "policy.triggerTokens",
2221
+ "policy.comparison",
2222
+ "policy.reason",
2223
+ "policy.crossed",
2224
+ "compaction.phase",
2225
+ "compaction.count",
2226
+ "compaction.armed",
2227
+ "compaction.blockedReason",
2228
+ "compaction.lastAttempt",
2229
+ "compaction.lastCompleted",
2230
+ "compaction.cooldownUntil",
2231
+ "cumulativeScope",
2232
+ "cumulative.input",
2233
+ "cumulative.output",
2234
+ "cumulative.reasoning",
2235
+ "cumulative.cache.read",
2236
+ "cumulative.cache.write",
2237
+ "lastObservedActive",
2238
+ "performance.sessionCreatedAt",
2239
+ "performance.sessionOutputTokens",
2240
+ "performance.observedSince",
2241
+ "performance.at",
2242
+ "performance.status",
2243
+ "performance.partial",
2244
+ "performance.liveTps",
2245
+ "performance.averageModelTps",
2246
+ "performance.averageSessionTps",
2247
+ "performance.ttftMs",
2248
+ "performance.latestRequestTps",
2249
+ "performance.requestId",
2250
+ "performance.requestAgent",
2251
+ "performance.requestModel",
2252
+ "performance.requestOutputTokens",
2253
+ "performance.requestStartedAt",
2254
+ "performance.firstOutputAt",
2255
+ "performance.streamEndedAt",
2256
+ "performance.estimatedOutputTokens",
2257
+ "performance.modelOutputTokens",
2258
+ "performance.modelDurationMs",
2259
+ "performance.completedRequests",
2260
+ "performance.tokenSource"
2261
+ ];
2262
+ terminalHelp = [
2263
+ "Context Guard standalone dashboard \u2014 keyboard help",
2264
+ "1 overview \xB7 2 detail \xB7 3 history \xB7 Enter detail",
2265
+ "Up/Down or j/k: select session in overview; scroll other screens",
2266
+ "PageUp/PageDown: move by a page \xB7 Home/End: first/last",
2267
+ "Left/Right: history metric (six metrics) \xB7 w: cycle 5m/1h/24h/7d",
2268
+ "[ / ]: previous/next exact history sample or request",
2269
+ "/: search session identity \xB7 f: agent filter, then model filter (exact matches)",
2270
+ "l: owning absolute location directory; Tab cycles up to four remembered locations",
2271
+ "Remembered locations are not complete server-wide discovery.",
2272
+ "Editors: type/paste text; Backspace deletes; Enter applies; Escape cancels",
2273
+ "r: refresh \xB7 ?: help \xB7 Escape: overview \xB7 q / Ctrl+C: quit",
2274
+ "Detail scrolls through full identifiers and every exposed snapshot field.",
2275
+ "Unknown metrics are unavailable, not zero. Stale occupancy is retained; pressure is unknown."
2276
+ ];
2277
+ });
2278
+
2279
+ // src/dashboard/terminal.ts
2280
+ var exports_terminal = {};
2281
+ __export(exports_terminal, {
2282
+ runTerminal: () => runTerminal,
2283
+ mountTerminal: () => mountTerminal,
2284
+ TerminalShell: () => TerminalShell
2285
+ });
2286
+ import { isAbsolute as isAbsolute3 } from "path";
2287
+
2288
+ class TerminalShell {
2289
+ controller;
2290
+ quit;
2291
+ scroll = 0;
2292
+ metric = 0;
2293
+ sample = 0;
2294
+ help = false;
2295
+ editor;
2296
+ input = "";
2297
+ notice = "";
2298
+ rowOffset = 0;
2299
+ historyIdentity = "";
2300
+ constructor(controller, quit) {
2301
+ this.controller = controller;
2302
+ this.quit = quit;
2303
+ }
2304
+ task(task) {
2305
+ task.catch(() => {
2306
+ this.notice = "Dashboard action unavailable; refresh to retry.";
2307
+ });
2308
+ }
2309
+ paste(text2) {
2310
+ if (this.editor)
2311
+ this.input = (this.input + clip(text2, 2048)).slice(0, this.editor === "location" ? 4096 : 512);
2312
+ }
2313
+ key(key, page = 10) {
2314
+ const view = this.controller.getView();
2315
+ const name = key.name;
2316
+ if (key.ctrl && name === "c") {
2317
+ this.quit();
2318
+ return;
2319
+ }
2320
+ if (this.editor) {
2321
+ if (name === "escape") {
2322
+ this.editor = undefined;
2323
+ this.notice = "";
2324
+ } else if (name === "backspace")
2325
+ this.input = Array.from(this.input).slice(0, -1).join("");
2326
+ else if (name === "tab" && this.editor === "location") {
2327
+ const i = view.locations.indexOf(this.input);
2328
+ this.input = view.locations[(i + 1) % view.locations.length] ?? view.location;
2329
+ } else if (name === "return" || name === "enter") {
2330
+ const editor = this.editor;
2331
+ if (editor === "location") {
2332
+ if (!isAbsolute3(this.input)) {
2333
+ this.notice = "Location must be an owning absolute server directory.";
2334
+ return;
2335
+ }
2336
+ this.task(this.controller.selectLocation(this.input));
2337
+ } else
2338
+ this.controller.setFilters({ [editor]: this.input });
2339
+ this.editor = editor === "agent" ? "model" : undefined;
2340
+ this.input = this.editor === "model" ? view.filters.model : "";
2341
+ this.scroll = 0;
2342
+ this.notice = "";
2343
+ } else if (!key.ctrl && !key.meta && key.sequence && !/[\x00-\x1f\x7f-\x9f]/.test(key.sequence))
2344
+ this.paste(key.sequence);
2345
+ return;
2346
+ }
2347
+ if (name === "q") {
2348
+ this.quit();
2349
+ return;
2350
+ }
2351
+ if (name === "?" || key.sequence === "?") {
2352
+ this.help = !this.help;
2353
+ this.scroll = 0;
2354
+ return;
2355
+ }
2356
+ if (name === "escape") {
2357
+ this.help = false;
2358
+ this.scroll = 0;
2359
+ this.controller.setScreen("overview");
2360
+ return;
2361
+ }
2362
+ if (name === "/" || key.sequence === "/") {
2363
+ this.editor = "search";
2364
+ this.input = view.filters.search;
2365
+ return;
2366
+ }
2367
+ if (name === "f") {
2368
+ this.editor = "agent";
2369
+ this.input = view.filters.agent;
2370
+ return;
2371
+ }
2372
+ if (name === "l") {
2373
+ this.editor = "location";
2374
+ this.input = view.location;
2375
+ return;
2376
+ }
2377
+ if (name === "r") {
2378
+ this.task(this.controller.refresh());
2379
+ return;
2380
+ }
2381
+ if (["1", "2", "3", "return", "enter"].includes(name)) {
2382
+ this.help = false;
2383
+ this.scroll = 0;
2384
+ this.controller.setScreen(name === "1" ? "overview" : name === "3" ? "history" : "detail");
2385
+ return;
2386
+ }
2387
+ if (view.screen === "history" && !this.help) {
2388
+ if (name === "left" || name === "right") {
2389
+ this.metric = (this.metric + (name === "left" ? 5 : 1)) % 6;
2390
+ this.sample = 0;
2391
+ this.scroll = 0;
2392
+ return;
2393
+ }
2394
+ if (name === "w") {
2395
+ this.controller.setWindow(historyWindows[(historyWindows.indexOf(view.history.windowMs) + 1) % historyWindows.length] ?? historyWindows[0]);
2396
+ this.sample = 0;
2397
+ return;
2398
+ }
2399
+ if (name === "[" || name === "]") {
2400
+ this.sample = Math.max(0, Math.min(historyObservations(view, this.metric).length - 1, this.sample + (name === "[" ? -1 : 1)));
2401
+ return;
2402
+ }
2403
+ }
2404
+ const delta = name === "down" || name === "j" ? 1 : name === "up" || name === "k" ? -1 : name === "pagedown" ? page : name === "pageup" ? -page : 0;
2405
+ if (view.screen === "overview" && !this.help) {
2406
+ const index = view.rows.findIndex((row3) => row3.node.sessionId === view.selectedSessionId);
2407
+ const next = name === "home" ? 0 : name === "end" ? view.rows.length - 1 : Math.max(0, Math.min(view.rows.length - 1, index + delta));
2408
+ const row2 = view.rows[next];
2409
+ if (row2 && (delta || name === "home" || name === "end"))
2410
+ this.controller.selectSession(row2.node.sessionId);
2411
+ } else
2412
+ this.scroll = name === "home" ? 0 : name === "end" ? Number.MAX_SAFE_INTEGER : Math.max(0, this.scroll + delta);
2413
+ }
2414
+ frame(width, height) {
2415
+ width = Math.max(1, width);
2416
+ height = Math.max(1, height);
2417
+ const view = this.controller.getView();
2418
+ const identity = `${view.location}\0${view.selectedSessionId}\0${view.history.windowMs}`;
2419
+ if (identity !== this.historyIdentity) {
2420
+ this.sample = 0;
2421
+ this.scroll = 0;
2422
+ this.historyIdentity = identity;
2423
+ }
2424
+ 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";
2425
+ if (height < 5)
2426
+ return [
2427
+ clip("Context Guard \xB7 ? help \xB7 q quit", width),
2428
+ ...wrapped([footer], width)
2429
+ ].slice(0, height).join("\n");
2430
+ const header = headerLines(view, width);
2431
+ const available = height - header.length - 1;
2432
+ const notices = [
2433
+ ...view.notices,
2434
+ ...view.inventory.notices,
2435
+ ...view.health?.reasons ?? [],
2436
+ ...this.notice ? [this.notice] : [],
2437
+ ...Object.values(view.filters).some(Boolean) ? [
2438
+ `Filters: search=${view.filters.search} agent=${view.filters.agent} model=${view.filters.model}`
2439
+ ] : []
2440
+ ];
2441
+ let body;
2442
+ if (this.help)
2443
+ body = wrapped(terminalHelp, width);
2444
+ else if (view.screen === "overview") {
2445
+ const rows = view.rows.map((row2) => wrapped([
2446
+ `${row2.node.sessionId === view.selectedSessionId ? "> " : " "}${overviewRow(row2)}`
2447
+ ], width));
2448
+ const selected = view.rows.findIndex((row2) => row2.node.sessionId === view.selectedSessionId);
2449
+ const start = rows.slice(0, Math.max(0, selected)).reduce((n, row2) => n + row2.length, 0);
2450
+ const end = start + (rows[selected]?.length ?? 0);
2451
+ const status = wrapped(notices, width);
2452
+ const budget = Math.max(1, available - Math.min(status.length, Math.floor(available / 3)));
2453
+ if (start < this.rowOffset)
2454
+ this.rowOffset = start;
2455
+ if (end > this.rowOffset + budget)
2456
+ this.rowOffset = Math.max(start, end - budget);
2457
+ const flat = rows.flat();
2458
+ this.rowOffset = Math.max(0, Math.min(this.rowOffset, Math.max(0, flat.length - budget)));
2459
+ body = [
2460
+ ...status.slice(0, available - budget),
2461
+ ...flat.length ? flat.slice(this.rowOffset, this.rowOffset + budget) : ["No matching sessions; / search, f filters, r refresh."]
2462
+ ];
2463
+ return [
2464
+ ...header,
2465
+ ...body,
2466
+ ...Array.from({ length: Math.max(0, available - body.length) }, () => ""),
2467
+ clip(footer, width)
2468
+ ].join("\n");
2469
+ } else
2470
+ body = wrapped([
2471
+ ...notices,
2472
+ ...view.screen === "detail" ? terminalDetail(view.selected) : terminalHistory(view, width, this.metric, this.sample)
2473
+ ], width);
2474
+ this.scroll = Math.max(0, Math.min(this.scroll, Math.max(0, body.length - available)));
2475
+ return [
2476
+ ...header,
2477
+ ...body.slice(this.scroll, this.scroll + available),
2478
+ ...Array.from({ length: Math.max(0, available - body.length) }, () => ""),
2479
+ clip(footer, width)
2480
+ ].join("\n");
2481
+ }
2482
+ }
2483
+ function mountTerminal(controller, renderer, text2, finished) {
2484
+ let closed = false;
2485
+ let unsubscribe = () => {
2486
+ };
2487
+ const shutdown = (failed = false) => {
2488
+ if (closed)
2489
+ return;
2490
+ closed = true;
2491
+ unsubscribe();
2492
+ controller.stop();
2493
+ renderer.keyInput.off("keypress", key);
2494
+ renderer.keyInput.off("paste", paste);
2495
+ renderer.off("resize", draw);
2496
+ renderer.off("render:error", fail);
2497
+ renderer.off("handler:error", fail);
2498
+ renderer.off("destroy", destroyed);
2499
+ for (const signal of signals)
2500
+ process.off(signal, onSignal);
2501
+ process.off("uncaughtException", fail);
2502
+ process.off("unhandledRejection", fail);
2503
+ try {
2504
+ renderer.destroy();
2505
+ } finally {
2506
+ finished(failed);
2507
+ }
2508
+ };
2509
+ const shell = new TerminalShell(controller, () => shutdown());
2510
+ const fail = () => shutdown(true);
2511
+ const destroyed = () => shutdown();
2512
+ const onSignal = () => shutdown();
2513
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
2514
+ const draw = () => {
2515
+ if (closed)
2516
+ return;
2517
+ try {
2518
+ text2.content = shell.frame(renderer.width, renderer.height);
2519
+ } catch {
2520
+ fail();
2521
+ }
2522
+ };
2523
+ const key = (event) => {
2524
+ try {
2525
+ shell.key(event, Math.max(1, renderer.height - 4));
2526
+ draw();
2527
+ } catch {
2528
+ fail();
2529
+ }
2530
+ };
2531
+ const paste = (event) => {
2532
+ shell.paste(new TextDecoder().decode(event.bytes));
2533
+ draw();
2534
+ };
2535
+ try {
2536
+ renderer.root.add(text2);
2537
+ renderer.keyInput.on("keypress", key);
2538
+ renderer.keyInput.on("paste", paste);
2539
+ renderer.on("resize", draw);
2540
+ renderer.on("render:error", fail);
2541
+ renderer.on("handler:error", fail);
2542
+ renderer.on("destroy", destroyed);
2543
+ for (const signal of signals)
2544
+ process.on(signal, onSignal);
2545
+ process.on("uncaughtException", fail);
2546
+ process.on("unhandledRejection", fail);
2547
+ unsubscribe = controller.subscribe(draw);
2548
+ if (closed) {
2549
+ unsubscribe();
2550
+ return { shell, shutdown };
2551
+ }
2552
+ controller.start().catch(fail);
2553
+ } catch {
2554
+ shutdown(true);
2555
+ }
2556
+ return { shell, shutdown };
2557
+ }
2558
+ async function runTerminal(options) {
2559
+ const { createCliRenderer, TextRenderable } = await loadTerminalRenderer();
2560
+ const connection = await connectDashboard(options);
2561
+ await connection.info(AbortSignal.timeout(15000));
2562
+ const controller = new DashboardController(connection);
2563
+ const renderer = await createCliRenderer({
2564
+ exitOnCtrlC: false,
2565
+ exitSignals: [],
2566
+ useMouse: false,
2567
+ screenMode: "alternate-screen",
2568
+ consoleMode: "disabled",
2569
+ openConsoleOnError: false
2570
+ });
2571
+ let failed = false;
2572
+ let app;
2573
+ try {
2574
+ await new Promise((done) => {
2575
+ const text2 = new TextRenderable(renderer, {
2576
+ id: "dashboard",
2577
+ width: "100%",
2578
+ height: "100%",
2579
+ content: "",
2580
+ wrapMode: "none"
2581
+ });
2582
+ app = mountTerminal(controller, renderer, text2, (error) => {
2583
+ failed = error;
2584
+ done();
2585
+ });
2586
+ });
2587
+ } finally {
2588
+ if (app)
2589
+ app.shutdown();
2590
+ else {
2591
+ controller.stop();
2592
+ renderer.destroy();
2593
+ }
2594
+ }
2595
+ if (failed)
2596
+ throw new Error("Terminal dashboard stopped after a runtime failure; terminal restored");
2597
+ }
2598
+ var init_terminal = __esm(() => {
2599
+ init_controller();
2600
+ init_connection();
2601
+ init_command();
2602
+ init_view();
2603
+ init_format();
2604
+ init_terminalview();
2605
+ });
2606
+
2607
+ // src/modelanalytics.ts
2608
+ function requestFact(record2) {
2609
+ const p = record2.performance;
2610
+ if (!p?.requestId || !record2.sessionId || !nonnegative2(p.requestStartedAt))
2611
+ return;
2612
+ const model = p.requestModel || record2.model;
2613
+ const agent = p.requestAgent || record2.agent;
2614
+ const ttft = p.ttftMs ?? (p.firstOutputAt === undefined ? undefined : p.firstOutputAt - p.requestStartedAt);
2615
+ return {
2616
+ location: "",
2617
+ sessionId: record2.sessionId,
2618
+ requestId: p.requestId,
2619
+ startedAt: p.requestStartedAt,
2620
+ ...p.streamEndedAt === undefined ? {} : { completedAt: p.streamEndedAt },
2621
+ ...model ? { model } : {},
2622
+ ...agent ? { agent } : {},
2623
+ attribution: p.requestModel ? "request" : model ? "legacy-record" : "unknown",
2624
+ partial: p.partial,
2625
+ ...p.tokenSource === "reported" && nonnegative2(p.requestOutputTokens) ? { outputTokens: p.requestOutputTokens } : {},
2626
+ ...ttft === undefined ? {} : { ttftMs: ttft }
2627
+ };
2628
+ }
2629
+ function reconcile(a, b) {
2630
+ for (const field of [
2631
+ "completedAt",
2632
+ "outputTokens",
2633
+ "ttftMs",
2634
+ "agent"
2635
+ ])
2636
+ if (a[field] !== undefined && b[field] !== undefined && a[field] !== b[field])
2637
+ return;
2638
+ if (a.model && b.model && a.model !== b.model)
2639
+ return;
2640
+ const rank = { unknown: 0, "legacy-record": 1, request: 2 };
2641
+ const preferred = rank[b.attribution] > rank[a.attribution] ? b : a;
2642
+ return {
2643
+ ...a,
2644
+ ...b,
2645
+ ...preferred.model ? { model: preferred.model } : {},
2646
+ attribution: preferred.attribution,
2647
+ partial: a.partial || b.partial
2648
+ };
2649
+ }
2650
+ function aggregateModels(records, options) {
2651
+ const maxRequests = options.maxRequests ?? 1e5;
2652
+ if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
2653
+ throw new Error("maxRequests must be an integer from 1 to 1000000");
2654
+ if (!options.location)
2655
+ throw new Error("Analytics requires a location identity");
2656
+ for (const value of [options.from, options.to])
2657
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
2658
+ throw new Error("Invalid analytics timestamp");
2659
+ if (options.from !== undefined && options.to !== undefined && options.from > options.to)
2660
+ throw new Error("--from must precede --to");
2661
+ const coverage = {
2662
+ source: "retained-request-records",
2663
+ scannedRecords: 0,
2664
+ unscannedRecords: 0,
2665
+ duplicateRecords: 0,
2666
+ conflictingRequests: 0,
2667
+ missingIdentityRecords: 0,
2668
+ incompleteRequests: 0,
2669
+ invalidTimingRequests: 0,
2670
+ gapRecords: 0,
2671
+ maxRequests,
2672
+ partial: false
2673
+ };
2674
+ const requests = new Map;
2675
+ for (const record2 of records) {
2676
+ coverage.scannedRecords++;
2677
+ if (record2.type === "gap") {
2678
+ coverage.gapRecords++;
2679
+ continue;
2680
+ }
2681
+ if (record2.type !== "request")
2682
+ continue;
2683
+ const fact = requestFact(record2);
2684
+ if (!fact) {
2685
+ coverage.missingIdentityRecords++;
2686
+ continue;
2687
+ }
2688
+ fact.location = options.location;
2689
+ const key = JSON.stringify([
2690
+ fact.sessionId,
2691
+ fact.requestId,
2692
+ fact.startedAt
2693
+ ]);
2694
+ const previous = requests.get(key);
2695
+ if (previous) {
2696
+ coverage.duplicateRecords++;
2697
+ const merged = reconcile(previous.fact, fact);
2698
+ if (!merged)
2699
+ previous.conflict = true;
2700
+ else
2701
+ previous.fact = merged;
2702
+ } else {
2703
+ if (requests.size === maxRequests) {
2704
+ coverage.scannedRecords--;
2705
+ coverage.unscannedRecords = records.length - coverage.scannedRecords;
2706
+ break;
2707
+ }
2708
+ requests.set(key, { fact, conflict: false });
2709
+ }
2710
+ }
2711
+ const groups = new Map;
2712
+ for (const { fact: r, conflict } of requests.values()) {
2713
+ if (conflict) {
2714
+ coverage.conflictingRequests++;
2715
+ continue;
2716
+ }
2717
+ if (r.completedAt === undefined) {
2718
+ coverage.incompleteRequests++;
2719
+ continue;
2720
+ }
2721
+ const duration = r.completedAt - r.startedAt;
2722
+ if (!nonnegative2(r.completedAt) || !Number.isFinite(duration) || duration <= 0) {
2723
+ coverage.invalidTimingRequests++;
2724
+ continue;
2725
+ }
2726
+ 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)
2727
+ continue;
2728
+ let group = groups.get(r.model);
2729
+ if (!group) {
2730
+ group = {
2731
+ summary: {
2732
+ ...r.model ? { model: r.model } : {},
2733
+ sessions: 0,
2734
+ completedRequests: 0,
2735
+ partialRequests: 0,
2736
+ legacyAttributionRequests: 0,
2737
+ tpsRequests: 0,
2738
+ ttftRequests: 0,
2739
+ excludedTpsRequests: 0,
2740
+ excludedTtftRequests: 0,
2741
+ outputTokens: 0,
2742
+ durationMs: 0
2743
+ },
2744
+ sessions: new Set,
2745
+ ttfts: []
2746
+ };
2747
+ groups.set(r.model, group);
2748
+ }
2749
+ const s = group.summary;
2750
+ group.sessions.add(r.sessionId);
2751
+ s.completedRequests++;
2752
+ if (r.partial)
2753
+ s.partialRequests++;
2754
+ if (r.attribution === "legacy-record")
2755
+ s.legacyAttributionRequests++;
2756
+ if (r.outputTokens !== undefined) {
2757
+ s.tpsRequests++;
2758
+ s.outputTokens += r.outputTokens;
2759
+ s.durationMs += duration;
2760
+ } else
2761
+ s.excludedTpsRequests++;
2762
+ if (nonnegative2(r.ttftMs) && r.ttftMs <= duration)
2763
+ group.ttfts.push(r.ttftMs);
2764
+ else
2765
+ s.excludedTtftRequests++;
2766
+ }
2767
+ const models = [...groups.values()].map(({ summary: s, sessions, ttfts }) => {
2768
+ s.sessions = sessions.size;
2769
+ s.ttftRequests = ttfts.length;
2770
+ if (s.tpsRequests)
2771
+ s.averageTps = s.outputTokens * 1000 / s.durationMs;
2772
+ ttfts.sort((a, b) => a - b);
2773
+ if (ttfts.length) {
2774
+ s.meanTtftMs = ttfts.reduce((sum, n) => sum + n, 0) / ttfts.length;
2775
+ s.medianTtftMs = ((ttfts[Math.floor((ttfts.length - 1) / 2)] ?? 0) + (ttfts[Math.floor(ttfts.length / 2)] ?? 0)) / 2;
2776
+ const p95 = ttfts[Math.ceil(ttfts.length * 0.95) - 1];
2777
+ if (p95 !== undefined)
2778
+ s.p95TtftMs = p95;
2779
+ }
2780
+ return s;
2781
+ }).sort((a, b) => (a.model ?? "").localeCompare(b.model ?? ""));
2782
+ 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);
2783
+ return { schemaVersion: 1, location: options.location, models, coverage };
2784
+ }
2785
+ var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
2786
+
2787
+ // src/modelcli.ts
2788
+ var exports_modelcli = {};
2789
+ __export(exports_modelcli, {
2790
+ inspectModels: () => inspectModels
2791
+ });
2792
+ import { isAbsolute as isAbsolute4, resolve as resolve2 } from "path";
2793
+ async function inspectModels(args) {
2794
+ const directory = args[0];
2795
+ if (!directory || !isAbsolute4(directory))
2796
+ throw new Error("Usage: inspect models /absolute/history-directory [--model ID] [--agent NAME] [--from ISO-or-ms] [--to ISO-or-ms] [--csv | --json]");
2797
+ const values = new Map;
2798
+ let csv = false, json = false;
2799
+ for (let i = 1;i < args.length; i++) {
2800
+ const name = args[i];
2801
+ if (name === "--csv" || name === "--json") {
2802
+ if (name === "--csv" && csv || name === "--json" && json)
2803
+ throw new Error(`Duplicate option ${name}`);
2804
+ if (name === "--csv")
2805
+ csv = true;
2806
+ else
2807
+ json = true;
2808
+ } else if (name && ["--model", "--agent", "--from", "--to"].includes(name)) {
2809
+ if (values.has(name))
2810
+ throw new Error(`Duplicate option ${name}`);
2811
+ const value = args[++i];
2812
+ if (!value || value.startsWith("--"))
2813
+ throw new Error(`Missing value for ${name}`);
2814
+ values.set(name, value);
2815
+ } else
2816
+ throw new Error(`Unknown models option ${name}`);
2817
+ }
2818
+ if (csv && json)
2819
+ throw new Error("Choose JSON or CSV export");
2820
+ const timestamp = (value) => {
2821
+ if (value === undefined)
2822
+ return;
2823
+ const n = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
2824
+ if (!Number.isSafeInteger(n) || n < 0)
2825
+ throw new Error("Invalid timestamp");
2826
+ return n;
2827
+ };
2828
+ const from = timestamp(values.get("--from")), to = timestamp(values.get("--to"));
2829
+ if (from !== undefined && to !== undefined && from > to)
2830
+ throw new Error("--from must precede --to");
2831
+ const model = values.get("--model"), agent = values.get("--agent");
2832
+ const history = await readPerformanceHistory(directory);
2833
+ const result = aggregateModels(history.records, {
2834
+ location: resolve2(directory),
2835
+ ...from === undefined ? {} : { from },
2836
+ ...to === undefined ? {} : { to },
2837
+ ...model === undefined ? {} : { model },
2838
+ ...agent === undefined ? {} : { agent }
2839
+ });
2840
+ const metadata = {
2841
+ ...history.metadata,
2842
+ ...result.coverage,
2843
+ partial: result.coverage.partial || history.metadata.skippedBytes > 0 || history.metadata.malformedRecords > 0,
2844
+ requested: { from, to, model, agent },
2845
+ timeBasis: "stream-completion-[from,to)",
2846
+ tpsBasis: "reported-output-plus-reasoning-over-summed-request-duration",
2847
+ ttftBasis: "observed-approximate-timing",
2848
+ percentileMethod: "nearest-rank-p95-midpoint-median",
2849
+ notices: [
2850
+ "bounded-retention",
2851
+ "scan-limit-64-MiB",
2852
+ "no-pre-observation-reconstruction"
2853
+ ]
2854
+ };
2855
+ if (!csv) {
2856
+ console.log(JSON.stringify({
2857
+ schemaVersion: 1,
2858
+ location: result.location,
2859
+ metadata,
2860
+ models: result.models
2861
+ }, null, 2));
2862
+ return;
2863
+ }
2864
+ console.error(JSON.stringify(metadata));
2865
+ const fields = [
2866
+ "model",
2867
+ "sessions",
2868
+ "completedRequests",
2869
+ "partialRequests",
2870
+ "legacyAttributionRequests",
2871
+ "tpsRequests",
2872
+ "ttftRequests",
2873
+ "excludedTpsRequests",
2874
+ "excludedTtftRequests",
2875
+ "outputTokens",
2876
+ "durationMs",
2877
+ "averageTps",
2878
+ "meanTtftMs",
2879
+ "medianTtftMs",
2880
+ "p95TtftMs"
2881
+ ];
2882
+ console.log(fields.join(","));
2883
+ for (const row2 of result.models)
2884
+ console.log(fields.map((field) => `"${String(row2[field] ?? "").replaceAll('"', '""')}"`).join(","));
2885
+ }
2886
+ var init_modelcli = __esm(() => {
2887
+ init_history();
2888
+ });
2889
+
2890
+ // src/cli.ts
2891
+ import { writeFile } from "fs/promises";
2892
+ import { resolve as resolve3 } from "path";
2893
+
2894
+ // src/charts.ts
2895
+ var rateFields = [
2896
+ "liveTps",
2897
+ "averageModelTps",
2898
+ "averageSessionTps",
2899
+ "sessionOutputTokens"
2900
+ ];
2901
+ function downsamplePoints(points, limit) {
2902
+ if (!Number.isSafeInteger(limit) || limit < 16 || limit > 5000)
2903
+ throw new Error("maxPoints must be an integer from 16 to 5000");
2904
+ if (points.length <= limit)
2905
+ return [...points];
2906
+ const buckets = Math.max(1, Math.floor(limit / (2 + rateFields.length * 2)));
2907
+ const selected = new Set;
2908
+ for (let b = 0;b < buckets; b++) {
2909
+ const start = Math.floor(b * points.length / buckets), end = Math.floor((b + 1) * points.length / buckets);
2910
+ selected.add(start);
2911
+ selected.add(end - 1);
2912
+ for (const field of rateFields) {
2913
+ let low, high;
2914
+ for (let i = start;i < end; i++) {
2915
+ const value = points[i]?.[field];
2916
+ if (value === undefined)
1124
2917
  continue;
2918
+ if (low === undefined || value < (points[low]?.[field] ?? Infinity))
2919
+ low = i;
2920
+ if (high === undefined || value > (points[high]?.[field] ?? -Infinity))
2921
+ high = i;
2922
+ }
2923
+ if (low !== undefined)
2924
+ selected.add(low);
2925
+ if (high !== undefined)
2926
+ selected.add(high);
2927
+ }
2928
+ }
2929
+ let previous = -1;
2930
+ return [...selected].sort((a, b) => a - b).flatMap((index) => {
2931
+ const point = points[index];
2932
+ if (!point)
2933
+ return [];
2934
+ const skippedGap = points.slice(previous + 1, index + 1).some((p) => p.breakBefore);
2935
+ previous = index;
2936
+ return [{ ...point, breakBefore: point.breakBefore || skippedGap }];
2937
+ });
2938
+ }
2939
+ function buildChartData(records, options = {}) {
2940
+ const maxPoints = options.maxPoints ?? 1000, maxSessions = options.maxSessions ?? 128;
2941
+ if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 1024)
2942
+ throw new Error("maxSessions must be from 1 to 1024");
2943
+ downsamplePoints([], maxPoints);
2944
+ const sorted = [...records].sort((a, b) => a.time - b.time || a.recordId.localeCompare(b.recordId));
2945
+ const lastSeen = new Map;
2946
+ for (const r of sorted)
2947
+ if (r.performance && (!options.sessionId || r.sessionId === options.sessionId))
2948
+ lastSeen.set(r.sessionId, r.time);
2949
+ const sessions = [...lastSeen].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
2950
+ const selected = new Set(sessions.slice(0, maxSessions).map(([id]) => id));
2951
+ const series = new Map, gaps = [];
2952
+ const seen = new Set, requestSeen = new Set;
2953
+ const pendingGaps = new Set;
2954
+ const observedSince = new Map;
2955
+ for (const r of sorted) {
2956
+ if (seen.has(r.recordId))
2957
+ continue;
2958
+ seen.add(r.recordId);
2959
+ if (r.type === "gap") {
2960
+ if (r.sessionId === "__history__" || selected.has(r.sessionId)) {
2961
+ gaps.push({
2962
+ time: r.time,
2963
+ sessionId: r.sessionId,
2964
+ reason: r.event ?? "observation-gap"
2965
+ });
2966
+ if (r.sessionId === "__history__")
2967
+ for (const id of selected)
2968
+ pendingGaps.add(id);
2969
+ else
2970
+ pendingGaps.add(r.sessionId);
2971
+ }
2972
+ continue;
2973
+ }
2974
+ const p = r.performance;
2975
+ if (!p || !selected.has(r.sessionId))
2976
+ continue;
2977
+ let s = series.get(r.sessionId);
2978
+ if (!s) {
2979
+ s = {
2980
+ sessionId: r.sessionId,
2981
+ ...r.parentId ? { parentId: r.parentId } : {},
2982
+ agents: [],
2983
+ models: [],
2984
+ points: [],
2985
+ requests: [],
2986
+ originalPoints: 0
2987
+ };
2988
+ series.set(r.sessionId, s);
2989
+ }
2990
+ const agent = p.requestAgent ?? r.agent, model = p.requestModel ?? r.model;
2991
+ if (agent && !s.agents.includes(agent))
2992
+ s.agents.push(agent);
2993
+ if (model && !s.models.includes(model))
2994
+ s.models.push(model);
2995
+ const previous = s.points.at(-1);
2996
+ const restart = observedSince.has(r.sessionId) && observedSince.get(r.sessionId) !== p.observedSince;
2997
+ const reset = previous?.sessionOutputTokens !== undefined && p.sessionOutputTokens !== undefined && p.sessionOutputTokens < previous.sessionOutputTokens;
2998
+ 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;
2999
+ const point = {
3000
+ time: r.time,
3001
+ ...agent ? { agent } : {},
3002
+ ...model ? { model } : {},
3003
+ breakBefore: discontinuity,
3004
+ partial: p.partial,
3005
+ ...p.liveTps === undefined ? {} : { liveTps: p.liveTps },
3006
+ ...p.averageModelTps === undefined ? {} : { averageModelTps: p.averageModelTps },
3007
+ ...p.averageSessionTps === undefined ? {} : { averageSessionTps: p.averageSessionTps },
3008
+ ...p.sessionOutputTokens === undefined ? {} : { sessionOutputTokens: p.sessionOutputTokens }
3009
+ };
3010
+ if (previous?.time === point.time)
3011
+ s.points[s.points.length - 1] = {
3012
+ ...point,
3013
+ breakBefore: point.breakBefore || previous.breakBefore
3014
+ };
3015
+ else
3016
+ s.points.push(point);
3017
+ observedSince.set(r.sessionId, p.observedSince);
3018
+ pendingGaps.delete(r.sessionId);
3019
+ if (r.type === "request" && p.requestId !== undefined && p.requestStartedAt !== undefined) {
3020
+ const key = JSON.stringify([
3021
+ r.sessionId,
3022
+ p.requestId,
3023
+ p.requestStartedAt
3024
+ ]);
3025
+ if (requestSeen.has(key))
3026
+ continue;
3027
+ requestSeen.add(key);
3028
+ const duration = p.streamEndedAt === undefined ? undefined : p.streamEndedAt - p.requestStartedAt;
3029
+ const validDuration = duration !== undefined && duration > 0;
3030
+ const output = p.tokenSource === "reported" ? p.requestOutputTokens : undefined;
3031
+ s.requests.push({
3032
+ time: r.time,
3033
+ requestId: p.requestId,
3034
+ started: p.requestStartedAt,
3035
+ ...agent ? { agent } : {},
3036
+ ...model ? { model } : {},
3037
+ partial: p.partial,
3038
+ ...p.ttftMs === undefined ? {} : { ttftMs: p.ttftMs },
3039
+ ...output === undefined ? {} : { outputTokens: output },
3040
+ ...validDuration ? { durationMs: duration } : {},
3041
+ ...output !== undefined && validDuration ? { tps: output * 1000 / duration } : {}
3042
+ });
3043
+ }
3044
+ }
3045
+ return {
3046
+ schema: 1,
3047
+ series: [...series.values()].map((s) => ({
3048
+ ...s,
3049
+ originalPoints: s.points.length,
3050
+ points: downsamplePoints(s.points, maxPoints)
3051
+ })),
3052
+ gaps: gaps.slice(-1024),
3053
+ omittedGaps: Math.max(0, gaps.length - 1024),
3054
+ omittedSessions: Math.max(0, sessions.length - selected.size),
3055
+ maxPoints
3056
+ };
3057
+ }
3058
+
3059
+ // src/chartreport.ts
3060
+ function renderChartReport(report) {
3061
+ const payload = JSON.stringify(report).replace(/[<>&\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
3062
+ return `<!doctype html>
3063
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3064
+ <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'">
3065
+ <title>Context Guard \xB7 Performance history</title>
3066
+ <style>
3067
+ :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}}
3068
+ </style></head><body>
3069
+ <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>
3070
+ <section class="toolbar" aria-label="Chart filters">
3071
+ <label>Session instance<select id="session"></select></label><label>Agent<select id="agent"></select></label><label>Model<select id="model"></select></label>
3072
+ <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>
3073
+ </section>
3074
+ <div id="notice" class="notice" role="status"></div><section id="stats" class="stats" aria-label="Request statistics"></section>
3075
+ <div id="legend" class="legend"></div><main id="charts" class="charts"></main>
3076
+ <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>
3077
+ <div id="tooltip" class="tooltip" hidden role="tooltip"></div>
3078
+ <script id="chart-data" type="application/json">${payload}</script>
3079
+ <script>(${chartClient.toString()})(JSON.parse(document.getElementById("chart-data").textContent));</script>
3080
+ </body></html>`;
3081
+ }
3082
+ function chartClient(report) {
3083
+ const { data, metadata } = report;
3084
+ const element = (id, kind) => {
3085
+ const node = document.getElementById(id);
3086
+ if (!(node instanceof kind))
3087
+ throw new Error(`Missing report element ${id}`);
3088
+ return node;
3089
+ };
3090
+ const session = element("session", HTMLSelectElement), agent = element("agent", HTMLSelectElement), model = element("model", HTMLSelectElement);
3091
+ const from = element("from", HTMLInputElement), to = element("to", HTMLInputElement);
3092
+ const charts = element("charts", HTMLElement), stats = element("stats", HTMLElement), legend = element("legend", HTMLElement);
3093
+ const notice = element("notice", HTMLElement), tooltip = element("tooltip", HTMLElement);
3094
+ const color = (index) => `hsl(${(index * 137.508 + 195) % 360}, 78%, 68%)`;
3095
+ const date = (value) => {
3096
+ const d = new Date(value);
3097
+ return Number.isFinite(d.getTime()) ? d.toISOString() : String(value);
3098
+ };
3099
+ const number = (n, digits = 1) => n === undefined ? "Unavailable" : n.toLocaleString("en-US", { maximumFractionDigits: digits });
3100
+ function options(select, values) {
3101
+ for (const v of values) {
3102
+ const o = document.createElement("option");
3103
+ o.value = v.value;
3104
+ o.textContent = v.label;
3105
+ select.append(o);
3106
+ }
3107
+ }
3108
+ options(session, [
3109
+ { value: "*", label: "Compare sessions (up to 12)" },
3110
+ ...data.series.map((s) => ({
3111
+ value: s.sessionId,
3112
+ label: `${s.agents.join(", ") || "Agent"} \xB7 ${s.sessionId}`
3113
+ }))
3114
+ ]);
3115
+ options(agent, [
3116
+ { value: "*", label: "All agents" },
3117
+ ...[...new Set(data.series.flatMap((s) => s.agents))].sort().map((v) => ({ value: v, label: v }))
3118
+ ]);
3119
+ options(model, [
3120
+ { value: "*", label: "All models" },
3121
+ ...[...new Set(data.series.flatMap((s) => s.models))].sort().map((v) => ({ value: v, label: v }))
3122
+ ]);
3123
+ function reset() {
3124
+ session.value = data.series[0]?.sessionId ?? "*";
3125
+ agent.value = report.agent ?? "*";
3126
+ model.value = report.model ?? "*";
3127
+ from.value = report.from === undefined ? "" : date(report.from).slice(0, 19);
3128
+ to.value = report.to === undefined ? "" : date(report.to).slice(0, 19);
3129
+ }
3130
+ const timestamp = (value) => value ? Date.parse(`${value}Z`) : undefined;
3131
+ function matches2(p, start, end) {
3132
+ return (agent.value === "*" || p.agent === agent.value) && (model.value === "*" || p.model === model.value) && (start === undefined || p.time >= start) && (end === undefined || p.time <= end);
3133
+ }
3134
+ const ns = "http://www.w3.org/2000/svg";
3135
+ function svgNode(name, attrs = {}, text2) {
3136
+ const n = document.createElementNS(ns, name);
3137
+ for (const [key, value] of Object.entries(attrs))
3138
+ n.setAttribute(key, String(value));
3139
+ if (text2 !== undefined)
3140
+ n.textContent = text2;
3141
+ return n;
3142
+ }
3143
+ function showTooltip(text2, x, y) {
3144
+ tooltip.textContent = text2;
3145
+ tooltip.hidden = false;
3146
+ tooltip.style.left = `${Math.max(8, Math.min(x + 12, window.innerWidth - 370))}px`;
3147
+ tooltip.style.top = `${Math.max(8, Math.min(y + 12, window.innerHeight - tooltip.offsetHeight - 10))}px`;
3148
+ }
3149
+ const definitions = [
3150
+ {
3151
+ key: "liveTps",
3152
+ title: "Live TPS",
3153
+ unit: "tokens / second",
3154
+ description: "Estimated text + reasoning throughput in the rolling collection window.",
3155
+ requests: false,
3156
+ estimated: true
3157
+ },
3158
+ {
3159
+ key: "averageModelTps",
3160
+ title: "Average model throughput",
3161
+ unit: "tokens / second",
3162
+ description: "Session output / matched model-request time, including TTFT and excluding subsequent tool time.",
3163
+ requests: false,
3164
+ estimated: false
3165
+ },
3166
+ {
3167
+ key: "averageSessionTps",
3168
+ title: "Average session throughput",
3169
+ unit: "tokens / second",
3170
+ description: "Session output / elapsed session lifetime, including tools and idle time.",
3171
+ requests: false,
3172
+ estimated: false
3173
+ },
3174
+ {
3175
+ key: "ttftMs",
3176
+ title: "Time to first output",
3177
+ unit: "seconds",
3178
+ description: "One point per request with observed text/reasoning output. Includes publication delay.",
3179
+ requests: true,
3180
+ estimated: true
3181
+ },
3182
+ {
3183
+ key: "tps",
3184
+ title: "Completed request throughput",
3185
+ unit: "tokens / second",
3186
+ description: "Reported output + reasoning / request duration. Requests are never joined into a continuous curve.",
3187
+ requests: true,
3188
+ estimated: false
3189
+ },
3190
+ {
3191
+ key: "sessionOutputTokens",
3192
+ title: "Cumulative output",
3193
+ unit: "tokens",
3194
+ description: "Authoritative session output + reasoning totals. Lifecycle and sample records are snapshots, not increments.",
3195
+ requests: false,
3196
+ estimated: false
3197
+ }
3198
+ ];
3199
+ function render() {
3200
+ tooltip.hidden = true;
3201
+ charts.replaceChildren();
3202
+ stats.replaceChildren();
3203
+ legend.replaceChildren();
3204
+ const start = timestamp(from.value), end = timestamp(to.value);
3205
+ if (start !== undefined && !Number.isFinite(start) || end !== undefined && !Number.isFinite(end) || start !== undefined && end !== undefined && start > end) {
3206
+ notice.textContent = "Choose a valid time range with From before To.";
3207
+ return;
3208
+ }
3209
+ 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)));
3210
+ const visible = selected.slice(0, 12);
3211
+ const requests = visible.flatMap((s) => s.requests.filter((r) => matches2(r, start, end)));
3212
+ const matched = requests.filter((r) => r.outputTokens !== undefined && r.durationMs !== undefined && r.durationMs > 0);
3213
+ const duration = matched.reduce((sum, r) => sum + (r.durationMs ?? 0), 0), tokens = matched.reduce((sum, r) => sum + (r.outputTokens ?? 0), 0);
3214
+ const ttfts = requests.flatMap((r) => r.ttftMs === undefined ? [] : [r.ttftMs]).sort((a, b) => a - b);
3215
+ const percentile = (q) => ttfts.length ? (ttfts[Math.max(0, Math.ceil(ttfts.length * q) - 1)] ?? 0) / 1000 : undefined;
3216
+ for (const [label, value] of [
3217
+ ["Matched / observed requests", `${matched.length} / ${requests.length}`],
3218
+ [
3219
+ "Retained request throughput",
3220
+ duration ? `${number(tokens * 1000 / duration)} t/s` : "Unavailable"
3221
+ ],
3222
+ [
3223
+ "Median observed TTFT",
3224
+ ttfts.length ? `${number(percentile(0.5), 2)}s` : "Unavailable"
3225
+ ],
3226
+ [
3227
+ "95th percentile TTFT",
3228
+ ttfts.length ? `${number(percentile(0.95), 2)}s` : "Unavailable"
3229
+ ]
3230
+ ]) {
3231
+ const box = document.createElement("div");
3232
+ box.className = "stat";
3233
+ const name = document.createElement("span"), valueNode = document.createElement("strong");
3234
+ name.textContent = label ?? "";
3235
+ valueNode.textContent = value ?? "";
3236
+ box.append(name, valueNode);
3237
+ stats.append(box);
3238
+ }
3239
+ 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));
3240
+ const notes = [
3241
+ `Bounded retained history: ${metadata.retainedFrom === undefined ? "empty archive" : `segments since ${date(metadata.retainedFrom)}`}.`,
3242
+ `${gaps.length} recorded gap markers.`
3243
+ ];
3244
+ if (metadata.skippedBytes)
3245
+ notes.push(`${number(metadata.skippedBytes, 0)} bytes excluded by the read limit.`);
3246
+ if (metadata.malformedRecords)
3247
+ notes.push(`${metadata.malformedRecords} malformed/partial records skipped.`);
3248
+ if (data.omittedGaps)
3249
+ notes.push(`${data.omittedGaps} older gap markers omitted; curve discontinuities remain preserved.`);
3250
+ if (data.omittedSessions)
3251
+ notes.push(`${data.omittedSessions} sessions omitted from the report limit; regenerate with --session to inspect one.`);
3252
+ if (selected.length > 12)
3253
+ notes.push(`Showing 12 of ${selected.length} matching sessions; select a session for detail.`);
3254
+ if (visible.some((s) => s.originalPoints > s.points.length))
3255
+ notes.push(`Curves downsampled to at most ${data.maxPoints} observations per session, preserving extrema and gaps.`);
3256
+ if (visible.some((s) => s.points.some((p) => p.partial && matches2(p, start, end))))
3257
+ notes.push("Partial timing coverage is present.");
3258
+ if (!visible.length)
3259
+ notes.push("No observations match these filters.");
3260
+ notice.textContent = notes.join(" ");
3261
+ visible.forEach((s, index) => {
3262
+ const item = document.createElement("span"), dot = document.createElement("i");
3263
+ dot.className = "dot";
3264
+ dot.style.backgroundColor = color(index);
3265
+ item.append(dot, document.createTextNode(`${s.agents.join(", ") || "Agent"} \xB7 ${s.sessionId}${s.parentId ? ` (child of ${s.parentId})` : ""}`));
3266
+ legend.append(item);
3267
+ });
3268
+ const times = visible.flatMap((s) => [...s.points, ...s.requests].filter((p) => matches2(p, start, end)).map((p) => p.time));
3269
+ let minTime = start ?? times.reduce((a, t) => Math.min(a, t), Infinity), maxTime = end ?? times.reduce((a, t) => Math.max(a, t), -Infinity);
3270
+ if (!Number.isFinite(minTime))
3271
+ minTime = 0;
3272
+ if (!Number.isFinite(maxTime))
3273
+ maxTime = minTime + 1000;
3274
+ if (maxTime <= minTime)
3275
+ maxTime = minTime + 1000;
3276
+ for (const def of definitions) {
3277
+ const card = document.createElement("section");
3278
+ card.className = "chart";
3279
+ const heading = document.createElement("div");
3280
+ heading.className = "charthead";
3281
+ const title = document.createElement("h2");
3282
+ title.textContent = def.title;
3283
+ const download = document.createElement("button");
3284
+ download.textContent = "Save SVG";
3285
+ download.setAttribute("aria-label", `Save ${def.title} as SVG`);
3286
+ heading.append(title, download);
3287
+ const desc = document.createElement("p");
3288
+ desc.textContent = def.description;
3289
+ card.append(heading, desc);
3290
+ charts.append(card);
3291
+ const plots = visible.map((s) => {
3292
+ if (def.requests)
3293
+ return s.requests.filter((p) => matches2(p, start, end)).map((p) => {
3294
+ const raw = def.key === "ttftMs" ? p.ttftMs : p.tps;
3295
+ return {
3296
+ ...p,
3297
+ breakBefore: true,
3298
+ ...raw === undefined ? {} : { value: def.key === "ttftMs" ? raw / 1000 : raw }
3299
+ };
3300
+ });
3301
+ let skipped = false;
3302
+ return s.points.flatMap((p) => {
3303
+ if (!matches2(p, start, end)) {
3304
+ skipped = true;
3305
+ return [];
3306
+ }
3307
+ const key = def.key;
3308
+ const value = key === "liveTps" ? p.liveTps : key === "averageModelTps" ? p.averageModelTps : key === "averageSessionTps" ? p.averageSessionTps : p.sessionOutputTokens;
3309
+ const point = {
3310
+ ...p,
3311
+ breakBefore: p.breakBefore || skipped,
3312
+ ...value === undefined ? {} : { value }
3313
+ };
3314
+ skipped = false;
3315
+ return [point];
3316
+ });
3317
+ });
3318
+ const drawn = plots.map((points) => {
3319
+ if (!def.requests || points.length <= data.maxPoints)
3320
+ return points;
3321
+ const bins = Math.max(1, Math.floor(data.maxPoints / 4)), chosen = new Set;
3322
+ for (let b = 0;b < bins; b++) {
3323
+ const first = Math.floor(b * points.length / bins), last = Math.floor((b + 1) * points.length / bins) - 1;
3324
+ chosen.add(first);
3325
+ chosen.add(last);
3326
+ let low = first, high = first;
3327
+ for (let i = first;i <= last; i++) {
3328
+ if ((points[i]?.value ?? Infinity) < (points[low]?.value ?? Infinity))
3329
+ low = i;
3330
+ if ((points[i]?.value ?? -Infinity) > (points[high]?.value ?? -Infinity))
3331
+ high = i;
3332
+ }
3333
+ chosen.add(low);
3334
+ chosen.add(high);
1125
3335
  }
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++;
3336
+ return [...chosen].sort((a, b) => a - b).flatMap((i) => points[i] ? [points[i]] : []);
3337
+ });
3338
+ const maxValue = drawn.reduce((maximum, points) => points.reduce((n, p) => Math.max(n, p.value ?? 0), maximum), 1) * 1.08;
3339
+ const w = 720, h = 300, left = 64, right = 20, top = 24, bottom = 47;
3340
+ const x = (t) => left + (t - minTime) / (maxTime - minTime) * (w - left - right);
3341
+ const y = (v) => h - bottom - v / maxValue * (h - top - bottom);
3342
+ const svg = svgNode("svg", {
3343
+ viewBox: `0 0 ${w} ${h}`,
3344
+ role: "img",
3345
+ "aria-label": `${def.title} over UTC timestamps`,
3346
+ xmlns: ns
3347
+ });
3348
+ svg.append(svgNode("rect", { width: w, height: h, fill: "#111c2e" }), svgNode("title", {}, `${def.title}. ${def.description}`));
3349
+ svg.append(svgNode("text", {
3350
+ x: left,
3351
+ y: 14,
3352
+ fill: "#a8b5cb",
3353
+ "font-size": 11,
3354
+ "font-family": "system-ui,sans-serif"
3355
+ }, def.unit));
3356
+ for (let tick = 0;tick <= 4; tick++) {
3357
+ const v = maxValue * tick / 4;
3358
+ svg.append(svgNode("line", {
3359
+ x1: left,
3360
+ x2: w - right,
3361
+ y1: y(v),
3362
+ y2: y(v),
3363
+ stroke: "#2a3950",
3364
+ "stroke-width": 1
3365
+ }));
3366
+ svg.append(svgNode("text", {
3367
+ x: left - 8,
3368
+ y: y(v) + 4,
3369
+ fill: "#a8b5cb",
3370
+ "text-anchor": "end",
3371
+ "font-size": 11,
3372
+ "font-family": "system-ui,sans-serif"
3373
+ }, v >= 1e4 ? `${number(v / 1000)}k` : number(v)));
3374
+ const t = minTime + (maxTime - minTime) * tick / 4;
3375
+ const label = maxTime - minTime > 86400000 ? date(t).slice(5, 16).replace("T", " ") : date(t).slice(11, 19);
3376
+ svg.append(svgNode("text", {
3377
+ x: x(t),
3378
+ y: h - 22,
3379
+ fill: "#a8b5cb",
3380
+ "text-anchor": tick === 0 ? "start" : tick === 4 ? "end" : "middle",
3381
+ "font-size": 11,
3382
+ "font-family": "system-ui,sans-serif"
3383
+ }, label));
1131
3384
  }
3385
+ for (const gap of gaps)
3386
+ if (gap.time >= minTime && gap.time <= maxTime) {
3387
+ const line = svgNode("line", {
3388
+ x1: x(gap.time),
3389
+ x2: x(gap.time),
3390
+ y1: top,
3391
+ y2: h - bottom,
3392
+ stroke: "#fb7185",
3393
+ "stroke-dasharray": "3 5",
3394
+ opacity: 0.65
3395
+ });
3396
+ line.append(svgNode("title", {}, `${date(gap.time)} \xB7 ${gap.reason}`));
3397
+ svg.append(line);
3398
+ }
3399
+ let count2 = 0;
3400
+ drawn.forEach((points, index) => {
3401
+ let run = [];
3402
+ const flush = () => {
3403
+ if (run.length > 1)
3404
+ svg.append(svgNode("polyline", {
3405
+ points: run.map((p) => `${x(p.time)},${y(p.value ?? 0)}`).join(" "),
3406
+ fill: "none",
3407
+ stroke: color(index),
3408
+ "stroke-width": 1.8,
3409
+ ...def.estimated || run.some((p) => p.partial) ? { "stroke-dasharray": "5 4" } : {}
3410
+ }));
3411
+ run = [];
3412
+ };
3413
+ for (const point of points) {
3414
+ if (point.breakBefore || point.value === undefined)
3415
+ flush();
3416
+ if (point.value === undefined)
3417
+ continue;
3418
+ count2++;
3419
+ if (!def.requests)
3420
+ run.push(point);
3421
+ 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" : ""}`;
3422
+ const dot = svgNode("circle", {
3423
+ cx: x(point.time),
3424
+ cy: y(point.value),
3425
+ r: def.requests ? 3 : 2.5,
3426
+ fill: color(index),
3427
+ tabindex: 0,
3428
+ "aria-label": label
3429
+ });
3430
+ dot.append(svgNode("title", {}, label));
3431
+ dot.addEventListener("mouseenter", (e) => showTooltip(label, e.clientX, e.clientY));
3432
+ dot.addEventListener("mouseleave", () => {
3433
+ tooltip.hidden = true;
3434
+ });
3435
+ dot.addEventListener("focus", () => {
3436
+ const box = dot.getBoundingClientRect();
3437
+ showTooltip(label, box.x, box.y);
3438
+ });
3439
+ dot.addEventListener("blur", () => {
3440
+ tooltip.hidden = true;
3441
+ });
3442
+ svg.append(dot);
3443
+ }
3444
+ flush();
3445
+ });
3446
+ if (!count2)
3447
+ svg.append(svgNode("text", {
3448
+ x: w / 2,
3449
+ y: h / 2,
3450
+ fill: "#a8b5cb",
3451
+ "text-anchor": "middle",
3452
+ "font-size": 14,
3453
+ "font-family": "system-ui,sans-serif"
3454
+ }, "No measurements in this selection"));
3455
+ card.append(svg);
3456
+ download.onclick = () => {
3457
+ const blob = new Blob([new XMLSerializer().serializeToString(svg)], {
3458
+ type: "image/svg+xml"
3459
+ });
3460
+ const url = URL.createObjectURL(blob);
3461
+ const link = document.createElement("a");
3462
+ link.href = url;
3463
+ link.download = `context-guard-${def.key}.svg`;
3464
+ link.click();
3465
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
3466
+ };
1132
3467
  }
1133
3468
  }
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"
3469
+ for (const control of [session, agent, model, from, to])
3470
+ control.addEventListener("change", render);
3471
+ element("reset", HTMLButtonElement).onclick = () => {
3472
+ reset();
3473
+ render();
1141
3474
  };
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);
3475
+ reset();
3476
+ render();
1165
3477
  }
1166
3478
 
3479
+ // src/cli.ts
3480
+ init_history();
3481
+
1167
3482
  // src/telemetry.ts
1168
3483
  import { mkdir as mkdir2, open as open2, rename, stat as stat2 } from "fs/promises";
1169
3484
  import { dirname, join as join2 } from "path";
@@ -1279,8 +3594,28 @@ async function readJournal(path, maxBytes = 16 * 1024 * 1024) {
1279
3594
 
1280
3595
  // src/cli.ts
1281
3596
  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]");
3597
+ if (args[0] === "dashboard") {
3598
+ const { parseDashboardArgs: parseDashboardArgs2, dashboardHelp: dashboardHelp2, checkTerminalRuntime: checkTerminalRuntime2 } = await Promise.resolve().then(() => (init_command(), exports_command));
3599
+ try {
3600
+ const options = parseDashboardArgs2(args.slice(1), process.cwd(), process.env);
3601
+ if (options === "help")
3602
+ console.log(dashboardHelp2);
3603
+ else {
3604
+ checkTerminalRuntime2(Bun.version, Boolean(process.stdin.isTTY && process.stdout.isTTY));
3605
+ const { runTerminal: runTerminal2 } = await Promise.resolve().then(() => (init_terminal(), exports_terminal));
3606
+ await runTerminal2(options);
3607
+ }
3608
+ } catch (error) {
3609
+ const message = error instanceof Error ? error.message : "";
3610
+ 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);
3611
+ console.error(safe ? message : "Dashboard startup failed; check server, owning directory, authentication and renderer installation");
3612
+ process.exitCode = 1;
3613
+ }
3614
+ } else if (!args[0] || args.includes("--help")) {
3615
+ 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]\nModels: inspect models /absolute/history-directory [--model provider/model] [--agent NAME] [--from ISO-or-ms] [--to ISO-or-ms] [--csv | --json]\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]");
3616
+ } else if (args[0] === "models") {
3617
+ const { inspectModels: inspectModels2 } = await Promise.resolve().then(() => (init_modelcli(), exports_modelcli));
3618
+ await inspectModels2(args.slice(1));
1284
3619
  } else if (args[0] === "metrics" || args[0] === "charts") {
1285
3620
  const directory = args[1];
1286
3621
  if (!directory || directory.startsWith("--"))
@@ -1324,7 +3659,7 @@ if (!args[0] || args.includes("--help")) {
1324
3659
  ...to === undefined ? {} : { to }
1325
3660
  });
1326
3661
  if (chart) {
1327
- const output = resolve(option("--chart") ?? option("--output") ?? "context-guard-charts.html");
3662
+ const output = resolve3(option("--chart") ?? option("--output") ?? "context-guard-charts.html");
1328
3663
  if (!output.toLowerCase().endsWith(".html"))
1329
3664
  throw new Error("Chart output must use an .html extension");
1330
3665
  const data = buildChartData(result.records, { maxPoints, maxSessions });