diff --git a/.github/scripts/classify-wework-desktop-e2e.sh b/.github/scripts/classify-wework-desktop-e2e.sh index e69ad8a9372..938bd30eaa7 100755 --- a/.github/scripts/classify-wework-desktop-e2e.sh +++ b/.github/scripts/classify-wework-desktop-e2e.sh @@ -8,6 +8,7 @@ core_segments=( core-task-flow window-lifecycle goal-lifecycle + supervisor-lifecycle resilience conversation-state workspace-attachments @@ -108,8 +109,12 @@ classify_wework_path() { # Queueing, cancellation, retry, rate-limit, and reconnect behavior. wework/src/components/chat/ConversationQueuePanel* | \ - wework/src/lib/chat-error* | \ - wework/src/features/workbench/runtimeTaskLifecycle/*) + wework/src/lib/chat-error*) + select_target "core:resilience" + return + ;; + wework/src/features/workbench/runtimeTaskLifecycle/*) + select_target "core:supervisor-lifecycle" select_target "core:resilience" return ;; @@ -126,6 +131,9 @@ classify_wework_path() { wework/src/components/chat/MessageTurnNavigation* | \ wework/src/components/chat/ScrollableMessageArea*) select_target "core:conversation-state" + if [[ "$path" == wework/src/features/workbench/runtimePaneMessages* ]]; then + select_target "core:supervisor-lifecycle" + fi return ;; wework/src/components/chat/MessageList*) diff --git a/.github/scripts/test-classify-ci-changes.sh b/.github/scripts/test-classify-ci-changes.sh index d6cd87b6a0f..1c153299d99 100755 --- a/.github/scripts/test-classify-ci-changes.sh +++ b/.github/scripts/test-classify-ci-changes.sh @@ -211,9 +211,25 @@ wework_desktop_other_e2e_matrix={"include":[]}' \ "wework/e2e/desktop/task-flow.e2e.mjs" \ "wework/src/components/chat/MessageList.tsx" +assert_desktop_case "turn lifecycle changes select supervisor and resilience coverage" \ + 'wework_desktop_e2e=true +wework_desktop_core_e2e=true +wework_desktop_core_e2e_matrix={"include":[{"id":"core-supervisor-lifecycle","name":"Core / supervisor-lifecycle","command":"e2e:desktop","segment":"supervisor-lifecycle"},{"id":"core-resilience","name":"Core / resilience","command":"e2e:desktop","segment":"resilience"}]} +wework_desktop_other_e2e=false +wework_desktop_other_e2e_matrix={"include":[]}' \ + "wework/src/features/workbench/runtimeTaskLifecycle/reducer.ts" + +assert_desktop_case "runtime pane events select supervisor and conversation coverage" \ + 'wework_desktop_e2e=true +wework_desktop_core_e2e=true +wework_desktop_core_e2e_matrix={"include":[{"id":"core-supervisor-lifecycle","name":"Core / supervisor-lifecycle","command":"e2e:desktop","segment":"supervisor-lifecycle"},{"id":"core-conversation-state","name":"Core / conversation-state","command":"e2e:desktop","segment":"conversation-state"}]} +wework_desktop_other_e2e=false +wework_desktop_other_e2e_matrix={"include":[]}' \ + "wework/src/features/workbench/runtimePaneMessages.ts" + full_desktop_expected='wework_desktop_e2e=true wework_desktop_core_e2e=true -wework_desktop_core_e2e_matrix={"include":[{"id":"core-workspace-tabs","name":"Core / workspace-tabs","command":"e2e:desktop","segment":"workspace-tabs"},{"id":"core-priority-filter","name":"Core / priority-filter","command":"e2e:desktop","segment":"priority-filter"},{"id":"core-core-task-flow","name":"Core / core-task-flow","command":"e2e:desktop","segment":"core-task-flow"},{"id":"core-window-lifecycle","name":"Core / window-lifecycle","command":"e2e:desktop","segment":"window-lifecycle"},{"id":"core-goal-lifecycle","name":"Core / goal-lifecycle","command":"e2e:desktop","segment":"goal-lifecycle"},{"id":"core-resilience","name":"Core / resilience","command":"e2e:desktop","segment":"resilience"},{"id":"core-conversation-state","name":"Core / conversation-state","command":"e2e:desktop","segment":"conversation-state"},{"id":"core-workspace-attachments","name":"Core / workspace-attachments","command":"e2e:desktop","segment":"workspace-attachments"},{"id":"core-rendering-extensions","name":"Core / rendering-extensions","command":"e2e:desktop","segment":"rendering-extensions"}]} +wework_desktop_core_e2e_matrix={"include":[{"id":"core-workspace-tabs","name":"Core / workspace-tabs","command":"e2e:desktop","segment":"workspace-tabs"},{"id":"core-priority-filter","name":"Core / priority-filter","command":"e2e:desktop","segment":"priority-filter"},{"id":"core-core-task-flow","name":"Core / core-task-flow","command":"e2e:desktop","segment":"core-task-flow"},{"id":"core-window-lifecycle","name":"Core / window-lifecycle","command":"e2e:desktop","segment":"window-lifecycle"},{"id":"core-goal-lifecycle","name":"Core / goal-lifecycle","command":"e2e:desktop","segment":"goal-lifecycle"},{"id":"core-supervisor-lifecycle","name":"Core / supervisor-lifecycle","command":"e2e:desktop","segment":"supervisor-lifecycle"},{"id":"core-resilience","name":"Core / resilience","command":"e2e:desktop","segment":"resilience"},{"id":"core-conversation-state","name":"Core / conversation-state","command":"e2e:desktop","segment":"conversation-state"},{"id":"core-workspace-attachments","name":"Core / workspace-attachments","command":"e2e:desktop","segment":"workspace-attachments"},{"id":"core-rendering-extensions","name":"Core / rendering-extensions","command":"e2e:desktop","segment":"rendering-extensions"}]} wework_desktop_other_e2e=true wework_desktop_other_e2e_matrix={"include":[{"id":"plugins","name":"Plugins","command":"e2e:desktop:plugins","segment":""},{"id":"cloud","name":"Cloud","command":"e2e:desktop:cloud","segment":""}]}' diff --git a/wework/DESIGN.md b/wework/DESIGN.md index 3ff2cca8c79..d5ad26dac0b 100644 --- a/wework/DESIGN.md +++ b/wework/DESIGN.md @@ -694,6 +694,8 @@ semantics for all three. - Runtime conversation events are keyed by task address and must update the shared conversation cache even when that task's pane is not active or mounted. +- Turn lifecycle events are additionally keyed by turn identity. A settlement + from an older turn must not clear the running state of a newer active turn. - A guidance item remains pending until a matching runtime `guidance_applied` event settles it. Match the client identifier first and use guidance content only when the runtime replaces that identifier. diff --git a/wework/e2e/desktop/task-flow.e2e.mjs b/wework/e2e/desktop/task-flow.e2e.mjs index 4334a85ee15..5bc4bccb107 100644 --- a/wework/e2e/desktop/task-flow.e2e.mjs +++ b/wework/e2e/desktop/task-flow.e2e.mjs @@ -2913,14 +2913,10 @@ async function verifyExpandedToolDetail( visible: true, timeoutMs: DEFAULT_STEP_TIMEOUT_MS, }) - await control.command( - 'waitFor', - `${selector} [data-tool-detail-toggle][aria-expanded="true"]`, - { - stableMs: 250, - timeoutMs: DEFAULT_STEP_TIMEOUT_MS, - } - ) + await control.command('waitFor', `${selector} [data-tool-detail-toggle][aria-expanded="true"]`, { + stableMs: 250, + timeoutMs: DEFAULT_STEP_TIMEOUT_MS, + }) await captureVerificationScreenshot(control, screenshotName, selector) await control.command('click', `${selector} [data-tool-detail-toggle]`) } @@ -6638,6 +6634,11 @@ async function verifyActiveGoalIdleUnreadLifecycle({ composerSelector, control, async function verifyTaskSupervisorLifecycle({ composerSelector, control }) { await ensureExperimentalFeaturesEnabled(control) control.setScenario('supervisor') + const taskRowsBeforeSupervisor = new Set( + JSON.parse(await control.command('snapshot', 'body')).testIds.filter(testId => + testId.startsWith('runtime-local-task-row-') + ) + ) await control.command('click', '[data-testid="new-chat-button"]') await control.command('waitFor', composerSelector, { timeoutMs: WORKBENCH_READY_TIMEOUT_MS, @@ -6669,6 +6670,54 @@ async function verifyTaskSupervisorLifecycle({ composerSelector, control }) { DEFAULT_STEP_TIMEOUT_MS, 'Auto-correction did not send a normal follow-up turn after the task became idle' ) + await withTimeout( + control.awaitSupervisorCorrectionResponseStarted(), + DEFAULT_STEP_TIMEOUT_MS, + 'The supervisor correction response did not start' + ) + const supervisorTaskRowTestId = await waitForNewTaskRow( + control, + taskRowsBeforeSupervisor, + 'WEWORK_DESKTOP_E2E_SUPERVISOR' + ) + const supervisorTaskId = supervisorTaskRowTestId.replace('runtime-local-task-row-', '') + const supervisorRunningTestId = `runtime-local-task-running-${supervisorTaskId}` + await control.command('waitFor', `[data-testid="${supervisorRunningTestId}"]`, { + timeoutMs: DEFAULT_STEP_TIMEOUT_MS, + }) + const beforeStaleSettlement = JSON.parse( + await control.command('getWorkbenchDebugSnapshot', 'body') + ) + assert.equal( + beforeStaleSettlement.workbench?.currentRuntimeTask?.taskId, + supervisorTaskId, + 'The supervisor correction task was not current before replaying the stale settlement' + ) + await control.command('dispatchRuntimeLifecycleEvent', 'body', { + value: JSON.stringify({ + address: beforeStaleSettlement.workbench.currentRuntimeTask, + type: 'turn_settled', + turnId: 'stale-supervisor-turn', + }), + }) + try { + const runningSnapshot = await waitForWorkbenchDebugState( + control, + snapshot => + snapshot.workbench?.currentRuntimeTask?.taskId === supervisorTaskId && + snapshot.workbench?.lifecycleCurrentTaskRunning === true && + snapshot.pane?.status?.isBusy === true, + 'The live supervisor correction was not represented as running' + ) + assert.equal( + runningSnapshot.pane?.status?.taskExecution?.running, + true, + 'The supervisor correction had a live executor response but task execution was idle' + ) + await captureVerificationScreenshot(control, 'supervisor-01-correction-running.png') + } finally { + control.releaseSupervisorCorrectionResponse() + } await control.command('waitFor', '[data-testid="message-user"]', { text: SUPERVISOR_CORRECTION, timeoutMs: DEFAULT_STEP_TIMEOUT_MS, @@ -7266,6 +7315,12 @@ class DesktopE2EServer { this.supervisorInitialRelease = new Promise(resolvePromise => { this.releaseSupervisorInitial = resolvePromise }) + this.supervisorCorrectionStarted = new Promise(resolvePromise => { + this.resolveSupervisorCorrectionStarted = resolvePromise + }) + this.supervisorCorrectionRelease = new Promise(resolvePromise => { + this.releaseSupervisorCorrection = resolvePromise + }) this.toolBlockNodeOutputObserved = new Promise(resolvePromise => { this.resolveToolBlockNodeOutputObserved = resolvePromise }) @@ -7601,6 +7656,14 @@ class DesktopE2EServer { this.releaseSupervisorInitial() } + awaitSupervisorCorrectionResponseStarted() { + return this.guard(this.supervisorCorrectionStarted) + } + + releaseSupervisorCorrectionResponse() { + this.releaseSupervisorCorrection() + } + releaseGoalIdleInitialResponse() { this.releaseGoalIdleInitial() } @@ -9066,11 +9129,21 @@ class DesktopE2EServer { return } if (requestText.includes(SUPERVISOR_CORRECTION)) { - this.writeSse(response, [ - responseCreated(responseId), - assistantMessage(SUPERVISOR_CORRECTION_COMPLETION_TEXT), - responseCompleted(responseId), - ]) + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Content-Type': 'text/event-stream; charset=utf-8', + }) + response.write( + createSse([ + responseCreated(responseId), + assistantMessage(SUPERVISOR_CORRECTION_COMPLETION_TEXT), + ]) + ) + this.resolveSupervisorCorrectionStarted() + await this.supervisorCorrectionRelease + response.end(createSse([responseCompleted(responseId)])) return } assert.ok(requestText.includes(SUPERVISOR_PROMPT), 'The supervisor task prompt was lost') diff --git a/wework/src/e2e/automation.ts b/wework/src/e2e/automation.ts index 8ca5a41409c..459d7a430d7 100644 --- a/wework/src/e2e/automation.ts +++ b/wework/src/e2e/automation.ts @@ -759,6 +759,13 @@ async function executeDesktopControlCommand(command: DesktopControlCommand): Pro case 'dispatchLocalModelSettingsChanged': window.dispatchEvent(new CustomEvent(LOCAL_MODEL_SETTINGS_CHANGED_EVENT)) return '' + case 'dispatchRuntimeLifecycleEvent': + window.dispatchEvent( + new CustomEvent('wework:e2e:runtime-task-lifecycle', { + detail: JSON.parse(command.value ?? '{}'), + }) + ) + return '' case 'storeLocalProxyUrl': return JSON.stringify(saveLocalProxyUrl(command.value?.trim() ?? '')) case 'getLocalStorageItem': diff --git a/wework/src/features/workbench/WorkbenchProvider.tsx b/wework/src/features/workbench/WorkbenchProvider.tsx index 974a06a727b..69852f8b44b 100644 --- a/wework/src/features/workbench/WorkbenchProvider.tsx +++ b/wework/src/features/workbench/WorkbenchProvider.tsx @@ -1244,13 +1244,13 @@ export function WorkbenchProvider({ createRuntimeConversationStreamHandlers({ onMessageAction: applyCanonicalRuntimeAction, onGuidanceApplied: settleCanonicalRuntimeGuidance, - onAssistantStart: address => { + onAssistantStart: (address, turnId) => { markRuntimeConversationAssistantStarted(address) - lifecycleStore.turnStarted(address) + lifecycleStore.turnStarted(address, turnId) }, - onAssistantSettled: address => { + onAssistantSettled: (address, turnId) => { settleRuntimeConversationSubagents(address) - lifecycleStore.turnSettled(address) + lifecycleStore.turnSettled(address, turnId) }, onContextUsageUpdated: updateCanonicalRuntimeContextUsage, onSubagentActivity: applyRuntimeConversationSubagentActivity, diff --git a/wework/src/features/workbench/runtimePaneMessages.ts b/wework/src/features/workbench/runtimePaneMessages.ts index df19f5b2692..7e620395be9 100644 --- a/wework/src/features/workbench/runtimePaneMessages.ts +++ b/wework/src/features/workbench/runtimePaneMessages.ts @@ -38,8 +38,8 @@ export type RuntimePaneMessageAction = WorkbenchMessageAction void - onAssistantStart?: () => void - onAssistantSettled?: () => void + onAssistantStart?: (turnId: string) => void + onAssistantSettled?: (turnId: string) => void onRefreshWorkLists?: () => void onContextUsageUpdated?: (usage: RuntimeContextUsage) => void onSubagentActivity?: (payload: RuntimeSubagentActivityPayload) => void @@ -54,8 +54,8 @@ export interface RuntimeTaskStreamHandlers { export interface RuntimeConversationStreamHandlers { onMessageAction: (address: RuntimeTaskAddress, action: RuntimePaneMessageAction) => void - onAssistantStart?: (address: RuntimeTaskAddress) => void - onAssistantSettled?: (address: RuntimeTaskAddress) => void + onAssistantStart?: (address: RuntimeTaskAddress, turnId: string) => void + onAssistantSettled?: (address: RuntimeTaskAddress, turnId: string) => void onRefreshWorkLists?: (address: RuntimeTaskAddress) => void onContextUsageUpdated?: (address: RuntimeTaskAddress, usage: RuntimeContextUsage) => void onSubagentActivity?: ( @@ -100,8 +100,8 @@ export function createRuntimeConversationStreamHandlers( const created = createRuntimeTaskStreamHandlers(address, { onMessageAction: action => handlers.onMessageAction(address, action), - onAssistantStart: () => handlers.onAssistantStart?.(address), - onAssistantSettled: () => handlers.onAssistantSettled?.(address), + onAssistantStart: turnId => handlers.onAssistantStart?.(address, turnId), + onAssistantSettled: turnId => handlers.onAssistantSettled?.(address, turnId), onRefreshWorkLists: () => handlers.onRefreshWorkLists?.(address), onContextUsageUpdated: usage => handlers.onContextUsageUpdated?.(address, usage), onSubagentActivity: payload => handlers.onSubagentActivity?.(address, payload), @@ -168,7 +168,7 @@ export function createRuntimeTaskStreamHandlers( return } debugRuntimeStreamEvent('chat:start', address, payload, true) - handlers.onAssistantStart?.() + handlers.onAssistantStart?.(identity.subtaskId) handlers.onMessageAction({ type: 'assistant_started', taskId: payload.taskId, @@ -266,7 +266,7 @@ export function createRuntimeTaskStreamHandlers( blocks, fileChanges, }) - handlers.onAssistantSettled?.() + handlers.onAssistantSettled?.(identity.subtaskId) handlers.onRefreshWorkLists?.() }, onChatError: payload => { @@ -308,7 +308,7 @@ export function createRuntimeTaskStreamHandlers( errorType: payload.type, }) } - handlers.onAssistantSettled?.() + handlers.onAssistantSettled?.(identity.subtaskId) streamedFileChanges.delete(identity.subtaskId) handlers.onRefreshWorkLists?.() }, @@ -345,7 +345,7 @@ export function createRuntimeTaskStreamHandlers( type: 'assistant_done', subtaskId: identity.subtaskId, }) - handlers.onAssistantSettled?.() + handlers.onAssistantSettled?.(identity.subtaskId) handlers.onRefreshWorkLists?.() } }, diff --git a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleProvider.tsx b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleProvider.tsx index 565bef4955e..d8d7d16a11b 100644 --- a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleProvider.tsx +++ b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleProvider.tsx @@ -1,7 +1,10 @@ -import type { ReactNode } from 'react' +import { useEffect, type ReactNode } from 'react' +import type { RuntimeTaskAddress } from '@/types/api' import { RuntimeTaskLifecycleContext } from './internalContext' import type { RuntimeTaskLifecycleStore } from './RuntimeTaskLifecycleStore' +const E2E_RUNTIME_LIFECYCLE_EVENT = 'wework:e2e:runtime-task-lifecycle' + export function RuntimeTaskLifecycleProvider({ store, children, @@ -9,6 +12,25 @@ export function RuntimeTaskLifecycleProvider({ store: RuntimeTaskLifecycleStore children: ReactNode }) { + useEffect(() => { + if (import.meta.env.VITE_WEWORK_E2E !== 'true') return + + const handleLifecycleEvent = (event: Event) => { + const detail = ( + event as CustomEvent<{ + address: RuntimeTaskAddress + type: string + turnId?: string | null + }> + ).detail + if (detail?.type === 'turn_settled') { + store.turnSettled(detail.address, detail.turnId) + } + } + window.addEventListener(E2E_RUNTIME_LIFECYCLE_EVENT, handleLifecycleEvent) + return () => window.removeEventListener(E2E_RUNTIME_LIFECYCLE_EVENT, handleLifecycleEvent) + }, [store]) + return ( {children} diff --git a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts index 929979e3daa..e2cdd9838d8 100644 --- a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts +++ b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.test.ts @@ -282,6 +282,32 @@ describe('RuntimeTaskLifecycleStore', () => { expect(store.getTask(address)?.execution.running).toBe(false) }) + test('ignores a settled event from an older turn after a newer turn starts', () => { + const store = new RuntimeTaskLifecycleStore('test') + store.turnStarted(address, 'turn-1') + store.turnStarted(address, 'turn-2') + + store.turnSettled(address, 'turn-1') + + const snapshot = store.getTask(address) + expect(snapshot?.execution.running).toBe(true) + expect(snapshot?.turn.phase).toBe('streaming') + expect(snapshot?.derived.shouldShowSidebarRunning).toBe(true) + }) + + test('keeps a live stream when an idle executor snapshot lags behind it', () => { + const store = new RuntimeTaskLifecycleStore('test') + store.syncRuntimeWork(runtimeWork(task({ running: false }))) + + store.turnStarted(address) + store.syncRuntimeWork(runtimeWork(task({ running: false, status: 'active' }))) + + const snapshot = store.getTask(address) + expect(snapshot?.execution.phase).toBe('running') + expect(snapshot?.turn.phase).toBe('streaming') + expect(snapshot?.derived.shouldShowSidebarRunning).toBe(true) + }) + test('marks only background non-Goal completion unread and clears it when opened', () => { const store = new RuntimeTaskLifecycleStore('test') store.syncRuntimeWork(runtimeWork(task({ running: true }))) diff --git a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts index fe8212d1cd3..4c79e75b24d 100644 --- a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts +++ b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskLifecycleStore.ts @@ -106,12 +106,12 @@ export class RuntimeTaskLifecycleStore { this.dispatch(address, { type: 'executor_settled' }) } - turnStarted(address: RuntimeTaskAddress): void { - this.dispatch(address, { type: 'turn_started' }) + turnStarted(address: RuntimeTaskAddress, turnId?: string | null): void { + this.dispatch(address, { type: 'turn_started', turnId }) } - turnSettled(address: RuntimeTaskAddress): void { - this.dispatch(address, { type: 'turn_settled' }) + turnSettled(address: RuntimeTaskAddress, turnId?: string | null): void { + this.dispatch(address, { type: 'turn_settled', turnId }) } syncTranscript( @@ -119,9 +119,10 @@ export class RuntimeTaskLifecycleStore { transcript: RuntimePaneTranscript, options: SyncTranscriptOptions = {} ): void { - const hasStreamingTurn = transcript.turns.some( + const streamingTurn = transcript.turns.findLast( turn => turn.status === 'pending' || turn.status === 'streaming' ) + const hasStreamingTurn = Boolean(streamingTurn) const ignoreStaleIdleTranscript = transcript.running === false && options.preserveActiveTurn === true && @@ -134,7 +135,11 @@ export class RuntimeTaskLifecycleStore { } if (hasStreamingTurn && transcript.running !== false) { - this.dispatch(address, { type: 'turn_recovered', streaming: true }) + this.dispatch(address, { + type: 'turn_recovered', + streaming: true, + turnId: streamingTurn?.id, + }) } else if (transcript.running === false && !ignoreStaleIdleTranscript) { this.turnSettled(address) } @@ -179,7 +184,7 @@ export class RuntimeTaskLifecycleStore { nextMachine.dispatch({ type: 'executor_started' }) } if (previousState.turnPhase === 'streaming') { - nextMachine.dispatch({ type: 'turn_started' }) + nextMachine.dispatch({ type: 'turn_started', turnId: previousState.activeTurnId }) } else if (previousState.turnPhase === 'submitting') { nextMachine.dispatch({ type: 'send_requested' }) } else if (previousState.turnPhase === 'awaiting') { diff --git a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskMachine.ts b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskMachine.ts index 09ff496201f..1aaa2864ee1 100644 --- a/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskMachine.ts +++ b/wework/src/features/workbench/runtimeTaskLifecycle/RuntimeTaskMachine.ts @@ -15,6 +15,7 @@ export class RuntimeTaskMachine { task: null, executionPhase: 'unknown', turnPhase: 'idle', + activeTurnId: null, goalStatus: null, continuable: false, unread, diff --git a/wework/src/features/workbench/runtimeTaskLifecycle/reducer.ts b/wework/src/features/workbench/runtimeTaskLifecycle/reducer.ts index 62c709f49b2..4a0d5e23eb7 100644 --- a/wework/src/features/workbench/runtimeTaskLifecycle/reducer.ts +++ b/wework/src/features/workbench/runtimeTaskLifecycle/reducer.ts @@ -23,6 +23,8 @@ export function reduceRuntimeTaskLifecycle( : 'idle' const turnPhase = snapshotRunning === false && !shouldIgnoreStaleSnapshot ? 'idle' : state.turnPhase + const activeTurnId = + snapshotRunning === false && !shouldIgnoreStaleSnapshot ? null : state.activeTurnId return { ...state, @@ -30,6 +32,7 @@ export function reduceRuntimeTaskLifecycle( task: event.task, executionPhase, turnPhase, + activeTurnId, goalStatus: event.task.goalStatus === undefined ? state.goalStatus : event.task.goalStatus, continuable: event.task.continuable !== false, expectedExecutorRunning: @@ -96,6 +99,7 @@ export function reduceRuntimeTaskLifecycle( ...state, executionPhase: 'idle', turnPhase: 'idle', + activeTurnId: null, expectedExecutorRunning: false, } @@ -104,18 +108,24 @@ export function reduceRuntimeTaskLifecycle( ...state, executionPhase: 'running', turnPhase: 'streaming', + activeTurnId: event.turnId ?? null, expectedExecutorRunning: true, unread: false, } - case 'turn_settled': + case 'turn_settled': { + if (event.turnId && state.activeTurnId && event.turnId !== state.activeTurnId) { + return state + } return { ...state, executionPhase: state.goalStatus === 'active' ? state.executionPhase : 'idle', turnPhase: 'idle', + activeTurnId: null, expectedExecutorRunning: state.goalStatus === 'active' ? state.expectedExecutorRunning : false, } + } case 'turn_recovered': return event.streaming @@ -123,6 +133,7 @@ export function reduceRuntimeTaskLifecycle( ...state, executionPhase: 'running', turnPhase: 'streaming', + activeTurnId: event.turnId ?? null, expectedExecutorRunning: true, } : state diff --git a/wework/src/features/workbench/runtimeTaskLifecycle/types.ts b/wework/src/features/workbench/runtimeTaskLifecycle/types.ts index ffdef4aceca..7f1be1910c0 100644 --- a/wework/src/features/workbench/runtimeTaskLifecycle/types.ts +++ b/wework/src/features/workbench/runtimeTaskLifecycle/types.ts @@ -9,6 +9,7 @@ export interface RuntimeTaskLifecycleState { task: RuntimeTaskSummary | null executionPhase: RuntimeTaskExecutionPhase turnPhase: RuntimeTaskTurnPhase + activeTurnId: string | null goalStatus: RuntimeGoalStatus | null continuable: boolean unread: boolean @@ -59,9 +60,9 @@ export type RuntimeTaskLifecycleEvent = | { type: 'stop_rejected' } | { type: 'executor_started' } | { type: 'executor_settled' } - | { type: 'turn_started' } - | { type: 'turn_settled' } - | { type: 'turn_recovered'; streaming: boolean } + | { type: 'turn_started'; turnId?: string | null } + | { type: 'turn_settled'; turnId?: string | null } + | { type: 'turn_recovered'; streaming: boolean; turnId?: string | null } | { type: 'goal_status_received'; goalStatus: RuntimeGoalStatus | null } | { type: 'marked_read' } | { type: 'marked_unread' }