From 2c333bfd98f8e50c2a920cf3bba6fe01e66a593f Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 27 Jan 2026 12:25:27 -0800 Subject: [PATCH] Lint --- .../[id]/execute-from-block/route.ts | 6 +- .../components/action-bar/action-bar.tsx | 5 -- .../hooks/use-workflow-execution.ts | 17 ++--- .../[workspaceId]/w/[workflowId]/workflow.tsx | 62 ++++++++----------- apps/sim/executor/execution/executor.ts | 6 +- apps/sim/executor/orchestrators/loop.ts | 10 ++- apps/sim/executor/orchestrators/node.ts | 2 - apps/sim/executor/orchestrators/parallel.ts | 10 ++- .../sim/executor/utils/run-from-block.test.ts | 54 +++++++++++++--- 9 files changed, 88 insertions(+), 84 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute-from-block/route.ts b/apps/sim/app/api/workflows/[id]/execute-from-block/route.ts index 5a2bb9f34..88d6de179 100644 --- a/apps/sim/app/api/workflows/[id]/execute-from-block/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute-from-block/route.ts @@ -11,8 +11,8 @@ import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' import { clearExecutionCancellation, markExecutionCancelled } from '@/lib/execution/cancellation' import { LoggingSession } from '@/lib/logs/execution/logging-session' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' -import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events' +import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { DAGExecutor } from '@/executor/execution/executor' import type { IterationContext, SerializableExecutionState } from '@/executor/execution/types' import type { NormalizedBlockOutput } from '@/executor/types' @@ -367,7 +367,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: logger.error(`[${requestId}] Run-from-block execution failed: ${errorMessage}`) const executionResult = hasExecutionResult(error) ? error.executionResult : undefined - const { traceSpans } = executionResult ? buildTraceSpans(executionResult) : { traceSpans: [] } + const { traceSpans } = executionResult + ? buildTraceSpans(executionResult) + : { traceSpans: [] } // Complete logging session with error await loggingSession.safeCompleteWithError({ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index 33edef0ec..cac127c00 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -111,17 +111,12 @@ export const ActionBar = memo( const isSubflowBlock = blockType === 'loop' || blockType === 'parallel' const isInsideSubflow = parentId && (parentType === 'loop' || parentType === 'parallel') - // Check if run-from-block is available - // Block can run if all its upstream dependencies have cached outputs const snapshot = activeWorkflowId ? getLastExecutionSnapshot(activeWorkflowId) : null const hasExecutionSnapshot = !!snapshot const dependenciesSatisfied = (() => { if (!snapshot) return false - // Find all blocks that feed into this block const incomingEdges = edges.filter((edge) => edge.target === blockId) - // If no incoming edges (trigger/start block), dependencies are satisfied if (incomingEdges.length === 0) return true - // All source blocks must have been executed (have cached outputs) return incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)) })() const canRunFromBlock = diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index f2907a19b..10ee2ba75 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { v4 as uuidv4 } from 'uuid' @@ -33,7 +33,6 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('useWorkflowExecution') - // Debug state validation result interface DebugValidationResult { isValid: boolean @@ -924,13 +923,9 @@ export function useWorkflowExecution() { logger.info('onBlockCompleted received:', { data }) activeBlocksSet.delete(data.blockId) - // Create a new Set to trigger React re-render setActiveBlocks(new Set(activeBlocksSet)) - - // Track successful block execution in run path setBlockRunStatus(data.blockId, 'success') - // Track block state for run-from-block snapshot executedBlockIds.add(data.blockId) accumulatedBlockStates.set(data.blockId, { output: data.output, @@ -938,17 +933,12 @@ export function useWorkflowExecution() { executionTime: data.durationMs, }) - // Skip adding loop/parallel containers to console and logs - // They're tracked for run-from-block but shouldn't appear in terminal const isContainerBlock = data.blockType === 'loop' || data.blockType === 'parallel' if (isContainerBlock) return - // Edges already tracked in onBlockStarted, no need to track again - const startedAt = new Date(Date.now() - data.durationMs).toISOString() const endedAt = new Date().toISOString() - // Accumulate block log for the execution result accumulatedBlockLogs.push({ blockId: data.blockId, blockName: data.blockName || 'Unknown Block', @@ -1461,7 +1451,10 @@ export function useWorkflowExecution() { incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)) if (!dependenciesSatisfied) { - logger.error('Upstream dependencies not satisfied for run-from-block', { workflowId, blockId }) + logger.error('Upstream dependencies not satisfied for run-from-block', { + workflowId, + blockId, + }) return } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 0d13f7d63..1a51f0a4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -1006,6 +1006,30 @@ const WorkflowContent = React.memo(() => { handleRunUntilBlock(blockId, workflowIdParam) }, [contextMenuBlocks, workflowIdParam, handleRunUntilBlock]) + const runFromBlockState = useMemo(() => { + if (contextMenuBlocks.length !== 1) { + return { canRun: false, reason: undefined } + } + const block = contextMenuBlocks[0] + const snapshot = getLastExecutionSnapshot(workflowIdParam) + if (!snapshot) return { canRun: false, reason: 'Run workflow first' } + + const incomingEdges = edges.filter((edge) => edge.target === block.id) + const dependenciesSatisfied = + incomingEdges.length === 0 || + incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)) + const isNoteBlock = block.type === 'note' + const isInsideSubflow = + block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel') + + if (!dependenciesSatisfied) return { canRun: false, reason: 'Run upstream blocks first' } + if (isInsideSubflow) return { canRun: false, reason: 'Cannot run from inside subflow' } + if (isNoteBlock) return { canRun: false, reason: undefined } + if (isExecuting) return { canRun: false, reason: undefined } + + return { canRun: true, reason: undefined } + }, [contextMenuBlocks, edges, workflowIdParam, getLastExecutionSnapshot, isExecuting]) + const handleContextAddBlock = useCallback(() => { useSearchModalStore.getState().open() }, []) @@ -3332,42 +3356,8 @@ const WorkflowContent = React.memo(() => { showRemoveFromSubflow={contextMenuBlocks.some( (b) => b.parentId && (b.parentType === 'loop' || b.parentType === 'parallel') )} - canRunFromBlock={ - contextMenuBlocks.length === 1 && - (() => { - const block = contextMenuBlocks[0] - const snapshot = getLastExecutionSnapshot(workflowIdParam) - if (!snapshot) return false - // Check if all upstream dependencies have cached outputs - const incomingEdges = edges.filter((edge) => edge.target === block.id) - const dependenciesSatisfied = - incomingEdges.length === 0 || - incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)) - const isNoteBlock = block.type === 'note' - const isInsideSubflow = - block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel') - return dependenciesSatisfied && !isNoteBlock && !isInsideSubflow && !isExecuting - })() - } - runFromBlockDisabledReason={ - contextMenuBlocks.length === 1 - ? (() => { - const block = contextMenuBlocks[0] - const snapshot = getLastExecutionSnapshot(workflowIdParam) - if (!snapshot) return 'Run workflow first' - // Check if all upstream dependencies have cached outputs - const incomingEdges = edges.filter((edge) => edge.target === block.id) - const dependenciesSatisfied = - incomingEdges.length === 0 || - incomingEdges.every((edge) => snapshot.executedBlocks.includes(edge.source)) - const isInsideSubflow = - block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel') - if (!dependenciesSatisfied) return 'Run upstream blocks first' - if (isInsideSubflow) return 'Cannot run from inside subflow' - return undefined - })() - : undefined - } + canRunFromBlock={runFromBlockState.canRun} + runFromBlockDisabledReason={runFromBlockState.reason} disableEdit={!effectivePermissions.canEdit} isExecuting={isExecuting} /> diff --git a/apps/sim/executor/execution/executor.ts b/apps/sim/executor/execution/executor.ts index 4112eb6e6..d20b5a22f 100644 --- a/apps/sim/executor/execution/executor.ts +++ b/apps/sim/executor/execution/executor.ts @@ -17,8 +17,8 @@ import { ParallelOrchestrator } from '@/executor/orchestrators/parallel' import type { BlockState, ExecutionContext, ExecutionResult } from '@/executor/types' import { computeDirtySet, - resolveContainerToSentinelStart, type RunFromBlockContext, + resolveContainerToSentinelStart, validateRunFromBlock, } from '@/executor/utils/run-from-block' import { @@ -233,8 +233,6 @@ export class DAGExecutor { userId: this.contextExtensions.userId, isDeployedContext: this.contextExtensions.isDeployedContext, blockStates: state.getBlockStates(), - // For run-from-block, start with empty logs - we only want fresh execution logs for trace spans - // The snapshot's blockLogs are preserved separately for history blockLogs: overrides?.runFromBlockContext ? [] : (snapshotState?.blockLogs ?? []), metadata: { ...this.contextExtensions.metadata, @@ -322,8 +320,6 @@ export class DAGExecutor { skipStarterBlockInit: true, }) } else if (overrides?.runFromBlockContext) { - // In run-from-block mode, skip starter block initialization - // All block states come from the snapshot logger.info('Run-from-block mode: skipping starter block initialization', { startBlockId: overrides.runFromBlockContext.startBlockId, }) diff --git a/apps/sim/executor/orchestrators/loop.ts b/apps/sim/executor/orchestrators/loop.ts index 52b0414ae..2cce72272 100644 --- a/apps/sim/executor/orchestrators/loop.ts +++ b/apps/sim/executor/orchestrators/loop.ts @@ -281,12 +281,10 @@ export class LoopOrchestrator { // Emit onBlockComplete for the loop container so the UI can track it if (this.contextExtensions?.onBlockComplete) { - this.contextExtensions.onBlockComplete( - loopId, - 'Loop', - 'loop', - { output, executionTime: DEFAULTS.EXECUTION_TIME } - ) + this.contextExtensions.onBlockComplete(loopId, 'Loop', 'loop', { + output, + executionTime: DEFAULTS.EXECUTION_TIME, + }) } return { diff --git a/apps/sim/executor/orchestrators/node.ts b/apps/sim/executor/orchestrators/node.ts index 244b54abd..be7698b50 100644 --- a/apps/sim/executor/orchestrators/node.ts +++ b/apps/sim/executor/orchestrators/node.ts @@ -31,7 +31,6 @@ export class NodeExecutionOrchestrator { throw new Error(`Node not found in DAG: ${nodeId}`) } - // In run-from-block mode, skip execution for non-dirty blocks and return cached output if (ctx.runFromBlockContext && !ctx.runFromBlockContext.dirtySet.has(nodeId)) { const cachedOutput = this.state.getBlockOutput(nodeId) || {} logger.debug('Skipping non-dirty block in run-from-block mode', { nodeId }) @@ -42,7 +41,6 @@ export class NodeExecutionOrchestrator { } } - // Skip hasExecuted check for dirty blocks in run-from-block mode - they need to be re-executed const isDirtyBlock = ctx.runFromBlockContext?.dirtySet.has(nodeId) ?? false if (!isDirtyBlock && this.state.hasExecuted(nodeId)) { const output = this.state.getBlockOutput(nodeId) || {} diff --git a/apps/sim/executor/orchestrators/parallel.ts b/apps/sim/executor/orchestrators/parallel.ts index 8536b1941..517d3f6ff 100644 --- a/apps/sim/executor/orchestrators/parallel.ts +++ b/apps/sim/executor/orchestrators/parallel.ts @@ -233,12 +233,10 @@ export class ParallelOrchestrator { // Emit onBlockComplete for the parallel container so the UI can track it if (this.contextExtensions?.onBlockComplete) { - this.contextExtensions.onBlockComplete( - parallelId, - 'Parallel', - 'parallel', - { output, executionTime: 0 } - ) + this.contextExtensions.onBlockComplete(parallelId, 'Parallel', 'parallel', { + output, + executionTime: 0, + }) } return { diff --git a/apps/sim/executor/utils/run-from-block.test.ts b/apps/sim/executor/utils/run-from-block.test.ts index 098d6e5c9..284379095 100644 --- a/apps/sim/executor/utils/run-from-block.test.ts +++ b/apps/sim/executor/utils/run-from-block.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' import type { DAG, DAGNode } from '@/executor/dag/builder' import type { DAGEdge, NodeMetadata } from '@/executor/dag/types' -import type { SerializedLoop, SerializedParallel } from '@/serializer/types' import { computeDirtySet, validateRunFromBlock } from '@/executor/utils/run-from-block' +import type { SerializedLoop, SerializedParallel } from '@/serializer/types' /** * Helper to create a DAG node for testing @@ -291,7 +291,9 @@ describe('validateRunFromBlock', () => { }) it('rejects blocks inside parallels', () => { - const dag = createDAG([createNode('A', [], { isParallelBranch: true, parallelId: 'parallel-1' })]) + const dag = createDAG([ + createNode('A', [], { isParallelBranch: true, parallelId: 'parallel-1' }), + ]) const executedBlocks = new Set(['A']) const result = validateRunFromBlock('A', dag, executedBlocks) @@ -352,9 +354,17 @@ describe('validateRunFromBlock', () => { const sentinelEndId = `loop-${loopId}-sentinel-end` const dag = createDAG([ createNode('A', [{ target: sentinelStartId }]), - createNode(sentinelStartId, [{ target: 'B' }], { isSentinel: true, sentinelType: 'start', loopId }), + createNode(sentinelStartId, [{ target: 'B' }], { + isSentinel: true, + sentinelType: 'start', + loopId, + }), createNode('B', [{ target: sentinelEndId }], { isLoopNode: true, loopId }), - createNode(sentinelEndId, [{ target: 'C' }], { isSentinel: true, sentinelType: 'end', loopId }), + createNode(sentinelEndId, [{ target: 'C' }], { + isSentinel: true, + sentinelType: 'end', + loopId, + }), createNode('C'), ]) dag.loopConfigs.set(loopId, { id: loopId, nodes: ['B'], iterations: 3, loopType: 'for' } as any) @@ -372,9 +382,17 @@ describe('validateRunFromBlock', () => { const sentinelEndId = `parallel-${parallelId}-sentinel-end` const dag = createDAG([ createNode('A', [{ target: sentinelStartId }]), - createNode(sentinelStartId, [{ target: 'B₍0₎' }], { isSentinel: true, sentinelType: 'start', parallelId }), + createNode(sentinelStartId, [{ target: 'B₍0₎' }], { + isSentinel: true, + sentinelType: 'start', + parallelId, + }), createNode('B₍0₎', [{ target: sentinelEndId }], { isParallelBranch: true, parallelId }), - createNode(sentinelEndId, [{ target: 'C' }], { isSentinel: true, sentinelType: 'end', parallelId }), + createNode(sentinelEndId, [{ target: 'C' }], { + isSentinel: true, + sentinelType: 'end', + parallelId, + }), createNode('C'), ]) dag.parallelConfigs.set(parallelId, { id: parallelId, nodes: ['B'], count: 2 } as any) @@ -412,9 +430,17 @@ describe('computeDirtySet with containers', () => { const sentinelEndId = `loop-${loopId}-sentinel-end` const dag = createDAG([ createNode('A', [{ target: sentinelStartId }]), - createNode(sentinelStartId, [{ target: 'B' }], { isSentinel: true, sentinelType: 'start', loopId }), + createNode(sentinelStartId, [{ target: 'B' }], { + isSentinel: true, + sentinelType: 'start', + loopId, + }), createNode('B', [{ target: sentinelEndId }], { isLoopNode: true, loopId }), - createNode(sentinelEndId, [{ target: 'C' }], { isSentinel: true, sentinelType: 'end', loopId }), + createNode(sentinelEndId, [{ target: 'C' }], { + isSentinel: true, + sentinelType: 'end', + loopId, + }), createNode('C'), ]) dag.loopConfigs.set(loopId, { id: loopId, nodes: ['B'], iterations: 3, loopType: 'for' } as any) @@ -438,9 +464,17 @@ describe('computeDirtySet with containers', () => { const sentinelEndId = `parallel-${parallelId}-sentinel-end` const dag = createDAG([ createNode('A', [{ target: sentinelStartId }]), - createNode(sentinelStartId, [{ target: 'B₍0₎' }], { isSentinel: true, sentinelType: 'start', parallelId }), + createNode(sentinelStartId, [{ target: 'B₍0₎' }], { + isSentinel: true, + sentinelType: 'start', + parallelId, + }), createNode('B₍0₎', [{ target: sentinelEndId }], { isParallelBranch: true, parallelId }), - createNode(sentinelEndId, [{ target: 'C' }], { isSentinel: true, sentinelType: 'end', parallelId }), + createNode(sentinelEndId, [{ target: 'C' }], { + isSentinel: true, + sentinelType: 'end', + parallelId, + }), createNode('C'), ]) dag.parallelConfigs.set(parallelId, { id: parallelId, nodes: ['B'], count: 2 } as any)