fix: 清理 Workbench 无效投影残余

This commit is contained in:
root
2026-07-14 08:21:58 +02:00
parent bef232804f
commit 2603b00caa
6 changed files with 11 additions and 51 deletions
@@ -11,6 +11,8 @@ const productionFiles = [
"internal/cloud/server-code-agent-admission-http.ts",
"internal/cloud/server.ts",
"web/hwlab-cloud-web/src/config/runtime.ts",
"web/hwlab-cloud-web/src/config/workbench-runtime-policy.ts",
"web/hwlab-cloud-web/src/types/global.d.ts",
"web/hwlab-cloud-web/src/api/workbench-events.ts",
"web/hwlab-cloud-web/src/utils/workbench-stream-transport.ts",
"web/hwlab-cloud-web/src/utils/workbench-realtime-runtime.ts",
@@ -48,6 +50,11 @@ const forbiddenArchitectureIdentifiers = [
"sessionDetailAutoReadDecision",
"projectionMergeCommitSummary",
"forceRead",
"sealRestoredActiveTurnMessages",
"messageNeedsRestoredTurnSeal",
"refreshSessionStatusAuthority",
"readTerminalTraceDetailGaps",
"workbenchTraceMessagesWindowLimit",
"handleWorkbenchSyncHttp",
"/v1/workbench/sync"
];
@@ -39,7 +39,6 @@ test("Workbench runtime policy reads injected config while preserving defaults",
traceDetailMaxPages: 2,
workbenchSessionDetailMinRefreshMs: 1234,
workbenchSessionMessagesWindowLimit: 9,
workbenchTraceMessagesWindowLimit: 4,
workbenchRealtimeErrorSyncReplayMinMs: 0,
workbenchRealtimeFlushMaxItemsPerChunk: 2,
workbenchRealtimeFlushMaxChunkMs: 6,
@@ -51,7 +50,6 @@ test("Workbench runtime policy reads injected config while preserving defaults",
assert.equal(policy.traceDetailMaxPages, 2);
assert.equal(policy.workbenchSessionDetailMinRefreshMs, 1234);
assert.equal(policy.workbenchSessionMessagesWindowLimit, 9);
assert.equal(policy.workbenchTraceMessagesWindowLimit, 4);
assert.equal(policy.workbenchRealtimeErrorSyncReplayMinMs, 0);
assert.equal(policy.workbenchRealtimeFlushMaxItemsPerChunk, 2);
assert.equal(policy.workbenchRealtimeFlushMaxChunkMs, 6);
@@ -18,7 +18,6 @@ export interface WorkbenchRuntimePolicy {
workbenchSessionDetailMinRefreshMs: number;
workbenchSessionMessagesMinRefreshMs: number;
workbenchSessionMessagesWindowLimit: number;
workbenchTraceMessagesWindowLimit: number;
workbenchRealtimeSessionMessagesMinRefreshMs: number;
workbenchTraceEventsTimeoutMs: number;
sessionListMinRefreshIntervalMs: number;
@@ -45,7 +44,6 @@ const DEFAULT_WORKBENCH_RUNTIME_POLICY: WorkbenchRuntimePolicy = Object.freeze({
workbenchSessionDetailMinRefreshMs: 5_000,
workbenchSessionMessagesMinRefreshMs: 5_000,
workbenchSessionMessagesWindowLimit: 20,
workbenchTraceMessagesWindowLimit: 8,
workbenchRealtimeSessionMessagesMinRefreshMs: 1_000,
workbenchTraceEventsTimeoutMs: 5_000,
sessionListMinRefreshIntervalMs: 15_000,
@@ -74,7 +72,6 @@ export function workbenchRuntimePolicy(input: unknown = runtimePolicyConfig()):
workbenchSessionDetailMinRefreshMs: nonNegativeNumber(source.workbenchSessionDetailMinRefreshMs, DEFAULT_WORKBENCH_RUNTIME_POLICY.workbenchSessionDetailMinRefreshMs),
workbenchSessionMessagesMinRefreshMs: nonNegativeNumber(source.workbenchSessionMessagesMinRefreshMs, DEFAULT_WORKBENCH_RUNTIME_POLICY.workbenchSessionMessagesMinRefreshMs),
workbenchSessionMessagesWindowLimit: positiveInteger(source.workbenchSessionMessagesWindowLimit, DEFAULT_WORKBENCH_RUNTIME_POLICY.workbenchSessionMessagesWindowLimit),
workbenchTraceMessagesWindowLimit: positiveInteger(source.workbenchTraceMessagesWindowLimit, DEFAULT_WORKBENCH_RUNTIME_POLICY.workbenchTraceMessagesWindowLimit),
workbenchRealtimeSessionMessagesMinRefreshMs: nonNegativeNumber(source.workbenchRealtimeSessionMessagesMinRefreshMs, DEFAULT_WORKBENCH_RUNTIME_POLICY.workbenchRealtimeSessionMessagesMinRefreshMs),
workbenchTraceEventsTimeoutMs: positiveNumber(source.workbenchTraceEventsTimeoutMs, DEFAULT_WORKBENCH_RUNTIME_POLICY.workbenchTraceEventsTimeoutMs),
sessionListMinRefreshIntervalMs: nonNegativeNumber(source.sessionListMinRefreshIntervalMs, DEFAULT_WORKBENCH_RUNTIME_POLICY.sessionListMinRefreshIntervalMs),
@@ -142,8 +142,7 @@ test("workbench active terminal paths seal final response from turn authority",
const crossTabSyncBlock = source.slice(source.indexOf("async function refreshCrossTabSessionFromSyncReplay"), source.indexOf("function completeTrace"));
const sessionDetailReadBlock = source.slice(source.indexOf("function fetchSessionDetailPage"), source.indexOf("function sessionMessageProjectionWindowLimit"));
const traceDetailReadBlock = source.slice(source.indexOf("async function readTraceEventsForExplicitDetailPages"), source.indexOf("async function fetchTraceDetailEventsPage"));
const loadBlock = source.slice(source.indexOf("async function loadWorkbenchSession"), source.indexOf("async function sealRestoredActiveTurnMessages"));
const restoreSealBlock = source.slice(source.indexOf("async function sealRestoredActiveTurnMessages"), source.indexOf("function reattachRestoredActiveTrace"));
const loadBlock = source.slice(source.indexOf("async function loadWorkbenchSession"), source.indexOf("function reattachRestoredActiveTrace"));
assert.match(projectBlock, /terminalAuthorityMessageFromTurnResult\(result\)/u);
assert.match(projectBlock, /type:\s*"message\.upsert"/u);
@@ -174,9 +173,7 @@ test("workbench active terminal paths seal final response from turn authority",
assert.match(loadBlock, /fetchSessionMessagesPage\(normalizedRequestId, \{ limit: messageLimit, reason: "load-session:messages", force: true \}\)/u);
assert.doesNotMatch(loadBlock, /messages:\s*\[\]/u);
assert.doesNotMatch(loadBlock, /limit:\s*100/u);
assert.match(restoreSealBlock, /workbench_restored_turn_auto_read_disabled/u);
assert.doesNotMatch(restoreSealBlock, /fetchWorkbenchTurnStatus|Promise\.all/u);
assert.doesNotMatch(restoreSealBlock, /force:\s*true/u);
assert.doesNotMatch(source, /sealRestoredActiveTurnMessages|messageNeedsRestoredTurnSeal|refreshSessionStatusAuthority|readTerminalTraceDetailGaps/u);
});
test("workbench active SSE path only recovers by reconnecting the events cursor", () => {
@@ -185,8 +182,7 @@ test("workbench active SSE path only recovers by reconnecting the events cursor"
source.slice(source.indexOf("async function submitMessage"), source.indexOf("async function cancelAgentMessage")),
source.slice(source.indexOf("function reattachTrace"), source.indexOf("function restartRealtime")),
source.slice(source.indexOf("function flushRealtimeTurnProjection"), source.indexOf("function installRealtimeVisibilityHandler")),
source.slice(source.indexOf("function completeTrace"), source.indexOf("function applyTerminalResultDiagnostics")),
source.slice(source.indexOf("async function sealRestoredActiveTurnMessages"), source.indexOf("function reattachRestoredActiveTrace"))
source.slice(source.indexOf("function completeTrace"), source.indexOf("function applyTerminalResultDiagnostics"))
].join("\n");
assert.doesNotMatch(activeBlocks, /refreshWorkbenchSyncReplay|scheduleActiveTraceSyncReplay|setTimeout|setInterval/u);
+1 -38
View File
@@ -24,7 +24,7 @@ import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from
import { reduceWorkbenchRealtimeEvent, type WorkbenchRealtimeAction } from "./workbench-event-reducer";
import { projectRejectedWorkbenchAdmission } from "./workbench-admission-failure";
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed } from "./workbench-terminal-authority";
import { boundedProjectionMessageLimit, traceProjectionIsTerminalSealed } from "./workbench-message-projection-budget";
import { boundedProjectionMessageLimit } from "./workbench-message-projection-budget";
import {
agentErrorDisplayText,
agentErrorFromApiFailure,
@@ -228,7 +228,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const nextSessions = stableSessionList(sessions.value, listedSessions, stableSelected?.sessionId ?? selected?.sessionId ?? routeSessionId ?? includeSessionId, stableSelected);
rememberSessionList(nextSessions);
sessionsReady.value = sessionsResult.ok || nextSessions.length > 0;
void refreshSessionStatusAuthority(nextSessions);
clearSessionDetailLoading(targetSessionId);
loading.value = false;
recordWorkbenchLoadingState({ scope: "session_detail", active: false, reason: "hydrate", sessionId: targetSessionId });
@@ -499,10 +498,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return projectedActiveSession.value ?? null;
}
async function refreshSessionStatusAuthority(source: WorkbenchSessionRecord[] = sessions.value): Promise<void> {
void source;
}
function fetchWorkbenchTraceEvents(traceId: string, afterProjectedSeq: number, useActivityTimeout = shouldUseActivityTimeoutForTrace(traceId), options: { force?: boolean } = {}): Promise<ApiResult<AgentChatResultResponse>> {
const activitySource = useActivityTimeout ? () => activityRef.value : null;
return workbenchColadaQueries.fetchTraceEvents(traceId, { timeoutMs: runtimePolicy.workbenchTraceEventsTimeoutMs, activityRef: activitySource, afterProjectedSeq, limit: runtimePolicy.traceDetailPageLimit, minIntervalMs: runtimePolicy.workbenchTraceEventsMinRefreshMs, force: options.force });
@@ -807,13 +802,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return fetchWorkbenchTraceEvents(traceId, afterProjectedSeq, shouldUseActivityTimeoutForTrace(traceId), { force: options.force });
}
function readTerminalTraceDetailGaps(source: ChatMessage[], reason: string): void {
// Terminal trace rows are loaded on explicit force/detail paths. Auto-filling
// historical terminal traces makes multi-turn Workbench sessions main-thread bound.
void source;
void reason;
}
async function readTraceEventsForMessages(source: ChatMessage[] = messages.value, options: { force?: boolean } = {}): Promise<void> {
const candidates = traceDetailReadCandidates(source);
if (options.force !== true) {
@@ -1571,23 +1559,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return { ...base, sessionId: id, messages: hydratedMessages, messageCount: base.messageCount ?? hydratedMessages.length };
}
async function sealRestoredActiveTurnMessages(source: ChatMessage[]): Promise<ChatMessage[]> {
const targets = source.filter(messageNeedsRestoredTurnSeal).slice(-1);
if (targets.length === 0) return source;
for (const message of targets) {
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId || traceProjectionIsTerminalSealed(traceId, source)) continue;
recordWorkbenchRuntimeDiagnostic({
module: "workbench-turn-status",
sessionId: message.sessionId ?? message.runnerTrace?.sessionId ?? null,
traceId,
outcome: "ok",
diagnostic: { code: "workbench_restored_turn_auto_read_disabled", reason: "load-session:restored-turn", source: "projection-sse-authority", valuesRedacted: true }
});
}
return source;
}
function reattachRestoredActiveTrace(): void {
const traceId = activeTraceIdFromMessages(messages.value, turnStatusAuthority.value);
if (traceId) reattachTrace(traceId);
@@ -1665,14 +1636,6 @@ function routeSelectedSessionRecord(sessionId: string | null | undefined, loadin
} as WorkbenchSessionRecord;
}
function messageNeedsRestoredTurnSeal(message: ChatMessage): boolean {
if (message.role !== "agent") return false;
if (messageHasTerminalResponse(message)) return false;
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId) return false;
return isTraceActiveStatus(message.status) || isTerminalMessageStatus(message.status) || message.traceAutoLifecycle === "running";
}
function providerThreadIdForRequest(threadId: string | null | undefined): string | null {
const value = firstNonEmptyString(threadId) ?? null;
if (!value) return null;
-1
View File
@@ -54,7 +54,6 @@ declare global {
workbenchSessionDetailMinRefreshMs?: number;
workbenchSessionMessagesMinRefreshMs?: number;
workbenchSessionMessagesWindowLimit?: number;
workbenchTraceMessagesWindowLimit?: number;
workbenchRealtimeSessionMessagesMinRefreshMs?: number;
workbenchTraceEventsTimeoutMs?: number;
sessionListMinRefreshIntervalMs?: number;