From 10e892ce95d33fcedfa9b17f3fd765df37ba6cb2 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:21:21 +0800 Subject: [PATCH] fix(workbench): preserve terminal final text and timing (#2042) --- internal/cloud/workbench-projection-writer.ts | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/internal/cloud/workbench-projection-writer.ts b/internal/cloud/workbench-projection-writer.ts index 00177896..8f357048 100644 --- a/internal/cloud/workbench-projection-writer.ts +++ b/internal/cloud/workbench-projection-writer.ts @@ -117,7 +117,7 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event // switching to a different clock origin. const startedAt = previousTiming?.startedAt ?? explicitStartedAt ?? (sourceSeq <= 1 ? occurredAt : null); const lastEventAt = latestTimestamp(previousTiming?.lastEventAt, occurredAt); - const finishedAt = terminal ? latestTimestamp(previousTiming?.lastEventAt, optionalTimestampValue(event.finishedAt), occurredAt) : null; + const finishedAt = terminal ? latestTimestamp(previousTiming?.lastEventAt, optionalTimestampValue(event.finishedAt), occurredAt, projectedAt) : null; const timing = eventTimingProjection({ startedAt, lastEventAt, finishedAt, terminal }); const eventTiming = suppressedAfterSeal ? previousTiming ?? timing : timing; const sourceEventId = workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt }); @@ -141,7 +141,9 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event const projectedSeq = nonNegativeInteger(allocation?.projectedSeq); 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 previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText); + const messageFinalText = finalResponseTextValue(event.finalResponse, event.assistantText, event.reply, previousFinalText); + const checkpointFinalResponse = messageFinalText ? { text: messageFinalText, status, traceId, valuesPrinted: false } : null; const messageFact = sessionId && !suppressedAfterSeal ? normalizeMessageFact({ messageId: eventProjectionAssistantMessageId(traceId, event), role: "agent", @@ -249,7 +251,9 @@ function eventProjectionAssistantMessageId(traceId, event = {}) { async function writeWorkbenchProjectionFacts({ runtimeStore = null, traceStore = defaultCodeAgentTraceStore, traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {} } = {}) { if (typeof runtimeStore?.writeWorkbenchFacts !== "function") return null; - const facts = buildWorkbenchProjectionFacts({ traceId, ownerUserId, ownerRole, sessionId, projectId, conversationId, threadId, status, session, payload, params }); + const safeId = safeTraceId(traceId ?? session?.traceId ?? payload?.traceId ?? params?.traceId); + const previousCheckpoint = safeId ? await latestWorkbenchCheckpoint(runtimeStore, safeId) : null; + const facts = buildWorkbenchProjectionFacts({ traceId, ownerUserId, ownerRole, sessionId, projectId, conversationId, threadId, status, session, payload, params, previousCheckpoint }); if (isEmptyFacts(facts)) return null; try { return await runtimeStore.writeWorkbenchFacts({ facts }, { @@ -273,7 +277,7 @@ async function writeWorkbenchProjectionFacts({ runtimeStore = null, traceStore = } } -function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {} } = {}) { +function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {}, previousCheckpoint = null } = {}) { const safeId = safeTraceId(traceId ?? session?.traceId ?? payload?.traceId ?? params?.traceId); const resolvedSessionId = textValue(sessionId ?? session?.sessionId ?? payload?.sessionId ?? params?.sessionId) || null; const resolvedConversationId = textValue(conversationId ?? session?.conversationId ?? payload?.conversationId ?? params?.conversationId) || null; @@ -285,9 +289,12 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own const projectedStatus = normalizeWorkbenchStatus(projection.status ?? normalizedStatus); const terminal = projection.terminal === true; const terminalStatus = terminal ? (TERMINAL_STATUSES.has(projectedStatus) ? projectedStatus : normalizedStatus) : projectedStatus; - const timing = projectionTimingForStatus(projection.timing, terminal); + const projectedAt = new Date().toISOString(); + const timing = terminalTimingAtLeastProjectedAt(projectionTimingForStatus(projection.timing, terminal), terminal, projectedAt); const diagnostic = projectionDiagnostics({ traceId: safeId, result: payload, trace: payload?.runnerTrace ?? null, projection }); - const finalText = finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText); + const previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText); + const finalText = finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText, previousFinalText); + const projectedFinalResponse = finalText ? { text: finalText, status: terminalStatus, traceId: safeId, valuesPrinted: false } : projection.finalResponse ?? null; const messages = normalizeMessages(workbenchProjectionInputMessages({ session, payload, params, traceId: safeId, timestamp, terminal, terminalStatus, finalText }), { traceId: safeId, sessionId: resolvedSessionId, @@ -352,8 +359,10 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own sourceEventId: safeId, terminal: projection.terminal, sealed: projection.terminal, - finalResponse: projection.finalResponse, + finalResponse: projectedFinalResponse, diagnostic, + finalResponse: checkpointFinalResponse, + assistantText: messageFinalText, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, @@ -375,6 +384,8 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own terminal: projection.terminal, sealed: projection.terminal, diagnostic, + finalResponse: projectedFinalResponse, + assistantText: finalText, timing, startedAt: timing.startedAt, lastEventAt: timing.lastEventAt, @@ -503,6 +514,18 @@ function projectionTimingForStatus(value, terminal) { return { ...timing, finishedAt: null, durationMs: null, valuesRedacted: timing.valuesRedacted !== false }; } +function terminalTimingAtLeastProjectedAt(value, terminal, projectedAt) { + const timing = normalizeTimingProjection(value) ?? emptyTimingProjection(); + if (!terminal) return timing; + const finishedAt = latestTimestamp(timing.finishedAt, projectedAt); + return { + ...timing, + finishedAt, + durationMs: elapsedBetween(timing.startedAt, finishedAt) ?? timing.durationMs, + valuesRedacted: timing.valuesRedacted !== false + }; +} + function checkpointIsTerminal(value) { const source = value && typeof value === "object" ? value : null; if (!source) return false;