Merge pull request #2346 from pikasTech/fix/1445-lane-b-terminal-authority

fix(web): 收敛 Workbench terminal authority helper
This commit is contained in:
Lyon
2026-07-02 17:37:59 +08:00
committed by GitHub
4 changed files with 119 additions and 33 deletions
@@ -3,6 +3,7 @@
import type { ChatMessage, ProjectionDiagnostic, ProviderProfile, WorkbenchSessionRecord } from "@/types";
import { firstNonEmptyString } from "@/utils";
import { messageHasSealedTerminalResult, messageIsSealedTerminal, turnStatusAuthorityIsSealed } from "./workbench-terminal-authority";
export interface ComposerState {
disabled: boolean;
@@ -82,10 +83,10 @@ export function resolveComposerState(input: { messages: ChatMessage[]; sessions?
const turn = activeTraceId ? input.turnStatusAuthority?.[activeTraceId] : null;
const activeByRequest = Boolean(input.chatPending && currentRequest && isActiveStatus(currentRequest.status ?? "running"));
const activeByMessage = latestMessage?.role === "agent" && isActiveStatus(latestMessage.status);
const terminalByMessage = messageIsTerminalForSession(latestMessage);
const terminalByMessage = messageIsSealedTerminal(latestMessage);
const turnStatus = normalizeSessionStatus(turn?.status);
const terminalTurn = turn?.terminal === true || isTerminalStatus(turnStatus);
const terminalByTurn = terminalTurn && messageHasTerminalResponse(latestMessage);
const terminalByTurn = terminalTurn && messageHasSealedTerminalResult(latestMessage);
const activeByStatus = !terminalByMessage && !terminalByTurn && (turn?.running === true || isActiveStatus(turnStatus) || activeByRequest || activeByMessage);
const terminal = terminalByMessage || terminalByTurn;
const active = activeSession(input.sessions ?? [], sessionId);
@@ -106,7 +107,7 @@ export function resolveCancelableAgentMessage(input: { messages: ChatMessage[];
if (message.role !== "agent") continue;
if (firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) !== targetTraceId) continue;
if (!messageBelongsToCancelTarget(message, input)) continue;
if (messageIsTerminalForSession(message)) continue;
if (messageIsSealedTerminal(message)) continue;
return message;
}
return null;
@@ -312,27 +313,10 @@ function traceNeedsTurnStatusRefresh(traceId: string | null | undefined, message
const turn = turnStatusAuthority?.[id];
const message = latestMessageForTrace(messages, id);
if (turnStatusAuthorityIsSealed(turn, message)) return false;
if (messageIsTerminalForSession(message)) return false;
if (messageIsSealedTerminal(message)) return false;
return true;
}
function turnStatusAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null): boolean {
if (!(turn?.terminal === true || isTerminalStatus(turn?.status))) return false;
return message ? messageHasTerminalResponse(message) : false;
}
function messageIsTerminalForSession(message: ChatMessage | null | undefined): boolean {
if (!message || message.role !== "agent") return false;
const status = normalizeSessionStatus(message.status);
return isTerminalStatus(status) && messageHasTerminalResponse(message);
}
function messageHasTerminalResponse(message: ChatMessage | null | undefined): boolean {
if (!message || message.role !== "agent") return false;
const finalResponse = recordValue((message as Record<string, unknown>).finalResponse);
return Boolean(firstNonEmptyString(message.text, message.content, finalResponse?.text, finalResponse?.content, finalResponse?.message));
}
function resolveSessionTabStatus(session: WorkbenchSessionRecord, authority: SessionStatusAuthority | null | undefined): string {
void session;
const authorityStatus = normalizeSessionStatus(authority?.status);
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import type { ChatMessage } from "@/types";
import type { TurnStatusAuthority } from "./workbench-session";
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed, turnStatusAuthorityIsSealed } from "./workbench-terminal-authority";
function agentMessage(input: Partial<ChatMessage> & Pick<ChatMessage, "status">): ChatMessage {
return {
id: `msg_${String(input.status)}`,
role: "agent",
title: "Agent",
text: "",
createdAt: "2026-07-02T00:00:00.000Z",
...input
};
}
function turnAuthority(input: Partial<TurnStatusAuthority> = {}): TurnStatusAuthority {
return {
traceId: "trc_terminal",
status: "completed",
terminal: true,
running: false,
...input
};
}
test("terminal authority requires terminal status and a final body for sealed messages", () => {
assert.equal(messageIsSealedTerminal(agentMessage({ status: "completed", text: "final answer" })), true);
assert.equal(messageIsSealedTerminal(agentMessage({ status: "completed" })), false);
assert.equal(messageIsSealedTerminal(agentMessage({ status: "running", text: "partial" })), false);
});
test("turn authority seals when terminal turn has final response even if message status is stale running", () => {
const turn = turnAuthority();
const message = agentMessage({ status: "running", traceId: "trc_terminal", finalResponse: { text: "final answer" } });
assert.equal(turnStatusAuthorityIsSealed(turn, message), true);
assert.equal(traceAuthorityIsSealed(turn, message), true);
});
test("terminal result text can come from reply and finalText authority fields", () => {
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", reply: { finalText: "done" } })), true);
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalText: "done" })), true);
});
test("terminal result keeps finalResponse text content and message field semantics", () => {
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalResponse: { text: "from text" } })), true);
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalResponse: { content: "from content" } })), true);
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalResponse: { message: "from message" } })), true);
});
@@ -0,0 +1,61 @@
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection.
// Responsibility: Shared terminal seal authority helpers for Workbench store reducers.
import type { ChatMessage } from "@/types";
import { firstNonEmptyString } from "@/utils";
import { finalResponseText, messageText, normalizedStatusText } from "./workbench-message-projection-runtime";
import type { TurnStatusAuthority } from "./workbench-session";
export function traceAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null | undefined): boolean {
return turnStatusAuthorityIsSealed(turn, message);
}
export function turnStatusAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null | undefined): boolean {
if (!(turn?.terminal === true || isTerminalAuthorityStatus(turn?.status))) return false;
return messageHasSealedTerminalResult(message);
}
export function messageIsSealedTerminal(message: ChatMessage | null | undefined): boolean {
if (!message || message.role !== "agent") return false;
return isTerminalAuthorityStatus(message.status) && messageHasSealedTerminalResult(message);
}
export function messageHasSealedTerminalResult(message: ChatMessage | null | undefined): boolean {
if (!message || message.role !== "agent") return false;
const record = message as Record<string, unknown>;
const reply = recordValue(record.reply);
return Boolean(firstNonEmptyString(
messageText(message.text),
messageText(record.content),
finalResponseText(record.finalResponse),
finalResponseText(record.reply),
messageText(reply?.finalText),
messageText(record.finalText),
messageText(record.final)
));
}
function recordValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? value as Record<string, unknown> : null;
}
function isTerminalAuthorityStatus(value: unknown): boolean {
return [
"completed",
"failed",
"blocked",
"timeout",
"canceled",
"stale",
"thread-resume-failed",
"interrupted",
"expired"
].includes(normalizeTerminalAuthorityStatus(value) ?? "");
}
function normalizeTerminalAuthorityStatus(value: unknown): string | null {
const status = normalizedStatusText(value);
if (status === "cancelled") return "canceled";
if (status === "error") return "failed";
return status;
}
+1 -12
View File
@@ -20,6 +20,7 @@ import { initialWorkbenchSessionIdFromLocation } from "./workbench-projection";
import { cleanupWorkbenchServerStateSessions, createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, selectActiveSession, selectSessionList, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority, type WorkbenchServerAction } from "./workbench-server-state";
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "./workbench-session-cache";
import { reduceWorkbenchRealtimeEvent, type WorkbenchRealtimeAction } from "./workbench-event-reducer";
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed } from "./workbench-terminal-authority";
import {
agentErrorDisplayText,
agentErrorFromApiFailure,
@@ -1438,18 +1439,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
}));
}
function messageHasSealedTerminalResult(message: ChatMessage | null): boolean {
return messageHasTerminalResponse(message);
}
function traceAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null): boolean {
return Boolean((turn?.terminal === true || isTerminalMessageStatus(turn?.status)) && messageHasSealedTerminalResult(message));
}
function messageIsSealedTerminal(message: ChatMessage | null): boolean {
return messageHasSealedTerminalResult(message);
}
function failTrace(traceId: string, message: string): void {
markMessage(traceId, { status: "failed", text: message });
chatPending.value = false;