From a9882b1b45707a03cd78ce0bd28b39d538526eba Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 15 Apr 2026 14:46:48 +1200 Subject: [PATCH] fix(workflow): validate loop count and handle resume inside loops 1. Normalize and clamp loopCount at execution time (1-100 range) - Validates imported/malformed values are finite numbers - Falls back to default of 3 if invalid - Prevents NaN, Infinity, or extreme values from bypassing setLoopCount validation 2. Fix resume logic for nodes inside loop bodies - Compute loopStartLevel when startFromNodeId is in loopLevels - On first iteration, start from that level instead of level 0 - On subsequent iterations, start from level 0 as normal - Preserves existing copyLoopOutput skip on first iteration Co-Authored-By: Claude Opus 4.6 --- src/store/workflowStore.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index bd0277ae..98980f89 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -1534,11 +1534,19 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ } } - // Remap startLevel to prefixLevels index (original index may not match after partitioning) + // Remap startLevel to prefixLevels and loopLevels indices let prefixStartLevel = 0; + let loopStartLevel = -1; if (startFromNodeId) { - const idx = prefixLevels.findIndex((l) => l.nodeIds.includes(startFromNodeId)); - prefixStartLevel = idx !== -1 ? idx : 0; + const prefixIdx = prefixLevels.findIndex((l) => l.nodeIds.includes(startFromNodeId)); + const loopIdx = loopLevels.findIndex((l) => l.nodeIds.includes(startFromNodeId)); + + if (prefixIdx !== -1) { + prefixStartLevel = prefixIdx; + } else if (loopIdx !== -1) { + // Resume target is inside loop - start from that level on first iteration + loopStartLevel = loopIdx; + } } // Execute prefix once @@ -1546,7 +1554,12 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ await executeLevels(prefixLevels, prefixStartLevel); // Execute loop N times - const loopCount = loopEdge.data?.loopCount ?? 3; + // Normalize and clamp loopCount to handle malformed/imported values + const rawLoopCount = loopEdge.data?.loopCount ?? 3; + const parsed = Number(rawLoopCount); + const loopCount = Number.isFinite(parsed) && !isNaN(parsed) + ? Math.max(1, Math.min(100, parsed)) + : 3; for (let i = 0; i < loopCount; i++) { if (abortController.signal.aborted || !get().isRunning) break; @@ -1569,7 +1582,9 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ logger.info('node.execution', `Loop iteration ${i + 1}/${loopCount}`); // Execute loop body + downstream levels with fresh node state - await executeLevels(loopLevels); + // On first iteration, use loopStartLevel if resuming from a node inside the loop + const startLevel = i === 0 && loopStartLevel !== -1 ? loopStartLevel : 0; + await executeLevels(loopLevels, startLevel); } }