export type SessionRunQueuedPrompt = { id: string; text: string; }; export type SessionRunPhase = 'idle' | 'posting' | 'running' | 'aborting'; export type SessionRunTerminalReason = 'completed' | 'aborted' | 'failed'; export type SessionRunState = { phase: SessionRunPhase; runId: number | null; promptId: string | null; queue: TPrompt[]; terminalReason: SessionRunTerminalReason | null; error: string | null; suppressNextAbortError: boolean; }; export type SessionRunEvent = | { type: 'send_started'; runId: number; promptId: string } | { type: 'post_accepted'; runId: number } | { type: 'remote_busy'; runId: number } | { type: 'remote_idle'; runId: number } | { type: 'remote_aborted'; runId: number } | { type: 'remote_failed'; runId: number; error: string } | { type: 'abort_requested'; runId: number } | { type: 'abort_confirmed'; runId: number } | { type: 'abort_failed'; runId: number; error: string } | { type: 'queue_prompt'; prompt: TPrompt } | { type: 'cancel_queued_prompt'; promptId: string }; export function createIdleSessionRunState( queue: TPrompt[] = [], ): SessionRunState { return { phase: 'idle', runId: null, promptId: null, queue, terminalReason: null, error: null, suppressNextAbortError: false, }; } export function isCurrentRunEvent( state: SessionRunState, runId: number, ): boolean { return state.runId === runId; } export function isSessionRunActive( state: SessionRunState, ): boolean { return state.phase === 'posting' || state.phase === 'running'; } export function canStartQueuedPrompt( state: SessionRunState, ): boolean { return state.phase === 'idle' && state.terminalReason !== 'failed' && state.queue.length > 0; } export function takeNextQueuedPrompt( state: SessionRunState, ): { prompt: TPrompt | null; state: SessionRunState; } { const [prompt, ...queue] = state.queue; return { prompt: prompt ?? null, state: { ...state, queue, }, }; } export function didSuppressAbortError( previousState: SessionRunState, nextState: SessionRunState, ): boolean { return previousState.suppressNextAbortError && !nextState.suppressNextAbortError && previousState.phase === nextState.phase && previousState.runId === nextState.runId && previousState.promptId === nextState.promptId && nextState.terminalReason === previousState.terminalReason && nextState.error === null; } export function transitionSessionRunState( state: SessionRunState, event: SessionRunEvent, ): SessionRunState { if (event.type === 'send_started') { return { phase: 'posting', runId: event.runId, promptId: event.promptId, queue: state.queue, terminalReason: null, error: null, suppressNextAbortError: state.suppressNextAbortError, }; } if (event.type === 'queue_prompt') { return { ...state, queue: [...state.queue, event.prompt], error: null, }; } if (event.type === 'cancel_queued_prompt') { return { ...state, queue: state.queue.filter((prompt) => prompt.id !== event.promptId), }; } if (!isCurrentRunEvent(state, event.runId)) { return state; } if ( state.phase === 'idle' && state.terminalReason !== null && ( event.type === 'post_accepted' || event.type === 'remote_busy' || event.type === 'remote_idle' ) ) { return state; } switch (event.type) { case 'post_accepted': return state; case 'remote_busy': return { ...state, phase: 'running', terminalReason: null, error: null, }; case 'remote_idle': if (state.phase === 'posting') return state; return { ...state, phase: 'idle', terminalReason: 'completed', error: null, suppressNextAbortError: false, }; case 'remote_aborted': if (state.suppressNextAbortError) { return { ...state, error: null, suppressNextAbortError: false, }; } return { ...state, phase: 'idle', queue: [], terminalReason: 'aborted', error: null, suppressNextAbortError: false, }; case 'remote_failed': return { ...state, phase: 'idle', queue: [], terminalReason: 'failed', error: event.error, suppressNextAbortError: false, }; case 'abort_requested': return { ...state, phase: 'aborting', queue: [], terminalReason: 'aborted', error: null, suppressNextAbortError: true, }; case 'abort_confirmed': return { ...state, phase: 'idle', queue: [], terminalReason: 'aborted', error: null, suppressNextAbortError: false, }; case 'abort_failed': return { ...state, phase: 'idle', queue: [], terminalReason: 'failed', error: event.error, suppressNextAbortError: false, }; default: return state; } }