From 44019f229e9052d5b0d298956aea2b557e5ada23 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:37:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20PJ2026-0104010803=20P4=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF/CLI=20=E5=8D=95=E8=AF=BB=E6=BA=90=E6=94=B6=E6=95=9B?= =?UTF-8?q?=20-=20=E5=88=A0=E9=99=A4=20events.at(-1)=20=E8=AF=BB=E4=BE=A7?= =?UTF-8?q?=E6=8E=A8=E7=90=86=20(#1758)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit workbench.ts (stores): - applyTraceHydrationResult: activityLabel 只读 result.lastEventLabel,不读 events.at(-1) - lastEventLabel: 只读 result.lastEventLabel,不 fallback 到 events.at(-1) - normalizeTraceSnapshot: lastEventLabel 只读 source.lastEventLabel - mergeRunnerTrace: lastEventLabel 不从 events.at(-1) 推断 useTraceSubscription.ts: - lastEventLabel 只读 snapshot.lastEventLabel,不读 events.at(-1) trace-renderer.ts (CLI): - traceNoiseSummaryRow: status 只读 trace.status/traceStatus,不 fallback 到 lastEvent.status - 保持 lastLabel 用于展示,但不参与 lifecycle 推断 api/workbench.ts: - 添加注释标记 observedAt/lastEventAgeMs 为 diagnostic-only,不作用户可见相对时间权威 Closes #1750 Refs #1742 --- tools/src/hwlab-cli/trace-renderer.ts | 2 +- web/hwlab-cloud-web/src/api/workbench.ts | 3 +++ .../src/composables/useTraceSubscription.ts | 2 +- web/hwlab-cloud-web/src/stores/workbench.ts | 8 ++++---- 4 files changed, 9 insertions(+), 6 deletions(-) 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;