fix: keep terminal completion trace sealed (#2122)

This commit is contained in:
Lyon
2026-06-25 13:32:50 +08:00
committed by GitHub
parent f70be64381
commit c474a19de6
4 changed files with 49 additions and 16 deletions
+4 -2
View File
@@ -23,6 +23,7 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
const finalResponseFingerprint = traceMessageFingerprint(finalResponseText);
const hasCompletionEvent = orderedEvents.some(isCompletionTraceEvent);
const rows: TraceEventRow[] = [];
let completionEvent: TraceEvent | null = null;
const renderedSourceEvents = new Set<string>();
const renderedToolIdentities = new Set<string>();
for (let index = 0; index < orderedEvents.length; index += 1) {
@@ -50,8 +51,8 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
continue;
}
if (isTerminalAssistantTraceEvent(event)) {
if (hasCompletionEvent) continue;
if (finalResponseText) {
if (hasCompletionEvent) continue;
rows.push(traceFinalResponseRow(event, finalResponseText, options));
} else {
rows.push(traceAssistantMessageRow(effectiveTrace, event, { terminal: true }, options));
@@ -64,11 +65,12 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
continue;
}
if (isCompletionTraceEvent(event)) {
rows.push(finalResponseText ? traceFinalResponseRow(event, finalResponseText, options) : traceCompletionSummaryRow(effectiveTrace, event, options));
completionEvent ??= event;
continue;
}
rows.push(traceDisplayRow(effectiveTrace, event, options));
}
if (completionEvent) rows.push(traceCompletionSummaryRow(effectiveTrace, completionEvent, options));
const progressRow = traceBackendProgressSummaryRow(effectiveTrace, rows, orderedEvents, options);
if (progressRow) rows.push(progressRow);
if (rows.length > 0) return rows;
@@ -60,7 +60,7 @@ test("R1 shared trace renderer starts at the first user-meaningful event", () =>
{ label: "agentrun:request:accepted", status: "accepted" },
{ label: "agentrun:runner-job:created", status: "running", message: "runner created" },
{ label: "agentrun:backend:resource-bundle-materialized", status: "running", source: "agentrun", sourceSeq: 1, message: "bundle" },
{ label: "agentrun:assistant:message", type: "assistant", message: "OK", source: "agentrun", sourceSeq: 2 }
{ projectedSeq: 2, label: "agentrun:assistant:message", type: "assistant", message: "OK", source: "agentrun", sourceSeq: 2 }
]);
assert.equal(rows.length, 1);
assert.equal(rows[0]?.body, "OK");
@@ -71,7 +71,7 @@ test("R1 shared trace renderer suppresses AgentRun reuse diagnostics", () => {
const rows = traceDisplayRows({ traceId: "trc_reuse", status: "running" }, [
{ label: "agentrun:run:reused", status: "reused", message: "run reused" },
{ label: "agentrun:runner-job:ensured", status: "runner-job-ensured", message: "runner ensured" },
{ label: "agentrun:assistant:message", type: "assistant", message: "OK", source: "agentrun", sourceSeq: 3 }
{ projectedSeq: 3, label: "agentrun:assistant:message", type: "assistant", message: "OK", source: "agentrun", sourceSeq: 3 }
]);
assert.equal(rows.length, 1);
assert.equal(rows[0]?.body, "OK");
@@ -80,9 +80,9 @@ test("R1 shared trace renderer suppresses AgentRun reuse diagnostics", () => {
test("R1 shared trace renderer keeps terminal assistant at its own event position", () => {
const rows = traceDisplayRows({ traceId: "trc_terminal_position", status: "completed", startedAt: "2026-06-20T12:00:00.000Z" }, [
{ seq: 1, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "FINAL", createdAt: "2026-06-20T12:00:01.000Z" },
{ seq: 2, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "make test", createdAt: "2026-06-20T12:00:02.000Z" },
{ seq: 3, label: "agentrun:assistant:message", type: "assistant", status: "completed", message: "FINAL", final: true, replyAuthority: true, terminal: true, createdAt: "2026-06-20T12:00:03.000Z" }
{ seq: 1, projectedSeq: 1, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "FINAL", createdAt: "2026-06-20T12:00:01.000Z" },
{ seq: 2, projectedSeq: 2, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "make test", createdAt: "2026-06-20T12:00:02.000Z" },
{ seq: 3, projectedSeq: 3, label: "agentrun:assistant:message", type: "assistant", status: "completed", message: "FINAL", final: true, replyAuthority: true, terminal: true, createdAt: "2026-06-20T12:00:03.000Z" }
]);
const toolIndex = rows.findIndex((row) => row.seq === 2);
const finalIndex = rows.findIndex((row) => row.seq === 3 && row.terminal === true && row.body === "FINAL");
@@ -91,21 +91,24 @@ test("R1 shared trace renderer keeps terminal assistant at its own event positio
assert.equal(rows.some((row) => row.seq === 1 && row.terminal === true), false);
});
test("R1 shared trace renderer keeps completion final response at completion event position", () => {
test("R1 shared trace renderer keeps completion row last when final response is separate", () => {
const rows = traceDisplayRows({ traceId: "trc_completion_position", status: "completed", startedAt: "2026-06-20T12:10:00.000Z", finalResponse: { text: "FINAL" } }, [
{ seq: 1, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "FINAL", createdAt: "2026-06-20T12:10:01.000Z" },
{ seq: 2, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "make test", createdAt: "2026-06-20T12:10:02.000Z" },
{ seq: 3, label: "agentrun:result:completed", type: "result", status: "completed", terminal: true, createdAt: "2026-06-20T12:10:03.000Z" }
{ seq: 1, projectedSeq: 1, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "FINAL", createdAt: "2026-06-20T12:10:01.000Z" },
{ seq: 2, projectedSeq: 2, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "make test", createdAt: "2026-06-20T12:10:02.000Z" },
{ seq: 3, projectedSeq: 3, label: "agentrun:result:completed", type: "result", status: "completed", terminal: true, createdAt: "2026-06-20T12:10:03.000Z" },
{ seq: 4, projectedSeq: 4, label: "agentrun:assistant:message", type: "assistant", status: "completed", message: "FINAL", final: true, replyAuthority: true, terminal: true, createdAt: "2026-06-20T12:10:04.000Z" }
]);
const toolIndex = rows.findIndex((row) => row.seq === 2);
const finalIndex = rows.findIndex((row) => row.seq === 3 && row.terminal === true && row.body === "FINAL");
const completionIndex = rows.findIndex((row) => row.seq === 3 && row.rowId.startsWith("trace-completion:"));
assert.ok(toolIndex >= 0);
assert.ok(finalIndex > toolIndex);
assert.equal(completionIndex, rows.length - 1);
assert.ok(completionIndex > toolIndex);
assert.equal(rows.some((row) => row.seq === 4), false);
assert.equal(rows.some((row) => row.seq === 1 && row.terminal === true), false);
});
test("R1 shared trace renderer keeps UTC by default and lets Web supply display clock", () => {
const events = [{ seq: 1, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "date", createdAt: "2026-06-17T06:34:14.586Z" }];
const events = [{ seq: 1, projectedSeq: 1, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "date", createdAt: "2026-06-17T06:34:14.586Z" }];
const defaultRows = traceDisplayRows({ traceId: "trc_clock" }, events);
const displayRows = traceDisplayRows({ traceId: "trc_clock" }, events, {
formatClock: (value) => new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).format(new Date(value))
@@ -130,3 +130,29 @@ test("Workbench realtime agent snapshots cannot create duplicate assistant messa
assert.deepEqual(selectActiveMessages(state, "ses_steer").map((message) => message.messageId), ["msg_user", "msg_agent"]);
});
test("Workbench session messages keep sealed terminal timing on bulk refresh", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_terminal_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal", timing: { startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:12.000Z", durationMs: 12_000, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:12.000Z", durationMs: 12_000 }
]
});
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId: "ses_terminal_seal",
messages: [
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal" },
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_terminal_seal", traceId: "trc_terminal_seal", timing: { startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:13.000Z", durationMs: 13_000, valuesRedacted: true }, startedAt: "2026-06-24T00:00:00.000Z", finishedAt: "2026-06-24T00:00:13.000Z", durationMs: 13_000 }
]
});
const agent = selectActiveMessages(state, "ses_terminal_seal").find((message) => message.role === "agent");
assert.equal(agent?.durationMs, 12_000);
assert.equal(agent?.timing?.durationMs, 12_000);
assert.equal(agent?.finishedAt, "2026-06-24T00:00:12.000Z");
});
@@ -136,11 +136,13 @@ function reduceSessionListItem(state: WorkbenchServerState, session: WorkbenchSe
function reduceSessionMessages(state: WorkbenchServerState, sessionId: string | null | undefined, messages: ChatMessage[]): WorkbenchServerState {
if (!sessionId) return state;
const existing = state.sessionsById[sessionId];
const session = existing ? { ...existing, messages, messageCount: messages.length } : null;
const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages ?? [];
const mergedMessages = mergeMessageList(existingMessages, messages);
const session = existing ? { ...existing, messages: mergedMessages, messageCount: mergedMessages.length } : null;
return {
...state,
sessionsById: session ? { ...state.sessionsById, [sessionId]: session } : state.sessionsById,
messagesBySessionId: { ...state.messagesBySessionId, [sessionId]: messages }
messagesBySessionId: { ...state.messagesBySessionId, [sessionId]: mergedMessages }
};
}