oc-metricboard 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/collector.ts +17 -3
- package/src/session-hydration.ts +29 -9
package/package.json
CHANGED
package/src/collector.ts
CHANGED
|
@@ -303,14 +303,28 @@ export function createCollector(
|
|
|
303
303
|
return { rootID: requestedSessionID, ids: requestedIDs }
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
function aggregateByModel(
|
|
306
|
+
function aggregateByModel(
|
|
307
|
+
ids: readonly string[],
|
|
308
|
+
now: number,
|
|
309
|
+
scope: MetricsScope,
|
|
310
|
+
rootID: string,
|
|
311
|
+
foregroundTurnStart: number,
|
|
312
|
+
): ModelMetrics[] {
|
|
307
313
|
// Group requests by (modelID, providerID) — one row per distinct model
|
|
308
|
-
// across all sub-agent sessions, with a ×N session count.
|
|
314
|
+
// across all sub-agent sessions, with a ×N session count. Apply the same
|
|
315
|
+
// foreground-turn membership filter as the token totals: a session whose
|
|
316
|
+
// last turn predates the foreground turn (e.g. a model the user switched
|
|
317
|
+
// away from hours ago) would otherwise linger as a stale zombie row that
|
|
318
|
+
// is absent from the aggregate totals.
|
|
309
319
|
const modelGroups = new Map<string, RequestMetrics[]>()
|
|
310
320
|
|
|
311
321
|
for (const id of ids) {
|
|
312
322
|
const request = state.requests.get(id)
|
|
313
323
|
if (!request) continue
|
|
324
|
+
const turn = state.turns.get(id)
|
|
325
|
+
const belongsToForegroundTurn = id === rootID
|
|
326
|
+
|| Boolean(turn && (turn.turnStartTime >= foregroundTurnStart || !turn.isComplete))
|
|
327
|
+
if (!belongsToForegroundTurn) continue
|
|
314
328
|
|
|
315
329
|
// Key: modelID|providerID (model-grouped, not per-session)
|
|
316
330
|
const key = `${request.modelID}|${request.providerID}`
|
|
@@ -557,7 +571,7 @@ export function createCollector(
|
|
|
557
571
|
}
|
|
558
572
|
|
|
559
573
|
// NEW: Build per-model breakdown for tree scope
|
|
560
|
-
const modelBreakdown = scope === "tree" ? aggregateByModel(ids, now, scope) : []
|
|
574
|
+
const modelBreakdown = scope === "tree" ? aggregateByModel(ids, now, scope, rootID, foregroundTurnStart) : []
|
|
561
575
|
// Turn-level TTFT: opencode stamps step.started at the first token, so
|
|
562
576
|
// intra-turn steps cannot measure their own request start (they would
|
|
563
577
|
// collapse to ~0). Anchor TTFT to the turn's user message instead —
|
package/src/session-hydration.ts
CHANGED
|
@@ -138,9 +138,8 @@ async function observablePartTimes(api: HydrationApi, messageID: string, now: nu
|
|
|
138
138
|
return { first: toPerformanceTime(firstWall, now), last: toPerformanceTime(lastWall, now) }
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function hydrateSessionTiming(
|
|
141
|
+
export function hydrateSessionTiming(
|
|
142
142
|
rawMessages: readonly unknown[],
|
|
143
|
-
status: string,
|
|
144
143
|
now: number,
|
|
145
144
|
): SessionTiming {
|
|
146
145
|
const intervals: Array<{ start: number; end: number }> = []
|
|
@@ -183,10 +182,16 @@ function hydrateSessionTiming(
|
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
184
|
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
// Hydration NEVER leaves an open active interval. The session-status lookup
|
|
186
|
+
// is unreliable across host SDK dialects (may resolve to '' — see the turn
|
|
187
|
+
// completion fix), and an open interval anchored to a pre-process wall time
|
|
188
|
+
// becomes {start: negative, end: now} in performance-time: its span then
|
|
189
|
+
// includes the entire era before this TUI process existed, inflating the
|
|
190
|
+
// Session readout by the session's full age and growing in real time while
|
|
191
|
+
// idle. Activity accounting resumes exclusively via live busy/idle events.
|
|
192
|
+
if (groupStartWall !== null) closeGroup()
|
|
188
193
|
const elapsedMs = Math.round(intervals.reduce((total, interval) => total + Math.max(0, interval.end - interval.start), 0))
|
|
189
|
-
return { elapsedMs, activeSince, intervals }
|
|
194
|
+
return { elapsedMs, activeSince: null, intervals }
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
export interface HydrateSessionInput {
|
|
@@ -235,6 +240,7 @@ export async function hydrateSession(input: HydrateSessionInput): Promise<boolea
|
|
|
235
240
|
await input.api.client.session.status({ id: input.sessionID }),
|
|
236
241
|
input.sessionID,
|
|
237
242
|
))
|
|
243
|
+
log(`hydrate session=${input.sessionID.slice(0, 8)} status='${sessionStatus}'`)
|
|
238
244
|
const shouldReuseExisting = existing?.messageID === assistant.messageID
|
|
239
245
|
|
|
240
246
|
const current = shouldReuseExisting ? existing : createFreshMetrics(
|
|
@@ -287,7 +293,12 @@ export async function hydrateSession(input: HydrateSessionInput): Promise<boolea
|
|
|
287
293
|
applyAssistantTokens(stepMetrics, step.tokens)
|
|
288
294
|
retireRequestIntoTurn(turn, stepMetrics)
|
|
289
295
|
}
|
|
290
|
-
|
|
296
|
+
// Turn completion: the session-status lookup is unreliable across host SDK
|
|
297
|
+
// dialects (may resolve to an unrecognizable shape → status ''). The last
|
|
298
|
+
// assistant message's own completed flag is authoritative: if it finished,
|
|
299
|
+
// its turn is over. An in-flight resumed message (completed=false) keeps
|
|
300
|
+
// the turn open.
|
|
301
|
+
if (sessionStatus === "idle" || current.isComplete) completeTurn(turn, completedTime ?? input.now)
|
|
291
302
|
input.state.turns.set(input.sessionID, turn)
|
|
292
303
|
|
|
293
304
|
// Write-point race guard: the checks above ran before several awaits. By
|
|
@@ -310,9 +321,18 @@ export async function hydrateSession(input: HydrateSessionInput): Promise<boolea
|
|
|
310
321
|
input.state.sessionModels.set(input.sessionID, { modelID: assistant.modelID, providerID: assistant.providerID })
|
|
311
322
|
}
|
|
312
323
|
if (!input.state.sessionTimings.has(input.sessionID)) {
|
|
313
|
-
input.
|
|
314
|
-
|
|
315
|
-
|
|
324
|
+
const timing = hydrateSessionTiming(rawMessages, input.now)
|
|
325
|
+
input.state.sessionTimings.set(input.sessionID, timing)
|
|
326
|
+
// Debug: timing hydration summary — pinpoints where inflated Session
|
|
327
|
+
// elapsed values come from (initial value vs runtime accumulation).
|
|
328
|
+
const intervals = timing.intervals ?? []
|
|
329
|
+
const maxSpan = intervals.reduce((m, iv) => Math.max(m, iv.end - iv.start), 0)
|
|
330
|
+
const oldestStart = intervals.reduce((m, iv) => Math.min(m, iv.start), Number.POSITIVE_INFINITY)
|
|
331
|
+
log(
|
|
332
|
+
`timing hydrate session=${input.sessionID.slice(0, 8)} elapsed=${Math.round(timing.elapsedMs)}ms`
|
|
333
|
+
+ ` activeSinceAge=${timing.activeSince === null ? "null" : `${Math.round(input.now - timing.activeSince)}ms`}`
|
|
334
|
+
+ ` intervals=${intervals.length} maxSpan=${Math.round(maxSpan)}ms`
|
|
335
|
+
+ ` oldestStartAge=${Number.isFinite(oldestStart) ? `${Math.round(input.now - oldestStart)}ms` : "n/a"}`,
|
|
316
336
|
)
|
|
317
337
|
}
|
|
318
338
|
return true
|