fix(workbench): remove frontend trace timing compensation
This commit is contained in:
@@ -7,7 +7,7 @@ import { useBottomFollowScroll } from "@/composables/useBottomFollowScroll";
|
||||
import { formatDisplayClock } from "@/config/runtime";
|
||||
import { traceDisplayRows, type TraceEventRow } from "../../../../../tools/src/hwlab-cli/trace-renderer.ts";
|
||||
|
||||
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null; hideSealedFinalResponse?: boolean }>();
|
||||
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null }>();
|
||||
|
||||
const listRef = ref<HTMLOListElement | null>(null);
|
||||
const expanded = ref(false);
|
||||
@@ -16,7 +16,7 @@ const { following, keepBottomAfterUpdate, onScroll, scrollToBottom } = useBottom
|
||||
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
|
||||
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
|
||||
const rawReadableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[], { formatClock: formatDisplayClock }));
|
||||
const readableRows = computed(() => filterReadableRows(rawReadableRows.value, props.hideSealedFinalResponse === true));
|
||||
const readableRows = computed(() => rawReadableRows.value);
|
||||
const projectionDiagnosticLabel = computed(() => {
|
||||
const projection = props.trace?.projection ?? null;
|
||||
const health = projection?.projectionHealth ?? props.trace?.projectionHealth;
|
||||
@@ -83,18 +83,6 @@ function rowIsTool(row: TraceEventRow): boolean {
|
||||
return row.rowId.startsWith("tool:") || /commandExecution/u.test(row.header);
|
||||
}
|
||||
|
||||
function filterReadableRows(rows: TraceEventRow[], hideSealedFinalResponse: boolean): TraceEventRow[] {
|
||||
if (!hideSealedFinalResponse) return rows;
|
||||
return rows.filter((row) => !traceRowDuplicatesSealedFinal(row));
|
||||
}
|
||||
|
||||
function traceRowDuplicatesSealedFinal(row: TraceEventRow): boolean {
|
||||
if (rowIsTool(row)) return false;
|
||||
if (row.rowId.startsWith("trace-final-response:")) return true;
|
||||
const rowText = [row.rowId, row.header, row.body, row.preview].map((value) => String(value ?? "")).join("\n");
|
||||
return /(?:助手(?:最终)?消息|assistant\s+(?:final\s+)?message)/iu.test(rowText);
|
||||
}
|
||||
|
||||
function toolCommandPreview(row: TraceEventRow): string | null {
|
||||
if (!rowIsTool(row)) return null;
|
||||
const preview = singleLinePreview(row.preview);
|
||||
|
||||
@@ -247,7 +247,7 @@ function traceForDisplay(message: ChatMessage): ChatMessage["runnerTrace"] {
|
||||
function messageDurationMeta(message: ChatMessage): { text: string; label: string } | null {
|
||||
if (message.role !== "agent") return null;
|
||||
const timing = messageTimingForDisplay(message);
|
||||
const durationMs = isRunningMessage(message) ? durationSince(timing?.startedAt) : terminalMessageDurationMs(message, timing);
|
||||
const durationMs = isRunningMessage(message) ? durationSince(timing?.startedAt) : terminalMessageDurationMs(timing);
|
||||
if (durationMs === null) return null;
|
||||
const value = formatDuration(durationMs);
|
||||
return { text: `耗时 ${value}`, label: `总耗时:${value}` };
|
||||
@@ -270,33 +270,8 @@ function messageTimingForDisplay(message: ChatMessage): ChatMessage["timing"] {
|
||||
return message.timing ?? null;
|
||||
}
|
||||
|
||||
function terminalMessageDurationMs(message: ChatMessage, timing: ChatMessage["timing"]): number | null {
|
||||
const elapsed = elapsedTerminalDurationMs(message, timing);
|
||||
if (elapsed !== null && elapsed > 0) return ceilDisplayDurationSecond(elapsed);
|
||||
const recorded = sealedTerminalRecordedDurationMs(message, timing);
|
||||
if (recorded !== null) return ceilDisplayDurationSecond(recorded);
|
||||
return null;
|
||||
}
|
||||
|
||||
function ceilDisplayDurationSecond(ms: number): number {
|
||||
if (!Number.isFinite(ms) || ms <= 0) return ms;
|
||||
return Math.ceil(ms / 1000) * 1000;
|
||||
}
|
||||
|
||||
function sealedTerminalRecordedDurationMs(message: ChatMessage, timing: ChatMessage["timing"]): number | null {
|
||||
void message;
|
||||
const messageHasTerminalTimestamp = timestampMs(timing?.finishedAt) !== null;
|
||||
return maxPositiveDurationMs(
|
||||
messageHasTerminalTimestamp ? timing?.durationMs : null,
|
||||
);
|
||||
}
|
||||
|
||||
function elapsedTerminalDurationMs(message: ChatMessage, timing: ChatMessage["timing"]): number | null {
|
||||
void message;
|
||||
const startedAt = timestampMs(timing?.startedAt);
|
||||
const endedAt = timestampMs(timing?.finishedAt);
|
||||
if (startedAt === null || endedAt === null || endedAt < startedAt) return null;
|
||||
return Math.trunc(endedAt - startedAt);
|
||||
function terminalMessageDurationMs(timing: ChatMessage["timing"]): number | null {
|
||||
return finiteDurationMs(timing?.durationMs);
|
||||
}
|
||||
|
||||
function durationSince(timestamp: unknown): number | null {
|
||||
@@ -317,15 +292,6 @@ function finiteDurationMs(value: unknown): number | null {
|
||||
return Number.isFinite(number) && number >= 0 ? Math.trunc(number) : null;
|
||||
}
|
||||
|
||||
function maxPositiveDurationMs(...values: unknown[]): number | null {
|
||||
let maximum: number | null = null;
|
||||
for (const value of values) {
|
||||
const duration = finiteDurationMs(value);
|
||||
if (duration !== null && duration > 0) maximum = maximum === null ? duration : Math.max(maximum, duration);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const seconds = ms > 0 && ms < 1000 ? 1 : Math.max(0, Math.floor(ms / 1000));
|
||||
if (seconds < 60) return `${seconds} 秒`;
|
||||
@@ -364,7 +330,7 @@ function formatDuration(ms: number): string {
|
||||
<button v-if="message.role === 'agent'" class="message-detail-button" type="button" aria-label="运行详情" title="运行详情" @click="openDetails(message)">!</button>
|
||||
</div>
|
||||
</header>
|
||||
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="traceForDisplay(message)" :auto-expanded="traceAutoExpanded(message)" :storage-key="traceStorageKey(message)" :hide-sealed-final-response="hasSealedCompletedText(message)" />
|
||||
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="traceForDisplay(message)" :auto-expanded="traceAutoExpanded(message)" :storage-key="traceStorageKey(message)" />
|
||||
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="思考中..." compact />
|
||||
<MessageMarkdown v-if="showMessageText(message)" class="message-text" :source="visibleMessageText(message)" />
|
||||
<ApiErrorDiagnostic v-if="messageHasDiagnostic(message)" class="message-diagnostic projection-diagnostic" :error="messageDiagnosticText(message)" :api-error="messageApiError(message)" :diagnostic="messageErrorDiagnostic(message)" compact />
|
||||
|
||||
@@ -133,37 +133,20 @@ export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: Non
|
||||
};
|
||||
}
|
||||
|
||||
type TraceTimingMergeOptions = {
|
||||
preserveVisibleTerminalDuration?: boolean;
|
||||
};
|
||||
|
||||
function mergeTraceTimingProjection(previous: unknown, next: unknown, options: TraceTimingMergeOptions = {}): TraceSnapshot["timing"] | null {
|
||||
function mergeTraceTimingProjection(previous: unknown, next: unknown): TraceSnapshot["timing"] | null {
|
||||
const previousTiming = traceTimingCandidate(previous);
|
||||
const nextTiming = traceTimingCandidate(next);
|
||||
if (!previousTiming && !nextTiming) return null;
|
||||
if (!previousTiming) return nextTiming;
|
||||
if (!nextTiming) return previousTiming;
|
||||
const startedAt = earliestTraceTimestamp(previousTiming.startedAt, nextTiming.startedAt);
|
||||
const lastEventAt = latestTraceTimestamp(previousTiming.lastEventAt, nextTiming.lastEventAt);
|
||||
const finishedAt = latestTraceTimestamp(previousTiming.finishedAt, nextTiming.finishedAt);
|
||||
const runningElapsedDurationMs = traceElapsedMs(startedAt, lastEventAt);
|
||||
const terminalElapsedDurationMs = traceElapsedMs(startedAt, finishedAt);
|
||||
const terminalTiming = traceTimingIsTerminal(next) || finishedAt !== null;
|
||||
const previousTerminalDurationMs = previousTiming.finishedAt ? previousTiming.durationMs : null;
|
||||
const preservedVisibleTerminalDurationMs = options.preserveVisibleTerminalDuration && !finishedAt
|
||||
? firstTraceDuration(previousTiming.durationMs)
|
||||
: null;
|
||||
const durationMs = terminalTiming
|
||||
? maxTraceDuration(finishedAt ? nextTiming.durationMs : null, terminalElapsedDurationMs, previousTerminalDurationMs, preservedVisibleTerminalDurationMs)
|
||||
: maxTraceDuration(previousTiming.durationMs, nextTiming.durationMs, runningElapsedDurationMs);
|
||||
return {
|
||||
...previousTiming,
|
||||
...nextTiming,
|
||||
startedAt,
|
||||
lastEventAt,
|
||||
finishedAt,
|
||||
durationMs,
|
||||
observedAt: latestTraceTimestamp(previousTiming.observedAt, nextTiming.observedAt),
|
||||
startedAt: nextTiming.startedAt ?? previousTiming.startedAt ?? null,
|
||||
lastEventAt: nextTiming.lastEventAt ?? previousTiming.lastEventAt ?? null,
|
||||
finishedAt: nextTiming.finishedAt ?? previousTiming.finishedAt ?? null,
|
||||
durationMs: firstTraceDuration(nextTiming.durationMs, previousTiming.durationMs),
|
||||
observedAt: nextTiming.observedAt ?? previousTiming.observedAt ?? null,
|
||||
lastEventAgeMs: null,
|
||||
valuesRedacted: previousTiming.valuesRedacted !== false && nextTiming.valuesRedacted !== false
|
||||
};
|
||||
@@ -177,7 +160,7 @@ function traceTimingCandidate(value: unknown): TraceSnapshot["timing"] | null {
|
||||
const lastEventAt = firstTraceTimestamp(timing?.lastEventAt, record?.lastEventAt);
|
||||
const finishedAt = firstTraceTimestamp(timing?.finishedAt, record?.finishedAt);
|
||||
const observedAt = firstTraceTimestamp(timing?.observedAt, record?.observedAt);
|
||||
const durationMs = maxTraceDuration(timing?.durationMs, record?.durationMs);
|
||||
const durationMs = firstTraceDuration(timing?.durationMs, record?.durationMs);
|
||||
const lastEventAgeMs = finiteTraceDuration(timing?.lastEventAgeMs ?? record?.lastEventAgeMs);
|
||||
if (!startedAt && !lastEventAt && !finishedAt && !observedAt && durationMs === null && lastEventAgeMs === null) return null;
|
||||
return { ...(timing ?? {}), startedAt, lastEventAt, finishedAt, observedAt, durationMs, lastEventAgeMs, valuesRedacted: timing?.valuesRedacted !== false };
|
||||
@@ -197,42 +180,6 @@ function firstTraceTimestamp(...values: unknown[]): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function earliestTraceTimestamp(...values: unknown[]): string | null {
|
||||
return selectTraceTimestamp("earliest", values);
|
||||
}
|
||||
|
||||
function latestTraceTimestamp(...values: unknown[]): string | null {
|
||||
return selectTraceTimestamp("latest", values);
|
||||
}
|
||||
|
||||
function selectTraceTimestamp(mode: "earliest" | "latest", values: unknown[]): string | null {
|
||||
let selected: { value: string; ms: number } | null = null;
|
||||
for (const value of values) {
|
||||
const timestamp = firstTraceTimestamp(value);
|
||||
if (!timestamp) continue;
|
||||
const ms = Date.parse(timestamp);
|
||||
if (!selected || (mode === "earliest" ? ms < selected.ms : ms > selected.ms)) selected = { value: timestamp, ms };
|
||||
}
|
||||
return selected?.value ?? null;
|
||||
}
|
||||
|
||||
function traceElapsedMs(startedAt: unknown, endedAt: unknown): number | null {
|
||||
const start = startedAt == null ? NaN : Date.parse(String(startedAt));
|
||||
const end = endedAt == null ? NaN : Date.parse(String(endedAt));
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
|
||||
return Math.trunc(end - start);
|
||||
}
|
||||
|
||||
function maxTraceDuration(...values: unknown[]): number | null {
|
||||
let max: number | null = null;
|
||||
for (const value of values) {
|
||||
const duration = finiteTraceDuration(value);
|
||||
if (duration === null) continue;
|
||||
if (max === null || duration > max) max = duration;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
function firstTraceDuration(...values: unknown[]): number | null {
|
||||
for (const value of values) {
|
||||
const duration = finiteTraceDuration(value);
|
||||
@@ -246,15 +193,6 @@ function finiteTraceDuration(value: unknown): number | null {
|
||||
return Number.isFinite(number) && number >= 0 ? Math.trunc(number) : null;
|
||||
}
|
||||
|
||||
function traceTimingWasRunning(value: unknown): boolean {
|
||||
const record = traceRecord(value);
|
||||
if (!record) return false;
|
||||
const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
|
||||
if (["completed", "failed", "cancelled", "canceled", "error"].includes(status)) return false;
|
||||
const timing = traceTimingCandidate(value);
|
||||
return !timing?.finishedAt;
|
||||
}
|
||||
|
||||
function traceTimingIsTerminal(value: unknown): boolean {
|
||||
const record = traceRecord(value);
|
||||
const status = typeof record?.status === "string" ? record.status.trim().toLowerCase() : "";
|
||||
@@ -308,17 +246,17 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac
|
||||
blocker: trace.blocker ?? terminal.blocker ?? null
|
||||
};
|
||||
const events = Array.isArray(trace.events) ? trace.events : [];
|
||||
const timing = mergedTrace.timing ?? terminal.timing ?? terminal.runnerTrace?.timing ?? null;
|
||||
const timing = mergedTrace.timing ?? terminal.timing ?? null;
|
||||
return {
|
||||
...terminal,
|
||||
traceId: mergedTrace.traceId ?? terminal.traceId,
|
||||
sessionId: mergedTrace.sessionId ?? terminal.sessionId,
|
||||
threadId: mergedTrace.threadId ?? terminal.threadId,
|
||||
timing,
|
||||
startedAt: mergedTrace.startedAt ?? timing?.startedAt ?? terminal.startedAt ?? terminal.runnerTrace?.startedAt,
|
||||
lastEventAt: mergedTrace.lastEventAt ?? timing?.lastEventAt ?? terminal.lastEventAt ?? terminal.runnerTrace?.lastEventAt,
|
||||
finishedAt: mergedTrace.finishedAt ?? timing?.finishedAt ?? terminal.finishedAt ?? terminal.runnerTrace?.finishedAt,
|
||||
durationMs: mergedTrace.durationMs ?? timing?.durationMs ?? terminal.durationMs ?? terminal.runnerTrace?.durationMs,
|
||||
startedAt: mergedTrace.startedAt ?? timing?.startedAt ?? terminal.startedAt,
|
||||
lastEventAt: mergedTrace.lastEventAt ?? timing?.lastEventAt ?? terminal.lastEventAt,
|
||||
finishedAt: mergedTrace.finishedAt ?? timing?.finishedAt ?? terminal.finishedAt,
|
||||
durationMs: mergedTrace.durationMs ?? timing?.durationMs ?? terminal.durationMs,
|
||||
agentRun: mergedTrace.agentRun ?? terminal.agentRun,
|
||||
error: mergedTrace.error ?? terminal.error,
|
||||
assistantText: terminal.assistantText,
|
||||
@@ -403,7 +341,7 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
||||
function resultToTraceSnapshot(traceId: string, result: AgentChatResultResponse, eventSource = "trace-api"): TraceSnapshot {
|
||||
const events = Array.isArray(result.events) ? result.events : Array.isArray(result.traceEvents) ? result.traceEvents : [];
|
||||
const lastEvent = events.at(-1);
|
||||
const timing = result.timing ?? result.runnerTrace?.timing ?? null;
|
||||
const timing = result.timing ?? null;
|
||||
return {
|
||||
traceId: result.traceId ?? traceId,
|
||||
status: result.status,
|
||||
@@ -424,10 +362,10 @@ function resultToTraceSnapshot(traceId: string, result: AgentChatResultResponse,
|
||||
traceSummary: result.traceSummary,
|
||||
error: result.error,
|
||||
timing,
|
||||
startedAt: result.startedAt ?? timing?.startedAt ?? result.runnerTrace?.startedAt ?? null,
|
||||
lastEventAt: result.lastEventAt ?? timing?.lastEventAt ?? result.runnerTrace?.lastEventAt ?? null,
|
||||
finishedAt: result.finishedAt ?? timing?.finishedAt ?? result.runnerTrace?.finishedAt ?? null,
|
||||
durationMs: result.durationMs ?? timing?.durationMs ?? result.runnerTrace?.durationMs ?? null,
|
||||
startedAt: result.startedAt ?? timing?.startedAt ?? null,
|
||||
lastEventAt: result.lastEventAt ?? timing?.lastEventAt ?? null,
|
||||
finishedAt: result.finishedAt ?? timing?.finishedAt ?? null,
|
||||
durationMs: result.durationMs ?? timing?.durationMs ?? null,
|
||||
projection: result.projection ?? result.runnerTrace?.projection ?? null,
|
||||
projectionStatus: result.projectionStatus ?? result.runnerTrace?.projectionStatus ?? null,
|
||||
projectionHealth: result.projectionHealth ?? result.runnerTrace?.projectionHealth ?? null,
|
||||
@@ -444,7 +382,7 @@ function mergeTraceSnapshots(previous: TraceSnapshot | null, next: TraceSnapshot
|
||||
const previousEvents = Array.isArray(previous.events) ? previous.events : [];
|
||||
const nextEvents = Array.isArray(next.events) ? next.events : [];
|
||||
const events = mergeTraceEvents(previousEvents, nextEvents);
|
||||
const timing = mergeTraceTimingProjection(previous, next, { preserveVisibleTerminalDuration: traceTimingWasRunning(previous) && traceTimingIsTerminal(next) });
|
||||
const timing = mergeTraceTimingProjection(previous, next);
|
||||
return {
|
||||
...previous,
|
||||
...next,
|
||||
@@ -461,14 +399,7 @@ function mergeTraceSnapshots(previous: TraceSnapshot | null, next: TraceSnapshot
|
||||
}
|
||||
|
||||
function traceSnapshotWithTurnStatus(trace: TraceSnapshot, turn: TraceSnapshot): TraceSnapshot {
|
||||
const turnTiming = traceTimingCandidate(turn);
|
||||
const turnIsTerminal = traceTimingIsTerminal(turn) || isTerminalStatus(turn.status);
|
||||
// Running card timing belongs to the turn status projection. Trace pages may
|
||||
// hydrate older detail rows later; they must not move the visible running
|
||||
// card's startedAt/lastEventAt source and create multi-second jumps.
|
||||
const timing = !turnIsTerminal && turnTiming
|
||||
? turnTiming
|
||||
: mergeTraceTimingProjection(trace, turn, { preserveVisibleTerminalDuration: traceTimingWasRunning(trace) && traceTimingIsTerminal(turn) });
|
||||
const timing = mergeTraceTimingProjection(trace, turn);
|
||||
return {
|
||||
...trace,
|
||||
traceId: trace.traceId ?? turn.traceId,
|
||||
|
||||
@@ -165,7 +165,7 @@ function mergeMessageSnapshot(existing: ChatMessage, incoming: ChatMessage): Cha
|
||||
return {
|
||||
...existing,
|
||||
...incoming,
|
||||
runnerTrace: existing.runnerTrace ?? incoming.runnerTrace ?? null,
|
||||
runnerTrace: incoming.runnerTrace ?? existing.runnerTrace ?? null,
|
||||
traceAutoLifecycle: existing.traceAutoLifecycle,
|
||||
updatedAt: incoming.updatedAt ?? existing.updatedAt
|
||||
};
|
||||
|
||||
@@ -1676,7 +1676,7 @@ function normalizeChatMessage(message: ChatMessage): ChatMessage {
|
||||
const baseText = firstNonEmptyString(message.text, messageText((message as Record<string, unknown>).content), messageText((message as Record<string, unknown>).message));
|
||||
const finalText = finalResponseText((message as Record<string, unknown>).finalResponse);
|
||||
const errorText = agentErrorDisplayText(error);
|
||||
const status = normalizeAgentMessageStatusFromEvidence(role, rawStatus, runnerTrace);
|
||||
const status = rawStatus;
|
||||
const text = role === "agent"
|
||||
? isTerminalMessageStatus(status)
|
||||
? projectedAgentMessageText({ status, finalText, errorText, baseText })
|
||||
@@ -1686,14 +1686,6 @@ function normalizeChatMessage(message: ChatMessage): ChatMessage {
|
||||
return { ...message, ...messageTimingPatch(message), role, text, id: messageId, messageId, title: normalizeWorkbenchMessageTitle(role, message.title), createdAt: message.createdAt ?? new Date().toISOString(), status, runnerTrace, error: error ?? message.error ?? null, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined };
|
||||
}
|
||||
|
||||
function normalizeAgentMessageStatusFromEvidence(role: ChatMessage["role"], status: ChatMessage["status"], runnerTrace: ChatMessage["runnerTrace"]): ChatMessage["status"] {
|
||||
if (role !== "agent") return status;
|
||||
const traceStatus = normalizedStatusText(runnerTrace?.status);
|
||||
if (isTerminalMessageStatus(traceStatus)) return traceStatus as ChatMessage["status"];
|
||||
if (isTerminalMessageStatus(status)) return status;
|
||||
return status;
|
||||
}
|
||||
|
||||
function activeTraceIdFromMessages(messages: ChatMessage[], turnStatusAuthority: Record<string, TurnStatusAuthority>): string | null {
|
||||
for (const message of [...messages].reverse()) {
|
||||
if (message.role !== "agent") continue;
|
||||
@@ -1850,27 +1842,8 @@ function messageTimingPatch(value: unknown): Partial<ChatMessage> {
|
||||
}
|
||||
|
||||
function messageTimingPatchForMerge(message: ChatMessage, value: unknown): Partial<ChatMessage> {
|
||||
// Trace/result hydration may update runnerTrace and TraceTimeline, but the
|
||||
// main Code Agent card timing is a durable message projection field. Do not
|
||||
// let trace snapshots or terminal result polling rewrite message.timing;
|
||||
// otherwise visible card elapsed can jump or diverge from the sealed message
|
||||
// projection when trace rows arrive out of order.
|
||||
const current = messageTimingPatch(message);
|
||||
const currentTiming = current.timing ?? null;
|
||||
const record = recordValue(value);
|
||||
const incomingStatus = normalizedStatusText(firstNonEmptyString(record?.status));
|
||||
const incomingTerminal = record?.terminal === true || isTerminalMessageStatus(incomingStatus);
|
||||
if (!incomingTerminal || isTerminalMessageStatus(message.status) || currentTiming?.finishedAt || currentTiming?.durationMs != null) return current;
|
||||
const incoming = normalizeTimingProjection(value);
|
||||
const terminalTiming = incoming ?? ({} as Partial<WorkbenchTurnTimingProjection>);
|
||||
const startedAt = firstNonEmptyString(currentTiming?.startedAt, terminalTiming.startedAt) ?? null;
|
||||
const terminalFinishedAt = firstNonEmptyString(terminalTiming.finishedAt, record?.finishedAt, record?.completedAt, record?.updatedAt, terminalTiming.lastEventAt, currentTiming?.finishedAt, currentTiming?.lastEventAt) ?? null;
|
||||
const durationMs = firstFiniteNumber(terminalTiming.durationMs, record?.durationMs) ?? inferredDurationMs(startedAt, terminalFinishedAt) ?? currentTiming?.durationMs ?? null;
|
||||
const inferredFinishedAt = inferFinishedAtFromDuration(startedAt, durationMs);
|
||||
const finishedAt = firstNonEmptyString(terminalTiming.finishedAt, terminalFinishedAt, inferredFinishedAt, terminalTiming.lastEventAt, currentTiming?.finishedAt) ?? null;
|
||||
const lastEventAt = firstNonEmptyString(terminalTiming.lastEventAt, finishedAt, currentTiming?.lastEventAt, startedAt) ?? null;
|
||||
const timing = { ...currentTiming, ...terminalTiming, startedAt, lastEventAt, finishedAt, durationMs, valuesRedacted: terminalTiming.valuesRedacted !== false && currentTiming?.valuesRedacted !== false } as WorkbenchTurnTimingProjection;
|
||||
return { ...current, timing, startedAt, lastEventAt, finishedAt, durationMs };
|
||||
void value;
|
||||
return messageTimingPatch(message);
|
||||
}
|
||||
|
||||
function messageStatusPatchForTerminalMerge(message: ChatMessage, resultStatus: string | null, terminal: boolean): Partial<ChatMessage> {
|
||||
@@ -1879,20 +1852,6 @@ function messageStatusPatchForTerminalMerge(message: ChatMessage, resultStatus:
|
||||
return {};
|
||||
}
|
||||
|
||||
function inferFinishedAtFromDuration(startedAt: unknown, durationMs: number | null): string | null {
|
||||
if (durationMs === null || !Number.isFinite(durationMs) || durationMs < 0) return null;
|
||||
const startedAtMs = timestampMs(startedAt);
|
||||
if (startedAtMs === null) return null;
|
||||
return new Date(startedAtMs + Math.trunc(durationMs)).toISOString();
|
||||
}
|
||||
|
||||
function inferredDurationMs(startedAt: unknown, finishedAt: unknown): number | null {
|
||||
const startedAtMs = timestampMs(startedAt);
|
||||
const finishedAtMs = timestampMs(finishedAt);
|
||||
if (startedAtMs === null || finishedAtMs === null || finishedAtMs < startedAtMs) return null;
|
||||
return Math.trunc(finishedAtMs - startedAtMs);
|
||||
}
|
||||
|
||||
function firstPositiveFiniteNumber(...values: unknown[]): number | null {
|
||||
for (const value of values) {
|
||||
const number = firstFiniteNumber(value);
|
||||
@@ -2091,7 +2050,6 @@ function messageNeedsTraceHydration(message: ChatMessage): boolean {
|
||||
const trace = message.runnerTrace;
|
||||
const events = Array.isArray(trace?.events) ? trace.events : [];
|
||||
const eventCount = firstFiniteNumber(trace?.eventCount) ?? events.length;
|
||||
if (messageHasCompletedFinalResponse(message) && events.length === 0) return false;
|
||||
if (trace?.fullTraceLoaded === true && trace.eventsCompacted !== true) return events.length === 0 && (eventCount > 0 || isTraceActiveStatus(trace?.status) || isTraceActiveStatus(message.status));
|
||||
return events.length === 0 || trace?.eventsCompacted === true || trace?.fullTraceLoaded !== true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user