Skip to content

Commit de90a01

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 d84cba6 commit de90a01

File tree

7 files changed

+716
-35
lines changed

7 files changed

+716
-35
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: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { databaseMock, loggerMock } from '@sim/testing'
22
import { beforeEach, describe, expect, test, vi } from 'vitest'
3-
import { ExecutionLogger } from '@/lib/logs/execution/logger'
3+
import { ExecutionLogger } from './logger'
44

55
vi.mock('@sim/db', () => databaseMock)
66

@@ -111,6 +111,43 @@ describe('ExecutionLogger', () => {
111111
test('should have getWorkflowExecution method', () => {
112112
expect(typeof logger.getWorkflowExecution).toBe('function')
113113
})
114+
115+
test('preserves progress markers and finalization summaries when execution completes', () => {
116+
const loggerInstance = new ExecutionLogger() as any
117+
118+
const completedData = loggerInstance.buildCompletedExecutionData({
119+
existingExecutionData: {
120+
lastStartedBlock: {
121+
blockId: 'block-start',
122+
blockName: 'Start',
123+
blockType: 'agent',
124+
startedAt: '2025-01-01T00:00:00.000Z',
125+
},
126+
lastCompletedBlock: {
127+
blockId: 'block-end',
128+
blockName: 'Finish',
129+
blockType: 'api',
130+
endedAt: '2025-01-01T00:00:05.000Z',
131+
success: true,
132+
},
133+
},
134+
traceSpans: [],
135+
finalOutput: { ok: true },
136+
finalizationPath: 'completed',
137+
completionFailure: 'fallback failure',
138+
executionCost: {
139+
tokens: { input: 0, output: 0, total: 0 },
140+
models: {},
141+
},
142+
})
143+
144+
expect(completedData.lastStartedBlock?.blockId).toBe('block-start')
145+
expect(completedData.lastCompletedBlock?.blockId).toBe('block-end')
146+
expect(completedData.finalizationPath).toBe('completed')
147+
expect(completedData.completionFailure).toBe('fallback failure')
148+
expect(completedData.hasTraceSpans).toBe(false)
149+
expect(completedData.traceSpanCount).toBe(0)
150+
})
114151
})
115152

116153
describe('file extraction', () => {

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

Lines changed: 82 additions & 12 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,7 +49,70 @@ 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 {
61+
private buildCompletedExecutionData(params: {
62+
existingExecutionData?: WorkflowExecutionLog['executionData']
63+
traceSpans?: TraceSpan[]
64+
finalOutput: BlockOutputData
65+
finalizationPath?: ExecutionFinalizationPath
66+
completionFailure?: string
67+
executionCost: {
68+
tokens: {
69+
input: number
70+
output: number
71+
total: number
72+
}
73+
models: NonNullable<WorkflowExecutionLog['executionData']['models']>
74+
}
75+
executionState?: SerializableExecutionState
76+
}): WorkflowExecutionLog['executionData'] {
77+
const {
78+
existingExecutionData,
79+
traceSpans,
80+
finalOutput,
81+
finalizationPath,
82+
completionFailure,
83+
executionCost,
84+
executionState,
85+
} = params
86+
const traceSpanCount = countTraceSpans(traceSpans)
87+
88+
return {
89+
...(existingExecutionData?.environment
90+
? { environment: existingExecutionData.environment }
91+
: {}),
92+
...(existingExecutionData?.trigger ? { trigger: existingExecutionData.trigger } : {}),
93+
...(existingExecutionData?.error ? { error: existingExecutionData.error } : {}),
94+
...(existingExecutionData?.lastStartedBlock
95+
? { lastStartedBlock: existingExecutionData.lastStartedBlock }
96+
: {}),
97+
...(existingExecutionData?.lastCompletedBlock
98+
? { lastCompletedBlock: existingExecutionData.lastCompletedBlock }
99+
: {}),
100+
...(completionFailure ? { completionFailure } : {}),
101+
...(finalizationPath ? { finalizationPath } : {}),
102+
hasTraceSpans: traceSpanCount > 0,
103+
traceSpanCount,
104+
traceSpans,
105+
finalOutput,
106+
tokens: {
107+
input: executionCost.tokens.input,
108+
output: executionCost.tokens.output,
109+
total: executionCost.tokens.total,
110+
},
111+
models: executionCost.models,
112+
...(executionState ? { executionState } : {}),
113+
}
114+
}
115+
52116
async startWorkflowExecution(params: {
53117
workflowId: string
54118
workspaceId: string
@@ -131,6 +195,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
131195
executionData: {
132196
environment,
133197
trigger,
198+
hasTraceSpans: false,
199+
traceSpanCount: 0,
134200
},
135201
cost: {
136202
total: BASE_EXECUTION_CHARGE,
@@ -189,6 +255,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
189255
traceSpans?: TraceSpan[]
190256
workflowInput?: any
191257
executionState?: SerializableExecutionState
258+
finalizationPath?: ExecutionFinalizationPath
259+
completionFailure?: string
192260
isResume?: boolean
193261
level?: 'info' | 'error'
194262
status?: 'completed' | 'failed' | 'cancelled' | 'pending'
@@ -202,6 +270,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
202270
traceSpans,
203271
workflowInput,
204272
executionState,
273+
finalizationPath,
274+
completionFailure,
205275
isResume,
206276
level: levelOverride,
207277
status: statusOverride,
@@ -216,7 +286,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
216286
.limit(1)
217287
const billingUserId = this.extractBillingUserId(existingLog?.executionData)
218288
const existingExecutionData = existingLog?.executionData as
219-
| { traceSpans?: TraceSpan[] }
289+
| WorkflowExecutionLog['executionData']
220290
| undefined
221291

222292
// Determine if workflow failed by checking trace spans for unhandled errors
@@ -272,6 +342,16 @@ export class ExecutionLogger implements IExecutionLoggerService {
272342
? Math.max(0, Math.round(rawDurationMs))
273343
: 0
274344

345+
const completedExecutionData = this.buildCompletedExecutionData({
346+
existingExecutionData,
347+
traceSpans: redactedTraceSpans,
348+
finalOutput: redactedFinalOutput,
349+
finalizationPath,
350+
completionFailure,
351+
executionCost,
352+
executionState,
353+
})
354+
275355
const [updatedLog] = await db
276356
.update(workflowExecutionLogs)
277357
.set({
@@ -280,17 +360,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
280360
endedAt: new Date(endedAt),
281361
totalDurationMs: totalDuration,
282362
files: executionFiles.length > 0 ? executionFiles : null,
283-
executionData: {
284-
traceSpans: redactedTraceSpans,
285-
finalOutput: redactedFinalOutput,
286-
tokens: {
287-
input: executionCost.tokens.input,
288-
output: executionCost.tokens.output,
289-
total: executionCost.tokens.total,
290-
},
291-
models: executionCost.models,
292-
...(executionState ? { executionState } : {}),
293-
},
363+
executionData: completedExecutionData,
294364
cost: executionCost,
295365
})
296366
.where(eq(workflowExecutionLogs.executionId, executionId))

0 commit comments

Comments
 (0)