feat: PJ2026-0104010803 P4 前端/CLI 单读源收敛 - 删除 events.at(-1) 读侧推理 (#1758)

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
This commit is contained in:
Lyon
2026-06-20 19:37:35 +08:00
committed by GitHub
parent 9574e2b514
commit 44019f229e
4 changed files with 9 additions and 6 deletions
+3
View File
@@ -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;
@@ -89,7 +89,7 @@ export function snapshotToRunnerTrace(snapshot: TraceSnapshot): NonNullable<Chat
eventSource: snapshot.eventSource ?? "trace-api",
runnerKind: snapshot.agentRun?.adapter,
sessionMode: snapshot.agentRun?.backendProfile,
lastEventLabel: snapshot.lastEventLabel ?? events.at(-1)?.label ?? events.at(-1)?.type,
lastEventLabel: snapshot.lastEventLabel ?? undefined,
waitingFor: snapshot.waitingFor,
timing: snapshot.timing ?? null,
startedAt: snapshot.startedAt ?? snapshot.timing?.startedAt ?? null,
+4 -4
View File
@@ -673,7 +673,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
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<ChatMessage["runnerTrace"]>;
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<ChatMessage["runnerTrace"]>;
if (traceDetailStatus) nextTrace.status = traceDetailStatus;