fix: seal workbench final response as message part (#2082)

This commit is contained in:
Lyon
2026-06-25 04:47:20 +08:00
committed by GitHub
parent abfd742bee
commit 19eee3b734
2 changed files with 99 additions and 24 deletions
@@ -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";
+37 -23
View File
@@ -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,