fix: align workbench terminal authorities
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user