Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,20 @@ export class PauseResumeManager {
static async enqueueOrStartResume(args: EnqueueResumeArgs): Promise<EnqueueResumeResult> {
const { executionId, contextId, resumeInput, userId } = args

return await db.transaction(async (tx) => {
const pausedExecution = await tx
.select()
.from(pausedExecutions)
// Retry to handle race condition where resume request arrives
// before persistPauseResult commits the paused execution row.
// The INSERT in persistPauseResult is awaited, so the race window
// is only between the method call and the await returning (~10-50ms).
const MAX_RETRIES = 3
const RETRY_DELAY_MS = 200
let lastError: Error | null = null

for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return await db.transaction(async (tx) => {
const pausedExecution = await tx
.select()
.from(pausedExecutions)
.where(eq(pausedExecutions.executionId, executionId))
.for('update')
.limit(1)
Expand Down Expand Up @@ -277,7 +287,22 @@ export class PauseResumeManager {
resumeInput,
userId,
}
})
})
} catch (err: any) {
lastError = err
const isNotFound = err.message?.includes('Paused execution not found')
const isLastAttempt = attempt === MAX_RETRIES - 1

if (!isNotFound || isLastAttempt) {
throw err
}
Comment on lines +291 to +298
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile string-based error matching

The retry selector err.message?.includes('Paused execution not found') couples the retry logic to the exact wording of the error message. If the message on line 187 is ever changed, refactored, or translated, the retry silently stops working — without any compile-time or test-time warning.

A more robust approach is to use a typed custom error class so the check is structural rather than textual:

class PausedExecutionNotFoundError extends Error {
  constructor() {
    super('Paused execution not found or already resumed')
    this.name = 'PausedExecutionNotFoundError'
  }
}

// At throw site (line 187):
throw new PausedExecutionNotFoundError()

// In the catch block:
const isNotFound = err instanceof PausedExecutionNotFoundError

This makes the intent explicit and refactor-safe.


await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS))
}
}

// This should never be reached due to the for loop logic, but TypeScript needs it
throw lastError ?? new Error('enqueueOrStartResume failed after retries')
}

static async startResumeExecution(args: StartResumeExecutionArgs): Promise<void> {
Expand Down