fix: keep terminal completion trace sealed (#2122)
This commit is contained in:
@@ -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 }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user