@@ -51,6 +51,7 @@ export class RunExecution {
5151
5252 private _runFriendlyId ?: string ;
5353 private currentSnapshotId ?: string ;
54+ private currentAttemptNumber ?: number ;
5455 private currentTaskRunEnv ?: Record < string , string > ;
5556
5657 private dequeuedAt ?: Date ;
@@ -241,6 +242,12 @@ export class RunExecution {
241242 return ;
242243 }
243244
245+ if ( this . currentAttemptNumber && this . currentAttemptNumber !== run . attemptNumber ) {
246+ this . sendDebugLog ( "ERROR: attempt number mismatch" , snapshotMetadata ) ;
247+ await this . taskRunProcess ?. suspend ( ) ;
248+ return ;
249+ }
250+
244251 this . sendDebugLog ( `snapshot has changed to: ${ snapshot . executionStatus } ` , snapshotMetadata ) ;
245252
246253 // Reset the snapshot poll interval so we don't do unnecessary work
@@ -453,6 +460,14 @@ export class RunExecution {
453460 // A snapshot was just created, so update the snapshot ID
454461 this . currentSnapshotId = start . data . snapshot . friendlyId ;
455462
463+ // Also set or update the attempt number - we do this to detect illegal attempt number changes, e.g. from stalled runners coming back online
464+ const attemptNumber = start . data . run . attemptNumber ;
465+ if ( attemptNumber ) {
466+ this . currentAttemptNumber = attemptNumber ;
467+ } else {
468+ this . sendDebugLog ( "ERROR: no attempt number returned from start attempt" ) ;
469+ }
470+
456471 const metrics = this . measureExecutionMetrics ( {
457472 attemptCreatedAt : attemptStartedAt ,
458473 dequeuedAt : this . dequeuedAt ?. getTime ( ) ,
0 commit comments