From 6c205cbf4c0bc49613e83e9df910b7789af42fb8 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Thu, 19 Feb 2026 22:10:01 -0800 Subject: [PATCH] fix: jobs stuck in initializing state when failing before execution_start (#8689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix jobs getting permanently stuck in "initializing" state when they fail before the `execution_start` WebSocket event fires. ## Changes - **What**: Added `reconcileInitializingPrompts(activeJobIds)` to `executionStore` that removes orphaned initializing prompt IDs not present in the active jobs set. Called from `queueStore.update()` after fetching Running/Pending jobs, ensuring stale initializing states are cleaned up on every queue poll. ## Review Focus - The reconciliation delegates to the existing `clearInitializationByPromptIds` to avoid duplicating Set-diffing logic. - Only runs during `queueStore.update()` which is already a periodic poll — no additional network calls. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8689-fix-jobs-stuck-in-initializing-state-when-failing-before-execution_start-2ff6d73d3650814dbeeeda71c8bb7d43) by [Unito](https://www.unito.io) --- src/stores/executionStore.test.ts | 42 +++++++++++++++++++++++++++++++ src/stores/executionStore.ts | 8 ++++++ src/stores/queueStore.ts | 12 +++++++++ 3 files changed, 62 insertions(+) diff --git a/src/stores/executionStore.test.ts b/src/stores/executionStore.test.ts index 3850a161b9..6cc2749ad0 100644 --- a/src/stores/executionStore.test.ts +++ b/src/stores/executionStore.test.ts @@ -132,6 +132,48 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { }) }) +describe('useExecutionStore - reconcileInitializingPrompts', () => { + let store: ReturnType + + beforeEach(() => { + vi.clearAllMocks() + setActivePinia(createTestingPinia({ stubActions: false })) + store = useExecutionStore() + }) + + it('should remove prompt IDs not present in active jobs', () => { + store.initializingPromptIds = new Set(['job-1', 'job-2', 'job-3']) + + store.reconcileInitializingPrompts(new Set(['job-1'])) + + expect(store.initializingPromptIds).toEqual(new Set(['job-1'])) + }) + + it('should be a no-op when all initializing IDs are active', () => { + store.initializingPromptIds = new Set(['job-1', 'job-2']) + + store.reconcileInitializingPrompts(new Set(['job-1', 'job-2', 'job-3'])) + + expect(store.initializingPromptIds).toEqual(new Set(['job-1', 'job-2'])) + }) + + it('should be a no-op when there are no initializing prompts', () => { + store.initializingPromptIds = new Set() + + store.reconcileInitializingPrompts(new Set(['job-1'])) + + expect(store.initializingPromptIds).toEqual(new Set()) + }) + + it('should clear all initializing IDs when no active jobs exist', () => { + store.initializingPromptIds = new Set(['job-1', 'job-2']) + + store.reconcileInitializingPrompts(new Set()) + + expect(store.initializingPromptIds).toEqual(new Set()) + }) +}) + describe('useExecutionStore - Node Error Lookups', () => { let store: ReturnType diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index ecd75ffae0..35f0fed0df 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -442,6 +442,13 @@ export const useExecutionStore = defineStore('execution', () => { initializingPromptIds.value = next } + function reconcileInitializingPrompts(activeJobIds: Set) { + const orphaned = [...initializingPromptIds.value].filter( + (id) => !activeJobIds.has(id) + ) + clearInitializationByPromptIds(orphaned) + } + function isPromptInitializing( promptId: string | number | undefined ): boolean { @@ -716,6 +723,7 @@ export const useExecutionStore = defineStore('execution', () => { isPromptInitializing, clearInitializationByPromptId, clearInitializationByPromptIds, + reconcileInitializingPrompts, bindExecutionEvents, unbindExecutionEvents, storePrompt, diff --git a/src/stores/queueStore.ts b/src/stores/queueStore.ts index fd4a77886a..07ffd9060a 100644 --- a/src/stores/queueStore.ts +++ b/src/stores/queueStore.ts @@ -537,6 +537,18 @@ export const useQueueStore = defineStore('queue', () => { } }) + // Only reconcile when the queue fetch returned data. api.getQueue() + // returns empty Running/Pending on transient errors, which would + // incorrectly clear all initializing prompts. + const queueHasData = queue.Running.length > 0 || queue.Pending.length > 0 + if (queueHasData) { + const activeJobIds = new Set([ + ...queue.Running.map((j) => j.id), + ...queue.Pending.map((j) => j.id) + ]) + executionStore.reconcileInitializingPrompts(activeJobIds) + } + // Sort by create_time descending and limit to maxItems const sortedHistory = [...history] .sort((a, b) => b.create_time - a.create_time)