From 19eee3b734f282bfe49d782e1806249e810a5dba Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Thu, 25 Jun 2026 04:47:20 +0800 Subject: [PATCH] fix: seal workbench final response as message part (#2082) --- .../cloud/workbench-projection-writer.test.ts | 63 ++++++++++++++++++- internal/cloud/workbench-projection-writer.ts | 60 +++++++++++------- 2 files changed, 99 insertions(+), 24 deletions(-) diff --git a/internal/cloud/workbench-projection-writer.test.ts b/internal/cloud/workbench-projection-writer.test.ts index 3d56938c..d04a3698 100644 --- a/internal/cloud/workbench-projection-writer.test.ts +++ b/internal/cloud/workbench-projection-writer.test.ts @@ -56,7 +56,7 @@ test("workbench projection writer commits terminal owner evidence as sealed dura finishedAt: "2026-06-20T11:00:00.000Z", elapsedMs: 30000 }, - agentRun: { runId: "run_writer_terminal", commandId: "cmd_writer_terminal", lastSeq: 42 }, + agentRun: { runId: "run_writer_terminal", commandId: "cmd_writer_terminal", status: "completed", terminalStatus: "completed", lastSeq: 42 }, updatedAt: "2026-06-20T11:00:00.000Z" }, session: { @@ -77,6 +77,7 @@ test("workbench projection writer commits terminal owner evidence as sealed dura assert.equal(facts.messages.length, 2); assert.equal(facts.messages.find((message) => message.messageId === "msg_writer_agent").sealed, true); assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_agent" && part.text === "final answer" && part.sealed === true), true); + assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_agent" && part.partType === "final_response" && part.text === "final answer" && part.sealed === true), true); assert.equal(facts.turns[0].terminal, true); assert.equal(facts.turns[0].sealed, true); assert.equal(facts.turns[0].finalResponse.text, "final answer"); @@ -89,6 +90,66 @@ test("workbench projection writer commits terminal owner evidence as sealed dura assert.equal(facts.checkpoints[0].timing.finishedAt, "2026-06-20T11:00:00.000Z"); }); +test("workbench projection writer does not seal running assistant text as final response", async () => { + const factWrites = []; + const runtimeStore = { + async writeWorkbenchFacts(params, requestMeta) { + factWrites.push({ params, requestMeta }); + return { written: true, facts: params.facts }; + } + }; + const accessController = { + async recordAgentSessionOwner(input) { + return { + id: input.sessionId, + projectId: input.projectId, + ownerUserId: input.ownerUserId, + conversationId: input.conversationId, + threadId: input.threadId, + lastTraceId: input.traceId, + status: input.status, + session: input.session, + updatedAt: "2026-06-20T11:05:00.000Z" + }; + } + }; + + await writeWorkbenchProjectionSession({ + accessController, + runtimeStore, + traceId: "trc_writer_running", + ownerUserId: "usr_writer", + ownerRole: "user", + sessionId: "ses_writer_running", + projectId: "prj_writer", + conversationId: "cnv_writer_running", + threadId: "thread-writer-running", + status: "running", + payload: { + traceId: "trc_writer_running", + status: "running", + assistantText: "backend diagnostic text should not become final", + updatedAt: "2026-06-20T11:05:00.000Z" + }, + session: { + sessionStatus: "running", + messages: [ + { messageId: "msg_writer_running_user", role: "user", text: "question", status: "sent", turnId: "trc_writer_running", traceId: "trc_writer_running" }, + { messageId: "msg_writer_running_agent", role: "agent", text: "", status: "running", turnId: "trc_writer_running", traceId: "trc_writer_running" } + ] + } + }); + + assert.equal(factWrites.length, 1); + const facts = factWrites[0].params.facts; + const agentMessage = facts.messages.find((message) => message.messageId === "msg_writer_running_agent"); + assert.equal(agentMessage.text, ""); + assert.equal(facts.parts.some((part) => part.messageId === "msg_writer_running_agent"), false); + assert.equal(facts.turns[0].terminal, false); + assert.equal(facts.turns[0].finalResponse, null); + assert.equal(facts.turns[0].assistantText, null); +}); + test("workbench projection event commit writes trace event and checkpoint facts", async () => { const factWrites = []; let now = "2026-06-20T11:01:05.000Z"; diff --git a/internal/cloud/workbench-projection-writer.ts b/internal/cloud/workbench-projection-writer.ts index 35cde634..f4ddfdd2 100644 --- a/internal/cloud/workbench-projection-writer.ts +++ b/internal/cloud/workbench-projection-writer.ts @@ -128,7 +128,7 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event // startedAt values, but it must not make the visible Code Agent card jump by // switching to a different clock origin. const provisionalLastEventAt = latestTimestamp(previousTiming?.lastEventAt, explicitEventTiming?.lastEventAt, occurredAt); - const provisionalFinishedAt = terminal ? latestTimestamp(explicitEventTiming?.finishedAt, optionalTimestampValue(event.finishedAt), provisionalLastEventAt, occurredAt, projectedAt) : null; + const provisionalFinishedAt = terminal ? latestTimestamp(explicitEventTiming?.finishedAt, optionalTimestampValue(event.finishedAt), provisionalLastEventAt, occurredAt) : null; const startedAt = previousTiming?.startedAt ?? explicitStartedAt ?? startedAtFromDuration(provisionalFinishedAt, explicitDurationMs) ?? (sourceSeq <= 1 ? occurredAt : null); const lastEventAt = provisionalLastEventAt; const finishedAt = terminal ? latestTimestamp(provisionalFinishedAt, lastEventAt) : null; @@ -163,8 +163,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 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 messageFinalText = terminal ? finalResponseTextValue(event.finalResponse, event.assistantText, event.reply, previousFinalText) : null; const userMessageFact = sessionId && !suppressedAfterSeal && previousCheckpoint?.userMessage ? normalizeMessageFact(previousCheckpoint.userMessage, 0, { traceId, sessionId, turnId, terminal: false, terminalStatus: "sent", finalText: null, timestamp: projectedAt, timing }) : null; const messageFact = sessionId && !suppressedAfterSeal ? normalizeMessageFact({ messageId: eventProjectionAssistantMessageId(traceId, event), @@ -229,6 +228,7 @@ export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event updatedAt: projectedAt }], messages: [userMessageFact, messageFact].filter(Boolean), + parts: [userMessageFact, messageFact].filter(Boolean).flatMap((message) => messagePartFacts(message, { finalText: message === messageFact ? messageFinalText : null })), checkpoints: !suppressedAfterSeal ? [{ traceId, sessionId, @@ -373,8 +373,8 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own ? { ...baseDiagnostic, projectionHealth: baseDiagnostic.projectionHealth === "unavailable" ? "unavailable" : "degraded", blocker: baseDiagnostic.blocker ?? timingAuthorityIssue, timingAuthorityIssue, valuesRedacted: true } : baseDiagnostic; 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 finalText = terminal ? finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText, previousFinalText) : null; + const projectedFinalResponse = terminal && finalText ? { text: finalText, status: terminalStatus, traceId: safeId, valuesPrinted: false } : null; const messages = normalizeMessages(workbenchProjectionInputMessages({ session, payload, params, traceId: safeId, timestamp, terminal, terminalStatus, finalText }), { traceId: safeId, sessionId: resolvedSessionId, @@ -411,23 +411,7 @@ function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, own updatedAt: timestamp }] : [], messages, - parts: messages.flatMap((message) => message.text ? [{ - partId: stableFactId("wpt", { messageId: message.messageId, index: 0 }), - messageId: message.messageId, - sessionId: message.sessionId, - turnId: message.turnId, - traceId: message.traceId, - partIndex: 0, - partType: "text", - status: message.status, - projectedSeq: message.projectedSeq, - sourceSeq: message.sourceSeq, - sourceEventId: message.sourceEventId, - terminal: message.terminal, - sealed: message.sealed, - text: message.text, - updatedAt: message.updatedAt - }] : []), + parts: messages.flatMap((message) => messagePartFacts(message, { finalText: message.role !== "user" ? finalText : null })), turns: safeId && resolvedSessionId ? [{ turnId: projection.turnId ?? safeId, sessionId: resolvedSessionId, @@ -562,6 +546,36 @@ function normalizeMessages(messages, context) { return normalized; } +function messagePartFacts(message = {}, { finalText = null } = {}) { + const messageId = textValue(message.messageId); + if (!messageId || !message.sessionId) return []; + const role = textValue(message.role) || "agent"; + const terminal = message.terminal === true; + const finalResponseText = role !== "user" && terminal ? finalResponseTextValue(finalText, message.finalResponse) : null; + const text = finalResponseText ?? textValue(message.text); + if (!text) return []; + const partType = finalResponseText ? "final_response" : "text"; + return [{ + partId: stableFactId("wpt", { messageId, index: 0, partType }), + messageId, + sessionId: message.sessionId, + turnId: message.turnId, + traceId: message.traceId, + partIndex: 0, + partType, + status: message.status, + projectedSeq: message.projectedSeq, + sourceSeq: message.sourceSeq, + sourceEventId: message.sourceEventId, + terminal, + sealed: finalResponseText ? true : message.sealed, + text, + createdAt: message.createdAt, + updatedAt: message.updatedAt, + valuesPrinted: false + }]; +} + function normalizeMessageFact(message = {}, index, { traceId, sessionId, conversationId, threadId, terminal, terminalStatus = "completed", finalText = null, timestamp, timing: contextTiming }) { const messageId = textValue(message.messageId ?? message.id) || stableFactId("msg", { traceId, index, role: message.role }); if (!sessionId || !messageId) return null; @@ -632,7 +646,7 @@ function projectionTimingForStatus(value, terminal) { function terminalTimingAtLeastProjectedAt(value, terminal, projectedAt) { const timing = normalizeTimingProjection(value) ?? emptyTimingProjection(); if (!terminal) return timing; - const finishedAt = latestTimestamp(timing.finishedAt, projectedAt); + const finishedAt = timing.finishedAt ?? timing.lastEventAt ?? optionalTimestampValue(projectedAt); return { ...timing, finishedAt,