oc-metricboard 0.1.17 → 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.
@@ -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/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;
@@ -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
@@ -81,7 +81,14 @@ export function translateExecutionStartedToBusy(event) {
81
81
  const sessionID = str(data.sessionID);
82
82
  if (!sessionID)
83
83
  return null;
84
- return { type: "session.status", properties: { sessionID, status: { type: "busy" } } };
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
+ };
85
92
  }
86
93
  /**
87
94
  * Turn-settled execution maps to the V1 `session.idle` shape that drives
@@ -93,7 +100,12 @@ export function translateExecutionSettledToIdle(event) {
93
100
  const sessionID = str(data.sessionID);
94
101
  if (!sessionID)
95
102
  return null;
96
- return { type: "session.idle", properties: { sessionID } };
103
+ const created = typeof event.created === "number" ? event.created : undefined;
104
+ return {
105
+ type: "session.idle",
106
+ ...(created !== undefined ? { created } : {}),
107
+ properties: { sessionID },
108
+ };
97
109
  }
98
110
  function eventSessionID(event) {
99
111
  if (!isRecord(event))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-metricboard",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "OpenCode TUI sidebar plugin showing real-time LLM metrics (TPS, TTFT, tokens) with sub-agent × model breakdown",
5
5
  "keywords": [
6
6
  "opencode",
@@ -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/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
@@ -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
@@ -133,7 +133,14 @@ 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
- return { type: "session.status", properties: { sessionID, status: { type: "busy" } } }
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
+ }
137
144
  }
138
145
 
139
146
  /**
@@ -145,7 +152,12 @@ export function translateExecutionSettledToIdle(event: V2EventLike): unknown {
145
152
  const data = isRecord(event.data) ? event.data : {}
146
153
  const sessionID = str(data.sessionID)
147
154
  if (!sessionID) return null
148
- return { type: "session.idle", properties: { sessionID } }
155
+ const created = typeof event.created === "number" ? event.created : undefined
156
+ return {
157
+ type: "session.idle",
158
+ ...(created !== undefined ? { created } : {}),
159
+ properties: { sessionID },
160
+ }
149
161
  }
150
162
 
151
163
  function eventSessionID(event: unknown): string {