Skip to content

Commit 21b2972

Browse files
authored
fix(mothership): report a browser-claimed workflow tool from its settled execution (#8481)
* fix(mothership): report a browser-claimed workflow tool from its settled execution When a browser claims a Chat workflow tool, the execute route runs the workflow and keeps running it after the browser detaches, but only the browser's confirmation completed the tool call. A tab that closed, lost its network, or dropped its pagehide beacon left the Chat turn waiting for the full client wait while the worker swept the call. The execute route now records the bound execution's structural completion itself once it settles, guarded on the call still running under that execution's claim, so a browser report or background detach that lands first is kept and nothing is delivered twice. * fix(mothership): report queued async runs and pre-log failures of a browser-claimed workflow tool * refactor(mothership): settle a browser-claimed workflow tool from its log status alone * test(mothership): prove a losing settlement report publishes no confirmation
1 parent 2178d6e commit 21b2972

6 files changed

Lines changed: 476 additions & 2 deletions

File tree

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ import {
9595
import { COPILOT_WORKFLOW_EXECUTION_CONFLICT_CODE } from '@/lib/mothership/constants'
9696
import { CopilotDegradedReason } from '@/lib/mothership/generated/trace-attribute-values-v1'
9797
import { recordDegraded } from '@/lib/mothership/request/metrics'
98+
import {
99+
reportQueuedClientWorkflowTool,
100+
reportSettledClientWorkflowTool,
101+
} from '@/lib/mothership/request/tools/workflow-client-settlement'
98102
import {
99103
ASYNC_WORKFLOW_DEPLOYMENT_ERRORS,
100104
type CopilotWorkflowToolBindingResult,
@@ -509,11 +513,25 @@ async function handleExecutePost(
509513
)
510514
await copilotSettlement
511515
}
516+
/** A bound execution reports its own outcome, so a browser that detached never strands the turn. */
512517
const executeBoundWorkflow = async <T>(execute: () => Promise<T>): Promise<T> => {
513518
try {
514519
return await execute()
515520
} finally {
516521
await settleCopilotExecution()
522+
if (copilotToolCallId && workflowToolClaimAcquired) {
523+
await reportSettledClientWorkflowTool({
524+
toolCallId: copilotToolCallId,
525+
executionId,
526+
workflowId,
527+
}).catch((error) => {
528+
reqLogger.warn('Could not report settled Copilot workflow execution', {
529+
copilotToolCallId,
530+
executionId,
531+
error: getErrorMessage(error),
532+
})
533+
})
534+
}
517535
}
518536
}
519537

@@ -1297,6 +1315,19 @@ async function handleExecutePost(
12971315
trustedInitialResolvedSecretTraceProvenance,
12981316
})
12991317
executionIdClaimCommitted = asyncResult.retainExecutionClaim
1318+
if (copilotToolCallId && workflowToolClaimAcquired && asyncResult.retainExecutionClaim) {
1319+
await reportQueuedClientWorkflowTool({
1320+
toolCallId: copilotToolCallId,
1321+
executionId,
1322+
workflowId,
1323+
}).catch((error) => {
1324+
reqLogger.warn('Could not report queued Copilot workflow execution', {
1325+
copilotToolCallId,
1326+
executionId,
1327+
error: getErrorMessage(error),
1328+
})
1329+
})
1330+
}
13001331
return asyncResult.response
13011332
}
13021333

‎apps/sim/lib/mothership/async-runs/repository.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,21 @@ export async function claimWorkflowToolExecution(
12161216
)
12171217
}
12181218

1219+
/**
1220+
* Finalizes a client-bound workflow tool from its own settled execution. It
1221+
* applies only while the call is still running under that execution's claim, so
1222+
* a browser report or a background detach that landed first always wins.
1223+
*/
1224+
export async function completeClientWorkflowToolCall(
1225+
input: CompleteAsyncToolCallInput,
1226+
executionId: string
1227+
) {
1228+
return await completeClaimedAsyncToolCall(
1229+
input,
1230+
`${WORKFLOW_EXECUTION_CLAIM_PREFIX}${executionId}`
1231+
)
1232+
}
1233+
12191234
export async function releaseWorkflowToolExecutionClaim(toolCallId: string, executionId: string) {
12201235
const claimedBy = `${WORKFLOW_EXECUTION_CLAIM_PREFIX}${executionId}`
12211236
return await withDbSpan(

‎apps/sim/lib/mothership/request/tools/workflow-client-fallback.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ interface RaceWorkflowToolClientPickupParams {
4949
*
5050
* After `graceMs` with no result, this competes for the same single-winner
5151
* execution claim that `/api/workflows/[id]/execute` takes on the browser's
52-
* behalf. Losing the claim means a browser really is running it, so we go back
53-
* to waiting; winning it means nobody was there, so we run it in-process.
52+
* behalf. Losing the claim means a browser started it through that route, so we
53+
* go back to waiting: the route reports the bound execution's outcome itself when
54+
* it settles, even if the browser has gone. Winning it means nobody was there, so
55+
* we run it in-process.
5456
* Because both sides contend on `claimedBy IS NULL`, the workflow can never run
5557
* twice — a browser arriving late gets a 409 it already treats as benign.
5658
*/

0 commit comments

Comments
 (0)