/** * Agent-run gate vs session Finish ordering (regression guard). * * Context (2026-07): session SSE Finish can arrive before agent-run SSE reports * succeeded. Re-entering streaming after the run gate completes leaves the UI stuck * on typing dots even though the assistant reply is already visible. * * Tests: chat-agent-run-gate.test.mjs * Consumer: src/hooks/useTKMindChat.ts */ /** * @param {{ chatState?: string; finishedViaPortalDirectChat?: boolean }} input * @returns {'idle' | 'streaming'} */ export function resolvePostAgentRunChatState({ chatState = 'waiting', finishedViaPortalDirectChat = false, } = {}) { if (finishedViaPortalDirectChat) return 'idle'; if (chatState === 'idle') return 'idle'; return 'streaming'; } /** * @param {string | undefined | null} chatState * @returns {boolean} */ export function shouldPromoteSessionIdToStreaming(chatState) { return chatState !== 'idle'; } /** * Agent-run POST tracks portal request_id; Goose SSE may emit a different chat_request_id. * Adopt the upstream id so Message/Finish events are not dropped in the UI. * * @param {{ activeRequestId?: string | null; chatState?: string; eventType?: string; eventRequestId?: string | null; activeRequestIds?: string[] }} input * @returns {{ activeRequestId: string | null; promoteStreaming: boolean; allowMissingGrace: boolean }} */ export function reconcileSessionEventRequestContext({ activeRequestId = null, chatState = 'idle', eventType, eventRequestId = null, activeRequestIds = [], } = {}) { if (eventType === 'ActiveRequests') { if (!activeRequestId && activeRequestIds.length > 0) { return { activeRequestId: activeRequestIds[0], promoteStreaming: true, allowMissingGrace: false }; } if (activeRequestId && activeRequestIds.includes(activeRequestId)) { return { activeRequestId, promoteStreaming: false, allowMissingGrace: false }; } if (activeRequestId && !activeRequestIds.includes(activeRequestId)) { if (chatState === 'waiting' && activeRequestIds.length > 0) { return { activeRequestId: activeRequestIds[0], promoteStreaming: true, allowMissingGrace: false }; } return { activeRequestId, promoteStreaming: false, allowMissingGrace: chatState !== 'waiting', }; } return { activeRequestId, promoteStreaming: false, allowMissingGrace: false }; } if ( eventRequestId && (chatState === 'waiting' || chatState === 'streaming') && (eventType === 'Message' || eventType === 'Finish') && (!activeRequestId || eventRequestId !== activeRequestId) ) { return { activeRequestId: eventRequestId, promoteStreaming: chatState === 'waiting', allowMissingGrace: false }; } return { activeRequestId, promoteStreaming: false, allowMissingGrace: false }; }