fix: align workbench terminal authorities

This commit is contained in:
UniDesk Codex
2026-07-01 21:18:37 +08:00
parent 124e20cd47
commit 6b40275ea9
6 changed files with 74 additions and 15 deletions
@@ -353,6 +353,9 @@ test("workbench projection writer seals failed AgentRun turns with failure final
assert.equal(facts.turns[0].terminal, true);
assert.equal(facts.turns[0].sealed, true);
assert.match(facts.turns[0].finalResponse.text, /provider-stream-disconnected/u);
assert.equal(facts.turns[0].diagnostic.projectionStatus, "caught-up");
assert.equal(facts.turns[0].diagnostic.projectionHealth, "healthy");
assert.equal(facts.turns[0].diagnostic.blocker, null);
assert.equal(facts.checkpoints[0].projectionStatus, "caught_up");
assert.equal(facts.checkpoints[0].terminal, true);
assert.equal(facts.checkpoints[0].sealed, true);
+9 -9
View File
@@ -119,22 +119,22 @@ export function createWorkbenchTurnTimingProjection({ result = null, session = n
export function projectionDiagnostics({ traceId = null, projection = null, result = null, trace = null, refreshError = null } = {}) {
const turn = projection ?? createWorkbenchTurnProjection({ traceId, result, trace });
const sealedCompleted = turn.terminal === true && turn.status === "completed" && Boolean(turn.finalResponse?.text);
const sealedTerminal = turn.terminal === true && Boolean(turn.finalResponse?.text);
const waitingFor = textValue(turn.waitingFor) || null;
const source = sealedCompleted ? null : projectionDiagnosticSource(trace);
const rawBlocker = sealedCompleted ? null : refreshError ?? source?.blocker ?? trace?.blocker ?? result?.blocker ?? result?.error ?? null;
const retryingProviderInterruption = !sealedCompleted && turn.running === true ? retryableProviderInterruptionEvidence(rawBlocker) : null;
const source = sealedTerminal ? null : projectionDiagnosticSource(trace);
const rawBlocker = sealedTerminal ? null : refreshError ?? source?.blocker ?? trace?.blocker ?? result?.blocker ?? result?.error ?? null;
const retryingProviderInterruption = !sealedTerminal && turn.running === true ? retryableProviderInterruptionEvidence(rawBlocker) : null;
const blocker = retryingProviderInterruption ? null : rawBlocker;
const hasProjectionInput = hasTraceProjection(trace) || Boolean(result || result?.agentRun);
const sourceStatus = sealedCompleted ? null : normalizeProjectionStatus(source?.projectionStatus ?? trace?.projectionStatus);
const sourceStatus = sealedTerminal ? null : normalizeProjectionStatus(source?.projectionStatus ?? trace?.projectionStatus);
const effectiveSourceStatus = retryingProviderInterruption && sourceStatus === "blocked" ? "projecting" : sourceStatus;
const status = sealedCompleted ? "caught-up" : waitingFor ? "projecting" : effectiveSourceStatus ?? (blocker ? "blocked" : turn.terminal ? "caught-up" : hasProjectionInput ? "projecting" : "unknown");
const status = sealedTerminal ? "caught-up" : waitingFor ? "projecting" : effectiveSourceStatus ?? (blocker ? "blocked" : turn.terminal ? "caught-up" : hasProjectionInput ? "projecting" : "unknown");
const diagnostic = blocker ? diagnosticBlocker(blocker) : null;
const sourceHealth = sealedCompleted ? null : normalizeProjectionHealth(source?.projectionHealth ?? trace?.projectionHealth);
const sourceHealth = sealedTerminal ? null : normalizeProjectionHealth(source?.projectionHealth ?? trace?.projectionHealth);
const effectiveSourceHealth = retryingProviderInterruption && (sourceHealth === "degraded" || sourceHealth === "unavailable" || sourceHealth === "stalled") ? "projecting" : sourceHealth;
const projectionHealth = sealedCompleted ? "healthy" : waitingFor ? "projecting" : effectiveSourceHealth
const projectionHealth = sealedTerminal ? "healthy" : waitingFor ? "projecting" : effectiveSourceHealth
?? projectionHealthFor({ status, turn, hasProjectionInput, blocker: diagnostic });
const staleMs = sealedCompleted ? null : projectionStaleMs(source?.staleMs ?? trace?.staleMs, turn.updatedAt ?? trace?.updatedAt ?? result?.updatedAt);
const staleMs = sealedTerminal ? null : projectionStaleMs(source?.staleMs ?? trace?.staleMs, turn.updatedAt ?? trace?.updatedAt ?? result?.updatedAt);
return {
projectionStatus: status,
projectionHealth,
@@ -47,3 +47,22 @@ test("turn result merge seals completed result with final response", () => {
assert.equal((patch?.finalResponse as any)?.text, "final answer");
assert.equal(patch?.text, "final answer");
});
test("turn result merge seals failed result with failure final response", () => {
const traceId = "trc_frontend_terminal_failed_with_body";
const message = { id: "msg_frontend_terminal_failed_with_body", role: "agent", status: "running", traceId } as any;
const result = {
traceId,
status: "failed",
terminal: true,
finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected", status: "failed" }
} as any;
const patch = terminalMessagePatchFromTurnResult(message, result);
assert.equal(turnResultStatusForMerge(result), "failed");
assert.equal(turnResultIsTerminalForMerge(result), true);
assert.equal(patch?.status, "failed");
assert.equal((patch?.finalResponse as any)?.status, "failed");
assert.equal((patch?.finalResponse as any)?.text, "Workbench terminal failed: provider-stream-disconnected");
assert.equal(patch?.text, "Workbench terminal failed: provider-stream-disconnected");
});
@@ -325,20 +325,21 @@ export function terminalMessagePatchFromTurnResult(message: ChatMessage, result:
}
function terminalMessageBodyPatchFromTurnResult(message: ChatMessage, result: AgentChatResultResponse, resultStatus: string | null): Partial<ChatMessage> {
if (normalizedStatusText(resultStatus) !== "completed") return {};
const status = normalizedStatusText(resultStatus);
if (!isTerminalMessageStatus(status)) return {};
const finalText = terminalFinalResponseTextFromTurnResult(result);
if (!finalText) return {};
const traceId = firstNonEmptyString(result.traceId, message.traceId, message.runnerTrace?.traceId) ?? null;
return {
finalResponse: {
text: finalText,
status: "completed",
status,
traceId,
sealed: true,
source: "turn-result",
valuesRedacted: true
},
text: projectedAgentMessageText({ status: "completed", finalText })
text: projectedAgentMessageText({ status, finalText })
};
}
@@ -55,3 +55,20 @@ test("server state reducer keeps sealed terminal messages when a stale partial s
assert.equal(terminal.finalResponse.text, "final answer");
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "completed");
});
test("server state reducer promotes failed terminal message authority and ignores running list rollback", () => {
const sessionId = "ses_state_failed_terminal_authority";
const traceId = "trc_state_failed_terminal_authority";
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, { type: "session.detail", session: { sessionId, status: "running", lastTraceId: traceId, messages: [] } as any });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [
{ id: "msg_state_failed_user", role: "user", status: "sent", text: "one", sessionId, traceId } as any,
{ id: "msg_state_failed_agent", role: "agent", status: "failed", text: "Workbench terminal failed: provider-stream-disconnected", finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected" }, sessionId, traceId, finishedAt: "2026-07-01T00:00:05.000Z", durationMs: 5000 } as any
] });
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "failed");
state = reduceWorkbenchServerState(state, { type: "session.list", sessions: [{ sessionId, status: "running", lastTraceId: traceId, messages: [] } as any] });
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "failed");
assert.equal(selectActiveMessages(state, sessionId).find((message) => message.id === "msg_state_failed_agent")?.status, "failed");
});
@@ -400,11 +400,12 @@ function sessionStatusAuthorityFromDetail(session: WorkbenchSessionRecord): Sess
}
function sessionStatusAuthorityFromMessages(sessionId: string, messages: ChatMessage[]): SessionStatusAuthority | null {
const message = [...messages].reverse().find((item) => messageHasCompletedFinalResponse(item));
const message = [...messages].reverse().find((item) => messageHasTerminalResponse(item));
if (!message) return null;
const status = canonicalTerminalMessageStatus(message.status);
return {
sessionId,
status: "completed",
status,
updatedAt: textValue(message.updatedAt) ?? textValue(message.finishedAt) ?? textValue(message.lastEventAt),
lastTraceId: textValue(message.traceId) ?? textValue(message.runnerTrace?.traceId),
projection: projectionFromMessageRecord(message),
@@ -413,7 +414,7 @@ function sessionStatusAuthorityFromMessages(sessionId: string, messages: ChatMes
}
function mergeSessionStatusAuthority(existing: SessionStatusAuthority | undefined, incoming: SessionStatusAuthority): SessionStatusAuthority {
if (existing?.status === "completed" && incoming.status !== "completed" && isSameTraceAuthority(existing, incoming) && isRunningSessionStatus(incoming.status)) return existing;
if (isTerminalSessionStatusAuthority(existing) && !isTerminalSessionStatusAuthority(incoming) && isSameTraceAuthority(existing, incoming) && isRunningSessionStatus(incoming.status)) return existing;
return {
...(existing ?? {}),
...incoming,
@@ -422,6 +423,11 @@ function mergeSessionStatusAuthority(existing: SessionStatusAuthority | undefine
};
}
function isTerminalSessionStatusAuthority(session: SessionStatusAuthority | undefined): boolean {
if (!session) return false;
return isTerminalMessageStatus(session.status) && !isRunningSessionStatus(session.status);
}
function mergeTurnStatusAuthority(existing: TurnStatusAuthority | undefined, incoming: TurnStatusAuthority): TurnStatusAuthority {
if (existing && isTerminalTurnStatusAuthority(existing) && !isTerminalTurnStatusAuthority(incoming) && isRunningSessionStatus(incoming.status)) return existing;
return {
@@ -459,6 +465,19 @@ function messageHasCompletedFinalResponse(message: ChatMessage): boolean {
return Boolean(messageFinalResponseText(message));
}
function messageHasTerminalResponse(message: ChatMessage): boolean {
if (message.role !== "agent") return false;
const status = normalizedMessageStatus(message.status);
if (!isTerminalMessageStatus(status)) return false;
if (status === "completed") return messageHasCompletedFinalResponse(message);
return Boolean(messageFinalResponseText(message));
}
function canonicalTerminalMessageStatus(value: unknown): string {
const status = normalizedMessageStatus(value);
return status === "cancelled" ? "canceled" : status;
}
function messageFinalResponseText(message: ChatMessage): string | null {
return textValue(message.text) ?? textValue(message.content) ?? nestedTextValue((message as Record<string, unknown>).finalResponse);
}