diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 6b8a112c8..131854fd2 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -1762,6 +1762,15 @@ const chatStore = (initial?: Partial) => // Create AbortController for this task's SSE connection // First check if there's already an active SSE connection for this task + if (activeSSEControllers[newTaskId] && type === 'replay') { + // A history replay must never tear down a live run's stream: the + // ongoing run is the fresher state, and aborting it kills the run + // on the backend. Leave the live connection alone. + console.warn( + `Task ${newTaskId} already has an active SSE connection, skipping history replay` + ); + return; + } if (activeSSEControllers[newTaskId]) { console.warn( `Task ${newTaskId} already has an active SSE connection, aborting old one` diff --git a/src/store/projectStore.ts b/src/store/projectStore.ts index 402ada3de..beae3913c 100644 --- a/src/store/projectStore.ts +++ b/src/store/projectStore.ts @@ -31,7 +31,11 @@ import { } from '@/types/constants'; import { create } from 'zustand'; import { getAuthStore } from './authStore'; -import { createChatStoreInstance, VanillaChatStore } from './chatStore'; +import { + createChatStoreInstance, + hasActiveSSEConnection, + VanillaChatStore, +} from './chatStore'; import { projectMetaFromServer, useSpaceStore, @@ -1042,6 +1046,18 @@ const projectStore = create()((set, get) => ({ ) { return; } + // Never evict a project that still has a live run. Eviction drops the + // runtime chat stores, so returning to the project rebuilds it from + // history and replays the ongoing task id -- which aborts the live + // run's stream and kills the run on the backend. Keep the stale flag + // so the eviction simply happens on a later, safe transition. + const outgoingProject = get().projects[previousProjectId]; + const outgoingTaskIds = Object.values( + outgoingProject?.chatStores ?? {} + ).flatMap((chatStore) => Object.keys(chatStore.getState().tasks)); + if (hasActiveSSEConnection(outgoingTaskIds)) { + return; + } // _evictProjectRuntime handles staleProjectIds cleanup itself. get()._evictProjectRuntime(previousProjectId); },