diff --git a/internal/cloud/server-workbench-http.ts b/internal/cloud/server-workbench-http.ts index fb5eb98a..961f48ce 100644 --- a/internal/cloud/server-workbench-http.ts +++ b/internal/cloud/server-workbench-http.ts @@ -763,8 +763,9 @@ function factMessageDto(message, parts = [], facts = {}) { const assistantLike = isAssistantLikeRole(role); const checkpointStatus = assistantLike ? normalizeTerminalStatus(checkpoint?.status) : null; const status = normalizeStatus(checkpointStatus ?? (assistantLike ? turn?.status : null) ?? messageStatus); - const timingSource = assistantLike ? (checkpointStatus ? checkpoint : turn ?? checkpoint ?? message) : message; - const timing = factTimingProjection(timingSource, status); + const timing = assistantLike + ? factCombinedTimingProjection(status, checkpoint, turn, message) + : factTimingProjection(message, status); const text = projectionText(message?.text, message?.content, message?.message, message?.finalResponse); const normalizedParts = parts.length > 0 ? [...parts].sort(compareFactPartsAsc).map((part) => factPartDto(part, messageId, traceId)).filter(Boolean) @@ -830,7 +831,7 @@ function factTurnSnapshot({ turn = null, session = null, facts = {}, traceId, tu const traceStatus = normalizeTerminalStatus(trace.status); const status = normalizeStatus(checkpointStatus ?? traceStatus ?? turn?.status ?? session?.status); const finalText = projectionText(turn?.finalResponse, turn?.assistantText, turn?.text, assistantMessage?.text); - const timing = factTimingProjection(checkpointStatus ? checkpoint : turn ?? checkpoint, status); + const timing = factCombinedTimingProjection(status, checkpoint, turn, trace); return { turnId: resolvedTurnId, traceId: safeTrace, @@ -960,16 +961,61 @@ function factProjectionForTrace(facts = {}, traceId) { function factTimingProjection(record = null, status = null) { const source = objectValue(record?.timing); const observedAt = new Date().toISOString(); - const startedAt = timestampIso(source?.startedAt); - const lastEventAt = timestampIso(source?.lastEventAt); + const startedAt = timestampIso(source?.startedAt ?? record?.startedAt); + const lastEventAt = timestampIso(source?.lastEventAt ?? record?.lastEventAt ?? record?.updatedAt ?? record?.createdAt); const normalizedStatus = normalizeStatus(status ?? record?.status); const terminal = record?.terminal === true || TERMINAL_STATUSES.has(normalizedStatus) && !RUNNING_STATUSES.has(normalizedStatus); - const finishedAt = terminal ? timestampIso(source?.finishedAt) : null; + const finishedAt = terminal ? timestampIso(source?.finishedAt ?? record?.finishedAt ?? record?.completedAt) : null; const durationMs = elapsedFactMs(startedAt, terminal ? finishedAt : observedAt); const lastEventAgeMs = terminal ? null : elapsedFactMs(lastEventAt, observedAt); return { startedAt, lastEventAt, finishedAt, durationMs, observedAt: terminal ? null : observedAt, lastEventAgeMs, valuesRedacted: source?.valuesRedacted !== false }; } +function factCombinedTimingProjection(status = null, ...records) { + const normalizedStatus = normalizeStatus(status ?? records.find((record) => record?.status)?.status); + const terminal = TERMINAL_STATUSES.has(normalizedStatus) && !RUNNING_STATUSES.has(normalizedStatus); + const observedAt = new Date().toISOString(); + const timings = records.map((record) => factTimingSource(record)).filter(Boolean); + const startedAt = firstTimestampIso(...timings.map((timing) => timing.startedAt)); + const lastEventAt = latestTimestampIso(...timings.map((timing) => timing.lastEventAt)); + const finishedAt = terminal ? latestTimestampIso(...timings.map((timing) => timing.finishedAt)) : null; + const durationMs = elapsedFactMs(startedAt, terminal ? finishedAt : observedAt); + const lastEventAgeMs = terminal ? null : elapsedFactMs(lastEventAt, observedAt); + return { startedAt, lastEventAt, finishedAt, durationMs, observedAt: terminal ? null : observedAt, lastEventAgeMs, valuesRedacted: true }; +} + +function factTimingSource(record = null) { + if (!record) return null; + const source = objectValue(record?.timing); + return { + startedAt: timestampIso(source?.startedAt ?? record?.startedAt), + lastEventAt: timestampIso(source?.lastEventAt ?? record?.lastEventAt ?? record?.updatedAt ?? record?.createdAt), + finishedAt: timestampIso(source?.finishedAt ?? record?.finishedAt ?? record?.completedAt) + }; +} + +function firstTimestampIso(...values) { + for (const value of values) { + const timestamp = timestampIso(value); + if (timestamp) return timestamp; + } + return null; +} + +function latestTimestampIso(...values) { + let latest = null; + let latestMs = Number.NEGATIVE_INFINITY; + for (const value of values) { + const timestamp = timestampIso(value); + if (!timestamp) continue; + const ms = Date.parse(timestamp); + if (!Number.isFinite(ms) || ms < latestMs) continue; + latest = timestamp; + latestMs = ms; + } + return latest; +} + function elapsedFactMs(startedAt, endedAt) { const start = Date.parse(String(startedAt ?? "")); const end = Date.parse(String(endedAt ?? ""));