oc-metricboard 0.1.16 → 0.1.18
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/assistant-progress.js +2 -2
- package/dist/collector.js +15 -2
- package/dist/event-bus.js +32 -0
- package/dist/event-handlers.js +11 -11
- package/dist/tui-v2.js +87 -26
- package/package.json +1 -1
- package/src/assistant-progress.ts +2 -2
- package/src/collector.ts +15 -2
- package/src/event-bus.ts +30 -0
- package/src/event-handlers.ts +11 -11
- package/src/tui-v2.tsx +87 -24
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { estimateTokens } from "./metrics";
|
|
2
2
|
import { parseAssistantTextDelta } from "./event-shapes";
|
|
3
3
|
import { currentRequest } from "./request-state";
|
|
4
|
-
import { eventAggregateID, eventID, eventProperties, stringEventProperty } from "./event-bus";
|
|
4
|
+
import { eventAggregateID, eventID, eventPerfTime, eventProperties, stringEventProperty } from "./event-bus";
|
|
5
5
|
import { recordLiveTokens } from "./live-speed";
|
|
6
6
|
import { ensureTurn, recordTurnFirstToken } from "./turn-state";
|
|
7
7
|
function partEstimateKey(sessionID, messageID, partID) {
|
|
@@ -51,7 +51,7 @@ export function applyAssistantDelta(ctx, event) {
|
|
|
51
51
|
sessionAliases.add(aggregateID);
|
|
52
52
|
state.sessionAliases.set(text.sessionID, sessionAliases);
|
|
53
53
|
}
|
|
54
|
-
const now = performance.now();
|
|
54
|
+
const now = eventPerfTime(event, performance.now());
|
|
55
55
|
actions.startSessionTiming(text.sessionID, now);
|
|
56
56
|
const key = partEstimateKey(text.sessionID, text.messageID, text.partID);
|
|
57
57
|
const deltaTokens = estimateTokens(text.delta, ctx.config.estimationRatio);
|
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/event-bus.js
CHANGED
|
@@ -51,6 +51,38 @@ export function statusType(event) {
|
|
|
51
51
|
return "";
|
|
52
52
|
return typeof status.type === "string" ? status.type : "";
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Host-stamped wall-clock time of an event (V2 envelope `created`, epoch ms),
|
|
56
|
+
* or null when absent/implausible. V1 events carry no timestamp at all.
|
|
57
|
+
*/
|
|
58
|
+
export function eventCreatedWallTime(event) {
|
|
59
|
+
if (!isRecord(event))
|
|
60
|
+
return null;
|
|
61
|
+
const created = event.created;
|
|
62
|
+
if (typeof created !== "number" || !Number.isFinite(created) || created <= 0)
|
|
63
|
+
return null;
|
|
64
|
+
// Guard against a foreign epoch or clock skew: anything more than a day out
|
|
65
|
+
// is not usable as event time.
|
|
66
|
+
if (Math.abs(created - Date.now()) > 86_400_000)
|
|
67
|
+
return null;
|
|
68
|
+
return created;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Performance-base timestamp for an event. Prefers the host stamp converted
|
|
72
|
+
* into this process's performance.now() base; falls back to `now`.
|
|
73
|
+
*
|
|
74
|
+
* WHY: the V2 host delivers events in batches — a whole turn's deltas can
|
|
75
|
+
* arrive in one flush long after generation. Stamping flush time
|
|
76
|
+
* (performance.now() at handling) collapses the generation window to
|
|
77
|
+
* milliseconds and inflates frozen TPS absurdly (e.g. 259 tokens / 6ms =
|
|
78
|
+
* 42627 t/s). V1 events have no `created`, so this is a no-op for V1.
|
|
79
|
+
*/
|
|
80
|
+
export function eventPerfTime(event, now) {
|
|
81
|
+
const wall = eventCreatedWallTime(event);
|
|
82
|
+
if (wall === null)
|
|
83
|
+
return now;
|
|
84
|
+
return now - Math.max(0, Date.now() - wall);
|
|
85
|
+
}
|
|
54
86
|
export function isMetricsEventApi(value) {
|
|
55
87
|
if (!isRecord(value))
|
|
56
88
|
return false;
|
package/dist/event-handlers.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createFreshMetrics, estimateTokens } from "./metrics";
|
|
|
3
3
|
import { parseAssistantMessage, parseAssistantProgressPart, parseAssistantStepEnded, parseAssistantStepStarted, parseSessionInfo, parseTextPart, parseUserMessage, } from "./event-shapes";
|
|
4
4
|
import { applyAssistantTokens, applySessionModel, hasPositiveAssistantTokens, mergeAssistantModel } from "./request-updates";
|
|
5
5
|
import { completeRequest, currentRequest } from "./request-state";
|
|
6
|
-
import { eventAggregateID, eventID, eventProperties, eventProperty, statusType, stringEventProperty } from "./event-bus";
|
|
6
|
+
import { eventAggregateID, eventID, eventPerfTime, eventProperties, eventProperty, statusType, stringEventProperty } from "./event-bus";
|
|
7
7
|
import { clearLiveSpeed, resetLiveSpeed } from "./live-speed";
|
|
8
8
|
import { ensureTurn, retireRequestIntoTurn, startTurn } from "./turn-state";
|
|
9
9
|
export function registerEventHandlers(ctx) {
|
|
@@ -86,7 +86,7 @@ export function registerEventHandlers(ctx) {
|
|
|
86
86
|
const type = statusType(event);
|
|
87
87
|
log(`session.status: ${sessionID} -> ${type}`);
|
|
88
88
|
if (type === "busy") {
|
|
89
|
-
const now = performance.now();
|
|
89
|
+
const now = eventPerfTime(event, performance.now());
|
|
90
90
|
actions.startSessionTiming(sessionID, now);
|
|
91
91
|
actions.clearHoldTimer(sessionID);
|
|
92
92
|
const existingTurn = state.turns.get(sessionID);
|
|
@@ -108,7 +108,7 @@ export function registerEventHandlers(ctx) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
if (type === "idle") {
|
|
111
|
-
completeRequest({ state, actions, sessionID, now: performance.now(), holdDurationMs: config.holdDurationMs, log });
|
|
111
|
+
completeRequest({ state, actions, sessionID, now: eventPerfTime(event, performance.now()), holdDurationMs: config.holdDurationMs, log });
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
114
|
on("session.idle", (event) => {
|
|
@@ -116,7 +116,7 @@ export function registerEventHandlers(ctx) {
|
|
|
116
116
|
if (sessionID.length === 0)
|
|
117
117
|
return;
|
|
118
118
|
recordEventAlias(event, sessionID);
|
|
119
|
-
completeRequest({ state, actions, sessionID, now: performance.now(), holdDurationMs: config.holdDurationMs, log });
|
|
119
|
+
completeRequest({ state, actions, sessionID, now: eventPerfTime(event, performance.now()), holdDurationMs: config.holdDurationMs, log });
|
|
120
120
|
});
|
|
121
121
|
// ── message.part.delta ──
|
|
122
122
|
on("message.part.delta", (event) => {
|
|
@@ -130,8 +130,8 @@ export function registerEventHandlers(ctx) {
|
|
|
130
130
|
recordEventAlias(event, sessionID);
|
|
131
131
|
if (field !== "text")
|
|
132
132
|
return;
|
|
133
|
-
actions.startSessionTiming(sessionID, performance.now());
|
|
134
|
-
const now = performance.now();
|
|
133
|
+
actions.startSessionTiming(sessionID, eventPerfTime(event, performance.now()));
|
|
134
|
+
const now = eventPerfTime(event, performance.now());
|
|
135
135
|
ensureTurn(state.turns, sessionID, now);
|
|
136
136
|
applyAssistantPartDelta(ctx, sessionID, messageID, partID, delta, now);
|
|
137
137
|
});
|
|
@@ -140,7 +140,7 @@ export function registerEventHandlers(ctx) {
|
|
|
140
140
|
if (!step)
|
|
141
141
|
return;
|
|
142
142
|
recordEventAlias(event, step.sessionID);
|
|
143
|
-
const now = performance.now();
|
|
143
|
+
const now = eventPerfTime(event, performance.now());
|
|
144
144
|
actions.startSessionTiming(step.sessionID, now);
|
|
145
145
|
const existingTurn = state.turns.get(step.sessionID);
|
|
146
146
|
if (!existingTurn || existingTurn.isComplete)
|
|
@@ -164,7 +164,7 @@ export function registerEventHandlers(ctx) {
|
|
|
164
164
|
if (!step)
|
|
165
165
|
return;
|
|
166
166
|
recordEventAlias(event, step.sessionID);
|
|
167
|
-
const now = performance.now();
|
|
167
|
+
const now = eventPerfTime(event, performance.now());
|
|
168
168
|
actions.startSessionTiming(step.sessionID, now);
|
|
169
169
|
const turn = ensureTurn(state.turns, step.sessionID, now);
|
|
170
170
|
const existing = state.requests.get(step.sessionID);
|
|
@@ -205,7 +205,7 @@ export function registerEventHandlers(ctx) {
|
|
|
205
205
|
recordEventAlias(event, sessionID);
|
|
206
206
|
const rawPart = eventProperty(event, "part");
|
|
207
207
|
const part = parseTextPart(rawPart);
|
|
208
|
-
const now = performance.now();
|
|
208
|
+
const now = eventPerfTime(event, performance.now());
|
|
209
209
|
const msgIds = state.userMessageIds.get(sessionID);
|
|
210
210
|
if (part && msgIds?.has(part.messageID)) {
|
|
211
211
|
const current = state.requests.get(sessionID);
|
|
@@ -235,7 +235,7 @@ export function registerEventHandlers(ctx) {
|
|
|
235
235
|
const nextModel = mergeAssistantModel(state.sessionModels.get(sessionID), assistant);
|
|
236
236
|
if (nextModel)
|
|
237
237
|
state.sessionModels.set(sessionID, nextModel);
|
|
238
|
-
const now = performance.now();
|
|
238
|
+
const now = eventPerfTime(event, performance.now());
|
|
239
239
|
const tokens = assistant.tokens;
|
|
240
240
|
const existing = state.requests.get(sessionID);
|
|
241
241
|
const turn = ensureTurn(state.turns, sessionID, now);
|
|
@@ -278,7 +278,7 @@ export function registerEventHandlers(ctx) {
|
|
|
278
278
|
if (userMsgID && state.userMessageIds.get(sessionID)?.has(userMsgID)) {
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
|
-
const now = performance.now();
|
|
281
|
+
const now = eventPerfTime(event, performance.now());
|
|
282
282
|
const previous = state.requests.get(sessionID);
|
|
283
283
|
const previousTurn = state.turns.get(sessionID);
|
|
284
284
|
if (previous && previousTurn)
|
package/dist/tui-v2.js
CHANGED
|
@@ -74,14 +74,38 @@ 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
|
+
// Preserve the host stamp so downstream timing uses generation time,
|
|
85
|
+
// not the (possibly batched) delivery time.
|
|
86
|
+
const created = typeof event.created === "number" ? event.created : undefined;
|
|
87
|
+
return {
|
|
88
|
+
type: "session.status",
|
|
89
|
+
...(created !== undefined ? { created } : {}),
|
|
90
|
+
properties: { sessionID, status: { type: "busy" } },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Turn-settled execution maps to the V1 `session.idle` shape that drives
|
|
95
|
+
* request completion. Used as a fallback next to the native V2
|
|
96
|
+
* `session.idle`, which some flows never emit. Exported for tests.
|
|
97
|
+
*/
|
|
98
|
+
export function translateExecutionSettledToIdle(event) {
|
|
99
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
100
|
+
const sessionID = str(data.sessionID);
|
|
101
|
+
if (!sessionID)
|
|
102
|
+
return null;
|
|
103
|
+
const created = typeof event.created === "number" ? event.created : undefined;
|
|
104
|
+
return {
|
|
105
|
+
type: "session.idle",
|
|
106
|
+
...(created !== undefined ? { created } : {}),
|
|
107
|
+
properties: { sessionID },
|
|
108
|
+
};
|
|
85
109
|
}
|
|
86
110
|
function eventSessionID(event) {
|
|
87
111
|
if (!isRecord(event))
|
|
@@ -110,39 +134,76 @@ function createV1EventShim(ctx, holder) {
|
|
|
110
134
|
// Family attribution is best-effort; the root session works without it.
|
|
111
135
|
}
|
|
112
136
|
};
|
|
137
|
+
const subscribeV2 = (v2Type, subscribee) => {
|
|
138
|
+
try {
|
|
139
|
+
const unsub = ctx.data.on(v2Type, (event) => {
|
|
140
|
+
try {
|
|
141
|
+
subscribee(event);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// One malformed event must never tear down the handler chain.
|
|
145
|
+
log(`v2 event dispatch failed: ${String(error)}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return typeof unsub === "function" ? unsub : () => { };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
log(`v2 event subscribe failed (${v2Type}): ${String(error)}`);
|
|
152
|
+
return () => { };
|
|
153
|
+
}
|
|
154
|
+
};
|
|
113
155
|
return {
|
|
114
156
|
event: {
|
|
115
157
|
on(type, handler) {
|
|
116
158
|
// V1 hosts subscribe `.1`/`.2` delivery variants; V2 has none.
|
|
117
159
|
if (/\.1$|\.2$/.test(type))
|
|
118
160
|
return () => { };
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
// V1 session lifecycle has no same-name V2 event: synthesize
|
|
162
|
+
// busy from execution.started. (Without this, turns, session
|
|
163
|
+
// timing, and completion never start on V2.)
|
|
164
|
+
if (type === "session.status") {
|
|
165
|
+
return subscribeV2("session.execution.started", (event) => {
|
|
166
|
+
const translated = translateExecutionStartedToBusy(event);
|
|
167
|
+
if (translated !== null)
|
|
168
|
+
handler(translated);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Completion: native V2 session.idle where emitted, plus the
|
|
172
|
+
// execution-settled events as fallback — some flows never emit
|
|
173
|
+
// session.idle, which used to leave the turn/request/timing
|
|
174
|
+
// open forever (TPS decaying in real time while idle).
|
|
175
|
+
// completeRequest is idempotent, so a doubled signal is safe.
|
|
176
|
+
if (type === "session.idle") {
|
|
177
|
+
const unsubs = [
|
|
178
|
+
subscribeV2("session.idle", handler),
|
|
179
|
+
];
|
|
180
|
+
for (const settled of [
|
|
181
|
+
"session.execution.succeeded",
|
|
182
|
+
"session.execution.failed",
|
|
183
|
+
"session.execution.interrupted",
|
|
184
|
+
]) {
|
|
185
|
+
unsubs.push(subscribeV2(settled, (event) => {
|
|
186
|
+
const translated = translateExecutionSettledToIdle(event);
|
|
187
|
+
if (translated !== null)
|
|
188
|
+
handler(translated);
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
return () => {
|
|
192
|
+
for (const unsub of unsubs) {
|
|
193
|
+
try {
|
|
194
|
+
unsub();
|
|
131
195
|
}
|
|
132
|
-
|
|
133
|
-
handler(event);
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
// One malformed event must never tear down the handler chain.
|
|
137
|
-
log(`v2 event dispatch failed: ${String(error)}`);
|
|
196
|
+
catch { /* ignore */ }
|
|
138
197
|
}
|
|
139
|
-
}
|
|
198
|
+
};
|
|
140
199
|
}
|
|
141
|
-
|
|
142
|
-
|
|
200
|
+
const v2Type = V2_EVENT_MAP[type];
|
|
201
|
+
if (!v2Type)
|
|
143
202
|
return () => { };
|
|
144
|
-
|
|
145
|
-
|
|
203
|
+
return subscribeV2(v2Type, (event) => {
|
|
204
|
+
syncFamily(eventSessionID(event));
|
|
205
|
+
handler(event);
|
|
206
|
+
});
|
|
146
207
|
},
|
|
147
208
|
},
|
|
148
209
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { estimateTokens } from "./metrics"
|
|
2
2
|
import { parseAssistantTextDelta } from "./event-shapes"
|
|
3
3
|
import { currentRequest } from "./request-state"
|
|
4
|
-
import { eventAggregateID, eventID, eventProperties, stringEventProperty } from "./event-bus"
|
|
4
|
+
import { eventAggregateID, eventID, eventPerfTime, eventProperties, stringEventProperty } from "./event-bus"
|
|
5
5
|
import type { EventHandlerContext } from "./collector-state"
|
|
6
6
|
import { recordLiveTokens } from "./live-speed"
|
|
7
7
|
import { ensureTurn, recordTurnFirstToken } from "./turn-state"
|
|
@@ -78,7 +78,7 @@ export function applyAssistantDelta(ctx: EventHandlerContext, event: unknown): v
|
|
|
78
78
|
sessionAliases.add(aggregateID)
|
|
79
79
|
state.sessionAliases.set(text.sessionID, sessionAliases)
|
|
80
80
|
}
|
|
81
|
-
const now = performance.now()
|
|
81
|
+
const now = eventPerfTime(event, performance.now())
|
|
82
82
|
actions.startSessionTiming(text.sessionID, now)
|
|
83
83
|
const key = partEstimateKey(text.sessionID, text.messageID, text.partID)
|
|
84
84
|
const deltaTokens = estimateTokens(text.delta, ctx.config.estimationRatio)
|
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/event-bus.ts
CHANGED
|
@@ -54,6 +54,36 @@ export function statusType(event: unknown): string {
|
|
|
54
54
|
return typeof status.type === "string" ? status.type : ""
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Host-stamped wall-clock time of an event (V2 envelope `created`, epoch ms),
|
|
59
|
+
* or null when absent/implausible. V1 events carry no timestamp at all.
|
|
60
|
+
*/
|
|
61
|
+
export function eventCreatedWallTime(event: unknown): number | null {
|
|
62
|
+
if (!isRecord(event)) return null
|
|
63
|
+
const created = event.created
|
|
64
|
+
if (typeof created !== "number" || !Number.isFinite(created) || created <= 0) return null
|
|
65
|
+
// Guard against a foreign epoch or clock skew: anything more than a day out
|
|
66
|
+
// is not usable as event time.
|
|
67
|
+
if (Math.abs(created - Date.now()) > 86_400_000) return null
|
|
68
|
+
return created
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Performance-base timestamp for an event. Prefers the host stamp converted
|
|
73
|
+
* into this process's performance.now() base; falls back to `now`.
|
|
74
|
+
*
|
|
75
|
+
* WHY: the V2 host delivers events in batches — a whole turn's deltas can
|
|
76
|
+
* arrive in one flush long after generation. Stamping flush time
|
|
77
|
+
* (performance.now() at handling) collapses the generation window to
|
|
78
|
+
* milliseconds and inflates frozen TPS absurdly (e.g. 259 tokens / 6ms =
|
|
79
|
+
* 42627 t/s). V1 events have no `created`, so this is a no-op for V1.
|
|
80
|
+
*/
|
|
81
|
+
export function eventPerfTime(event: unknown, now: number): number {
|
|
82
|
+
const wall = eventCreatedWallTime(event)
|
|
83
|
+
if (wall === null) return now
|
|
84
|
+
return now - Math.max(0, Date.now() - wall)
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
export function isMetricsEventApi(value: unknown): value is MetricsEventApi {
|
|
58
88
|
if (!isRecord(value)) return false
|
|
59
89
|
const event = value.event
|
package/src/event-handlers.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./event-shapes"
|
|
12
12
|
import { applyAssistantTokens, applySessionModel, hasPositiveAssistantTokens, mergeAssistantModel } from "./request-updates"
|
|
13
13
|
import { completeRequest, currentRequest } from "./request-state"
|
|
14
|
-
import { eventAggregateID, eventID, eventProperties, eventProperty, statusType, stringEventProperty } from "./event-bus"
|
|
14
|
+
import { eventAggregateID, eventID, eventPerfTime, eventProperties, eventProperty, statusType, stringEventProperty } from "./event-bus"
|
|
15
15
|
import type { EventHandlerContext } from "./collector-state"
|
|
16
16
|
import { clearLiveSpeed, resetLiveSpeed } from "./live-speed"
|
|
17
17
|
import { ensureTurn, retireRequestIntoTurn, startTurn } from "./turn-state"
|
|
@@ -97,7 +97,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
97
97
|
log(`session.status: ${sessionID} -> ${type}`)
|
|
98
98
|
|
|
99
99
|
if (type === "busy") {
|
|
100
|
-
const now = performance.now()
|
|
100
|
+
const now = eventPerfTime(event, performance.now())
|
|
101
101
|
actions.startSessionTiming(sessionID, now)
|
|
102
102
|
actions.clearHoldTimer(sessionID)
|
|
103
103
|
const existingTurn = state.turns.get(sessionID)
|
|
@@ -118,7 +118,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (type === "idle") {
|
|
121
|
-
completeRequest({ state, actions, sessionID, now: performance.now(), holdDurationMs: config.holdDurationMs, log })
|
|
121
|
+
completeRequest({ state, actions, sessionID, now: eventPerfTime(event, performance.now()), holdDurationMs: config.holdDurationMs, log })
|
|
122
122
|
}
|
|
123
123
|
})
|
|
124
124
|
|
|
@@ -126,7 +126,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
126
126
|
const sessionID = stringEventProperty(event, "sessionID")
|
|
127
127
|
if (sessionID.length === 0) return
|
|
128
128
|
recordEventAlias(event, sessionID)
|
|
129
|
-
completeRequest({ state, actions, sessionID, now: performance.now(), holdDurationMs: config.holdDurationMs, log })
|
|
129
|
+
completeRequest({ state, actions, sessionID, now: eventPerfTime(event, performance.now()), holdDurationMs: config.holdDurationMs, log })
|
|
130
130
|
})
|
|
131
131
|
|
|
132
132
|
// ── message.part.delta ──
|
|
@@ -139,9 +139,9 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
139
139
|
if (sessionID.length === 0 || messageID.length === 0) return
|
|
140
140
|
recordEventAlias(event, sessionID)
|
|
141
141
|
if (field !== "text") return
|
|
142
|
-
actions.startSessionTiming(sessionID, performance.now())
|
|
142
|
+
actions.startSessionTiming(sessionID, eventPerfTime(event, performance.now()))
|
|
143
143
|
|
|
144
|
-
const now = performance.now()
|
|
144
|
+
const now = eventPerfTime(event, performance.now())
|
|
145
145
|
ensureTurn(state.turns, sessionID, now)
|
|
146
146
|
applyAssistantPartDelta(ctx, sessionID, messageID, partID, delta, now)
|
|
147
147
|
})
|
|
@@ -150,7 +150,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
150
150
|
const step = parseAssistantStepStarted(eventProperties(event), eventID(event))
|
|
151
151
|
if (!step) return
|
|
152
152
|
recordEventAlias(event, step.sessionID)
|
|
153
|
-
const now = performance.now()
|
|
153
|
+
const now = eventPerfTime(event, performance.now())
|
|
154
154
|
actions.startSessionTiming(step.sessionID, now)
|
|
155
155
|
const existingTurn = state.turns.get(step.sessionID)
|
|
156
156
|
if (!existingTurn || existingTurn.isComplete) startTurn(state.turns, step.sessionID, now)
|
|
@@ -175,7 +175,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
175
175
|
const step = parseAssistantStepEnded(eventProperties(event), fallbackMessageID)
|
|
176
176
|
if (!step) return
|
|
177
177
|
recordEventAlias(event, step.sessionID)
|
|
178
|
-
const now = performance.now()
|
|
178
|
+
const now = eventPerfTime(event, performance.now())
|
|
179
179
|
actions.startSessionTiming(step.sessionID, now)
|
|
180
180
|
const turn = ensureTurn(state.turns, step.sessionID, now)
|
|
181
181
|
const existing = state.requests.get(step.sessionID)
|
|
@@ -215,7 +215,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
215
215
|
recordEventAlias(event, sessionID)
|
|
216
216
|
const rawPart = eventProperty(event, "part")
|
|
217
217
|
const part = parseTextPart(rawPart)
|
|
218
|
-
const now = performance.now()
|
|
218
|
+
const now = eventPerfTime(event, performance.now())
|
|
219
219
|
const msgIds = state.userMessageIds.get(sessionID)
|
|
220
220
|
if (part && msgIds?.has(part.messageID)) {
|
|
221
221
|
const current = state.requests.get(sessionID)
|
|
@@ -246,7 +246,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
246
246
|
const nextModel = mergeAssistantModel(state.sessionModels.get(sessionID), assistant)
|
|
247
247
|
if (nextModel) state.sessionModels.set(sessionID, nextModel)
|
|
248
248
|
|
|
249
|
-
const now = performance.now()
|
|
249
|
+
const now = eventPerfTime(event, performance.now())
|
|
250
250
|
const tokens = assistant.tokens
|
|
251
251
|
const existing = state.requests.get(sessionID)
|
|
252
252
|
const turn = ensureTurn(state.turns, sessionID, now)
|
|
@@ -288,7 +288,7 @@ export function registerEventHandlers(ctx: EventHandlerContext): Array<() => voi
|
|
|
288
288
|
if (userMsgID && state.userMessageIds.get(sessionID)?.has(userMsgID)) {
|
|
289
289
|
return
|
|
290
290
|
}
|
|
291
|
-
const now = performance.now()
|
|
291
|
+
const now = eventPerfTime(event, performance.now())
|
|
292
292
|
const previous = state.requests.get(sessionID)
|
|
293
293
|
const previousTurn = state.turns.get(sessionID)
|
|
294
294
|
if (previous && previousTurn) retireRequestIntoTurn(previousTurn, previous)
|
package/src/tui-v2.tsx
CHANGED
|
@@ -127,13 +127,37 @@ 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
|
+
// Preserve the host stamp so downstream timing uses generation time,
|
|
137
|
+
// not the (possibly batched) delivery time.
|
|
138
|
+
const created = typeof event.created === "number" ? event.created : undefined
|
|
139
|
+
return {
|
|
140
|
+
type: "session.status",
|
|
141
|
+
...(created !== undefined ? { created } : {}),
|
|
142
|
+
properties: { sessionID, status: { type: "busy" } },
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Turn-settled execution maps to the V1 `session.idle` shape that drives
|
|
148
|
+
* request completion. Used as a fallback next to the native V2
|
|
149
|
+
* `session.idle`, which some flows never emit. Exported for tests.
|
|
150
|
+
*/
|
|
151
|
+
export function translateExecutionSettledToIdle(event: V2EventLike): unknown {
|
|
152
|
+
const data = isRecord(event.data) ? event.data : {}
|
|
153
|
+
const sessionID = str(data.sessionID)
|
|
154
|
+
if (!sessionID) return null
|
|
155
|
+
const created = typeof event.created === "number" ? event.created : undefined
|
|
156
|
+
return {
|
|
157
|
+
type: "session.idle",
|
|
158
|
+
...(created !== undefined ? { created } : {}),
|
|
159
|
+
properties: { sessionID },
|
|
160
|
+
}
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
function eventSessionID(event: unknown): string {
|
|
@@ -164,34 +188,73 @@ function createV1EventShim(ctx: V2Context, holder: CollectorHolder): MetricsEven
|
|
|
164
188
|
}
|
|
165
189
|
}
|
|
166
190
|
|
|
191
|
+
const subscribeV2 = (
|
|
192
|
+
v2Type: string,
|
|
193
|
+
subscribee: (event: unknown) => void,
|
|
194
|
+
): (() => void) => {
|
|
195
|
+
try {
|
|
196
|
+
const unsub = ctx.data.on(v2Type, (event: unknown) => {
|
|
197
|
+
try {
|
|
198
|
+
subscribee(event)
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// One malformed event must never tear down the handler chain.
|
|
201
|
+
log(`v2 event dispatch failed: ${String(error)}`)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
return typeof unsub === "function" ? unsub : () => {}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
log(`v2 event subscribe failed (${v2Type}): ${String(error)}`)
|
|
207
|
+
return () => {}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
167
211
|
return {
|
|
168
212
|
event: {
|
|
169
213
|
on(type: string, handler: (event: unknown) => void): () => void {
|
|
170
214
|
// V1 hosts subscribe `.1`/`.2` delivery variants; V2 has none.
|
|
171
215
|
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
|
-
}
|
|
216
|
+
// V1 session lifecycle has no same-name V2 event: synthesize
|
|
217
|
+
// busy from execution.started. (Without this, turns, session
|
|
218
|
+
// timing, and completion never start on V2.)
|
|
219
|
+
if (type === "session.status") {
|
|
220
|
+
return subscribeV2("session.execution.started", (event) => {
|
|
221
|
+
const translated = translateExecutionStartedToBusy(event as V2EventLike)
|
|
222
|
+
if (translated !== null) handler(translated)
|
|
189
223
|
})
|
|
190
|
-
} catch (error) {
|
|
191
|
-
log(`v2 event subscribe failed (${type}): ${String(error)}`)
|
|
192
|
-
return () => {}
|
|
193
224
|
}
|
|
194
|
-
|
|
225
|
+
// Completion: native V2 session.idle where emitted, plus the
|
|
226
|
+
// execution-settled events as fallback — some flows never emit
|
|
227
|
+
// session.idle, which used to leave the turn/request/timing
|
|
228
|
+
// open forever (TPS decaying in real time while idle).
|
|
229
|
+
// completeRequest is idempotent, so a doubled signal is safe.
|
|
230
|
+
if (type === "session.idle") {
|
|
231
|
+
const unsubs: Array<() => void> = [
|
|
232
|
+
subscribeV2("session.idle", handler),
|
|
233
|
+
]
|
|
234
|
+
for (const settled of [
|
|
235
|
+
"session.execution.succeeded",
|
|
236
|
+
"session.execution.failed",
|
|
237
|
+
"session.execution.interrupted",
|
|
238
|
+
] as const) {
|
|
239
|
+
unsubs.push(subscribeV2(settled, (event) => {
|
|
240
|
+
const translated = translateExecutionSettledToIdle(event as V2EventLike)
|
|
241
|
+
if (translated !== null) handler(translated)
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
return () => {
|
|
245
|
+
for (const unsub of unsubs) {
|
|
246
|
+
try {
|
|
247
|
+
unsub()
|
|
248
|
+
} catch { /* ignore */ }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const v2Type = V2_EVENT_MAP[type]
|
|
253
|
+
if (!v2Type) return () => {}
|
|
254
|
+
return subscribeV2(v2Type, (event) => {
|
|
255
|
+
syncFamily(eventSessionID(event))
|
|
256
|
+
handler(event)
|
|
257
|
+
})
|
|
195
258
|
},
|
|
196
259
|
},
|
|
197
260
|
}
|