Skip to content

Commit 1e2fbab

Browse files
author
test
committed
fix(logs): persist execution diagnostics markers
Store last-started and last-completed block markers with finalization metadata so later read surfaces can explain how a run ended without reconstructing executor state.
1 parent 9229002 commit 1e2fbab

File tree

7 files changed

+567
-32
lines changed

7 files changed

+567
-32
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { buildExecutionDiagnostics } from '@/lib/logs/execution/diagnostics'
3+
4+
describe('buildExecutionDiagnostics', () => {
5+
it('derives trace span counts and preserves finalization details', () => {
6+
const diagnostics = buildExecutionDiagnostics({
7+
status: 'failed',
8+
level: 'error',
9+
startedAt: '2025-01-01T00:00:00.000Z',
10+
endedAt: '2025-01-01T00:00:05.000Z',
11+
executionData: {
12+
traceSpans: [
13+
{
14+
id: 'span-1',
15+
children: [{ id: 'span-1-child' }],
16+
},
17+
{ id: 'span-2' },
18+
],
19+
lastStartedBlock: { blockId: 'block-1' },
20+
lastCompletedBlock: { blockId: 'block-2' },
21+
finalizationPath: 'force_failed',
22+
completionFailure: 'fallback store failed',
23+
executionState: { blockStates: {} },
24+
},
25+
})
26+
27+
expect(diagnostics.traceSpanCount).toBe(3)
28+
expect(diagnostics.hasTraceSpans).toBe(true)
29+
expect(diagnostics.lastStartedBlock).toEqual({ blockId: 'block-1' })
30+
expect(diagnostics.lastCompletedBlock).toEqual({ blockId: 'block-2' })
31+
expect(diagnostics.finalizationPath).toBe('force_failed')
32+
expect(diagnostics.completionFailure).toBe('fallback store failed')
33+
expect(diagnostics.errorMessage).toBe('fallback store failed')
34+
expect(diagnostics.hasExecutionState).toBe(true)
35+
})
36+
37+
it('uses explicit trace flags and falls back to final output errors', () => {
38+
const diagnostics = buildExecutionDiagnostics({
39+
status: 'completed',
40+
startedAt: '2025-01-01T00:00:00.000Z',
41+
executionData: {
42+
hasTraceSpans: false,
43+
traceSpanCount: 7,
44+
finalOutput: { error: 'stored error' },
45+
finalizationPath: 'not-valid',
46+
},
47+
})
48+
49+
expect(diagnostics.hasTraceSpans).toBe(false)
50+
expect(diagnostics.traceSpanCount).toBe(7)
51+
expect(diagnostics.errorMessage).toBe('stored error')
52+
expect(diagnostics.finalizationPath).toBeUndefined()
53+
expect(diagnostics.hasExecutionState).toBe(false)
54+
})
55+
})
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { ExecutionFinalizationPath } from '@/lib/logs/types'
2+
import { isExecutionFinalizationPath } from '@/lib/logs/types'
3+
4+
type ExecutionData = {
5+
error?: string
6+
traceSpans?: unknown[]
7+
executionState?: unknown
8+
finalOutput?: { error?: unknown }
9+
lastStartedBlock?: unknown
10+
lastCompletedBlock?: unknown
11+
hasTraceSpans?: boolean
12+
traceSpanCount?: number
13+
completionFailure?: string
14+
finalizationPath?: unknown
15+
}
16+
17+
function countTraceSpans(traceSpans: unknown[] | undefined): number {
18+
if (!Array.isArray(traceSpans) || traceSpans.length === 0) {
19+
return 0
20+
}
21+
22+
return traceSpans.reduce<number>((count, span) => {
23+
const children =
24+
span && typeof span === 'object' && 'children' in span && Array.isArray(span.children)
25+
? (span.children as unknown[])
26+
: undefined
27+
28+
return count + 1 + countTraceSpans(children)
29+
}, 0)
30+
}
31+
32+
export function buildExecutionDiagnostics(params: {
33+
status: string
34+
level?: string | null
35+
startedAt: string
36+
endedAt?: string | null
37+
executionData?: ExecutionData | null
38+
}): {
39+
status: string
40+
level?: string
41+
startedAt: string
42+
endedAt?: string
43+
lastStartedBlock?: unknown
44+
lastCompletedBlock?: unknown
45+
hasTraceSpans: boolean
46+
traceSpanCount: number
47+
hasExecutionState: boolean
48+
finalizationPath?: ExecutionFinalizationPath
49+
completionFailure?: string
50+
errorMessage?: string
51+
} {
52+
const executionData = params.executionData ?? {}
53+
const derivedTraceSpanCount = countTraceSpans(executionData.traceSpans)
54+
const traceSpanCount =
55+
typeof executionData.traceSpanCount === 'number'
56+
? executionData.traceSpanCount
57+
: derivedTraceSpanCount
58+
const hasTraceSpans =
59+
typeof executionData.hasTraceSpans === 'boolean'
60+
? executionData.hasTraceSpans
61+
: traceSpanCount > 0
62+
const completionFailure =
63+
typeof executionData.completionFailure === 'string'
64+
? executionData.completionFailure
65+
: undefined
66+
const errorMessage =
67+
completionFailure ||
68+
(typeof executionData.error === 'string' ? executionData.error : undefined) ||
69+
(typeof executionData.finalOutput?.error === 'string'
70+
? executionData.finalOutput.error
71+
: undefined)
72+
const finalizationPath = isExecutionFinalizationPath(executionData.finalizationPath)
73+
? executionData.finalizationPath
74+
: undefined
75+
76+
return {
77+
status: params.status,
78+
level: params.level ?? undefined,
79+
startedAt: params.startedAt,
80+
endedAt: params.endedAt ?? undefined,
81+
lastStartedBlock: executionData.lastStartedBlock,
82+
lastCompletedBlock: executionData.lastCompletedBlock,
83+
hasTraceSpans,
84+
traceSpanCount,
85+
hasExecutionState: executionData.executionState !== undefined,
86+
...(finalizationPath ? { finalizationPath } : {}),
87+
...(completionFailure ? { completionFailure } : {}),
88+
...(errorMessage ? { errorMessage } : {}),
89+
}
90+
}

apps/sim/lib/logs/execution/logger.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('ExecutionLogger', () => {
112112
expect(typeof logger.getWorkflowExecution).toBe('function')
113113
})
114114

115-
test('preserves start correlation data when execution completes', () => {
115+
test('preserves correlation and diagnostics when execution completes', () => {
116116
const loggerInstance = new ExecutionLogger() as any
117117

118118
const completedData = loggerInstance.buildCompletedExecutionData({
@@ -140,9 +140,24 @@ describe('ExecutionLogger', () => {
140140
},
141141
},
142142
},
143+
lastStartedBlock: {
144+
blockId: 'block-start',
145+
blockName: 'Start',
146+
blockType: 'agent',
147+
startedAt: '2025-01-01T00:00:00.000Z',
148+
},
149+
lastCompletedBlock: {
150+
blockId: 'block-end',
151+
blockName: 'Finish',
152+
blockType: 'api',
153+
endedAt: '2025-01-01T00:00:05.000Z',
154+
success: true,
155+
},
143156
},
144157
traceSpans: [],
145158
finalOutput: { ok: true },
159+
finalizationPath: 'completed',
160+
completionFailure: 'fallback failure',
146161
executionCost: {
147162
tokens: { input: 0, output: 0, total: 0 },
148163
models: {},
@@ -161,6 +176,12 @@ describe('ExecutionLogger', () => {
161176
})
162177
expect(completedData.correlation).toEqual(completedData.trigger?.data?.correlation)
163178
expect(completedData.finalOutput).toEqual({ ok: true })
179+
expect(completedData.lastStartedBlock?.blockId).toBe('block-start')
180+
expect(completedData.lastCompletedBlock?.blockId).toBe('block-end')
181+
expect(completedData.finalizationPath).toBe('completed')
182+
expect(completedData.completionFailure).toBe('fallback failure')
183+
expect(completedData.hasTraceSpans).toBe(false)
184+
expect(completedData.traceSpanCount).toBe(0)
164185
})
165186
})
166187

apps/sim/lib/logs/execution/logger.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { snapshotService } from '@/lib/logs/execution/snapshot/service'
2626
import type {
2727
BlockOutputData,
2828
ExecutionEnvironment,
29+
ExecutionFinalizationPath,
2930
ExecutionTrigger,
3031
ExecutionLoggerService as IExecutionLoggerService,
3132
TraceSpan,
@@ -48,11 +49,21 @@ export interface ToolCall {
4849

4950
const logger = createLogger('ExecutionLogger')
5051

52+
function countTraceSpans(traceSpans?: TraceSpan[]): number {
53+
if (!Array.isArray(traceSpans) || traceSpans.length === 0) {
54+
return 0
55+
}
56+
57+
return traceSpans.reduce((count, span) => count + 1 + countTraceSpans(span.children), 0)
58+
}
59+
5160
export class ExecutionLogger implements IExecutionLoggerService {
5261
private buildCompletedExecutionData(params: {
5362
existingExecutionData?: WorkflowExecutionLog['executionData']
5463
traceSpans?: TraceSpan[]
5564
finalOutput: BlockOutputData
65+
finalizationPath?: ExecutionFinalizationPath
66+
completionFailure?: string
5667
executionCost: {
5768
tokens: {
5869
input: number
@@ -63,7 +74,16 @@ export class ExecutionLogger implements IExecutionLoggerService {
6374
}
6475
executionState?: SerializableExecutionState
6576
}): WorkflowExecutionLog['executionData'] {
66-
const { existingExecutionData, traceSpans, finalOutput, executionCost, executionState } = params
77+
const {
78+
existingExecutionData,
79+
traceSpans,
80+
finalOutput,
81+
finalizationPath,
82+
completionFailure,
83+
executionCost,
84+
executionState,
85+
} = params
86+
const traceSpanCount = countTraceSpans(traceSpans)
6787

6888
return {
6989
...(existingExecutionData?.environment
@@ -77,6 +97,17 @@ export class ExecutionLogger implements IExecutionLoggerService {
7797
existingExecutionData?.trigger?.data?.correlation,
7898
}
7999
: {}),
100+
...(existingExecutionData?.error ? { error: existingExecutionData.error } : {}),
101+
...(existingExecutionData?.lastStartedBlock
102+
? { lastStartedBlock: existingExecutionData.lastStartedBlock }
103+
: {}),
104+
...(existingExecutionData?.lastCompletedBlock
105+
? { lastCompletedBlock: existingExecutionData.lastCompletedBlock }
106+
: {}),
107+
...(completionFailure ? { completionFailure } : {}),
108+
...(finalizationPath ? { finalizationPath } : {}),
109+
hasTraceSpans: traceSpanCount > 0,
110+
traceSpanCount,
80111
traceSpans,
81112
finalOutput,
82113
tokens: {
@@ -172,6 +203,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
172203
environment,
173204
trigger,
174205
...(trigger.data?.correlation ? { correlation: trigger.data.correlation } : {}),
206+
hasTraceSpans: false,
207+
traceSpanCount: 0,
175208
},
176209
cost: {
177210
total: BASE_EXECUTION_CHARGE,
@@ -230,6 +263,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
230263
traceSpans?: TraceSpan[]
231264
workflowInput?: any
232265
executionState?: SerializableExecutionState
266+
finalizationPath?: ExecutionFinalizationPath
267+
completionFailure?: string
233268
isResume?: boolean
234269
level?: 'info' | 'error'
235270
status?: 'completed' | 'failed' | 'cancelled' | 'pending'
@@ -243,6 +278,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
243278
traceSpans,
244279
workflowInput,
245280
executionState,
281+
finalizationPath,
282+
completionFailure,
246283
isResume,
247284
level: levelOverride,
248285
status: statusOverride,
@@ -313,6 +350,16 @@ export class ExecutionLogger implements IExecutionLoggerService {
313350
? Math.max(0, Math.round(rawDurationMs))
314351
: 0
315352

353+
const completedExecutionData = this.buildCompletedExecutionData({
354+
existingExecutionData,
355+
traceSpans: redactedTraceSpans,
356+
finalOutput: redactedFinalOutput,
357+
finalizationPath,
358+
completionFailure,
359+
executionCost,
360+
executionState,
361+
})
362+
316363
const [updatedLog] = await db
317364
.update(workflowExecutionLogs)
318365
.set({
@@ -321,13 +368,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
321368
endedAt: new Date(endedAt),
322369
totalDurationMs: totalDuration,
323370
files: executionFiles.length > 0 ? executionFiles : null,
324-
executionData: this.buildCompletedExecutionData({
325-
existingExecutionData,
326-
traceSpans: redactedTraceSpans,
327-
finalOutput: redactedFinalOutput,
328-
executionCost,
329-
executionState,
330-
}),
371+
executionData: completedExecutionData,
331372
cost: executionCost,
332373
})
333374
.where(eq(workflowExecutionLogs.executionId, executionId))

0 commit comments

Comments
 (0)