Skip to content

Commit 9e40342

Browse files
fix(snapshot): consolidate to use hasWorkflowChanges check (#3051)
* fix(snapshot): consolidate to use hasWorkflowChanges check * Remove debug logs * fix normalization logic * fix serializer for canonical modes
1 parent 0c0f19c commit 9e40342

File tree

7 files changed

+456
-196
lines changed

7 files changed

+456
-196
lines changed

apps/sim/lib/logs/execution/snapshot/service.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ describe('SnapshotService', () => {
8686
type: 'agent',
8787
position: { x: 100, y: 200 },
8888

89-
subBlocks: {},
89+
subBlocks: {
90+
prompt: {
91+
id: 'prompt',
92+
type: 'short-input',
93+
value: 'Hello world',
94+
},
95+
},
9096
outputs: {},
9197
enabled: true,
9298
horizontalHandles: true,
@@ -104,8 +110,14 @@ describe('SnapshotService', () => {
104110
blocks: {
105111
block1: {
106112
...baseState.blocks.block1,
107-
// Different block state - we can change outputs to make it different
108-
outputs: { response: { type: 'string', description: 'different result' } },
113+
// Different subBlock value - this is a meaningful change
114+
subBlocks: {
115+
prompt: {
116+
id: 'prompt',
117+
type: 'short-input',
118+
value: 'Different prompt',
119+
},
120+
},
109121
},
110122
},
111123
}

apps/sim/lib/logs/execution/snapshot/service.ts

Lines changed: 8 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,7 @@ import type {
1111
WorkflowExecutionSnapshotInsert,
1212
WorkflowState,
1313
} from '@/lib/logs/types'
14-
import {
15-
normalizedStringify,
16-
normalizeEdge,
17-
normalizeValue,
18-
sortEdges,
19-
} from '@/lib/workflows/comparison'
14+
import { normalizedStringify, normalizeWorkflowState } from '@/lib/workflows/comparison'
2015

2116
const logger = createLogger('SnapshotService')
2217

@@ -38,7 +33,9 @@ export class SnapshotService implements ISnapshotService {
3833

3934
const existingSnapshot = await this.getSnapshotByHash(workflowId, stateHash)
4035
if (existingSnapshot) {
41-
logger.debug(`Reusing existing snapshot for workflow ${workflowId} with hash ${stateHash}`)
36+
logger.info(
37+
`Reusing existing snapshot for workflow ${workflowId} (hash: ${stateHash.slice(0, 12)}...)`
38+
)
4239
return {
4340
snapshot: existingSnapshot,
4441
isNew: false,
@@ -59,8 +56,9 @@ export class SnapshotService implements ISnapshotService {
5956
.values(snapshotData)
6057
.returning()
6158

62-
logger.debug(`Created new snapshot for workflow ${workflowId} with hash ${stateHash}`)
63-
logger.debug(`Stored full state with ${Object.keys(state.blocks || {}).length} blocks`)
59+
logger.info(
60+
`Created new snapshot for workflow ${workflowId} (hash: ${stateHash.slice(0, 12)}..., blocks: ${Object.keys(state.blocks || {}).length})`
61+
)
6462
return {
6563
snapshot: {
6664
...newSnapshot,
@@ -112,7 +110,7 @@ export class SnapshotService implements ISnapshotService {
112110
}
113111

114112
computeStateHash(state: WorkflowState): string {
115-
const normalizedState = this.normalizeStateForHashing(state)
113+
const normalizedState = normalizeWorkflowState(state)
116114
const stateString = normalizedStringify(normalizedState)
117115
return createHash('sha256').update(stateString).digest('hex')
118116
}
@@ -130,69 +128,6 @@ export class SnapshotService implements ISnapshotService {
130128
logger.info(`Cleaned up ${deletedCount} orphaned snapshots older than ${olderThanDays} days`)
131129
return deletedCount
132130
}
133-
134-
private normalizeStateForHashing(state: WorkflowState): any {
135-
// 1. Normalize and sort edges
136-
const normalizedEdges = sortEdges((state.edges || []).map(normalizeEdge))
137-
138-
// 2. Normalize blocks
139-
const normalizedBlocks: Record<string, any> = {}
140-
141-
for (const [blockId, block] of Object.entries(state.blocks || {})) {
142-
const { position, layout, height, ...blockWithoutLayoutFields } = block
143-
144-
// Also exclude width/height from data object (container dimensions from autolayout)
145-
const {
146-
width: _dataWidth,
147-
height: _dataHeight,
148-
...dataRest
149-
} = blockWithoutLayoutFields.data || {}
150-
151-
// Normalize subBlocks
152-
const subBlocks = blockWithoutLayoutFields.subBlocks || {}
153-
const normalizedSubBlocks: Record<string, any> = {}
154-
155-
for (const [subBlockId, subBlock] of Object.entries(subBlocks)) {
156-
const value = subBlock.value ?? null
157-
158-
normalizedSubBlocks[subBlockId] = {
159-
type: subBlock.type,
160-
value: normalizeValue(value),
161-
...Object.fromEntries(
162-
Object.entries(subBlock).filter(([key]) => key !== 'value' && key !== 'type')
163-
),
164-
}
165-
}
166-
167-
normalizedBlocks[blockId] = {
168-
...blockWithoutLayoutFields,
169-
data: dataRest,
170-
subBlocks: normalizedSubBlocks,
171-
}
172-
}
173-
174-
// 3. Normalize loops and parallels
175-
const normalizedLoops: Record<string, any> = {}
176-
for (const [loopId, loop] of Object.entries(state.loops || {})) {
177-
normalizedLoops[loopId] = normalizeValue(loop)
178-
}
179-
180-
const normalizedParallels: Record<string, any> = {}
181-
for (const [parallelId, parallel] of Object.entries(state.parallels || {})) {
182-
normalizedParallels[parallelId] = normalizeValue(parallel)
183-
}
184-
185-
// 4. Normalize variables (if present)
186-
const normalizedVariables = state.variables ? normalizeValue(state.variables) : undefined
187-
188-
return {
189-
blocks: normalizedBlocks,
190-
edges: normalizedEdges,
191-
loops: normalizedLoops,
192-
parallels: normalizedParallels,
193-
...(normalizedVariables !== undefined && { variables: normalizedVariables }),
194-
}
195-
}
196131
}
197132

198133
export const snapshotService = new SnapshotService()

apps/sim/lib/workflows/comparison/compare.ts

Lines changed: 32 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
1-
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
2-
import { SYSTEM_SUBBLOCK_IDS, TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
1+
import type { WorkflowState } from '@/stores/workflows/workflow/types'
32
import {
3+
extractBlockFieldsForComparison,
4+
extractSubBlockRest,
5+
filterSubBlockIds,
46
normalizedStringify,
57
normalizeEdge,
68
normalizeLoop,
79
normalizeParallel,
10+
normalizeSubBlockValue,
811
normalizeValue,
912
normalizeVariables,
10-
sanitizeInputFormat,
11-
sanitizeTools,
1213
sanitizeVariable,
1314
} from './normalize'
1415

15-
/** Block with optional diff markers added by copilot */
16-
type BlockWithDiffMarkers = BlockState & {
17-
is_diff?: string
18-
field_diffs?: Record<string, unknown>
19-
}
20-
21-
/** SubBlock with optional diff marker */
22-
type SubBlockWithDiffMarker = {
23-
id: string
24-
type: string
25-
value: unknown
26-
is_diff?: string
27-
}
28-
2916
/**
3017
* Compare the current workflow state with the deployed state to detect meaningful changes.
3118
* Uses generateWorkflowDiffSummary internally to ensure consistent change detection.
@@ -125,36 +112,21 @@ export function generateWorkflowDiffSummary(
125112
for (const id of currentBlockIds) {
126113
if (!previousBlockIds.has(id)) continue
127114

128-
const currentBlock = currentBlocks[id] as BlockWithDiffMarkers
129-
const previousBlock = previousBlocks[id] as BlockWithDiffMarkers
115+
const currentBlock = currentBlocks[id]
116+
const previousBlock = previousBlocks[id]
130117
const changes: FieldChange[] = []
131118

132-
// Compare block-level properties (excluding visual-only fields)
119+
// Use shared helpers for block field extraction (single source of truth)
133120
const {
134-
position: _currentPos,
135-
subBlocks: currentSubBlocks = {},
136-
layout: _currentLayout,
137-
height: _currentHeight,
138-
outputs: _currentOutputs,
139-
is_diff: _currentIsDiff,
140-
field_diffs: _currentFieldDiffs,
141-
...currentRest
142-
} = currentBlock
143-
121+
blockRest: currentRest,
122+
normalizedData: currentDataRest,
123+
subBlocks: currentSubBlocks,
124+
} = extractBlockFieldsForComparison(currentBlock)
144125
const {
145-
position: _previousPos,
146-
subBlocks: previousSubBlocks = {},
147-
layout: _previousLayout,
148-
height: _previousHeight,
149-
outputs: _previousOutputs,
150-
is_diff: _previousIsDiff,
151-
field_diffs: _previousFieldDiffs,
152-
...previousRest
153-
} = previousBlock
154-
155-
// Exclude width/height from data object (container dimensions from autolayout)
156-
const { width: _cw, height: _ch, ...currentDataRest } = currentRest.data || {}
157-
const { width: _pw, height: _ph, ...previousDataRest } = previousRest.data || {}
126+
blockRest: previousRest,
127+
normalizedData: previousDataRest,
128+
subBlocks: previousSubBlocks,
129+
} = extractBlockFieldsForComparison(previousBlock)
158130

159131
const normalizedCurrentBlock = { ...currentRest, data: currentDataRest, subBlocks: undefined }
160132
const normalizedPreviousBlock = {
@@ -179,10 +151,11 @@ export function generateWorkflowDiffSummary(
179151
newValue: currentBlock.enabled,
180152
})
181153
}
182-
// Check other block properties
154+
// Check other block properties (boolean fields)
155+
// Use !! to normalize: null/undefined/false are all equivalent (falsy)
183156
const blockFields = ['horizontalHandles', 'advancedMode', 'triggerMode'] as const
184157
for (const field of blockFields) {
185-
if (currentBlock[field] !== previousBlock[field]) {
158+
if (!!currentBlock[field] !== !!previousBlock[field]) {
186159
changes.push({
187160
field,
188161
oldValue: previousBlock[field],
@@ -195,42 +168,27 @@ export function generateWorkflowDiffSummary(
195168
}
196169
}
197170

198-
// Compare subBlocks
199-
const allSubBlockIds = [
171+
// Compare subBlocks using shared helper for filtering (single source of truth)
172+
const allSubBlockIds = filterSubBlockIds([
200173
...new Set([...Object.keys(currentSubBlocks), ...Object.keys(previousSubBlocks)]),
201-
]
202-
.filter(
203-
(subId) =>
204-
!TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subId) && !SYSTEM_SUBBLOCK_IDS.includes(subId)
205-
)
206-
.sort()
174+
])
207175

208176
for (const subId of allSubBlockIds) {
209-
const currentSub = currentSubBlocks[subId]
210-
const previousSub = previousSubBlocks[subId]
177+
const currentSub = currentSubBlocks[subId] as Record<string, unknown> | undefined
178+
const previousSub = previousSubBlocks[subId] as Record<string, unknown> | undefined
211179

212180
if (!currentSub || !previousSub) {
213181
changes.push({
214182
field: subId,
215-
oldValue: previousSub?.value ?? null,
216-
newValue: currentSub?.value ?? null,
183+
oldValue: (previousSub as Record<string, unknown> | undefined)?.value ?? null,
184+
newValue: (currentSub as Record<string, unknown> | undefined)?.value ?? null,
217185
})
218186
continue
219187
}
220188

221-
// Compare subBlock values with sanitization
222-
let currentValue: unknown = currentSub.value ?? null
223-
let previousValue: unknown = previousSub.value ?? null
224-
225-
if (subId === 'tools' && Array.isArray(currentValue) && Array.isArray(previousValue)) {
226-
currentValue = sanitizeTools(currentValue)
227-
previousValue = sanitizeTools(previousValue)
228-
}
229-
230-
if (subId === 'inputFormat' && Array.isArray(currentValue) && Array.isArray(previousValue)) {
231-
currentValue = sanitizeInputFormat(currentValue)
232-
previousValue = sanitizeInputFormat(previousValue)
233-
}
189+
// Use shared helper for subBlock value normalization (single source of truth)
190+
const currentValue = normalizeSubBlockValue(subId, currentSub.value)
191+
const previousValue = normalizeSubBlockValue(subId, previousSub.value)
234192

235193
// For string values, compare directly to catch even small text changes
236194
if (typeof currentValue === 'string' && typeof previousValue === 'string') {
@@ -245,11 +203,9 @@ export function generateWorkflowDiffSummary(
245203
}
246204
}
247205

248-
// Compare subBlock REST properties (type, id, etc. - excluding value and is_diff)
249-
const currentSubWithDiff = currentSub as SubBlockWithDiffMarker
250-
const previousSubWithDiff = previousSub as SubBlockWithDiffMarker
251-
const { value: _cv, is_diff: _cd, ...currentSubRest } = currentSubWithDiff
252-
const { value: _pv, is_diff: _pd, ...previousSubRest } = previousSubWithDiff
206+
// Use shared helper for subBlock REST extraction (single source of truth)
207+
const currentSubRest = extractSubBlockRest(currentSub)
208+
const previousSubRest = extractSubBlockRest(previousSub)
253209

254210
if (normalizedStringify(currentSubRest) !== normalizedStringify(previousSubRest)) {
255211
changes.push({
Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
1-
export { hasWorkflowChanged } from './compare'
1+
export type { FieldChange, WorkflowDiffSummary } from './compare'
22
export {
3+
formatDiffSummaryForDescription,
4+
generateWorkflowDiffSummary,
5+
hasWorkflowChanged,
6+
} from './compare'
7+
export type {
8+
BlockWithDiffMarkers,
9+
NormalizedWorkflowState,
10+
SubBlockWithDiffMarker,
11+
} from './normalize'
12+
export {
13+
EXCLUDED_BLOCK_DATA_FIELDS,
14+
extractBlockFieldsForComparison,
15+
extractSubBlockRest,
16+
filterSubBlockIds,
17+
normalizeBlockData,
318
normalizedStringify,
419
normalizeEdge,
20+
normalizeLoop,
21+
normalizeParallel,
22+
normalizeSubBlockValue,
523
normalizeValue,
24+
normalizeVariables,
25+
normalizeWorkflowState,
26+
sanitizeInputFormat,
27+
sanitizeTools,
28+
sanitizeVariable,
629
sortEdges,
730
} from './normalize'

0 commit comments

Comments
 (0)