diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 2b6365dd..29ccd5ec 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -8547,7 +8547,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec "const fullSource = reportJson || source;", "const objectOrNull = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;", "const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, sampleCount: v.sampleCount ?? null, loadingSamples: v.loadingSamples ?? null, maxLoadingCount: v.maxLoadingCount ?? null, loadingOwnerCount: v.loadingOwnerCount ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, promptTextHash: clip(v.promptTextHash, 80) }; };", - "const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };", + "const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };", "const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, nextHopProtocol: clip(v.nextHopProtocol, 24), timingStatus: clip(v.timingStatus, 16), serverTimingNames: Array.isArray(v.serverTimingNames) ? v.serverTimingNames.slice(0, 4).map((x) => clip(x, 32)) : [], otelTraceId: clip(v.otelTraceId, 32) }; };", "const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, budgetMs: v.budgetMs ?? null, overBudgetCount: v.overBudgetCount ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: Array.isArray(v.slowSamples) ? v.slowSamples.slice(0, 3).map(slimSlowSample) : [] }; };", "const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };", @@ -8559,7 +8559,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec "const slimRunnerError = (item) => { const v = objectOrNull(item) || {}; const readiness = objectOrNull(v.lastReadiness); return { ts: v.ts ?? null, type: clip(v.type, 32), commandId: clip(v.commandId, 80), sampleSeq: v.sampleSeq ?? null, message: clip(v.message, 240), retry: clip(v.retry, 24), retryExhausted: v.retryExhausted === true, lastError: clip(v.lastError, 180), attemptCount: v.attemptCount ?? null, lastFailureKind: clip(v.lastFailureKind, 48), lastReadinessReason: clip(v.lastReadinessReason, 48), lastReadiness: readiness ? { reason: clip(readiness.reason, 48), path: clip(readiness.path, 96), readyState: clip(readiness.readyState, 24), workbenchShellVisible: readiness.workbenchShellVisible === true, sessionCreateVisible: readiness.sessionCreateVisible === true, commandInputPresent: readiness.commandInputPresent === true, activeTabPresent: readiness.activeTabPresent === true, warningPresent: readiness.warningPresent === true, loginVisible: readiness.loginVisible === true, bodyTextHash: clip(readiness.bodyTextHash, 80) } : null }; };", "const slimRunnerErrorFromJsonl = (item) => { const v = objectOrNull(item) || {}; const error = objectOrNull(v.error) || {}; const attempts = Array.isArray(error.attempts) ? error.attempts : []; const lastAttempt = attempts.length > 0 ? objectOrNull(attempts[attempts.length - 1]) || {} : {}; const rawReadiness = objectOrNull(lastAttempt.readiness) || objectOrNull(error.navigationReadiness); const readiness = objectOrNull(rawReadiness?.snapshot) || rawReadiness; return { ts: v.ts ?? null, type: v.type ?? null, commandId: v.commandId ?? null, sampleSeq: v.sampleSeq ?? null, message: error.message ?? v.message ?? null, attemptCount: attempts.length, lastFailureKind: lastAttempt.failureKind ?? null, lastReadinessReason: rawReadiness?.reason ?? readiness?.reason ?? null, lastReadiness: readiness ?? null }; };", "const slimCommandFailure = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, commandId: clip(v.commandId, 80), type: clip(v.type, 32), source: clip(v.source, 24), durationMs: v.durationMs ?? null, beforePath: clip(v.beforePath, 80), afterPath: clip(v.afterPath, 80), name: clip(v.name, 48), failureKind: clip(v.failureKind, 48), sampleSeq: v.sampleSeq ?? null, failureSampleOk: v.failureSampleOk === true, message: clip(v.message, 240) }; };", - "const slimJump = (item) => { const v = objectOrNull(item) || {}; return { columnLabel: v.columnLabel ?? null, pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), promptIndex: v.promptIndex ?? null, fromSeq: v.fromSeq ?? null, toSeq: v.toSeq ?? null, fromValue: v.fromValue ?? null, toValue: v.toValue ?? null, delta: v.delta ?? null, sampleDeltaSeconds: v.sampleDeltaSeconds ?? null, allowedIncreaseSeconds: v.allowedIncreaseSeconds ?? null, traceId: v.traceId ?? null }; };", + "const slimJump = (item) => { const v = objectOrNull(item) || {}; return { columnLabel: v.columnLabel ?? null, pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, fromSeq: v.fromSeq ?? null, toSeq: v.toSeq ?? null, fromValue: v.fromValue ?? null, toValue: v.toValue ?? null, delta: v.delta ?? null, sampleDeltaSeconds: v.sampleDeltaSeconds ?? null, allowedIncreaseSeconds: v.allowedIncreaseSeconds ?? null, traceId: v.traceId ?? null }; };", "const slimTraceOrderAnomaly = (item) => { const v = objectOrNull(item) || {}; return { sampleIndex: v.sampleIndex ?? v.seq ?? null, seq: v.seq ?? null, timestamp: v.timestamp ?? v.ts ?? null, pageRole: clip(v.pageRole, 24), traceId: clip(v.traceId, 64), previousRowIndex: v.previousRowIndex ?? null, currentRowIndex: v.currentRowIndex ?? null, reasons: Array.isArray(v.reasons) ? v.reasons.slice(0, 6).map((x) => clip(x, 48)) : [], previousTotalSeconds: v.previousTotalSeconds ?? null, currentTotalSeconds: v.currentTotalSeconds ?? null, previousClockSeconds: v.previousClockSeconds ?? null, currentClockSeconds: v.currentClockSeconds ?? null, previousPreview: clip(v.previousPreview, 180), currentPreview: clip(v.currentPreview, 180) }; };", "const slimTraceOrderMetrics = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary); return { summary: { sampleCount: v.sampleCount ?? s?.sampleCount ?? null, traceRowCount: v.traceRowCount ?? s?.traceRowCount ?? null, orderAnomalyCount: v.orderAnomalyCount ?? s?.orderAnomalyCount ?? (Array.isArray(v.orderAnomalies) ? v.orderAnomalies.length : null), completionNotLastCount: v.completionNotLastCount ?? s?.completionNotLastCount ?? (Array.isArray(v.completionNotLast) ? v.completionNotLast.length : null) }, orderAnomalies: takeHead(v.orderAnomalies, 8).map(slimTraceOrderAnomaly), valuesRedacted: true }; };", "const slimLoadingOwner = (item) => { const v = objectOrNull(item) || {}; return { ownerKey: clip(v.ownerKey, 120), ownerKind: clip(v.ownerKind, 32), ownerLabel: clip(v.ownerLabel, 120), ownerTraceId: clip(v.ownerTraceId, 64), ownerMessageId: clip(v.ownerMessageId, 64), ownerSessionId: clip(v.ownerSessionId, 64), sampleCount: v.sampleCount ?? null, occurrenceCount: v.occurrenceCount ?? null, maxSimultaneousCount: v.maxSimultaneousCount ?? null, longestContinuousSeconds: v.longestContinuousSeconds ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, promptIndexes: Array.isArray(v.promptIndexes) ? v.promptIndexes.slice(0, 8) : [] }; };", @@ -8936,7 +8936,7 @@ function recoverWebObserveAnalyzeTurnDetails(options: NodeWebProbeObserveOptions "const firstNonEmptyArray = (...values) => { for (const value of values) if (Array.isArray(value) && value.length > 0) return value; return []; };", "const clip = (value, limit = 160) => value === null || value === undefined ? null : String(value).slice(0, limit);", "const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, sampleCount: v.sampleCount ?? null, loadingSamples: v.loadingSamples ?? null, maxLoadingCount: v.maxLoadingCount ?? null, loadingOwnerCount: v.loadingOwnerCount ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, promptTextHash: clip(v.promptTextHash, 80) }; };", - "const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };", + "const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };", "const rounds = firstNonEmptyArray(stdoutJson.sampleMetrics?.rounds, stdoutJson.sampleMetrics?.roundItems, report.windows?.recent?.sampleMetrics?.rounds, report.sampleMetrics?.rounds).slice(-8).map(slimRound);", "const turnColumns = firstNonEmptyArray(stdoutJson.sampleMetrics?.turnColumns, report.windows?.recent?.sampleMetrics?.turnColumns, report.sampleMetrics?.turnColumns).slice(-12).map(slimTurnColumn);", "console.log(JSON.stringify({ ok: true, rounds, turnColumns }));", @@ -9004,8 +9004,9 @@ function renderWebObserveAnalyzeTable(value: Record): string { const pageLabel = (item: Record): string => { const role = webObserveText(item.pageRole); const pageId = webObserveShort(webObserveText(item.pageId), 18); + const epoch = webObserveText(item.pageEpoch); if (role === "-" && pageId === "-") return "-"; - return `${role}/${pageId}`; + return epoch === "-" ? `${role}/${pageId}` : `${role}/${pageId}#${epoch}`; }; const promptNetwork = record(analysis?.promptNetwork); const loading = record(sampleMetrics?.loading); diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 76dedfa9..27f9ef38 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -62,6 +62,8 @@ let terminalStatus = "starting"; let lastScreenshotAtMs = 0; let auth = null; let pageLoadSeq = 0; +let controlPageEpoch = 0; +let observerPageEpoch = 0; let currentPageProvenance = null; const jsonlRotation = { stamp: compactFileTimestamp(startedAt), files: [] }; @@ -361,21 +363,22 @@ async function syncObserverPageToControlSession(reason, explicitSessionId = null const targetUrl = new URL(target, baseUrl).toString(); const beforeUrl = pageUrl(observerPage); const beforeSessionId = routeSessionIdFromUrl(beforeUrl); - if (sessionId && beforeSessionId === sessionId && !forceRefresh) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId }; + if (sessionId && beforeSessionId === sessionId && !forceRefresh) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch }; + observerPageEpoch += 1; let status = null; let statusText = null; const response = await observerPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 45000 }).catch((error) => ({ observerGotoError: errorSummary(error) })); - if (response?.observerGotoError) return { ok: false, reason, changed: false, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, error: response.observerGotoError, valuesRedacted: true }; + if (response?.observerGotoError) return { ok: false, reason, changed: false, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: response.observerGotoError, valuesRedacted: true }; status = typeof response?.status === "function" ? response.status() : null; statusText = typeof response?.statusText === "function" ? response.statusText() : null; const readiness = await waitForTargetPageReady(observerPage, targetUrl, { timeoutMs: 15000 }); if (!readiness.ok) { lastObserverRefreshAtMs = Date.now(); - return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration: null, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true }; + return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration: null, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true }; } const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: 15000 }); lastObserverRefreshAtMs = Date.now(); - return { ok: hydration.ok === true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", valuesRedacted: true }; + return { ok: hydration.ok === true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", valuesRedacted: true }; } async function waitForWorkbenchSessionHydrated(targetPage, sessionId, options = {}) { @@ -1332,9 +1335,9 @@ async function preflightSummary() { async function samplePage(reason, options = {}) { if (options?.refreshObserver !== false) await maybeRefreshObserverPage(reason); const groupSeq = sampleSeq + 1; - if (page && !page.isClosed()) await sampleOnePage(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId }); + if (page && !page.isClosed()) await sampleOnePage(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId, pageEpoch: controlPageEpoch }); if (observerPage && !observerPage.isClosed()) { - await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, error: errorSummary(error) }))); + await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId, pageEpoch: observerPageEpoch }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: errorSummary(error) }))); } if (options?.screenshot !== false && screenshotIntervalMs > 0 && Date.now() - lastScreenshotAtMs >= screenshotIntervalMs) { await captureScreenshot("checkpoint", "jpeg").catch((error) => appendJsonl(files.errors, eventRecord("screenshot-error", { pageRole: "control", pageId, error: errorSummary(error) }))); @@ -1342,7 +1345,7 @@ async function samplePage(reason, options = {}) { await writeHeartbeat({ status: terminalStatus }); } -async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPageId }) { +async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPageId, pageEpoch }) { sampleSeq += 1; const dom = await targetPage.evaluate(() => { const trim = (value, limit = 500) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit); @@ -1804,6 +1807,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag reason, pageRole, pageId: targetPageId, + pageEpoch: Number.isFinite(Number(pageEpoch)) ? Number(pageEpoch) : 0, commandId: activeCommandId, observerInitiated: false, ...digestDom(dom, pageRole), @@ -2198,7 +2202,7 @@ console.log(JSON.stringify({ sampleMetrics: { ...recentWindow.sampleMetrics.summary, rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, loadingSamples: item.loadingSamples, maxLoadingCount: item.maxLoadingCount, loadingOwnerCount: item.loadingOwnerCount, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingTotalElapsedForwardJumpCount: item.turnTimingTotalElapsedForwardJumpCount, turnTimingTotalElapsedForwardJumpMaxSeconds: item.turnTimingTotalElapsedForwardJumpMaxSeconds, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })), - turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })), + turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })), loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading), sessionRailTitles: compactSessionRailTitleMetricsForOutput(recentWindow.sampleMetrics.sessionRailTitles), }, @@ -2271,6 +2275,9 @@ console.log(JSON.stringify({ })), turnTimingRecentUpdateJumps: recentWindow.sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 8).map((item) => ({ columnLabel: item.columnLabel ?? item.columnId ?? null, + pageRole: item.pageRole ?? null, + pageId: item.pageId ?? null, + pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex ?? null, traceId: item.traceId ?? null, fromSeq: item.fromSeq ?? null, @@ -2286,6 +2293,9 @@ console.log(JSON.stringify({ })), turnTimingElapsedZeroResets: recentWindow.sampleMetrics.turnTimingElapsedZeroResets.slice(0, 8).map((item) => ({ columnLabel: item.columnLabel ?? item.columnId ?? null, + pageRole: item.pageRole ?? null, + pageId: item.pageId ?? null, + pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex ?? null, traceId: item.traceId ?? null, fromSeq: item.fromSeq ?? null, @@ -2299,6 +2309,9 @@ console.log(JSON.stringify({ })), turnTimingTotalElapsedForwardJumps: recentWindow.sampleMetrics.turnTimingTotalElapsedForwardJumps.slice(0, 8).map((item) => ({ columnLabel: item.columnLabel ?? item.columnId ?? null, + pageRole: item.pageRole ?? null, + pageId: item.pageId ?? null, + pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex ?? null, traceId: item.traceId ?? null, fromSeq: item.fromSeq ?? null, @@ -4587,14 +4600,15 @@ function buildTurnTimingTable(samples, timeline) { const domIndexes = rawMetrics.map((item) => Number(item.domIndex)).filter(Number.isFinite); const maxDomIndex = domIndexes.length > 0 ? Math.max(...domIndexes) : null; for (const rawMetric of rawMetrics) { + const scopedKey = turnTimingScopedMetricKey(rawMetric, sample); const samplePromptIndex = Number(timelineItem.promptIndex ?? 0); const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex); if (evidencePromptIndex !== null) { - const existingPromptIndex = promptAssignmentByKey.get(rawMetric.key); - if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(rawMetric.key, evidencePromptIndex); + const existingPromptIndex = promptAssignmentByKey.get(scopedKey); + if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(scopedKey, evidencePromptIndex); } - const assignedPromptIndex = promptAssignmentByKey.get(rawMetric.key) ?? null; - const metric = { ...rawMetric, promptIndex: assignedPromptIndex, samplePromptIndex }; + const assignedPromptIndex = promptAssignmentByKey.get(scopedKey) ?? null; + const metric = { ...rawMetric, key: scopedKey, baseKey: rawMetric.key, promptIndex: assignedPromptIndex, samplePromptIndex, pageEpoch: Number(sample?.pageEpoch ?? rawMetric.pageEpoch ?? 0) || 0 }; let column = registry.get(metric.key); if (!column) { column = { @@ -4612,7 +4626,8 @@ function buildTurnTimingTable(samples, timeline) { messageId: metric.messageId ?? null, domIndex: metric.domIndex ?? null, pageRole: metric.pageRole ?? sample.pageRole ?? null, - pageId: metric.pageId ?? sample.pageId ?? null + pageId: metric.pageId ?? sample.pageId ?? null, + pageEpoch: metric.pageEpoch ?? null }; registry.set(metric.key, column); columns.push(column); @@ -4633,6 +4648,7 @@ function buildTurnTimingTable(samples, timeline) { source: metric.source, pageRole: metric.pageRole ?? sample.pageRole ?? null, pageId: metric.pageId ?? sample.pageId ?? null, + pageEpoch: metric.pageEpoch ?? null, sampleGroupSeq: sample.sampleGroupSeq ?? null, traceId: metric.traceId ?? null, messageId: metric.messageId ?? null, @@ -4646,6 +4662,7 @@ function buildTurnTimingTable(samples, timeline) { sampleGroupSeq: sample.sampleGroupSeq ?? null, pageRole: sample.pageRole ?? null, pageId: sample.pageId ?? null, + pageEpoch: Number(sample?.pageEpoch ?? 0) || 0, promptIndex: timelineItem.promptIndex ?? 0, routeSessionId: sample.routeSessionId ?? null, activeSessionId: sample.activeSessionId ?? null, @@ -4665,6 +4682,15 @@ function buildTurnTimingTable(samples, timeline) { }; } +function turnTimingScopedMetricKey(metric, sample) { + return [ + metric?.key ?? "unknown", + metric?.pageRole ?? sample?.pageRole ?? "unknown-role", + metric?.pageId ?? sample?.pageId ?? "unknown-page", + Number(sample?.pageEpoch ?? metric?.pageEpoch ?? 0) || 0 + ].join("|page:"); +} + function inferTurnMetricPromptIndex(metric, samplePromptIndex, maxDomIndex) { const promptIndex = Number(samplePromptIndex); if (!Number.isFinite(promptIndex) || promptIndex <= 0) return null; @@ -4715,6 +4741,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { source: cell.source ?? column.source ?? null, pageRole: cell.pageRole ?? column.pageRole ?? null, pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, valuesRedacted: true }; anomalies.push(event); @@ -4751,6 +4778,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { source: cell.source ?? column.source ?? null, pageRole: cell.pageRole ?? column.pageRole ?? null, pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, valuesRedacted: true }); } @@ -4779,6 +4807,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { source: cell.source ?? column.source ?? null, pageRole: cell.pageRole ?? column.pageRole ?? null, pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, valuesRedacted: true }); } @@ -4815,6 +4844,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { source: cell.source ?? column.source ?? null, pageRole: cell.pageRole ?? column.pageRole ?? null, pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, valuesRedacted: true }); if (increase < 0) { @@ -4838,6 +4868,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { source: cell.source ?? column.source ?? null, pageRole: cell.pageRole ?? column.pageRole ?? null, pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, valuesRedacted: true }); } @@ -4865,6 +4896,7 @@ function detectTurnTimingNonMonotonic(columns, rows) { source: cell.source ?? column.source ?? null, pageRole: cell.pageRole ?? column.pageRole ?? null, pageId: cell.pageId ?? column.pageId ?? null, + pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null, valuesRedacted: true }); }