Skip to content

Commit 12c07bd

Browse files
committed
Fix copilot ooo
1 parent 9a6c687 commit 12c07bd

File tree

3 files changed

+214
-38
lines changed

3 files changed

+214
-38
lines changed

apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts

Lines changed: 124 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createLogger } from '@/lib/logs/console/logger'
88
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
99
import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
1010
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
11+
import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
1112
import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
1213
import { getAllBlocks, getBlock } from '@/blocks/registry'
1314
import type { SubBlockConfig } from '@/blocks/types'
@@ -77,6 +78,7 @@ function logSkippedItem(skippedItems: SkippedItem[], item: SkippedItem): void {
7778
skippedItems.push(item)
7879
}
7980

81+
8082
/**
8183
* Result of input validation
8284
*/
@@ -850,13 +852,18 @@ function applyOperationsToWorkflowState(
850852
* Reorder operations to ensure correct execution sequence:
851853
* 1. delete - Remove blocks first to free up IDs and clean state
852854
* 2. extract_from_subflow - Extract blocks from subflows before modifications
853-
* 3. add - Create new blocks so they exist before being referenced
855+
* 3. add - Create new blocks (sorted by connection dependencies)
854856
* 4. insert_into_subflow - Insert blocks into subflows (sorted by parent dependency)
855857
* 5. edit - Edit existing blocks last, so connections to newly added blocks work
856858
*
857-
* This ordering is CRITICAL: edit operations may reference blocks being added
858-
* in the same batch (e.g., connecting block A to newly added block B).
859-
* Without proper ordering, the target block wouldn't exist yet.
859+
* This ordering is CRITICAL: operations may reference blocks being added/inserted
860+
* in the same batch. Without proper ordering, target blocks wouldn't exist yet.
861+
*
862+
* For add operations, we use a two-pass approach:
863+
* - Pass 1: Create all blocks (without connections)
864+
* - Pass 2: Add all connections (now all blocks exist)
865+
* This ensures that if block A connects to block B, and both are being added,
866+
* B will exist when we try to create the edge from A to B.
860867
*/
861868
const deletes = operations.filter((op) => op.operation_type === 'delete')
862869
const extracts = operations.filter((op) => op.operation_type === 'extract_from_subflow')
@@ -868,6 +875,8 @@ function applyOperationsToWorkflowState(
868875
// This handles cases where a loop/parallel is being added along with its children
869876
const sortedInserts = topologicalSortInserts(inserts, adds)
870877

878+
// We'll process add operations in two passes (handled in the switch statement below)
879+
// This is tracked via a separate flag to know which pass we're in
871880
const orderedOperations: EditWorkflowOperation[] = [
872881
...deletes,
873882
...extracts,
@@ -877,15 +886,46 @@ function applyOperationsToWorkflowState(
877886
]
878887

879888
logger.info('Operations after reordering:', {
880-
order: orderedOperations.map(
889+
totalOperations: orderedOperations.length,
890+
deleteCount: deletes.length,
891+
extractCount: extracts.length,
892+
addCount: adds.length,
893+
insertCount: sortedInserts.length,
894+
editCount: edits.length,
895+
operationOrder: orderedOperations.map(
881896
(op) =>
882897
`${op.operation_type}:${op.block_id}${op.params?.subflowId ? `(parent:${op.params.subflowId})` : ''}`
883898
),
884899
})
885900

901+
// Two-pass processing for add operations:
902+
// Pass 1: Create all blocks (without connections)
903+
// Pass 2: Add all connections (all blocks now exist)
904+
const addOperationsWithConnections: Array<{
905+
blockId: string
906+
connections: Record<string, any>
907+
}> = []
908+
886909
for (const operation of orderedOperations) {
887910
const { operation_type, block_id, params } = operation
888911

912+
// CRITICAL: Validate block_id is a valid string and not "undefined"
913+
// This prevents undefined keys from being set in the workflow state
914+
if (!isValidKey(block_id)) {
915+
logSkippedItem(skippedItems, {
916+
type: 'missing_required_params',
917+
operationType: operation_type,
918+
blockId: String(block_id || 'invalid'),
919+
reason: `Invalid block_id "${block_id}" (type: ${typeof block_id}) - operation skipped. Block IDs must be valid non-empty strings.`,
920+
})
921+
logger.error('Invalid block_id detected in operation', {
922+
operation_type,
923+
block_id,
924+
block_id_type: typeof block_id,
925+
})
926+
continue
927+
}
928+
889929
logger.debug(`Executing operation: ${operation_type} for block ${block_id}`, {
890930
params: params ? Object.keys(params) : [],
891931
currentBlockCount: Object.keys(modifiedState.blocks).length,
@@ -1128,6 +1168,22 @@ function applyOperationsToWorkflowState(
11281168

11291169
// Add new nested blocks
11301170
Object.entries(params.nestedNodes).forEach(([childId, childBlock]: [string, any]) => {
1171+
// Validate childId is a valid string
1172+
if (!isValidKey(childId)) {
1173+
logSkippedItem(skippedItems, {
1174+
type: 'missing_required_params',
1175+
operationType: 'add_nested_node',
1176+
blockId: String(childId || 'invalid'),
1177+
reason: `Invalid childId "${childId}" in nestedNodes - child block skipped`,
1178+
})
1179+
logger.error('Invalid childId detected in nestedNodes', {
1180+
parentBlockId: block_id,
1181+
childId,
1182+
childId_type: typeof childId,
1183+
})
1184+
return
1185+
}
1186+
11311187
const childBlockState = createBlockFromParams(
11321188
childId,
11331189
childBlock,
@@ -1360,6 +1416,22 @@ function applyOperationsToWorkflowState(
13601416
// Handle nested nodes (for loops/parallels created from scratch)
13611417
if (params.nestedNodes) {
13621418
Object.entries(params.nestedNodes).forEach(([childId, childBlock]: [string, any]) => {
1419+
// Validate childId is a valid string
1420+
if (!isValidKey(childId)) {
1421+
logSkippedItem(skippedItems, {
1422+
type: 'missing_required_params',
1423+
operationType: 'add_nested_node',
1424+
blockId: String(childId || 'invalid'),
1425+
reason: `Invalid childId "${childId}" in nestedNodes - child block skipped`,
1426+
})
1427+
logger.error('Invalid childId detected in nestedNodes', {
1428+
parentBlockId: block_id,
1429+
childId,
1430+
childId_type: typeof childId,
1431+
})
1432+
return
1433+
}
1434+
13631435
const childBlockState = createBlockFromParams(
13641436
childId,
13651437
childBlock,
@@ -1368,21 +1440,22 @@ function applyOperationsToWorkflowState(
13681440
)
13691441
modifiedState.blocks[childId] = childBlockState
13701442

1443+
// Defer connection processing to ensure all blocks exist first
13711444
if (childBlock.connections) {
1372-
addConnectionsAsEdges(
1373-
modifiedState,
1374-
childId,
1375-
childBlock.connections,
1376-
logger,
1377-
skippedItems
1378-
)
1445+
addOperationsWithConnections.push({
1446+
blockId: childId,
1447+
connections: childBlock.connections,
1448+
})
13791449
}
13801450
})
13811451
}
13821452

1383-
// Add connections as edges
1453+
// Defer connection processing to ensure all blocks exist first (pass 2)
13841454
if (params.connections) {
1385-
addConnectionsAsEdges(modifiedState, block_id, params.connections, logger, skippedItems)
1455+
addOperationsWithConnections.push({
1456+
blockId: block_id,
1457+
connections: params.connections,
1458+
})
13861459
}
13871460
break
13881461
}
@@ -1506,13 +1579,18 @@ function applyOperationsToWorkflowState(
15061579
modifiedState.blocks[block_id] = newBlock
15071580
}
15081581

1509-
// Add/update connections as edges
1582+
// Defer connection processing to ensure all blocks exist first
1583+
// This is particularly important when multiple blocks are being inserted
1584+
// and they have connections to each other
15101585
if (params.connections) {
1511-
// Remove existing edges from this block
1586+
// Remove existing edges from this block first
15121587
modifiedState.edges = modifiedState.edges.filter((edge: any) => edge.source !== block_id)
15131588

1514-
// Add new connections
1515-
addConnectionsAsEdges(modifiedState, block_id, params.connections, logger, skippedItems)
1589+
// Add to deferred connections list
1590+
addOperationsWithConnections.push({
1591+
blockId: block_id,
1592+
connections: params.connections,
1593+
})
15161594
}
15171595
break
15181596
}
@@ -1562,6 +1640,34 @@ function applyOperationsToWorkflowState(
15621640
}
15631641
}
15641642

1643+
// Pass 2: Add all deferred connections from add/insert operations
1644+
// Now all blocks exist (from add, insert, and edit operations), so connections can be safely created
1645+
// This ensures that if block A connects to block B, and both are being added/inserted,
1646+
// B will exist when we create the edge from A to B
1647+
if (addOperationsWithConnections.length > 0) {
1648+
logger.info('Processing deferred connections from add/insert operations', {
1649+
deferredConnectionCount: addOperationsWithConnections.length,
1650+
totalBlocks: Object.keys(modifiedState.blocks).length,
1651+
})
1652+
1653+
for (const { blockId, connections } of addOperationsWithConnections) {
1654+
// Verify the source block still exists (it might have been deleted by a later operation)
1655+
if (!modifiedState.blocks[blockId]) {
1656+
logger.warn('Source block no longer exists for deferred connection', {
1657+
blockId,
1658+
availableBlocks: Object.keys(modifiedState.blocks),
1659+
})
1660+
continue
1661+
}
1662+
1663+
addConnectionsAsEdges(modifiedState, blockId, connections, logger, skippedItems)
1664+
}
1665+
1666+
logger.info('Finished processing deferred connections', {
1667+
totalEdges: modifiedState.edges.length,
1668+
})
1669+
}
1670+
15651671
// Regenerate loops and parallels after modifications
15661672
modifiedState.loops = generateLoopBlocks(modifiedState.blocks)
15671673
modifiedState.parallels = generateParallelBlocks(modifiedState.blocks)

0 commit comments

Comments
 (0)