Merge pull request #2364 from pikasTech/fix/2356-dom-final-projection
fix: bound Workbench terminal projection hydrate
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user