diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index e1e03769..2c2a1bf3 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -220,7 +220,7 @@ function traceCompletionSummaryRow(trace: Record, event: TraceE function traceNoiseSummaryRow(trace: Record, events: TraceEvent[], options: TraceDisplayRowsOptions): TraceEventRow { const lastEvent = events.at(-1); - const status = nonEmptyString(trace.status ?? trace.traceStatus ?? lastEvent?.status) ?? "running"; + const status = nonEmptyString(trace.status ?? trace.traceStatus) ?? "running"; const lastLabel = lastEvent ? readableTraceLabel(lastEvent) : "未观测"; return { rowId: "trace-noise-summary", diff --git a/web/hwlab-cloud-web/src/api/workbench.ts b/web/hwlab-cloud-web/src/api/workbench.ts index 47d75aad..6b14409b 100644 --- a/web/hwlab-cloud-web/src/api/workbench.ts +++ b/web/hwlab-cloud-web/src/api/workbench.ts @@ -191,6 +191,9 @@ function normalizeTimingProjection(value: unknown): WorkbenchTurnTimingProjectio const lastEventAt = textValue(source.lastEventAt); const finishedAt = textValue(source.finishedAt); const durationMs = numberValue(source.durationMs); + // observedAt and lastEventAgeMs are server-side diagnostic snapshots. + // They must NOT be used as the user-visible relative time authority. + // Running relative time display must use startedAt/lastEventAt + browser local now(). const observedAt = textValue(source.observedAt); const lastEventAgeMs = numberValue(source.lastEventAgeMs); if (!startedAt && !lastEventAt && !finishedAt && durationMs == null && lastEventAgeMs == null) return undefined; diff --git a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts index 4123e272..95a4209f 100644 --- a/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts +++ b/web/hwlab-cloud-web/src/composables/useTraceSubscription.ts @@ -89,7 +89,7 @@ export function snapshotToRunnerTrace(snapshot: TraceSnapshot): NonNullable { const ownerSessionId = traceOwnerSessionId(traceId, authoritySessionId); if (!ownerSessionId) return; const events = Array.isArray(result.events) ? result.events : Array.isArray(result.traceEvents) ? result.traceEvents : []; - const activityLabel = firstNonEmptyString(result.lastEventLabel, events.at(-1)?.label, events.at(-1)?.type, result.status); + const activityLabel = firstNonEmptyString(result.lastEventLabel, result.status); if (ownerSessionId === activeSessionId.value && (events.length > 0 || result.terminal === true || isTerminalMessageStatus(result.status))) recordActivity(`trace:${activityLabel ?? "hydrated"}`); markWorkbenchTraceEventsReceived({ traceId, events, transport: "rest_gap" }); updateSessionMessages(ownerSessionId, (source) => source.map((message) => { @@ -706,7 +706,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { staleMs: result.staleMs ?? result.projection?.staleMs, blocker: result.blocker ?? result.projection?.blocker, ...messageTimingPatch(result), - lastEventLabel: result.lastEventLabel ?? events.at(-1)?.label ?? events.at(-1)?.type, + lastEventLabel: result.lastEventLabel ?? undefined, updatedAt: new Date().toISOString() } as NonNullable; const runnerTrace = mergeRunnerTrace(message.runnerTrace, nextTrace); @@ -1340,7 +1340,7 @@ function realtimeSnapshotToTraceSnapshot(traceId: string, snapshot: WorkbenchRea staleMs: firstFiniteNumber(source.staleMs, recordValue(source.projection)?.staleMs), blocker: recordValue(source.blocker) ?? recordValue(recordValue(source.projection)?.blocker) ?? undefined, ...messageTimingPatch(source), - lastEventLabel: firstNonEmptyString(source.lastEventLabel, source.lastEvent?.label, source.lastEvent?.type, events.at(-1)?.label, events.at(-1)?.type) ?? undefined, + lastEventLabel: firstNonEmptyString(source.lastEventLabel) ?? undefined, eventSource: "trace-api", updatedAt: firstNonEmptyString(source.updatedAt) ?? new Date().toISOString() } as TraceSnapshot; @@ -1396,7 +1396,7 @@ function mergeTerminalResultTrace(previous: ChatMessage["runnerTrace"], result: durationMs: timing?.durationMs ?? null, runnerKind: firstNonEmptyString(agentRun?.adapter, resultTrace?.runnerKind, previous?.runnerKind) ?? undefined, sessionMode: firstNonEmptyString(agentRun?.backendProfile, resultTrace?.sessionMode, previous?.sessionMode) ?? undefined, - lastEventLabel: firstNonEmptyString(result.lastEventLabel, resultTrace?.lastEventLabel, previous?.lastEventLabel, events.at(-1)?.label, events.at(-1)?.type) ?? undefined, + lastEventLabel: firstNonEmptyString(result.lastEventLabel, resultTrace?.lastEventLabel, previous?.lastEventLabel) ?? undefined, updatedAt: new Date().toISOString() } as NonNullable; if (traceDetailStatus) nextTrace.status = traceDetailStatus;