oc-metricboard 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collector.js +15 -2
- package/dist/tui-v2.js +74 -25
- package/package.json +1 -1
- package/src/collector.ts +15 -2
- package/src/tui-v2.tsx +74 -23
package/dist/collector.js
CHANGED
|
@@ -350,7 +350,12 @@ export function createCollector(api, config, log) {
|
|
|
350
350
|
const latest = metrics.reduce((a, b) => (b.requestStartTime >= a.requestStartTime ? b : a));
|
|
351
351
|
const frTokens = getDisplayOutputTokens(latest);
|
|
352
352
|
const genStart = latest.firstTokenTime ?? latest.requestStartTime;
|
|
353
|
-
|
|
353
|
+
// Same freeze rule as the main aggregate: an open-but-silent group
|
|
354
|
+
// ends its window at the latest request's last activity, not `now`.
|
|
355
|
+
const latestLastActivity = latest.lastDeltaTime ?? null;
|
|
356
|
+
const genEnd = isComplete && completeTime !== null
|
|
357
|
+
? completeTime
|
|
358
|
+
: latest.isStreaming || latestLastActivity === null ? now : latestLastActivity;
|
|
354
359
|
const saneStart = genStart !== null && Number.isFinite(genStart) && genStart >= 0 && genStart <= now;
|
|
355
360
|
const saneEnd = genEnd !== null && Number.isFinite(genEnd) && genEnd >= 0 && genEnd <= now + 60_000;
|
|
356
361
|
if (frTokens > 0 && saneStart && saneEnd && genEnd > genStart) {
|
|
@@ -490,7 +495,15 @@ export function createCollector(api, config, log) {
|
|
|
490
495
|
// the foreground window would inflate TPS absurdly (1800+ t/s).
|
|
491
496
|
const frTokens = getDisplayOutputTokens(foregroundRequest);
|
|
492
497
|
const genStart = firstTokenTime ?? requestStartTime;
|
|
493
|
-
|
|
498
|
+
// Window end: complete → frozen completeTime; streaming → now;
|
|
499
|
+
// open but silent (e.g. a lost completion event) → last observed
|
|
500
|
+
// activity, so the reading freezes instead of decaying in real time
|
|
501
|
+
// while idle.
|
|
502
|
+
const foregroundActive = foregroundRequest?.isStreaming ?? false;
|
|
503
|
+
const foregroundLastActivity = foregroundRequest?.lastDeltaTime ?? null;
|
|
504
|
+
const genEnd = isComplete
|
|
505
|
+
? completeTime
|
|
506
|
+
: foregroundActive || foregroundLastActivity === null ? now : foregroundLastActivity;
|
|
494
507
|
const saneStart = genStart !== null && Number.isFinite(genStart) && genStart >= 0 && genStart <= now;
|
|
495
508
|
const saneEnd = genEnd !== null && Number.isFinite(genEnd) && genEnd >= 0 && genEnd <= now + 60_000;
|
|
496
509
|
if (frTokens > 0 && saneStart && saneEnd && genEnd > genStart) {
|
package/dist/tui-v2.js
CHANGED
|
@@ -74,15 +74,27 @@ function str(value) {
|
|
|
74
74
|
/**
|
|
75
75
|
* `session.execution.started` = "the user's prompt was admitted and work
|
|
76
76
|
* started" — the V2 equivalent of V1's `session.status -> busy`, which drives
|
|
77
|
-
* turn start and session timing.
|
|
77
|
+
* turn start and session timing. Exported for tests.
|
|
78
78
|
*/
|
|
79
|
-
function
|
|
79
|
+
export function translateExecutionStartedToBusy(event) {
|
|
80
80
|
const data = isRecord(event.data) ? event.data : {};
|
|
81
81
|
const sessionID = str(data.sessionID);
|
|
82
82
|
if (!sessionID)
|
|
83
83
|
return null;
|
|
84
84
|
return { type: "session.status", properties: { sessionID, status: { type: "busy" } } };
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Turn-settled execution maps to the V1 `session.idle` shape that drives
|
|
88
|
+
* request completion. Used as a fallback next to the native V2
|
|
89
|
+
* `session.idle`, which some flows never emit. Exported for tests.
|
|
90
|
+
*/
|
|
91
|
+
export function translateExecutionSettledToIdle(event) {
|
|
92
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
93
|
+
const sessionID = str(data.sessionID);
|
|
94
|
+
if (!sessionID)
|
|
95
|
+
return null;
|
|
96
|
+
return { type: "session.idle", properties: { sessionID } };
|
|
97
|
+
}
|
|
86
98
|
function eventSessionID(event) {
|
|
87
99
|
if (!isRecord(event))
|
|
88
100
|
return "";
|
|
@@ -110,39 +122,76 @@ function createV1EventShim(ctx, holder) {
|
|
|
110
122
|
// Family attribution is best-effort; the root session works without it.
|
|
111
123
|
}
|
|
112
124
|
};
|
|
125
|
+
const subscribeV2 = (v2Type, subscribee) => {
|
|
126
|
+
try {
|
|
127
|
+
const unsub = ctx.data.on(v2Type, (event) => {
|
|
128
|
+
try {
|
|
129
|
+
subscribee(event);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// One malformed event must never tear down the handler chain.
|
|
133
|
+
log(`v2 event dispatch failed: ${String(error)}`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return typeof unsub === "function" ? unsub : () => { };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
log(`v2 event subscribe failed (${v2Type}): ${String(error)}`);
|
|
140
|
+
return () => { };
|
|
141
|
+
}
|
|
142
|
+
};
|
|
113
143
|
return {
|
|
114
144
|
event: {
|
|
115
145
|
on(type, handler) {
|
|
116
146
|
// V1 hosts subscribe `.1`/`.2` delivery variants; V2 has none.
|
|
117
147
|
if (/\.1$|\.2$/.test(type))
|
|
118
148
|
return () => { };
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
// V1 session lifecycle has no same-name V2 event: synthesize
|
|
150
|
+
// busy from execution.started. (Without this, turns, session
|
|
151
|
+
// timing, and completion never start on V2.)
|
|
152
|
+
if (type === "session.status") {
|
|
153
|
+
return subscribeV2("session.execution.started", (event) => {
|
|
154
|
+
const translated = translateExecutionStartedToBusy(event);
|
|
155
|
+
if (translated !== null)
|
|
156
|
+
handler(translated);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Completion: native V2 session.idle where emitted, plus the
|
|
160
|
+
// execution-settled events as fallback — some flows never emit
|
|
161
|
+
// session.idle, which used to leave the turn/request/timing
|
|
162
|
+
// open forever (TPS decaying in real time while idle).
|
|
163
|
+
// completeRequest is idempotent, so a doubled signal is safe.
|
|
164
|
+
if (type === "session.idle") {
|
|
165
|
+
const unsubs = [
|
|
166
|
+
subscribeV2("session.idle", handler),
|
|
167
|
+
];
|
|
168
|
+
for (const settled of [
|
|
169
|
+
"session.execution.succeeded",
|
|
170
|
+
"session.execution.failed",
|
|
171
|
+
"session.execution.interrupted",
|
|
172
|
+
]) {
|
|
173
|
+
unsubs.push(subscribeV2(settled, (event) => {
|
|
174
|
+
const translated = translateExecutionSettledToIdle(event);
|
|
175
|
+
if (translated !== null)
|
|
176
|
+
handler(translated);
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
return () => {
|
|
180
|
+
for (const unsub of unsubs) {
|
|
181
|
+
try {
|
|
182
|
+
unsub();
|
|
131
183
|
}
|
|
132
|
-
|
|
133
|
-
handler(event);
|
|
184
|
+
catch { /* ignore */ }
|
|
134
185
|
}
|
|
135
|
-
|
|
136
|
-
// One malformed event must never tear down the handler chain.
|
|
137
|
-
log(`v2 event dispatch failed: ${String(error)}`);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
186
|
+
};
|
|
140
187
|
}
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
const v2Type = V2_EVENT_MAP[type];
|
|
189
|
+
if (!v2Type)
|
|
143
190
|
return () => { };
|
|
144
|
-
|
|
145
|
-
|
|
191
|
+
return subscribeV2(v2Type, (event) => {
|
|
192
|
+
syncFamily(eventSessionID(event));
|
|
193
|
+
handler(event);
|
|
194
|
+
});
|
|
146
195
|
},
|
|
147
196
|
},
|
|
148
197
|
};
|
package/package.json
CHANGED
package/src/collector.ts
CHANGED
|
@@ -418,7 +418,12 @@ export function createCollector(
|
|
|
418
418
|
const latest = metrics.reduce((a, b) => (b.requestStartTime >= a.requestStartTime ? b : a))
|
|
419
419
|
const frTokens = getDisplayOutputTokens(latest)
|
|
420
420
|
const genStart = latest.firstTokenTime ?? latest.requestStartTime
|
|
421
|
-
|
|
421
|
+
// Same freeze rule as the main aggregate: an open-but-silent group
|
|
422
|
+
// ends its window at the latest request's last activity, not `now`.
|
|
423
|
+
const latestLastActivity = latest.lastDeltaTime ?? null
|
|
424
|
+
const genEnd = isComplete && completeTime !== null
|
|
425
|
+
? completeTime
|
|
426
|
+
: latest.isStreaming || latestLastActivity === null ? now : latestLastActivity
|
|
422
427
|
const saneStart = genStart !== null && Number.isFinite(genStart) && genStart >= 0 && genStart <= now
|
|
423
428
|
const saneEnd = genEnd !== null && Number.isFinite(genEnd) && genEnd >= 0 && genEnd <= now + 60_000
|
|
424
429
|
if (frTokens > 0 && saneStart && saneEnd && genEnd > genStart) {
|
|
@@ -564,7 +569,15 @@ export function createCollector(
|
|
|
564
569
|
// the foreground window would inflate TPS absurdly (1800+ t/s).
|
|
565
570
|
const frTokens = getDisplayOutputTokens(foregroundRequest)
|
|
566
571
|
const genStart = firstTokenTime ?? requestStartTime
|
|
567
|
-
|
|
572
|
+
// Window end: complete → frozen completeTime; streaming → now;
|
|
573
|
+
// open but silent (e.g. a lost completion event) → last observed
|
|
574
|
+
// activity, so the reading freezes instead of decaying in real time
|
|
575
|
+
// while idle.
|
|
576
|
+
const foregroundActive = foregroundRequest?.isStreaming ?? false
|
|
577
|
+
const foregroundLastActivity = foregroundRequest?.lastDeltaTime ?? null
|
|
578
|
+
const genEnd = isComplete
|
|
579
|
+
? completeTime
|
|
580
|
+
: foregroundActive || foregroundLastActivity === null ? now : foregroundLastActivity
|
|
568
581
|
const saneStart = genStart !== null && Number.isFinite(genStart) && genStart >= 0 && genStart <= now
|
|
569
582
|
const saneEnd = genEnd !== null && Number.isFinite(genEnd) && genEnd >= 0 && genEnd <= now + 60_000
|
|
570
583
|
if (frTokens > 0 && saneStart && saneEnd && genEnd > genStart) {
|
package/src/tui-v2.tsx
CHANGED
|
@@ -127,15 +127,27 @@ function str(value: unknown): string {
|
|
|
127
127
|
/**
|
|
128
128
|
* `session.execution.started` = "the user's prompt was admitted and work
|
|
129
129
|
* started" — the V2 equivalent of V1's `session.status -> busy`, which drives
|
|
130
|
-
* turn start and session timing.
|
|
130
|
+
* turn start and session timing. Exported for tests.
|
|
131
131
|
*/
|
|
132
|
-
function
|
|
132
|
+
export function translateExecutionStartedToBusy(event: V2EventLike): unknown {
|
|
133
133
|
const data = isRecord(event.data) ? event.data : {}
|
|
134
134
|
const sessionID = str(data.sessionID)
|
|
135
135
|
if (!sessionID) return null
|
|
136
136
|
return { type: "session.status", properties: { sessionID, status: { type: "busy" } } }
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Turn-settled execution maps to the V1 `session.idle` shape that drives
|
|
141
|
+
* request completion. Used as a fallback next to the native V2
|
|
142
|
+
* `session.idle`, which some flows never emit. Exported for tests.
|
|
143
|
+
*/
|
|
144
|
+
export function translateExecutionSettledToIdle(event: V2EventLike): unknown {
|
|
145
|
+
const data = isRecord(event.data) ? event.data : {}
|
|
146
|
+
const sessionID = str(data.sessionID)
|
|
147
|
+
if (!sessionID) return null
|
|
148
|
+
return { type: "session.idle", properties: { sessionID } }
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
function eventSessionID(event: unknown): string {
|
|
140
152
|
if (!isRecord(event)) return ""
|
|
141
153
|
return str(isRecord(event.data) ? event.data.sessionID : undefined)
|
|
@@ -164,34 +176,73 @@ function createV1EventShim(ctx: V2Context, holder: CollectorHolder): MetricsEven
|
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
const subscribeV2 = (
|
|
180
|
+
v2Type: string,
|
|
181
|
+
subscribee: (event: unknown) => void,
|
|
182
|
+
): (() => void) => {
|
|
183
|
+
try {
|
|
184
|
+
const unsub = ctx.data.on(v2Type, (event: unknown) => {
|
|
185
|
+
try {
|
|
186
|
+
subscribee(event)
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// One malformed event must never tear down the handler chain.
|
|
189
|
+
log(`v2 event dispatch failed: ${String(error)}`)
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
return typeof unsub === "function" ? unsub : () => {}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
log(`v2 event subscribe failed (${v2Type}): ${String(error)}`)
|
|
195
|
+
return () => {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
167
199
|
return {
|
|
168
200
|
event: {
|
|
169
201
|
on(type: string, handler: (event: unknown) => void): () => void {
|
|
170
202
|
// V1 hosts subscribe `.1`/`.2` delivery variants; V2 has none.
|
|
171
203
|
if (/\.1$|\.2$/.test(type)) return () => {}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const translated = translateExecutionStarted(event as V2EventLike)
|
|
180
|
-
if (translated !== null) handler(translated)
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
syncFamily(eventSessionID(event))
|
|
184
|
-
handler(event)
|
|
185
|
-
} catch (error) {
|
|
186
|
-
// One malformed event must never tear down the handler chain.
|
|
187
|
-
log(`v2 event dispatch failed: ${String(error)}`)
|
|
188
|
-
}
|
|
204
|
+
// V1 session lifecycle has no same-name V2 event: synthesize
|
|
205
|
+
// busy from execution.started. (Without this, turns, session
|
|
206
|
+
// timing, and completion never start on V2.)
|
|
207
|
+
if (type === "session.status") {
|
|
208
|
+
return subscribeV2("session.execution.started", (event) => {
|
|
209
|
+
const translated = translateExecutionStartedToBusy(event as V2EventLike)
|
|
210
|
+
if (translated !== null) handler(translated)
|
|
189
211
|
})
|
|
190
|
-
} catch (error) {
|
|
191
|
-
log(`v2 event subscribe failed (${type}): ${String(error)}`)
|
|
192
|
-
return () => {}
|
|
193
212
|
}
|
|
194
|
-
|
|
213
|
+
// Completion: native V2 session.idle where emitted, plus the
|
|
214
|
+
// execution-settled events as fallback — some flows never emit
|
|
215
|
+
// session.idle, which used to leave the turn/request/timing
|
|
216
|
+
// open forever (TPS decaying in real time while idle).
|
|
217
|
+
// completeRequest is idempotent, so a doubled signal is safe.
|
|
218
|
+
if (type === "session.idle") {
|
|
219
|
+
const unsubs: Array<() => void> = [
|
|
220
|
+
subscribeV2("session.idle", handler),
|
|
221
|
+
]
|
|
222
|
+
for (const settled of [
|
|
223
|
+
"session.execution.succeeded",
|
|
224
|
+
"session.execution.failed",
|
|
225
|
+
"session.execution.interrupted",
|
|
226
|
+
] as const) {
|
|
227
|
+
unsubs.push(subscribeV2(settled, (event) => {
|
|
228
|
+
const translated = translateExecutionSettledToIdle(event as V2EventLike)
|
|
229
|
+
if (translated !== null) handler(translated)
|
|
230
|
+
}))
|
|
231
|
+
}
|
|
232
|
+
return () => {
|
|
233
|
+
for (const unsub of unsubs) {
|
|
234
|
+
try {
|
|
235
|
+
unsub()
|
|
236
|
+
} catch { /* ignore */ }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const v2Type = V2_EVENT_MAP[type]
|
|
241
|
+
if (!v2Type) return () => {}
|
|
242
|
+
return subscribeV2(v2Type, (event) => {
|
|
243
|
+
syncFamily(eventSessionID(event))
|
|
244
|
+
handler(event)
|
|
245
|
+
})
|
|
195
246
|
},
|
|
196
247
|
},
|
|
197
248
|
}
|