diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index df49ee49..b560b74a 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -3903,11 +3903,7 @@ function isFinalResultText(text) { } function buildSampleMetrics(samples, control) { - const promptCommands = control - .filter((item) => item.type === "sendPrompt" && item.phase === "completed") - .map((item) => ({ ts: item.ts, tsMs: Date.parse(item.ts), commandId: item.commandId ?? null, textHash: item.input?.textHash ?? null, textBytes: item.input?.textBytes ?? null })) - .filter((item) => Number.isFinite(item.tsMs)) - .sort((a, b) => a.tsMs - b.tsMs); + const promptCommands = buildSendPromptCommandTimeline(control); const promptTimes = promptCommands.map((item) => item.tsMs); const timeline = samples.map((sample) => { const texts = sampleTexts(sample); @@ -4070,6 +4066,49 @@ function buildSampleMetrics(samples, control) { }; } +function buildSendPromptCommandTimeline(control) { + const byCommand = new Map(); + let ordinal = 0; + for (const item of Array.isArray(control) ? control : []) { + if (item?.type !== "sendPrompt") continue; + const commandId = item.commandId ? String(item.commandId) : "sendPrompt-" + String(ordinal++); + const existing = byCommand.get(commandId) || { + commandId, + startedAt: null, + startedTsMs: null, + completedAt: null, + completedTsMs: null, + failedAt: null, + failedTsMs: null, + textHash: null, + textBytes: null, + }; + if (!existing.textHash && item.input?.textHash) existing.textHash = item.input.textHash; + if (!existing.textBytes && item.input?.textBytes) existing.textBytes = item.input.textBytes; + const tsMs = Date.parse(item.ts); + if (item.phase === "started" && Number.isFinite(tsMs)) { + existing.startedAt = item.ts ?? existing.startedAt; + existing.startedTsMs = tsMs; + } else if (item.phase === "completed" && Number.isFinite(tsMs)) { + existing.completedAt = item.ts ?? existing.completedAt; + existing.completedTsMs = tsMs; + } else if (item.phase === "failed" && Number.isFinite(tsMs)) { + existing.failedAt = item.ts ?? existing.failedAt; + existing.failedTsMs = tsMs; + } + byCommand.set(commandId, existing); + } + return Array.from(byCommand.values()) + .map((item) => { + const tsMs = Number.isFinite(item.startedTsMs) ? item.startedTsMs : Number.isFinite(item.completedTsMs) ? item.completedTsMs : item.failedTsMs; + const ts = Number.isFinite(item.startedTsMs) ? item.startedAt : Number.isFinite(item.completedTsMs) ? item.completedAt : item.failedAt; + return { ...item, ts, tsMs }; + }) + .filter((item) => Number.isFinite(item.tsMs)) + .sort((a, b) => a.tsMs - b.tsMs) + .map((item, index) => ({ ...item, promptIndex: index + 1 })); +} + function buildSessionRailTitleMetrics(samples, timeline) { const rows = []; const examplesByHash = new Map(); @@ -4384,7 +4423,10 @@ function buildTurnTimingTable(samples, timeline) { for (const rawMetric of rawMetrics) { const samplePromptIndex = Number(timelineItem.promptIndex ?? 0); const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex); - if (evidencePromptIndex !== null && !promptAssignmentByKey.has(rawMetric.key)) promptAssignmentByKey.set(rawMetric.key, evidencePromptIndex); + if (evidencePromptIndex !== null) { + const existingPromptIndex = promptAssignmentByKey.get(rawMetric.key); + if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(rawMetric.key, evidencePromptIndex); + } const assignedPromptIndex = promptAssignmentByKey.get(rawMetric.key) ?? null; const metric = { ...rawMetric, promptIndex: assignedPromptIndex, samplePromptIndex }; let column = registry.get(metric.key); @@ -4411,7 +4453,7 @@ function buildTurnTimingTable(samples, timeline) { } else { column.lastSeq = sample.seq ?? null; column.lastTs = sample.ts ?? null; - if (!column.promptIndex && metric.promptIndex) column.promptIndex = metric.promptIndex; + if (metric.promptIndex && column.promptIndex !== metric.promptIndex) column.promptIndex = metric.promptIndex; column.lastPromptIndex = metric.promptIndex ?? column.lastPromptIndex ?? null; if (!column.traceId && metric.traceId) column.traceId = metric.traceId; if (!column.messageId && metric.messageId) column.messageId = metric.messageId;