From 7ca7a9974b2f8725e0ec700340d134e83130b521 Mon Sep 17 00:00:00 2001 From: lyon Date: Sun, 21 Jun 2026 05:37:30 +0800 Subject: [PATCH] fix(workbench): keep active turn polling until terminal --- web/hwlab-cloud-web/src/stores/workbench.ts | 68 +++++++++++++-------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 40fd20b0..5e559945 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -19,8 +19,8 @@ const TRACE_HYDRATION_PAGE_LIMIT = 100; const TRACE_HYDRATION_MAX_PAGES = 60; const TRACE_HYDRATION_MAX_ATTEMPTS = 3; const TRACE_HYDRATION_RETRY_DELAY_MS = 700; -const ACTIVE_TURN_GAP_DELAY_MS = 1_000; -const ACTIVE_TURN_GAP_MAX_ATTEMPTS = 12; +const ACTIVE_TURN_GAP_INITIAL_DELAY_MS = 1_000; +const ACTIVE_TURN_GAP_MAX_DELAY_MS = 5_000; const SESSION_LIST_PAGE_LIMIT = 20; const WORKBENCH_READ_HYDRATION_CONCURRENCY = 3; @@ -726,14 +726,29 @@ export const useWorkbenchStore = defineStore("workbench", () => { lastEventLabel: result.lastEventLabel ?? undefined, updatedAt: new Date().toISOString() } as NonNullable; - const runnerTrace = mergeRunnerTrace(message.runnerTrace, nextTrace); - const error = isCompletedResultWithFinalResponse(result) ? null : normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error); - const projection = projectionFromResult(result) ?? runnerTrace.projection ?? message.projection ?? null; + const resultStatus = normalizedStatusText(result.status) ?? null; + const terminal = result.terminal === true || isTerminalMessageStatus(resultStatus); + const resultError = normalizeAgentError(result.error ?? null); + const resultProjection = projectionFromResult(result); + const mergedRunnerTrace = mergeRunnerTrace(message.runnerTrace, nextTrace); + const runnerTrace = terminal && !resultError && !resultProjection ? clearRunnerTraceTransientDiagnostics(mergedRunnerTrace) : mergedRunnerTrace; + const error = resultError ?? (terminal ? null : normalizeAgentError(runnerTrace?.error ?? message.error)); + const projection = resultProjection ?? (terminal ? null : runnerTrace.projection ?? message.projection ?? null); const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message); rememberTraceAuthority(runnerTrace); - return { ...message, ...messageTimingPatch(result), runnerTrace, error: error ?? message.error ?? null, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; + return { ...message, ...messageTimingPatch(result), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; })); markWorkbenchTraceProjected(traceId); + if (result.terminal === true || isTerminalMessageStatus(result.status)) { + rememberTurnStatus(traceId, result); + clearActiveTurnGapRefresh(traceId); + if (ownerSessionId === activeSessionId.value) { + chatPending.value = false; + currentRequest.value = null; + void clearActiveTrace(traceId, "trace-hydration-terminal"); + restartRealtime("trace-hydration-terminal"); + } + } } function setProviderProfile(value: ProviderProfile): void { @@ -983,13 +998,14 @@ export const useWorkbenchStore = defineStore("workbench", () => { } if (activeTurnGapTimer) return; const attempts = activeTurnGapAttempts.get(traceId) ?? 0; - if (attempts >= ACTIVE_TURN_GAP_MAX_ATTEMPTS) return; - activeTurnGapAttempts.set(traceId, attempts + 1); + const nextAttempt = attempts + 1; + activeTurnGapAttempts.set(traceId, nextAttempt); if (typeof window === "undefined") return; + const delayMs = Math.min(ACTIVE_TURN_GAP_MAX_DELAY_MS, ACTIVE_TURN_GAP_INITIAL_DELAY_MS * Math.max(1, nextAttempt)); activeTurnGapTimer = window.setTimeout(() => { activeTurnGapTimer = null; void hydrateRealtimeGap(`active-turn-gap:${reason}`); - }, ACTIVE_TURN_GAP_DELAY_MS); + }, delayMs); } function activeTurnGapTraceId(): string | null { @@ -1048,12 +1064,17 @@ export const useWorkbenchStore = defineStore("workbench", () => { if (ownerSessionId === activeSessionId.value) recordActivity(`trace:terminal:${firstNonEmptyString(result.lastEventLabel, result.status, "completed") ?? "completed"}`); updateSessionMessages(ownerSessionId, (source) => source.map((message) => { if (!messageMatchesTraceAuthority(message, traceId, authoritySessionId, ownerSessionId)) return message; - const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result); + const resultStatus = normalizedStatusText(result.status) ?? null; + const terminal = result.terminal === true || isTerminalMessageStatus(resultStatus); + const resultError = normalizeAgentError(result.error ?? null); + const resultProjection = projectionFromResult(result); + const mergedRunnerTrace = mergeTerminalResultTrace(message.runnerTrace, result); + const runnerTrace = terminal && !resultError && !resultProjection ? clearRunnerTraceTransientDiagnostics(mergedRunnerTrace) : mergedRunnerTrace; rememberTraceAuthority(runnerTrace); - const error = isCompletedResultWithFinalResponse(result) ? null : normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error); - const projection = projectionFromResult(result) ?? runnerTrace.projection ?? message.projection ?? null; + const error = resultError ?? (terminal ? null : normalizeAgentError(runnerTrace?.error ?? message.error)); + const projection = resultProjection ?? (terminal ? null : runnerTrace.projection ?? message.projection ?? null); const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message); - return { ...message, ...messageTimingPatch(result), runnerTrace, error: error ?? message.error ?? null, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; + return { ...message, ...messageTimingPatch(result), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; })); rememberTurnStatus(traceId, result); markWorkbenchTraceProjected(traceId); @@ -1087,12 +1108,17 @@ export const useWorkbenchStore = defineStore("workbench", () => { if (!ownerSessionId) return; updateSessionMessages(ownerSessionId, (source) => source.map((message) => { if (!messageMatchesTraceAuthority(message, traceId, authoritySessionId, ownerSessionId)) return message; - const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result); + const resultStatus = normalizedStatusText(result.status) ?? null; + const terminal = result.terminal === true || isTerminalMessageStatus(resultStatus); + const resultError = normalizeAgentError(result.error ?? null); + const resultProjection = projectionFromResult(result); + const mergedRunnerTrace = mergeTerminalResultTrace(message.runnerTrace, result); + const runnerTrace = terminal && !resultError && !resultProjection ? clearRunnerTraceTransientDiagnostics(mergedRunnerTrace) : mergedRunnerTrace; rememberTraceAuthority(runnerTrace); - const error = isCompletedResultWithFinalResponse(result) ? null : normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error); + const error = resultError ?? (terminal ? null : normalizeAgentError(runnerTrace?.error ?? message.error)); const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message); - const projection = projectionFromResult(result) ?? runnerTrace.projection ?? message.projection ?? null; - return { ...message, ...messageTimingPatch(result), title: normalizeWorkbenchMessageTitle(message.role, message.title), runnerTrace, error: error ?? message.error ?? null, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; + const projection = resultProjection ?? (terminal ? null : runnerTrace.projection ?? message.projection ?? null); + return { ...message, ...messageTimingPatch(result), title: normalizeWorkbenchMessageTitle(message.role, message.title), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; })); void hydrateTraceEvents(serverState.value.messagesBySessionId[ownerSessionId] ?? []); void refreshSessions(ownerSessionId); @@ -1765,14 +1791,6 @@ function projectedAgentMessageText(input: { status: unknown; finalText?: string return firstNonEmptyString(input.errorText, text) ?? ""; } -function isCompletedResultWithFinalResponse(value: unknown): boolean { - const record = recordValue(value); - if (!record) return false; - const status = normalizedStatusText(record.status); - if (status !== "completed") return false; - return Boolean(finalResponseText(record.finalResponse) ?? messageText(record.assistantText) ?? messageText(record.text) ?? messageText(record.summary)); -} - function isFailureTerminalStatus(value: unknown): boolean { return ["failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-")); }