fix: seal workbench final response as message part (#2082)
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user