Skip to content

Commit e9ecae8

Browse files
icecrasher321claude
andcommitted
fix(copilot): mark a run that failed after the core returned
Once `executeWorkflowCore` returns, the run happened. Everything after it in `executeWorkflow` — analytics, pause persistence, post-execution settling — is bookkeeping that can still throw, and the core's own catch no longer runs, so those failures named no run. The copilot handler then reported `not_attempted` for an execution that had already produced side effects, which is the one direction that duplicates work. Mark it as soon as the core settles, so any later failure carries it. The `finally` had the same shape and is now contained: a throw there replaces whatever the function was about to do, turning a successful run into an error or an error that names its run into one that does not. Settling post-execution work is bookkeeping and must not be able to do either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c33f494 commit e9ecae8

1 file changed

Lines changed: 24 additions & 1 deletion

File tree

apps/sim/lib/workflows/executor/execute-workflow.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { WorkflowExecutionPrincipal } from '@sim/auth/principal'
22
import { createLogger } from '@sim/logger'
3+
import { getErrorMessage } from '@sim/utils/errors'
34
import { generateId } from '@sim/utils/id'
45
import {
56
assertBillingAttributionSnapshot,
@@ -13,6 +14,7 @@ import { handlePostExecutionPauseState } from '@/lib/workflows/executor/pause-pe
1314
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
1415
import type { ExecutionMetadata, SerializableExecutionState } from '@/executor/execution/types'
1516
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
17+
import { attachAttemptedExecutionId } from '@/executor/utils/errors'
1618
import type { ResolvedSecretTraceProvenanceV1 } from '@/executor/utils/resolved-secret-trace-registry'
1719
import type { CoreTriggerType } from '@/stores/logs/filters/types'
1820

@@ -128,6 +130,7 @@ export async function executeWorkflow(
128130
loggingSession.setTrustedExecutionCorrelation(streamConfig.trustedExecutionCorrelation)
129131
}
130132
let postExecutionOwnershipTransferred = false
133+
let coreReturned = false
131134

132135
try {
133136
const metadata: ExecutionMetadata = {
@@ -169,6 +172,12 @@ export async function executeWorkflow(
169172

170173
const executionStartMs = Date.now()
171174

175+
/**
176+
* Once the core returns, the run happened. Everything after it here — analytics, pause
177+
* persistence, post-execution settling — is bookkeeping that can still throw, and a
178+
* failure there names no run unless it is marked, so an execution that really occurred
179+
* would report itself as never started and invite a duplicate.
180+
*/
172181
const result = await executeWorkflowCore({
173182
snapshot,
174183
callbacks: {
@@ -198,6 +207,7 @@ export async function executeWorkflow(
198207
streamConfig?.trustedInitialResolvedSecretTraceProvenance,
199208
runFromBlock: streamConfig?.runFromBlock,
200209
})
210+
coreReturned = true
201211

202212
const blockTypes = [
203213
...new Set(
@@ -241,6 +251,7 @@ export async function executeWorkflow(
241251

242252
return result
243253
} catch (error: unknown) {
254+
if (coreReturned) attachAttemptedExecutionId(error, executionId)
244255
const errorDiagnostic = loggingSession.projectDiagnosticError(error)
245256
logger.error(`[${requestId}] Workflow execution failed`, errorDiagnostic)
246257

@@ -262,7 +273,19 @@ export async function executeWorkflow(
262273
throw error
263274
} finally {
264275
if (!postExecutionOwnershipTransferred) {
265-
await loggingSession.waitForPostExecution()
276+
/**
277+
* A `finally` that throws replaces whatever the function was about to do — turning a
278+
* successful run into an error, or an error that names its run into one that does not.
279+
* Settling post-execution work is bookkeeping and must not be able to do either.
280+
*/
281+
try {
282+
await loggingSession.waitForPostExecution()
283+
} catch (postExecutionError) {
284+
logger.error(`[${requestId}] Failed to settle post-execution work`, {
285+
executionId,
286+
error: getErrorMessage(postExecutionError),
287+
})
288+
}
266289
}
267290
}
268291
}

0 commit comments

Comments
 (0)