Merge pull request #1779 from pikasTech/fix/1770-turn-timing-authority
fix: keep workbench turn timing monotonic
This commit is contained in:
@@ -233,7 +233,8 @@ function traceForDisplay(message: ChatMessage): ChatMessage["runnerTrace"] {
|
||||
|
||||
function messageDurationMeta(message: ChatMessage): { text: string; label: string } | null {
|
||||
if (message.role !== "agent") return null;
|
||||
const durationMs = isRunningMessage(message) ? durationSince(message.timing?.startedAt) : finiteDurationMs(message.timing?.durationMs);
|
||||
const timing = messageTimingForDisplay(message);
|
||||
const durationMs = isRunningMessage(message) ? durationSince(timing?.startedAt) : finiteDurationMs(timing?.durationMs);
|
||||
if (durationMs === null) return null;
|
||||
const value = formatDuration(durationMs);
|
||||
return { text: `耗时 ${value}`, label: `总耗时:${value}` };
|
||||
@@ -241,12 +242,17 @@ function messageDurationMeta(message: ChatMessage): { text: string; label: strin
|
||||
|
||||
function messageActivityMeta(message: ChatMessage): { text: string; label: string } | null {
|
||||
if (!isRunningMessage(message)) return null;
|
||||
const ageMs = durationSince(message.timing?.lastEventAt);
|
||||
const timing = messageTimingForDisplay(message);
|
||||
const ageMs = durationSince(timing?.lastEventAt);
|
||||
if (ageMs === null) return null;
|
||||
const value = formatDuration(ageMs) + "前";
|
||||
return { text: `最近 ${value}`, label: `最近事件:${value}` };
|
||||
}
|
||||
|
||||
function messageTimingForDisplay(message: ChatMessage): ChatMessage["timing"] {
|
||||
return message.runnerTrace?.timing ?? message.timing ?? null;
|
||||
}
|
||||
|
||||
function durationSince(timestamp: unknown): number | null {
|
||||
const startedAtMs = timestampMs(timestamp);
|
||||
return startedAtMs === null ? null : Math.max(0, nowMs.value - startedAtMs);
|
||||
|
||||
@@ -118,7 +118,128 @@ export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: Non
|
||||
: keepPreviousEvents && previousAuthoritative && next.eventsCompacted === true ? previousEvents
|
||||
: keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents;
|
||||
const eventCount = keepPreviousEvents ? previous.eventCount ?? events.length : next.eventCount ?? previous.eventCount ?? events.length;
|
||||
return { ...previous, ...next, events, eventCount, updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString() };
|
||||
const timing = mergeTraceTimingProjection(previous, next, { preserveVisibleTerminalDuration: traceTimingWasRunning(previous) && traceTimingIsTerminal(next) });
|
||||
return {
|
||||
...previous,
|
||||
...next,
|
||||
events,
|
||||
eventCount,
|
||||
timing,
|
||||
startedAt: timing?.startedAt ?? next.startedAt ?? previous.startedAt ?? null,
|
||||
lastEventAt: timing?.lastEventAt ?? next.lastEventAt ?? previous.lastEventAt ?? null,
|
||||
finishedAt: timing?.finishedAt ?? next.finishedAt ?? previous.finishedAt ?? null,
|
||||
durationMs: timing?.durationMs ?? next.durationMs ?? previous.durationMs ?? null,
|
||||
updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
function mergeTraceTimingProjection(previous: unknown, next: unknown, options: { preserveVisibleTerminalDuration?: boolean } = {}): 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 elapsedDurationMs = traceElapsedMs(startedAt, finishedAt ?? lastEventAt);
|
||||
const visibleTerminalDurationMs = options.preserveVisibleTerminalDuration === true ? traceElapsedMs(startedAt, new Date().toISOString()) : null;
|
||||
const durationMs = maxTraceDuration(previousTiming.durationMs, nextTiming.durationMs, elapsedDurationMs, visibleTerminalDurationMs);
|
||||
return {
|
||||
...previousTiming,
|
||||
...nextTiming,
|
||||
startedAt,
|
||||
lastEventAt,
|
||||
finishedAt,
|
||||
durationMs,
|
||||
observedAt: latestTraceTimestamp(previousTiming.observedAt, nextTiming.observedAt),
|
||||
lastEventAgeMs: null,
|
||||
valuesRedacted: previousTiming.valuesRedacted !== false && nextTiming.valuesRedacted !== false
|
||||
};
|
||||
}
|
||||
|
||||
function traceTimingCandidate(value: unknown): TraceSnapshot["timing"] | null {
|
||||
const record = traceRecord(value);
|
||||
const timing = traceRecord(record?.timing);
|
||||
if (!record && !timing) return null;
|
||||
const startedAt = firstTraceTimestamp(timing?.startedAt, record?.startedAt);
|
||||
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 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 };
|
||||
}
|
||||
|
||||
function traceRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function firstTraceTimestamp(...values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
if (typeof value !== "string") continue;
|
||||
const text = value.trim();
|
||||
if (!text) continue;
|
||||
if (Number.isFinite(Date.parse(text))) return text;
|
||||
}
|
||||
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 finiteTraceDuration(value: unknown): number | null {
|
||||
const number = Number(value);
|
||||
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() : "";
|
||||
return ["completed", "failed", "cancelled", "canceled", "error"].includes(status) || traceTimingCandidate(value)?.finishedAt != null;
|
||||
}
|
||||
|
||||
function mergeTraceEvents(previousEvents: TraceEvent[], nextEvents: TraceEvent[]): TraceEvent[] {
|
||||
@@ -298,22 +419,24 @@ 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) });
|
||||
return {
|
||||
...previous,
|
||||
...next,
|
||||
events,
|
||||
eventCount: next.eventCount ?? previous.eventCount ?? events.length,
|
||||
timing: next.timing ?? previous.timing ?? null,
|
||||
startedAt: next.startedAt ?? next.timing?.startedAt ?? previous.startedAt ?? previous.timing?.startedAt ?? null,
|
||||
lastEventAt: next.lastEventAt ?? next.timing?.lastEventAt ?? previous.lastEventAt ?? previous.timing?.lastEventAt ?? null,
|
||||
finishedAt: next.finishedAt ?? next.timing?.finishedAt ?? previous.finishedAt ?? previous.timing?.finishedAt ?? null,
|
||||
durationMs: next.durationMs ?? next.timing?.durationMs ?? previous.durationMs ?? previous.timing?.durationMs ?? null,
|
||||
timing,
|
||||
startedAt: timing?.startedAt ?? next.startedAt ?? next.timing?.startedAt ?? previous.startedAt ?? previous.timing?.startedAt ?? null,
|
||||
lastEventAt: timing?.lastEventAt ?? next.lastEventAt ?? next.timing?.lastEventAt ?? previous.lastEventAt ?? previous.timing?.lastEventAt ?? null,
|
||||
finishedAt: timing?.finishedAt ?? next.finishedAt ?? next.timing?.finishedAt ?? previous.finishedAt ?? previous.timing?.finishedAt ?? null,
|
||||
durationMs: timing?.durationMs ?? next.durationMs ?? next.timing?.durationMs ?? previous.durationMs ?? previous.timing?.durationMs ?? null,
|
||||
lastEventLabel: next.lastEventLabel ?? previous.lastEventLabel ?? undefined,
|
||||
updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
function traceSnapshotWithTurnStatus(trace: TraceSnapshot, turn: TraceSnapshot): TraceSnapshot {
|
||||
const timing = mergeTraceTimingProjection(trace, turn, { preserveVisibleTerminalDuration: traceTimingWasRunning(trace) && traceTimingIsTerminal(turn) });
|
||||
return {
|
||||
...trace,
|
||||
traceId: trace.traceId ?? turn.traceId,
|
||||
@@ -325,11 +448,11 @@ function traceSnapshotWithTurnStatus(trace: TraceSnapshot, turn: TraceSnapshot):
|
||||
terminalEvidence: trace.terminalEvidence ?? turn.terminalEvidence,
|
||||
traceSummary: trace.traceSummary ?? turn.traceSummary,
|
||||
error: trace.error ?? turn.error,
|
||||
timing: turn.timing ?? trace.timing ?? null,
|
||||
startedAt: turn.startedAt ?? turn.timing?.startedAt ?? trace.startedAt ?? trace.timing?.startedAt ?? null,
|
||||
lastEventAt: turn.lastEventAt ?? turn.timing?.lastEventAt ?? trace.lastEventAt ?? trace.timing?.lastEventAt ?? null,
|
||||
finishedAt: turn.finishedAt ?? turn.timing?.finishedAt ?? trace.finishedAt ?? trace.timing?.finishedAt ?? null,
|
||||
durationMs: turn.durationMs ?? turn.timing?.durationMs ?? trace.durationMs ?? trace.timing?.durationMs ?? null,
|
||||
timing,
|
||||
startedAt: timing?.startedAt ?? turn.startedAt ?? turn.timing?.startedAt ?? trace.startedAt ?? trace.timing?.startedAt ?? null,
|
||||
lastEventAt: timing?.lastEventAt ?? turn.lastEventAt ?? turn.timing?.lastEventAt ?? trace.lastEventAt ?? trace.timing?.lastEventAt ?? null,
|
||||
finishedAt: timing?.finishedAt ?? turn.finishedAt ?? turn.timing?.finishedAt ?? trace.finishedAt ?? trace.timing?.finishedAt ?? null,
|
||||
durationMs: timing?.durationMs ?? turn.durationMs ?? turn.timing?.durationMs ?? trace.durationMs ?? trace.timing?.durationMs ?? null,
|
||||
projection: trace.projection ?? turn.projection ?? null,
|
||||
projectionStatus: trace.projectionStatus ?? turn.projectionStatus ?? null,
|
||||
projectionHealth: trace.projectionHealth ?? turn.projectionHealth ?? null,
|
||||
|
||||
Reference in New Issue
Block a user