( config: LifecycleConfig, deps: LifecycleDependencies )
| 36 | onSseEvent: () => void; |
| 37 | }; |
| 38 | |
| 39 | export function createLifecycleManager( |
| 40 | config: LifecycleConfig, |
| 41 | deps: LifecycleDependencies |
| 42 | ): LifecycleManager { |
| 43 | const { state, kiloClient } = deps; |
| 44 | let sseTransportTimer: ReturnType<typeof setTimeout> | null = null; |
| 45 | let stableIdleTimer: ReturnType<typeof setTimeout> | null = null; |
| 46 | let isAborted = false; |
| 47 | let rootIdleCandidatePresent = false; |
| 48 | let idleObservedDuringDelivery = false; |
| 49 | let postProcessingResolve: (() => void) | null = null; |
| 50 | let drainPromise: Promise<void> | null = null; |
| 51 | let lifecycleGeneration = 0; |
| 52 | let postProcessingCompleted = false; |
| 53 | |
| 54 | function clearSseTransportTimer(): void { |
| 55 | if (!sseTransportTimer) return; |
| 56 | clearTimeout(sseTransportTimer); |
| 57 | sseTransportTimer = null; |
| 58 | } |
| 59 | |
| 60 | function clearStableIdleCandidate(): void { |
| 61 | rootIdleCandidatePresent = false; |
| 62 | idleObservedDuringDelivery = false; |
| 63 | if (!stableIdleTimer) return; |
| 64 | clearTimeout(stableIdleTimer); |
| 65 | stableIdleTimer = null; |
| 66 | } |
| 67 | |
| 68 | function resetSseTransportTimer(): void { |
| 69 | clearSseTransportTimer(); |
| 70 | // Idle is the last expected SSE event. Reconnecting during drain races |
| 71 | // auto-commit and used to abort the complete event. |
| 72 | if (!state.hasSession || drainPromise) return; |
| 73 | sseTransportTimer = setTimeout(() => { |
| 74 | logToFile('SSE transport timeout — reconnecting event subscription'); |
| 75 | deps.reconnectEventSubscription(); |
| 76 | }, SSE_TRANSPORT_TIMEOUT_MS); |
| 77 | } |
| 78 | |
| 79 | function signalCompletion(): void { |
| 80 | postProcessingCompleted = true; |
| 81 | postProcessingResolve?.(); |
| 82 | postProcessingResolve = null; |
| 83 | } |
| 84 | |
| 85 | async function runPostCompletionTasks(): Promise<void> { |
| 86 | const session = state.currentSession; |
| 87 | const msgConfig = state.batchFinalizationConfig; |
| 88 | if (!session || !msgConfig || isAborted) return; |
| 89 | |
| 90 | if (msgConfig.autoCommit) { |
| 91 | try { |
| 92 | const autoCommitController = new AbortController(); |
| 93 | let autoCommitTimedOut = false; |
| 94 | const timeout = setTimeout(() => { |
| 95 | autoCommitTimedOut = true; |
no test coverage detected