99 lines
3.8 KiB
JavaScript
99 lines
3.8 KiB
JavaScript
/**
|
|
* 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; agentRunSucceeded?: boolean }} input
|
|
* @returns {'idle' | 'streaming'}
|
|
*/
|
|
export function resolvePostAgentRunChatState({
|
|
chatState = 'waiting',
|
|
finishedViaPortalDirectChat = false,
|
|
agentRunSucceeded = false,
|
|
} = {}) {
|
|
if (finishedViaPortalDirectChat || agentRunSucceeded) return 'idle';
|
|
if (chatState === 'idle') return 'idle';
|
|
return 'streaming';
|
|
}
|
|
|
|
/**
|
|
* @param {string | undefined | null} chatState
|
|
* @returns {boolean}
|
|
*/
|
|
export function shouldPromoteSessionIdToStreaming(chatState) {
|
|
return chatState !== 'idle';
|
|
}
|
|
|
|
/**
|
|
* Only transport uncertainty may continue a run after submit fails. A
|
|
* deterministic gateway error already has a terminal outcome and must return
|
|
* the chat UI to an error/idle state instead of leaving a Stop button active.
|
|
*
|
|
* @param {number | undefined | null} status
|
|
* @returns {boolean}
|
|
*/
|
|
export function shouldKeepStreamingAfterRunError(status, message = '', code = '') {
|
|
if (status === 0 || Number(status) >= 500) return true;
|
|
// Goose can surface the session-level concurrency guard as a failed agent
|
|
// run (rather than an HTTP 409). The request may already be streaming and
|
|
// the session SSE is still the source of truth, so recover from the session
|
|
// instead of showing a terminal error in the chat composer.
|
|
const text = `${String(code ?? '')} ${String(message ?? '')}`.toLowerCase();
|
|
if (String(code ?? '').trim() === 'SESSION_RUN_CONFLICT') return false;
|
|
return text.includes('session already has an active request')
|
|
|| text.includes('active request. cancel it first');
|
|
}
|
|
|
|
/**
|
|
* 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 };
|
|
}
|