Skip to content

Commit e65f6c7

Browse files
committed
fix(mothership): clear a dropped run's saved pointer when its hold is released
A reconnect cancelled by navigating away leaves the settled run's execution pointer saved. Releasing the hold now clears it when it still names that execution, before the next run is admitted, so recovery cannot later report the finished run as a live one.
1 parent 9e1037d commit e65f6c7

2 files changed

Lines changed: 21 additions & 3 deletions

File tree

‎apps/sim/lib/mothership/tools/client/run-tool-execution.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,12 @@ describe('run tool execution cancellation', () => {
913913
getWorkflowExecution.mockImplementation(() => ({ isExecuting: false }))
914914
})
915915

916+
loadExecutionPointer.mockResolvedValue({
917+
workflowId: 'wf-1',
918+
executionId: 'exec-abandoned',
919+
lastEventId: 3,
920+
})
921+
916922
executeRunToolOnClient('tool-a', 'run_block', { workflowId: 'wf-1', blockId: 'block-1' })
917923
executeRunToolOnClient('tool-b', 'run_block', { workflowId: 'wf-1', blockId: 'block-2' })
918924
await vi.waitFor(() => expect(released).toHaveBeenCalledWith('wf-1'))
@@ -928,6 +934,11 @@ describe('run tool execution cancellation', () => {
928934

929935
reconnect.close()
930936
await vi.waitFor(() => expect(launchedToolCallIds()).toEqual(['tool-a', 'tool-b']))
937+
// The abandoned run's pointer is gone before the next run saves its own.
938+
expect(clearExecutionPointer).toHaveBeenCalledWith('wf-1')
939+
expect(clearExecutionPointer.mock.invocationCallOrder[0]).toBeLessThan(
940+
saveExecutionPointer.mock.invocationCallOrder.at(-1) ?? 0
941+
)
931942
finish('tool-b')
932943
await drain()
933944
expect(confirmStatuses()).toEqual([

‎apps/sim/lib/mothership/tools/client/run-tool-execution.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,17 @@ function releaseRunToolSlot(
186186
}
187187
}
188188

189-
function releaseInterruptedHold(workflowId: string, slot: WorkflowRunSlot, executionId: string) {
189+
async function releaseInterruptedHold(
190+
workflowId: string,
191+
slot: WorkflowRunSlot,
192+
executionId: string
193+
): Promise<void> {
190194
if (slot.owner?.kind !== 'interrupted' || slot.owner.executionId !== executionId) return
191-
// A reconnect cancelled by navigating away leaves the settled run marked current.
195+
// A reconnect cancelled by navigating away leaves the settled run marked current and its
196+
// pointer saved; both go before the next run can write its own.
192197
clearVisibleExecution(workflowId, executionId)
198+
const pointer = await loadExecutionPointer(workflowId).catch(() => null)
199+
if (pointer?.executionId === executionId) await clearExecutionPointer(workflowId)
193200
slot.owner = null
194201
admitNextRunTool(workflowId, slot)
195202
}
@@ -218,7 +225,7 @@ function watchInterruptedExecution(workflowId: string, slot: WorkflowRunSlot): v
218225
serverSettled = true
219226
if (isReconnectStreamOpen(workflowId, executionId)) continue
220227
}
221-
releaseInterruptedHold(workflowId, slot, executionId)
228+
await releaseInterruptedHold(workflowId, slot, executionId)
222229
break
223230
}
224231
slot.watchingInterrupted = false

0 commit comments

Comments
 (0)