@cesarus777/opencode-cgp 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +33 -5
- package/dist/cli.js +3363 -1028
- package/dist/core.js +311 -102
- package/dist/index.js +873 -836
- package/dist/rpc.js +275 -246
- package/dist/tui.js +679 -627
- package/dist/types/core.d.ts +1 -0
- package/dist/types/dashboard/command.d.ts +5 -0
- package/dist/types/dashboard/connection.d.ts +36 -0
- package/dist/types/dashboard/controller.d.ts +57 -0
- package/dist/types/dashboard/inventory.d.ts +17 -0
- package/dist/types/dashboard/port.d.ts +41 -0
- package/dist/types/dashboard/terminal.d.ts +36 -0
- package/dist/types/dashboard/terminalview.d.ts +15 -0
- package/dist/types/dashboard/view.d.ts +83 -0
- package/dist/types/modelanalytics.d.ts +60 -0
- package/dist/types/modelcli.d.ts +1 -0
- package/dist/types/tui/contextchart.d.ts +2 -1
- package/package.json +5 -2
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/
|
|
317
|
-
import {
|
|
318
|
-
import {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
...
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
760
|
-
chosen.add(high);
|
|
1010
|
+
retry = 0;
|
|
761
1011
|
}
|
|
762
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
866
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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/
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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.
|
|
966
|
-
this.kick();
|
|
1294
|
+
await this.refresh();
|
|
967
1295
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
this.
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1497
|
+
if (this.current(state, generation, signal))
|
|
1498
|
+
delete state.probing;
|
|
1041
1499
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1500
|
+
})();
|
|
1501
|
+
return state.probing;
|
|
1044
1502
|
}
|
|
1045
|
-
|
|
1046
|
-
return
|
|
1503
|
+
cacheKey() {
|
|
1504
|
+
return JSON.stringify([
|
|
1505
|
+
this.state.port.location.directory,
|
|
1506
|
+
this.state.selected,
|
|
1507
|
+
this.windowMs
|
|
1508
|
+
]);
|
|
1047
1509
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1510
|
+
cancelHistory() {
|
|
1511
|
+
this.historyGeneration++;
|
|
1512
|
+
this.pendingHistory = undefined;
|
|
1513
|
+
this.historyAbort?.abort();
|
|
1051
1514
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
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 (
|
|
1283
|
-
|
|
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 =
|
|
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 });
|