diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 9ba3d642..b979e6de 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -1810,36 +1810,13 @@ function messageTimingPatch(value: unknown): Partial { } function messageTimingPatchForMerge(message: ChatMessage, value: unknown): Partial { - const patch = messageTimingPatch(value); - if (!patch.timing) return patch; - const terminal = timingPatchIsTerminal(value, patch.timing); - const durationMs = terminal - ? terminalTimingDurationMsForMerge(message, patch.timing) - : maxFiniteNumber(patch.timing.durationMs, message.durationMs, message.timing?.durationMs, message.runnerTrace?.durationMs, message.runnerTrace?.timing?.durationMs); - const timing = { ...patch.timing, durationMs } as WorkbenchTurnTimingProjection; - return { timing, startedAt: timing.startedAt ?? null, lastEventAt: timing.lastEventAt ?? null, finishedAt: timing.finishedAt ?? null, durationMs }; -} - -function terminalTimingDurationMsForMerge(message: ChatMessage, timing: WorkbenchTurnTimingProjection): number | null { - const elapsed = elapsedTimingDurationMs(timing); - const maximum = maxFiniteNumber(timing.durationMs, elapsed, message.durationMs, message.timing?.durationMs, message.runnerTrace?.durationMs, message.runnerTrace?.timing?.durationMs); - if (maximum !== null) return maximum; - return firstFiniteNumber(timing.durationMs, elapsed, message.durationMs, message.timing?.durationMs, message.runnerTrace?.durationMs, message.runnerTrace?.timing?.durationMs); -} - -function timingPatchIsTerminal(value: unknown, timing: WorkbenchTurnTimingProjection): boolean { - const record = recordValue(value); - const trace = recordValue(record?.runnerTrace); - const status = normalizedStatusText(record?.status ?? record?.traceStatus ?? trace?.status ?? trace?.traceStatus); - return Boolean(timing.finishedAt || isTerminalMessageStatus(status)); -} - -function elapsedTimingDurationMs(timing: WorkbenchTurnTimingProjection): number | null { - const start = timing.startedAt ? Date.parse(timing.startedAt) : NaN; - const endedAt = timing.finishedAt ?? timing.lastEventAt; - const end = endedAt ? Date.parse(endedAt) : NaN; - if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null; - return Math.trunc(end - start); + void value; + // 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. + return messageTimingPatch(message); } function firstPositiveFiniteNumber(...values: unknown[]): number | null {