Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,13 @@ export function useWorkflowExecution() {
updateActiveBlocks(data.blockId, false)
setBlockRunStatus(data.blockId, 'error')

executedBlockIds.add(data.blockId)
accumulatedBlockStates.set(data.blockId, {
output: { error: data.error },
executed: true,
executionTime: data.durationMs || 0,
})

accumulatedBlockLogs.push(
createBlockLogEntry(data, { success: false, output: {}, error: data.error })
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,15 @@ export function PreviewWorkflow({
if (block.type === 'loop' || block.type === 'parallel') {
const isSelected = selectedBlockId === blockId
const dimensions = calculateContainerDimensions(blockId, workflowState.blocks)
const subflowExecutionStatus = getSubflowExecutionStatus(blockId)

// Check for direct error on the subflow block itself (e.g., loop resolution errors)
// before falling back to children-derived status
const directExecution = blockExecutionMap.get(blockId)
const subflowExecutionStatus: ExecutionStatus | undefined =
directExecution?.status === 'error'
? 'error'
: (getSubflowExecutionStatus(blockId) ??
(directExecution ? (directExecution.status as ExecutionStatus) : undefined))

nodeArray.push({
id: blockId,
Expand Down
81 changes: 81 additions & 0 deletions apps/sim/executor/execution/edge-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2478,6 +2478,9 @@ describe('EdgeManager', () => {
expect(readyNodes).toContain(otherBranchId)
expect(readyNodes).not.toContain(sentinelStartId)

// sentinel_end should NOT be ready - it's on a fully deactivated path
expect(readyNodes).not.toContain(sentinelEndId)

// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
expect(readyNodes).not.toContain(afterLoopId)

Expand Down Expand Up @@ -2545,6 +2548,84 @@ describe('EdgeManager', () => {
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
})

it('should not queue loop sentinel-end when upstream condition deactivates entire loop branch', () => {
// Regression test for: upstream condition → (if) → ... many blocks ... → sentinel_start → body → sentinel_end
// → (else) → exit_block
// When condition takes "else", the deep cascade deactivation should NOT queue sentinel_end.
// Previously, sentinel_end was flagged as a cascadeTarget (terminal control node) and
// spuriously queued, causing it to attempt loop scope initialization and fail.

const conditionId = 'condition'
const intermediateId = 'intermediate'
const sentinelStartId = 'sentinel-start'
const loopBodyId = 'loop-body'
const sentinelEndId = 'sentinel-end'
const afterLoopId = 'after-loop'
const exitBlockId = 'exit-block'

const conditionNode = createMockNode(conditionId, [
{ target: intermediateId, sourceHandle: 'condition-if' },
{ target: exitBlockId, sourceHandle: 'condition-else' },
])

const intermediateNode = createMockNode(
intermediateId,
[{ target: sentinelStartId }],
[conditionId]
)

const sentinelStartNode = createMockNode(
sentinelStartId,
[{ target: loopBodyId }],
[intermediateId]
)

const loopBodyNode = createMockNode(
loopBodyId,
[{ target: sentinelEndId }],
[sentinelStartId]
)

const sentinelEndNode = createMockNode(
sentinelEndId,
[
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
{ target: afterLoopId, sourceHandle: 'loop_exit' },
],
[loopBodyId]
)

const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
const exitBlockNode = createMockNode(exitBlockId, [], [conditionId])

const nodes = new Map<string, DAGNode>([
[conditionId, conditionNode],
[intermediateId, intermediateNode],
[sentinelStartId, sentinelStartNode],
[loopBodyId, loopBodyNode],
[sentinelEndId, sentinelEndNode],
[afterLoopId, afterLoopNode],
[exitBlockId, exitBlockNode],
])

const dag = createMockDAG(nodes)
const edgeManager = new EdgeManager(dag)

const readyNodes = edgeManager.processOutgoingEdges(conditionNode, {
selectedOption: 'else',
})

// Only exitBlock should be ready
expect(readyNodes).toContain(exitBlockId)

// Nothing on the deactivated path should be queued
expect(readyNodes).not.toContain(intermediateId)
expect(readyNodes).not.toContain(sentinelStartId)
expect(readyNodes).not.toContain(loopBodyId)
expect(readyNodes).not.toContain(sentinelEndId)
expect(readyNodes).not.toContain(afterLoopId)
})

it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
// When a loop actually executes and exits normally, after_loop should become ready
const sentinelStartId = 'sentinel-start'
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/executor/execution/edge-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ export class EdgeManager {

for (const targetId of cascadeTargets) {
if (!readyNodes.includes(targetId) && !activatedTargets.includes(targetId)) {
if (this.isTargetReady(targetId)) {
// Only queue cascade terminal control nodes when ALL outgoing edges from the
// current node were deactivated (dead-end scenario). When some edges are
// activated, terminal control nodes on deactivated branches should NOT be
// queued - they will be reached through the normal activated path's completion.
// This prevents loop/parallel sentinels on fully deactivated paths (e.g., an
// upstream condition took a different branch) from being spuriously executed.
if (activatedTargets.length === 0 && this.isTargetReady(targetId)) {
readyNodes.push(targetId)
}
}
Expand Down