@@ -376,6 +376,55 @@ function getEarliestRunningWaitResumeAt(
376376 return earliest ;
377377}
378378
379+ /**
380+ * Complete running sleep step attempts whose resume timestamp has elapsed.
381+ * Malformed historical resume timestamps are treated as elapsed for backward
382+ * compatibility.
383+ * @param options - Sleep pre-pass options
384+ * @returns Whether any running sleep remains pending after completion pass
385+ */
386+ async function completeElapsedRunningSleepAttempts (
387+ options : Readonly < {
388+ backend : Backend ;
389+ workflowRunId : string ;
390+ workerId : string ;
391+ attempts : StepAttempt [ ] ;
392+ } > ,
393+ ) : Promise < boolean > {
394+ let hasPendingRunningSleep = false ;
395+
396+ for ( let i = 0 ; i < options . attempts . length ; i += 1 ) {
397+ const attempt = options . attempts [ i ] ;
398+ if ( ! attempt ) continue ;
399+
400+ if (
401+ attempt . status !== "running" ||
402+ attempt . kind !== "sleep" ||
403+ attempt . context ?. kind !== "sleep"
404+ ) {
405+ continue ;
406+ }
407+
408+ const resumeAt = new Date ( attempt . context . resumeAt ) ;
409+ const resumeAtMs = resumeAt . getTime ( ) ;
410+ if ( Number . isFinite ( resumeAtMs ) && Date . now ( ) < resumeAtMs ) {
411+ hasPendingRunningSleep = true ;
412+ continue ;
413+ }
414+
415+ const completed = await options . backend . completeStepAttempt ( {
416+ workflowRunId : options . workflowRunId ,
417+ stepAttemptId : attempt . id ,
418+ workerId : options . workerId ,
419+ output : null ,
420+ } ) ;
421+
422+ options . attempts [ i ] = completed ;
423+ }
424+
425+ return hasPendingRunningSleep ;
426+ }
427+
379428/**
380429 * Load all step attempts for a workflow run.
381430 * @param backend - Backend instance
@@ -1009,37 +1058,23 @@ export async function executeWorkflow(
10091058 workflowRun . id ,
10101059 ) ;
10111060
1012- // mark any sleep steps as completed if their sleep duration has elapsed,
1013- // or rethrow SleepSignal if still sleeping
1014- for ( let i = 0 ; i < attempts . length ; i ++ ) {
1015- const attempt = attempts [ i ] ;
1016- if ( ! attempt ) continue ;
1061+ // complete any elapsed sleep waits first, then park on the earliest
1062+ // remaining running wait (sleep or runWorkflow timeout).
1063+ const hasPendingRunningSleep = await completeElapsedRunningSleepAttempts ( {
1064+ backend,
1065+ workflowRunId : workflowRun . id ,
1066+ workerId,
1067+ attempts,
1068+ } ) ;
10171069
1070+ if ( hasPendingRunningSleep ) {
1071+ const earliestRunningWaitResumeAt =
1072+ getEarliestRunningWaitResumeAt ( attempts ) ;
10181073 if (
1019- attempt . status === "running" &&
1020- attempt . kind === "sleep" &&
1021- attempt . context ?. kind === "sleep"
1074+ earliestRunningWaitResumeAt &&
1075+ Date . now ( ) < earliestRunningWaitResumeAt . getTime ( )
10221076 ) {
1023- const now = Date . now ( ) ;
1024- const resumeAt = new Date ( attempt . context . resumeAt ) ;
1025- const resumeAtMs = resumeAt . getTime ( ) ;
1026-
1027- if ( now < resumeAtMs ) {
1028- // sleep duration HAS NOT elapsed yet, throw signal to put workflow
1029- // back to sleep
1030- throw new SleepSignal ( resumeAt ) ;
1031- }
1032-
1033- // sleep duration HAS elapsed, mark the step as completed and continue
1034- const completed = await backend . completeStepAttempt ( {
1035- workflowRunId : workflowRun . id ,
1036- stepAttemptId : attempt . id ,
1037- workerId,
1038- output : null ,
1039- } ) ;
1040-
1041- // update cache w/ completed attempt
1042- attempts [ i ] = completed ;
1077+ throw new SleepSignal ( earliestRunningWaitResumeAt ) ;
10431078 }
10441079 }
10451080
0 commit comments