fix: jobs stuck in initializing state when failing before execution_start (#8689)

## 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)
This commit is contained in:
Christian Byrne
2026-02-19 22:10:01 -08:00
committed by GitHub
parent 351d43a95a
commit 6c205cbf4c
3 changed files with 62 additions and 0 deletions

View File

@@ -132,6 +132,48 @@ describe('useExecutionStore - NodeLocatorId conversions', () => {
})
})
describe('useExecutionStore - reconcileInitializingPrompts', () => {
let store: ReturnType<typeof useExecutionStore>
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<typeof useExecutionStore>

View File

@@ -442,6 +442,13 @@ export const useExecutionStore = defineStore('execution', () => {
initializingPromptIds.value = next
}
function reconcileInitializingPrompts(activeJobIds: Set<string>) {
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,

View File

@@ -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)