From ab969070d0e2ca3ba9c03abb35670e5e28cab822 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 7 May 2026 20:17:53 -0700 Subject: [PATCH 1/2] fix(wait): poll partially_resumed rows so chained waits resume The chained-pause flow leaves a row in 'partially_resumed' status (wait1 done, wait2 still waiting). The poll's WHERE filter only matched 'paused', so wait2 was never picked up. Include 'partially_resumed' in the filter. --- apps/sim/app/api/resume/poll/route.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/resume/poll/route.ts b/apps/sim/app/api/resume/poll/route.ts index 09f76ff7f1..bb053411b1 100644 --- a/apps/sim/app/api/resume/poll/route.ts +++ b/apps/sim/app/api/resume/poll/route.ts @@ -3,7 +3,7 @@ import { pausedExecutions } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' -import { and, asc, eq, isNotNull, lte } from 'drizzle-orm' +import { and, asc, inArray, isNotNull, lte } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' import { acquireLock, releaseLock } from '@/lib/core/config/redis' @@ -62,7 +62,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => { .from(pausedExecutions) .where( and( - eq(pausedExecutions.status, 'paused'), + // 'partially_resumed' rows occur when a chained-pause workflow advanced past + // an earlier wait — e.g. wait1 → agent → wait2 — and now wait2's time pause + // is the one waiting for the cron. Include it alongside fresh 'paused' rows. + inArray(pausedExecutions.status, ['paused', 'partially_resumed']), isNotNull(pausedExecutions.nextResumeAt), lte(pausedExecutions.nextResumeAt, now) ) From 91f88a7030277417ce4b1ff4f54e30c73d1a8cad Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 7 May 2026 20:23:12 -0700 Subject: [PATCH 2/2] feat(wait): make in-process threshold env-overridable for local testing Adds WAIT_INPROCESS_MAX_MS env var (default 300000ms = 5 min). Lower it locally (e.g. 5000) to exercise the suspend/cron-resume path with short waits. --- apps/sim/executor/handlers/wait/wait-handler.ts | 7 ++++--- apps/sim/lib/core/config/env.ts | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/sim/executor/handlers/wait/wait-handler.ts b/apps/sim/executor/handlers/wait/wait-handler.ts index 43fbc4a2f3..9cbb2d8049 100644 --- a/apps/sim/executor/handlers/wait/wait-handler.ts +++ b/apps/sim/executor/handlers/wait/wait-handler.ts @@ -1,3 +1,4 @@ +import { env, envNumber } from '@/lib/core/config/env' import { isExecutionCancelled, isRedisCancellationEnabled } from '@/lib/execution/cancellation' import type { BlockOutput } from '@/blocks/types' import { BlockType } from '@/executor/constants' @@ -11,7 +12,7 @@ import type { SerializedBlock } from '@/serializer/types' const CANCELLATION_CHECK_INTERVAL_MS = 500 /** Threshold below which we hold the wait in-process; above, we suspend via PauseMetadata. */ -const INPROCESS_MAX_MS = 5 * 60 * 1000 +const inprocessMaxMs = (): number => envNumber(env.WAIT_INPROCESS_MAX_MS, 5 * 60 * 1000) /** Hard ceiling on configurable wait duration. */ const MAX_WAIT_MS = 30 * 24 * 60 * 60 * 1000 @@ -91,7 +92,7 @@ function isWaitUnit(value: string): value is WaitUnit { /** * Handler for Wait blocks that pause workflow execution for a time delay. * - * Waits up to {@link INPROCESS_MAX_MS} are held in-process via an interruptible sleep. + * Waits up to `WAIT_INPROCESS_MAX_MS` (default 5 minutes) are held in-process via an interruptible sleep. * Longer waits suspend the workflow by returning {@link PauseMetadata} with * `pauseKind: 'time'`; the cron-driven resume poller (see `/api/resume/poll`) picks * the execution back up once `resumeAt` is reached. @@ -140,7 +141,7 @@ export class WaitBlockHandler implements BlockHandler { throw new Error('Wait time exceeds maximum of 30 days') } - if (waitMs <= INPROCESS_MAX_MS) { + if (waitMs <= inprocessMaxMs()) { const completed = await sleep(waitMs, { signal: ctx.abortSignal, executionId: ctx.executionId, diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index c95fa7fa99..529c1dca18 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -231,6 +231,9 @@ export const env = createEnv({ EXECUTION_TIMEOUT_ASYNC_TEAM: z.string().optional().default('5400'), // 90 minutes EXECUTION_TIMEOUT_ASYNC_ENTERPRISE: z.string().optional().default('5400'), // 90 minutes + // Wait block configuration + WAIT_INPROCESS_MAX_MS: z.string().optional().default('300000'), // Threshold below which a wait runs in-process; above it, the wait suspends and the cron poller resumes. Default 5 min. Lower this locally (e.g. '5000' = 5s) to test the suspend path with short waits. + // Isolated-VM Worker Pool Configuration IVM_POOL_SIZE: z.string().optional().default('4'), // Max worker processes in pool IVM_MAX_CONCURRENT: z.string().optional().default('10000'), // Max concurrent executions globally