Merge pull request #2364 from pikasTech/fix/2356-dom-final-projection

fix: bound Workbench terminal projection hydrate
This commit is contained in:
Lyon
2026-07-03 04:55:11 +08:00
committed by GitHub
3 changed files with 58 additions and 9 deletions
@@ -1,8 +1,8 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import type { ChatMessage } from "../types";
import { resolveCancelableAgentMessage, resolveComposerState, selectActiveTurnStatusRefreshTraceIds } from "./workbench-session";
import type { ChatMessage, WorkbenchSessionRecord } from "../types";
import { resolveCancelableAgentMessage, resolveComposerState, selectActiveTurnStatusRefreshTraceIds, sessionToSessionTab, shouldHydrateTerminalTraceFromRest } from "./workbench-session";
import type { TurnStatusAuthority } from "./workbench-session";
function agentMessage(input: Partial<ChatMessage> & Pick<ChatMessage, "id" | "traceId" | "sessionId">): ChatMessage {
@@ -99,3 +99,27 @@ test("turn status refresh selector skips sealed terminal traces", () => {
assert.deepEqual(selectActiveTurnStatusRefreshTraceIds({ messages: [sealed], currentRequestTraceId: sealed.traceId, turnStatusAuthority: { [sealed.traceId]: turnStatus({ traceId: sealed.traceId, sessionId, status: "completed", running: false, terminal: true }) }, limit: 1 }), []);
});
test("terminal REST hydrate skips sealed terminal trace authority", () => {
const sessionId = "ses_terminal_hydrate_sealed";
const traceId = "trc_terminal_hydrate_sealed";
const sealed = agentMessage({ id: "msg_terminal_hydrate_sealed", traceId, sessionId, status: "completed", text: "final response" });
const unsealed = agentMessage({ id: "msg_terminal_hydrate_unsealed", traceId, sessionId, status: "completed" });
const turnStatusAuthority = { [traceId]: turnStatus({ traceId, sessionId, status: "completed", running: false, terminal: true }) };
assert.equal(shouldHydrateTerminalTraceFromRest({ traceId, messages: [sealed], turnStatusAuthority }), false);
assert.equal(shouldHydrateTerminalTraceFromRest({ traceId, messages: [unsealed], turnStatusAuthority }), true);
assert.equal(shouldHydrateTerminalTraceFromRest({ traceId, messages: [], turnStatusAuthority }), true);
});
test("session rail status prefers newer running trace over stale completed authority", () => {
const sessionId = "ses_session_rail_stale";
const older = agentMessage({ id: "msg_session_rail_old", traceId: "trc_session_rail_old", sessionId, status: "completed", text: "previous final" });
const current = agentMessage({ id: "msg_session_rail_current", traceId: "trc_session_rail_current", sessionId, status: "running" });
const session: WorkbenchSessionRecord = { sessionId, status: "completed", lastTraceId: older.traceId, messageCount: 2, messages: [older, current] };
const tab = sessionToSessionTab(session, sessionId, { [sessionId]: { sessionId, status: "completed", lastTraceId: older.traceId } });
assert.equal(tab.status, "running");
assert.equal(tab.running, true);
assert.equal(tab.lastTraceId, current.traceId);
});
@@ -129,6 +129,15 @@ export function selectActiveTurnStatusRefreshTraceIds(input: { messages: ChatMes
return [...traceIds];
}
export function shouldHydrateTerminalTraceFromRest(input: { traceId?: string | null; messages: ChatMessage[]; turnStatusAuthority?: TurnStatusAuthorityMap }): boolean {
const traceId = firstNonEmptyString(input.traceId);
if (!traceId) return false;
const message = latestMessageForTrace(input.messages, traceId);
if (turnStatusAuthorityIsSealed(input.turnStatusAuthority?.[traceId], message)) return false;
if (messageIsSealedTerminal(message)) return false;
return true;
}
export function shouldShowSessionListLoading(input: { loading: boolean; sessionsReady: boolean }): boolean {
return input.loading && !input.sessionsReady;
}
@@ -136,10 +145,11 @@ export function shouldShowSessionListLoading(input: { loading: boolean; sessions
export function sessionToSessionTab(session: WorkbenchSessionRecord, activeSessionId: string | null, sessionStatusAuthority: SessionStatusAuthorityMap = {}): SessionTab {
const sessionId = session.sessionId;
const updatedAt = sessionDisplayUpdatedAt(session);
const status = resolveSessionTabStatus(session, sessionStatusAuthority[sessionId]);
const latestAgent = latestAgentMessage(session.messages);
const status = resolveSessionTabStatus(session, sessionStatusAuthority[sessionId], latestAgent);
const projection = projectionFromSession(session, sessionStatusAuthority[sessionId]);
const projectionText = projectionDiagnosticText(projection);
const trace = firstNonEmptyString(latestAgentMessage(session.messages)?.traceId, session.messages?.at(-1)?.traceId, session.lastTraceId);
const trace = firstNonEmptyString(latestAgent?.traceId, latestAgent?.runnerTrace?.traceId, session.messages?.at(-1)?.traceId, session.lastTraceId);
const userMessage = session.messages?.find((message) => message.role === "user");
const running = isRunningIndicatorStatus(status);
const preview = firstReadableSentence(
@@ -318,10 +328,13 @@ function traceNeedsTurnStatusRefresh(traceId: string | null | undefined, message
return true;
}
function resolveSessionTabStatus(session: WorkbenchSessionRecord, authority: SessionStatusAuthority | null | undefined): string {
void session;
function resolveSessionTabStatus(session: WorkbenchSessionRecord, authority: SessionStatusAuthority | null | undefined, latestAgent: ChatMessage | null = latestAgentMessage(session.messages)): string {
const latestTraceId = firstNonEmptyString(latestAgent?.traceId, latestAgent?.runnerTrace?.traceId);
const authorityTraceId = firstNonEmptyString(authority?.lastTraceId);
const latestStatus = normalizeSessionStatus(latestAgent?.status);
if (latestTraceId && authorityTraceId && latestTraceId !== authorityTraceId && latestStatus && isActiveStatus(latestStatus)) return latestStatus;
const authorityStatus = normalizeSessionStatus(authority?.status);
return authorityStatus ?? "unknown";
return authorityStatus ?? latestStatus ?? normalizeSessionStatus(session.status) ?? "unknown";
}
function projectionFromSession(session: WorkbenchSessionRecord, authority: SessionStatusAuthority | null | undefined): ProjectionDiagnostic | null {
+14 -2
View File
@@ -16,7 +16,7 @@ import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, Ap
import { firstNonEmptyString, nextProtocolId, normalizeWorkbenchSessionId, normalizeWorkbenchSessionRouteId } from "@/utils";
import { composeWorkbenchScopedKey } from "@/utils/workbench-key";
import { failWorkbenchSessionSwitch, failWorkbenchSubmitJourney, finishWorkbenchSessionSwitchFullLoad, markWorkbenchSubmitApiAccepted, markWorkbenchTraceEventsReceived, markWorkbenchTraceProjected, recordWorkbenchLoadingState, recordWorkbenchRuntimeDiagnostic, startWorkbenchSessionSwitch, startWorkbenchSubmitJourney } from "@/utils/workbench-performance";
import { RECENT_DRAFTS_STORAGE_KEY, appendSessionPage, defaultProviderProfileOptions, isArchivedSession, mergeSessionIntoList, normalizeChatMessageStatus, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, selectActiveTurnStatusRefreshTraceIds, shouldShowSessionListLoading, sortSessionTabs, stableSessionList, type DraftEntry, type ProviderProfileOption, type TurnStatusAuthority } from "./workbench-session";
import { RECENT_DRAFTS_STORAGE_KEY, appendSessionPage, defaultProviderProfileOptions, isArchivedSession, mergeSessionIntoList, normalizeChatMessageStatus, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, selectActiveTurnStatusRefreshTraceIds, shouldHydrateTerminalTraceFromRest, shouldShowSessionListLoading, sortSessionTabs, stableSessionList, type DraftEntry, type ProviderProfileOption, type TurnStatusAuthority } from "./workbench-session";
import { initialWorkbenchSessionIdFromLocation } from "./workbench-projection";
import { cleanupWorkbenchServerStateSessions, selectActiveMessages, selectActiveSession, selectSessionList, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority, type WorkbenchServerAction } from "./workbench-server-state";
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "./workbench-session-cache";
@@ -476,7 +476,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
if (!id) return false;
const ownerSessionId = normalizeWorkbenchSessionId(sessionId) ?? traceOwnerSessionId(id, null);
const source = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value;
return traceProjectionIsTerminalSealed(id, source);
return !shouldHydrateTerminalTraceFromRest({ traceId: id, messages: source, turnStatusAuthority: turnStatusAuthority.value });
}
function appendActiveMessages(...items: ChatMessage[]): void {
@@ -1140,6 +1140,12 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return;
}
const ownerBefore = traceOwnerSessionId(id, null);
const ownerMessagesBeforeRest = ownerBefore ? serverState.value.messagesBySessionId[ownerBefore] ?? [] : messages.value;
if (!shouldHydrateTerminalTraceFromRest({ traceId: id, messages: ownerMessagesBeforeRest, turnStatusAuthority: turnStatusAuthority.value })) {
clearActiveTraceRestGapFill(id);
recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: ownerBefore, traceId: id, outcome: "ok", diagnostic: { code: "terminal_low_priority_rest_gap_skip", reason, source: "active-rest-gap-preflight", valuesRedacted: true } });
return;
}
if (ownerBefore === activeSessionId.value) recordActivity(reason);
await refreshTurnStatusByTraceId(id);
const ownerSessionId = traceOwnerSessionId(id, turnStatusAuthority.value[id]?.sessionId ?? null) ?? ownerBefore;
@@ -1376,6 +1382,12 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return;
}
const ownerBefore = traceOwnerSessionId(id, null);
const ownerMessagesBeforeRest = ownerBefore ? serverState.value.messagesBySessionId[ownerBefore] ?? [] : messages.value;
if (!shouldHydrateTerminalTraceFromRest({ traceId: id, messages: ownerMessagesBeforeRest, turnStatusAuthority: turnStatusAuthority.value })) {
clearActiveTraceRestGapFill(id);
recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: ownerBefore, traceId: id, outcome: "ok", diagnostic: { code: "terminal_low_priority_terminal_rest_skip", reason, source: "terminal-rest-preflight", valuesRedacted: true } });
return;
}
if (ownerBefore === activeSessionId.value) recordActivity(reason);
await refreshTurnStatusByTraceId(id);
const ownerSessionId = traceOwnerSessionId(id, turnStatusAuthority.value[id]?.sessionId ?? null) ?? ownerBefore;