Skip to content

Commit c9302a5

Browse files
committed
fix(execution): preserve functional state during trace projection
1 parent be7408a commit c9302a5

10 files changed

Lines changed: 199 additions & 8 deletions

File tree

apps/sim/app/api/workflows/[id]/log/route.test.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,15 +312,22 @@ describe('POST /api/workflows/[id]/log completion attribution', () => {
312312
})
313313

314314
it('restores trusted Secrets provenance before projecting legacy completion traces', async () => {
315+
const trustedExecutionState = {
316+
blockStates: { 'function-1': { output: { result: 'raw-secret-value' } } },
317+
executedBlocks: ['function-1'],
318+
blockLogs: [],
319+
decisions: { router: {}, condition: {} },
320+
completedLoops: [],
321+
activeExecutionPath: ['function-1'],
322+
resolvedSecretTraceProvenance: { version: 1, complete: true, entries: [] },
323+
}
315324
dbChainMockFns.limit.mockResolvedValueOnce([
316325
{
317326
workflowId: OWNER_WORKFLOW_ID,
318327
workspaceId: 'workspace-1',
319328
executionData: {
320329
billingAttribution: storedBillingAttribution,
321-
executionState: {
322-
resolvedSecretTraceProvenance: { version: 1, complete: true, entries: [] },
323-
},
330+
executionState: trustedExecutionState,
324331
},
325332
},
326333
])
@@ -342,6 +349,44 @@ describe('POST /api/workflows/[id]/log completion attribution', () => {
342349
entries: [],
343350
scope: { userId: 'user-1', workspaceId: 'workspace-1' },
344351
})
352+
expect(mockSafeComplete).toHaveBeenCalledWith(
353+
expect.objectContaining({ executionState: trustedExecutionState })
354+
)
355+
})
356+
357+
it('forwards trusted execution state through legacy error completion', async () => {
358+
const trustedExecutionState = {
359+
blockStates: { 'function-1': { output: { error: 'raw-secret-value' } } },
360+
executedBlocks: ['function-1'],
361+
blockLogs: [],
362+
decisions: { router: {}, condition: {} },
363+
completedLoops: [],
364+
activeExecutionPath: ['function-1'],
365+
resolvedSecretTraceProvenance: { version: 1, complete: true, entries: [] },
366+
}
367+
dbChainMockFns.limit.mockResolvedValueOnce([
368+
{
369+
workflowId: OWNER_WORKFLOW_ID,
370+
workspaceId: 'workspace-1',
371+
executionData: {
372+
billingAttribution: storedBillingAttribution,
373+
executionState: trustedExecutionState,
374+
},
375+
},
376+
])
377+
378+
const res = await POST(
379+
makeRequest(OWNER_WORKFLOW_ID, {
380+
executionId: 'trusted-error-execution-id',
381+
result: { success: false, error: 'failed' },
382+
}),
383+
{ params: Promise.resolve({ id: OWNER_WORKFLOW_ID }) }
384+
)
385+
386+
expect(res.status).toBe(200)
387+
expect(mockSafeCompleteWithError).toHaveBeenCalledWith(
388+
expect.objectContaining({ executionState: trustedExecutionState })
389+
)
345390
})
346391

347392
it('forces structural-only traces when trusted stored provenance is unavailable', async () => {

apps/sim/app/api/workflows/[id]/log/route.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
1616
import { materializeExecutionData } from '@/lib/logs/execution/trace-store'
1717
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
1818
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
19+
import type { SerializableExecutionState } from '@/executor/execution/types'
1920
import type { ExecutionResult } from '@/executor/types'
2021
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
2122

@@ -133,11 +134,13 @@ export const POST = withRouteHandler(
133134
executionId,
134135
}
135136
)
136-
const trustedExecutionState = trustedExecutionData.executionState
137-
const trustedProvenance =
138-
trustedExecutionState && typeof trustedExecutionState === 'object'
139-
? (trustedExecutionState as Record<string, unknown>).resolvedSecretTraceProvenance
137+
const trustedExecutionState =
138+
trustedExecutionData.executionState &&
139+
typeof trustedExecutionData.executionState === 'object' &&
140+
!Array.isArray(trustedExecutionData.executionState)
141+
? (trustedExecutionData.executionState as SerializableExecutionState)
140142
: undefined
143+
const trustedProvenance = trustedExecutionState?.resolvedSecretTraceProvenance
141144
if (trustedProvenance === undefined) {
142145
resolvedSecretTraceRegistry.markIncomplete()
143146
} else {
@@ -168,13 +171,15 @@ export const POST = withRouteHandler(
168171
totalDurationMs: totalDuration || result.metadata?.duration || 0,
169172
error: { message },
170173
traceSpans,
174+
executionState: trustedExecutionState,
171175
})
172176
} else {
173177
await loggingSession.safeComplete({
174178
endedAt: new Date().toISOString(),
175179
totalDurationMs: totalDuration || result.metadata?.duration || 0,
176180
finalOutput: result.output || {},
177181
traceSpans,
182+
executionState: trustedExecutionState,
178183
})
179184
}
180185

apps/sim/executor/handlers/workflow/workflow-handler.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,25 +1242,42 @@ describe('WorkflowBlockHandler', () => {
12421242
})
12431243

12441244
it('completes the child session and disposes the cancellation bridge', async () => {
1245+
const executionState = {
1246+
blockStates: { 'function-1': { output: { result: 'raw-secret-value' } } },
1247+
}
1248+
mockExecutorExecute.mockResolvedValue({
1249+
success: true,
1250+
output: { data: 'ok' },
1251+
executionState,
1252+
})
1253+
12451254
await handler.execute(customBlockContext(), customBlock(), {})
12461255

12471256
expect(mockSafeComplete).toHaveBeenCalledTimes(1)
1257+
expect(mockSafeComplete).toHaveBeenCalledWith(expect.objectContaining({ executionState }))
12481258
expect(mockSafeCompleteWithError).not.toHaveBeenCalled()
12491259
expect(mockDispose).toHaveBeenCalledTimes(1)
12501260
})
12511261

12521262
it('records a cancelled child through the cancellation path', async () => {
12531263
// Production shape: the engine reports cancellation as `success: false`
12541264
// plus `status: 'cancelled'` on the ExecutionResult (never on metadata).
1265+
const executionState = {
1266+
blockStates: { 'function-1': { output: { result: 'raw-secret-value' } } },
1267+
}
12551268
mockExecutorExecute.mockResolvedValue({
12561269
success: false,
12571270
output: {},
12581271
status: 'cancelled',
1272+
executionState,
12591273
})
12601274

12611275
await handler.execute(customBlockContext(), customBlock(), {}).catch(() => {})
12621276

12631277
expect(mockSafeCompleteWithCancellation).toHaveBeenCalledTimes(1)
1278+
expect(mockSafeCompleteWithCancellation).toHaveBeenCalledWith(
1279+
expect.objectContaining({ executionState })
1280+
)
12641281
expect(mockSafeComplete).not.toHaveBeenCalled()
12651282
// Already finalized as cancelled — must not be re-completed as an error.
12661283
expect(mockSafeCompleteWithError).not.toHaveBeenCalled()

apps/sim/executor/handlers/workflow/workflow-handler.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,12 @@ export class WorkflowBlockHandler implements BlockHandler {
805805
// Cancellation lives on `ExecutionResult.status` — `ExecutionMetadata.status`
806806
// has no 'cancelled' member, so reading it there never matches.
807807
if (executionResult.status === 'cancelled') {
808-
await session.safeCompleteWithCancellation({ endedAt, totalDurationMs, traceSpans })
808+
await session.safeCompleteWithCancellation({
809+
endedAt,
810+
totalDurationMs,
811+
traceSpans,
812+
executionState: executionResult.executionState,
813+
})
809814
return
810815
}
811816

@@ -815,6 +820,7 @@ export class WorkflowBlockHandler implements BlockHandler {
815820
finalOutput: executionResult.output ?? {},
816821
traceSpans,
817822
workflowInput,
823+
executionState: executionResult.executionState,
818824
})
819825
}
820826

apps/sim/lib/logs/execution/logging-session.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,41 @@ describe('LoggingSession terminal provenance', () => {
151151
})
152152
)
153153
})
154+
155+
it.each(['cancellation', 'pause'] as const)(
156+
'preserves raw execution state on %s finalization',
157+
async (finalization) => {
158+
const session = new LoggingSession('workflow-1', `execution-state-${finalization}`, 'manual')
159+
session.setResolvedSecretTraceRegistry(createSecretRegistry([]))
160+
const executionState = {
161+
blockStates: { 'function-1': { output: { result: 'raw-secret-value' } } },
162+
executedBlocks: ['function-1'],
163+
blockLogs: [],
164+
decisions: { router: {}, condition: {} },
165+
completedLoops: [],
166+
activeExecutionPath: ['function-1'],
167+
}
168+
169+
if (finalization === 'cancellation') {
170+
await session.completeWithCancellation({ executionState })
171+
} else {
172+
await session.completeWithPause({ executionState })
173+
}
174+
175+
expect(completeWorkflowExecutionMock).toHaveBeenCalledWith(
176+
expect.objectContaining({
177+
executionState: expect.objectContaining({
178+
blockStates: executionState.blockStates,
179+
resolvedSecretTraceProvenance: {
180+
version: 1,
181+
complete: true,
182+
entries: [],
183+
},
184+
}),
185+
})
186+
)
187+
}
188+
)
154189
})
155190

156191
beforeEach(() => {
@@ -596,15 +631,25 @@ describe('LoggingSession completion retries', () => {
596631
completeWorkflowExecutionMock
597632
.mockRejectedValueOnce(new Error('primary persistence failed'))
598633
.mockResolvedValueOnce({})
634+
const executionState = {
635+
blockStates: { 'function-1': { output: { result: secret } } },
636+
executedBlocks: ['function-1'],
637+
blockLogs: [],
638+
decisions: { router: {}, condition: {} },
639+
completedLoops: [],
640+
activeExecutionPath: ['function-1'],
641+
}
599642

600643
await session.safeComplete({
601644
finalOutput: { echoed: secret },
645+
executionState,
602646
})
603647

604648
expect(completeWorkflowExecutionMock).toHaveBeenLastCalledWith(
605649
expect.objectContaining({
606650
finalOutput: { echoed: secret },
607651
finalizationPath: 'fallback_completed',
652+
executionState: expect.objectContaining({ blockStates: executionState.blockStates }),
608653
})
609654
)
610655
})
@@ -944,18 +989,33 @@ describe('LoggingSession completion retries', () => {
944989
completeWorkflowExecutionMock
945990
.mockRejectedValueOnce(new Error('pause finalize failed'))
946991
.mockResolvedValueOnce({})
992+
const executionState = {
993+
blockStates: { 'function-1': { output: { result: 'raw-secret-value' } } },
994+
executedBlocks: ['function-1'],
995+
blockLogs: [],
996+
decisions: { router: {}, condition: {} },
997+
completedLoops: [],
998+
activeExecutionPath: ['function-1'],
999+
}
9471000

9481001
await expect(
9491002
session.safeCompleteWithPause({
9501003
endedAt: new Date().toISOString(),
9511004
totalDurationMs: 10,
9521005
traceSpans: [],
9531006
workflowInput: { hello: 'world' },
1007+
executionState,
9541008
})
9551009
).resolves.toBeUndefined()
9561010

9571011
expect(session.hasCompleted()).toBe(true)
9581012
expect(completeWorkflowExecutionMock).toHaveBeenCalledTimes(2)
1013+
expect(completeWorkflowExecutionMock).toHaveBeenLastCalledWith(
1014+
expect.objectContaining({
1015+
finalizationPath: 'paused',
1016+
executionState: expect.objectContaining({ blockStates: executionState.blockStates }),
1017+
})
1018+
)
9591019
})
9601020

9611021
it('persists last started block independently from cost accumulation', async () => {

apps/sim/lib/logs/execution/logging-session.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,15 @@ export interface SessionCancelledParams {
156156
endedAt?: string
157157
totalDurationMs?: number
158158
traceSpans?: TraceSpan[]
159+
executionState?: SerializableExecutionState
159160
}
160161

161162
export interface SessionPausedParams {
162163
endedAt?: string
163164
totalDurationMs?: number
164165
traceSpans?: TraceSpan[]
165166
workflowInput?: any
167+
executionState?: SerializableExecutionState
166168
}
167169

168170
export interface LoggingSessionOptions {
@@ -902,6 +904,7 @@ export class LoggingSession {
902904
costSummary,
903905
finalOutput: { cancelled: true },
904906
traceSpans,
907+
executionState: params.executionState,
905908
finalizationPath: 'cancelled',
906909
status: 'cancelled',
907910
})
@@ -999,6 +1002,7 @@ export class LoggingSession {
9991002
finalOutput: { paused: true },
10001003
traceSpans,
10011004
workflowInput,
1005+
executionState: params.executionState,
10021006
finalizationPath: 'paused',
10031007
status: 'pending',
10041008
})
@@ -1197,6 +1201,7 @@ export class LoggingSession {
11971201
isError: false,
11981202
finalizationPath: 'fallback_completed',
11991203
finalOutput: params.finalOutput || {},
1204+
executionState: params.executionState,
12001205
})
12011206
}
12021207
}
@@ -1256,6 +1261,7 @@ export class LoggingSession {
12561261
isError: false,
12571262
finalizationPath: 'cancelled',
12581263
finalOutput: { cancelled: true },
1264+
executionState: params?.executionState,
12591265
status: 'cancelled',
12601266
})
12611267
}
@@ -1283,6 +1289,7 @@ export class LoggingSession {
12831289
isError: false,
12841290
finalizationPath: 'paused',
12851291
finalOutput: { paused: true },
1292+
executionState: params?.executionState,
12861293
status: 'pending',
12871294
})
12881295
}

apps/sim/lib/workflows/executor/execution-core.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,12 +1032,16 @@ describe('executeWorkflowCore terminal finalization sequencing', () => {
10321032
})
10331033

10341034
it('routes cancelled executions through safeCompleteWithCancellation', async () => {
1035+
const executionState = {
1036+
blockStates: { 'function-1': { output: { result: 'raw-secret-value' } } },
1037+
}
10351038
executorExecuteMock.mockResolvedValue({
10361039
success: false,
10371040
status: 'cancelled',
10381041
output: {},
10391042
logs: [],
10401043
metadata: { duration: 123, startTime: 'start', endTime: 'end' },
1044+
executionState,
10411045
})
10421046

10431047
const result = await executeWorkflowCore({
@@ -1052,6 +1056,7 @@ describe('executeWorkflowCore terminal finalization sequencing', () => {
10521056
expect.objectContaining({
10531057
totalDurationMs: 123,
10541058
traceSpans: [{ id: 'span-1' }],
1059+
executionState,
10551060
})
10561061
)
10571062
expect(safeCompleteMock).not.toHaveBeenCalled()
@@ -1060,12 +1065,16 @@ describe('executeWorkflowCore terminal finalization sequencing', () => {
10601065
})
10611066

10621067
it('routes paused executions through safeCompleteWithPause', async () => {
1068+
const executionState = {
1069+
blockStates: { 'function-1': { output: { result: 'raw-secret-value' } } },
1070+
}
10631071
executorExecuteMock.mockResolvedValue({
10641072
success: true,
10651073
status: 'paused',
10661074
output: {},
10671075
logs: [],
10681076
metadata: { duration: 123, startTime: 'start', endTime: 'end' },
1077+
executionState,
10691078
})
10701079

10711080
const result = await executeWorkflowCore({
@@ -1081,6 +1090,7 @@ describe('executeWorkflowCore terminal finalization sequencing', () => {
10811090
totalDurationMs: 123,
10821091
traceSpans: [{ id: 'span-1' }],
10831092
workflowInput: { hello: 'world' },
1093+
executionState,
10841094
})
10851095
)
10861096
expect(safeCompleteMock).not.toHaveBeenCalled()

0 commit comments

Comments
 (0)