diff --git a/src/__tests__/renderer/hooks/useBatchHandlers.test.ts b/src/__tests__/renderer/hooks/useBatchHandlers.test.ts index 80a7d154e..791d75fc7 100644 --- a/src/__tests__/renderer/hooks/useBatchHandlers.test.ts +++ b/src/__tests__/renderer/hooks/useBatchHandlers.test.ts @@ -57,6 +57,7 @@ import { import { useBatchProcessor } from '../../../renderer/hooks/batch/useBatchProcessor'; import { useSessionStore } from '../../../renderer/stores/sessionStore'; import { useSettingsStore } from '../../../renderer/stores/settingsStore'; +import { useBatchStore } from '../../../renderer/stores/batchStore'; import { useModalStore } from '../../../renderer/stores/modalStore'; // ============================================================================ @@ -447,8 +448,13 @@ describe('useBatchHandlers', () => { }); describe('handleSkipCurrentDocument', () => { - it('calls skipCurrentDocument and clears agent error for active batch session', () => { + it('calls skipCurrentDocument for the error-paused session', () => { mockActiveBatchSessionIds = ['session-2']; + useBatchStore.setState({ + batchRunStates: { + 'session-2': createDefaultBatchState({ isRunning: true, errorPaused: true }), + }, + }); const { result } = renderHook(() => useBatchHandlers(createDeps())); act(() => { @@ -459,9 +465,14 @@ describe('useBatchHandlers', () => { expect(mockHandleClearAgentError).toHaveBeenCalledWith('session-2'); }); - it('falls back to active session when no active batch sessions', () => { + it('prefers active session when it is error-paused', () => { const session = createMockSession({ id: 'session-1' }); useSessionStore.setState({ sessions: [session], activeSessionId: 'session-1' }); + useBatchStore.setState({ + batchRunStates: { + 'session-1': createDefaultBatchState({ isRunning: true, errorPaused: true }), + }, + }); mockActiveBatchSessionIds = []; const { result } = renderHook(() => useBatchHandlers(createDeps())); @@ -474,8 +485,9 @@ describe('useBatchHandlers', () => { expect(mockHandleClearAgentError).toHaveBeenCalledWith('session-1'); }); - it('does nothing when no session ID can be resolved', () => { + it('does nothing when no session is error-paused', () => { useSessionStore.setState({ sessions: [], activeSessionId: '' }); + useBatchStore.setState({ batchRunStates: {} }); mockActiveBatchSessionIds = []; const { result } = renderHook(() => useBatchHandlers(createDeps())); @@ -490,8 +502,13 @@ describe('useBatchHandlers', () => { }); describe('handleResumeAfterError', () => { - it('calls resumeAfterError and clears agent error for active batch session', () => { + it('calls resumeAfterError for the error-paused session', () => { mockActiveBatchSessionIds = ['session-2']; + useBatchStore.setState({ + batchRunStates: { + 'session-2': createDefaultBatchState({ isRunning: true, errorPaused: true }), + }, + }); const { result } = renderHook(() => useBatchHandlers(createDeps())); act(() => { @@ -502,9 +519,14 @@ describe('useBatchHandlers', () => { expect(mockHandleClearAgentError).toHaveBeenCalledWith('session-2'); }); - it('falls back to active session when no active batch sessions', () => { + it('prefers active session when it is error-paused', () => { const session = createMockSession({ id: 'session-1' }); useSessionStore.setState({ sessions: [session], activeSessionId: 'session-1' }); + useBatchStore.setState({ + batchRunStates: { + 'session-1': createDefaultBatchState({ isRunning: true, errorPaused: true }), + }, + }); mockActiveBatchSessionIds = []; const { result } = renderHook(() => useBatchHandlers(createDeps())); @@ -517,8 +539,9 @@ describe('useBatchHandlers', () => { expect(mockHandleClearAgentError).toHaveBeenCalledWith('session-1'); }); - it('does nothing when no session ID can be resolved', () => { + it('does nothing when no session is error-paused', () => { useSessionStore.setState({ sessions: [], activeSessionId: '' }); + useBatchStore.setState({ batchRunStates: {} }); mockActiveBatchSessionIds = []; const { result } = renderHook(() => useBatchHandlers(createDeps())); @@ -532,8 +555,13 @@ describe('useBatchHandlers', () => { }); describe('handleAbortBatchOnError', () => { - it('calls abortBatchOnError and clears agent error for active batch session', () => { + it('calls abortBatchOnError for the error-paused session', () => { mockActiveBatchSessionIds = ['session-3']; + useBatchStore.setState({ + batchRunStates: { + 'session-3': createDefaultBatchState({ isRunning: true, errorPaused: true }), + }, + }); const { result } = renderHook(() => useBatchHandlers(createDeps())); act(() => { @@ -544,9 +572,10 @@ describe('useBatchHandlers', () => { expect(mockHandleClearAgentError).toHaveBeenCalledWith('session-3'); }); - it('falls back to active session when no active batch sessions', () => { + it('does nothing when no session is error-paused', () => { const session = createMockSession({ id: 'session-1' }); useSessionStore.setState({ sessions: [session], activeSessionId: 'session-1' }); + useBatchStore.setState({ batchRunStates: {} }); mockActiveBatchSessionIds = []; const { result } = renderHook(() => useBatchHandlers(createDeps())); @@ -555,12 +584,13 @@ describe('useBatchHandlers', () => { result.current.handleAbortBatchOnError(); }); - expect(mockAbortBatchOnError).toHaveBeenCalledWith('session-1'); - expect(mockHandleClearAgentError).toHaveBeenCalledWith('session-1'); + expect(mockAbortBatchOnError).not.toHaveBeenCalled(); + expect(mockHandleClearAgentError).not.toHaveBeenCalled(); }); it('does nothing when no session ID can be resolved', () => { useSessionStore.setState({ sessions: [], activeSessionId: '' }); + useBatchStore.setState({ batchRunStates: {} }); mockActiveBatchSessionIds = []; const { result } = renderHook(() => useBatchHandlers(createDeps())); diff --git a/src/renderer/hooks/batch/useBatchHandlers.ts b/src/renderer/hooks/batch/useBatchHandlers.ts index 6dfc93c5c..6d05381e7 100644 --- a/src/renderer/hooks/batch/useBatchHandlers.ts +++ b/src/renderer/hooks/batch/useBatchHandlers.ts @@ -29,11 +29,27 @@ import { CONDUCTOR_BADGES, getBadgeForTime } from '../../constants/conductorBadg import { getActiveTab } from '../../utils/tabHelpers'; import { generateId } from '../../utils/ids'; import { useBatchProcessor } from './useBatchProcessor'; +import { useBatchStore } from '../../stores/batchStore'; import { consumeGroupChatAutoRun } from '../../utils/groupChatAutoRunRegistry'; import type { RightPanelHandle } from '../../components/RightPanel'; import type { AgentSpawnResult } from '../agent/useAgentExecution'; import * as Sentry from '@sentry/electron/renderer'; +/** + * Find the session that is actually paused on error. + * Prefer the active session when it is paused; otherwise pick the first errorPaused session. + * Returns undefined when nothing is error-paused — callers bail via the existing guard. + */ +function resolveBatchSessionIdForPausedError( + batchRunStates: Record, + activeSessionId: string | undefined +): string | undefined { + if (activeSessionId && batchRunStates[activeSessionId]?.errorPaused) { + return activeSessionId; + } + return Object.keys(batchRunStates).find((id) => batchRunStates[id]?.errorPaused); +} + // ============================================================================ // Dependencies interface // ============================================================================ @@ -640,28 +656,37 @@ export function useBatchHandlers(deps: UseBatchHandlersDeps): UseBatchHandlersRe ); const handleSkipCurrentDocument = useCallback(() => { - const sessionId = - activeBatchSessionIds.length > 0 ? activeBatchSessionIds[0] : activeSession?.id; + // Reads batchRunStates imperatively at call time + const sessionId = resolveBatchSessionIdForPausedError( + useBatchStore.getState().batchRunStates, + activeSession?.id + ); if (!sessionId) return; skipCurrentDocument(sessionId); handleClearAgentError(sessionId); - }, [activeBatchSessionIds, activeSession, skipCurrentDocument, handleClearAgentError]); + }, [activeSession, skipCurrentDocument, handleClearAgentError]); const handleResumeAfterError = useCallback(() => { - const sessionId = - activeBatchSessionIds.length > 0 ? activeBatchSessionIds[0] : activeSession?.id; + // Reads batchRunStates imperatively at call time + const sessionId = resolveBatchSessionIdForPausedError( + useBatchStore.getState().batchRunStates, + activeSession?.id + ); if (!sessionId) return; resumeAfterError(sessionId); handleClearAgentError(sessionId); - }, [activeBatchSessionIds, activeSession, resumeAfterError, handleClearAgentError]); + }, [activeSession, resumeAfterError, handleClearAgentError]); const handleAbortBatchOnError = useCallback(() => { - const sessionId = - activeBatchSessionIds.length > 0 ? activeBatchSessionIds[0] : activeSession?.id; + // Reads batchRunStates imperatively at call time + const sessionId = resolveBatchSessionIdForPausedError( + useBatchStore.getState().batchRunStates, + activeSession?.id + ); if (!sessionId) return; abortBatchOnError(sessionId); handleClearAgentError(sessionId); - }, [activeBatchSessionIds, activeSession, abortBatchOnError, handleClearAgentError]); + }, [activeSession, abortBatchOnError, handleClearAgentError]); // ==================================================================== // Sync auto-run stats from server