fix(workbench): keep sealed turn timing immutable (#2033)

This commit is contained in:
Lyon
2026-06-24 09:25:31 +08:00
committed by GitHub
parent 6e3d6f8aa6
commit 8274a31f37
+21 -9
View File
@@ -108,11 +108,14 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event
const status = terminal ? normalizeWorkbenchStatus(event.status) : "running";
const previousCheckpoint = await latestWorkbenchCheckpoint(runtimeStore, traceId);
const previousTiming = normalizeTimingProjection(previousCheckpoint?.timing) ?? normalizeTimingProjection(previousCheckpoint);
const previousTerminal = checkpointIsTerminal(previousCheckpoint);
const suppressedAfterSeal = previousTerminal && !terminal;
const explicitStartedAt = optionalTimestampValue(event.startedAt ?? event.traceStartedAt ?? event.runnerStartedAt);
const startedAt = explicitStartedAt ?? previousTiming?.startedAt ?? (sourceSeq <= 1 ? occurredAt : null);
const lastEventAt = latestTimestamp(previousTiming?.lastEventAt, occurredAt);
const finishedAt = terminal ? latestTimestamp(previousTiming?.lastEventAt, optionalTimestampValue(event.finishedAt), occurredAt) : null;
const timing = eventTimingProjection({ startedAt, lastEventAt, finishedAt, terminal });
const eventTiming = suppressedAfterSeal ? previousTiming ?? timing : timing;
const sourceEventId = workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt });
const allocation = await runtimeStore.allocateWorkbenchProjectedSeq({
traceId,
@@ -135,7 +138,7 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event
if (projectedSeq <= 0) throw new Error("Workbench projection allocator returned an invalid projectedSeq.");
const eventId = textValue(event.id) || stableFactId("wte", { traceId, sourceEventId });
const messageFinalText = finalResponseTextValue(event.finalResponse, event.assistantText, event.reply);
const messageFact = sessionId ? normalizeMessageFact({
const messageFact = sessionId && !suppressedAfterSeal ? normalizeMessageFact({
messageId: eventProjectionAssistantMessageId(traceId, event),
role: "agent",
status,
@@ -157,7 +160,7 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event
source: "workbench-event-projection"
}, 0, { traceId, sessionId, turnId, terminal, terminalStatus: status, finalText: messageFinalText, timestamp: projectedAt, timing }) : null;
const facts = {
sessions: sessionId ? [{
sessions: sessionId && !suppressedAfterSeal ? [{
sessionId,
status,
lastTraceId: traceId,
@@ -186,16 +189,17 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event
eventType: textValue(event.eventType ?? event.type ?? event.label) || "event",
terminal,
sealed: terminal,
timing,
startedAt: timing.startedAt,
lastEventAt: timing.lastEventAt,
finishedAt: timing.finishedAt,
durationMs: timing.durationMs,
timing: eventTiming,
startedAt: eventTiming?.startedAt ?? null,
lastEventAt: eventTiming?.lastEventAt ?? null,
finishedAt: eventTiming?.finishedAt ?? null,
durationMs: eventTiming?.durationMs ?? null,
suppressedAfterSeal,
occurredAt,
updatedAt: projectedAt
}],
messages: messageFact ? [messageFact] : [],
checkpoints: [{
checkpoints: !suppressedAfterSeal ? [{
traceId,
sessionId,
turnId,
@@ -219,7 +223,7 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event
valuesRedacted: true
},
updatedAt: projectedAt
}]
}] : []
};
return runtimeStore.writeWorkbenchFacts({ facts }, {
traceId,
@@ -495,6 +499,14 @@ function projectionTimingForStatus(value, terminal) {
return { ...timing, finishedAt: null, durationMs: null, valuesRedacted: timing.valuesRedacted !== false };
}
function checkpointIsTerminal(value) {
const source = value && typeof value === "object" ? value : null;
if (!source) return false;
if (source.terminal === true || source.sealed === true) return true;
const status = normalizeWorkbenchStatus(source.status ?? source.terminalStatus ?? source.traceStatus);
return TERMINAL_STATUSES.has(status);
}
function eventTimingProjection({ startedAt = null, lastEventAt = null, finishedAt = null, terminal = false } = {}) {
const normalizedStartedAt = optionalTimestampValue(startedAt);
const normalizedLastEventAt = optionalTimestampValue(lastEventAt);