snow-flow 1.3.22 → 1.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,8 @@ class BaseMCPServer {
26
26
  this.tools = new Map();
27
27
  // Performance tracking
28
28
  this.toolMetrics = new Map();
29
+ // 🔴 SNOW-003 FIX: Circuit breaker implementation
30
+ this.circuitBreakers = new Map();
29
31
  /**
30
32
  * Tool handlers map
31
33
  */
@@ -159,48 +161,214 @@ class BaseMCPServer {
159
161
  }
160
162
  }
161
163
  /**
162
- * Execute tool with retry logic
164
+ * 🔴 SNOW-003 FIX: Enhanced retry logic with intelligent backoff and circuit breaker
165
+ * Addresses the 19% failure rate with better retry strategies and failure prevention
163
166
  */
164
167
  async executeWithRetry(toolName, args, attempt = 1) {
165
- const maxRetries = 3;
166
- const backoffMs = 1000 * Math.pow(2, attempt - 1); // Exponential backoff
168
+ // 🔴 CRITICAL: Increased retries from 3 to 6 for better resilience
169
+ const maxRetries = 6;
170
+ // 🔴 CRITICAL: Intelligent backoff based on error type
171
+ const backoffMs = this.calculateBackoff(attempt, toolName);
172
+ // 🔴 CRITICAL: Dynamic timeout based on tool complexity
173
+ const timeout = this.calculateTimeout(toolName);
167
174
  try {
168
175
  // Get tool handler
169
176
  const handler = this.getToolHandler(toolName);
170
177
  if (!handler) {
171
178
  throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
172
179
  }
173
- // Execute with timeout
174
- const timeout = 30000; // 30 seconds
180
+ // 🔴 CRITICAL: Memory usage check before execution
181
+ if (attempt === 1) {
182
+ await this.checkMemoryUsage();
183
+ }
184
+ // Execute with dynamic timeout
175
185
  const result = await Promise.race([
176
186
  handler(args),
177
- new Promise((_, reject) => setTimeout(() => reject(new Error('Tool execution timeout')), timeout)),
187
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool execution timeout after ${timeout}ms`)), timeout)),
178
188
  ]);
189
+ // 🔴 SUCCESS: Reset circuit breaker on success
190
+ this.resetCircuitBreaker(toolName);
179
191
  return result;
180
192
  }
181
193
  catch (error) {
182
- this.logger.error(`Tool ${toolName} execution failed (attempt ${attempt}):`, error);
183
- // Check if retryable
184
- if (attempt < maxRetries && this.isRetryableError(error)) {
185
- this.logger.info(`Retrying ${toolName} after ${backoffMs}ms...`);
194
+ this.logger.error(`🔴 Tool ${toolName} execution failed (attempt ${attempt}/${maxRetries}):`, error);
195
+ // 🔴 CRITICAL: Update circuit breaker
196
+ this.updateCircuitBreaker(toolName, error);
197
+ // Check if retryable and within limits
198
+ if (attempt < maxRetries && this.isRetryableError(error) && !this.isCircuitBreakerOpen(toolName)) {
199
+ this.logger.info(`🔄 Retrying ${toolName} after ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
186
200
  await new Promise(resolve => setTimeout(resolve, backoffMs));
187
201
  return this.executeWithRetry(toolName, args, attempt + 1);
188
202
  }
189
- // Final failure
190
- throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
203
+ // 🔴 FINAL FAILURE: Enhanced error reporting
204
+ const errorMessage = this.createEnhancedErrorMessage(toolName, error, attempt, maxRetries);
205
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, errorMessage);
191
206
  }
192
207
  }
193
208
  /**
194
- * Check if error is retryable
209
+ * 🔴 SNOW-003 FIX: Calculate intelligent backoff based on error type and attempt
210
+ */
211
+ calculateBackoff(attempt, toolName) {
212
+ // Base exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s
213
+ const baseBackoff = 1000 * Math.pow(2, attempt - 1);
214
+ // Add jitter to prevent thundering herd (±25%)
215
+ const jitter = baseBackoff * 0.25 * (Math.random() - 0.5);
216
+ // Cap maximum backoff at 30 seconds
217
+ const maxBackoff = 30000;
218
+ return Math.min(baseBackoff + jitter, maxBackoff);
219
+ }
220
+ /**
221
+ * 🔴 SNOW-003 FIX: Calculate dynamic timeout based on tool complexity
222
+ */
223
+ calculateTimeout(toolName) {
224
+ // Tool-specific timeouts based on complexity
225
+ const timeoutMap = {
226
+ 'snow_create_flow': 90000, // Flow creation: 90s
227
+ 'snow_deploy': 120000, // Deployment: 2 minutes
228
+ 'snow_comprehensive_search': 45000, // Search: 45s
229
+ 'snow_find_artifact': 30000, // Find: 30s
230
+ 'snow_validate_live_connection': 15000, // Validation: 15s
231
+ };
232
+ // Default timeout for unknown tools
233
+ return timeoutMap[toolName] || 60000; // 60s default (increased from 30s)
234
+ }
235
+ updateCircuitBreaker(toolName, error) {
236
+ const breaker = this.circuitBreakers.get(toolName) || { failures: 0, lastFailure: 0, isOpen: false };
237
+ breaker.failures++;
238
+ breaker.lastFailure = Date.now();
239
+ // Open circuit breaker after 5 failures within 5 minutes
240
+ if (breaker.failures >= 5 && (Date.now() - breaker.lastFailure) < 300000) {
241
+ breaker.isOpen = true;
242
+ this.logger.warn(`🚨 Circuit breaker opened for ${toolName} due to repeated failures`);
243
+ }
244
+ this.circuitBreakers.set(toolName, breaker);
245
+ }
246
+ isCircuitBreakerOpen(toolName) {
247
+ const breaker = this.circuitBreakers.get(toolName);
248
+ if (!breaker || !breaker.isOpen)
249
+ return false;
250
+ // Auto-reset circuit breaker after 10 minutes
251
+ if (Date.now() - breaker.lastFailure > 600000) {
252
+ breaker.isOpen = false;
253
+ breaker.failures = 0;
254
+ this.circuitBreakers.set(toolName, breaker);
255
+ this.logger.info(`✅ Circuit breaker reset for ${toolName}`);
256
+ return false;
257
+ }
258
+ return true;
259
+ }
260
+ resetCircuitBreaker(toolName) {
261
+ const breaker = this.circuitBreakers.get(toolName);
262
+ if (breaker) {
263
+ breaker.failures = 0;
264
+ breaker.isOpen = false;
265
+ this.circuitBreakers.set(toolName, breaker);
266
+ }
267
+ }
268
+ /**
269
+ * 🔴 SNOW-003 FIX: Memory usage monitoring to prevent memory exhaustion failures
270
+ */
271
+ async checkMemoryUsage() {
272
+ try {
273
+ const memUsage = process.memoryUsage();
274
+ const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
275
+ const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
276
+ // Log memory usage if high (>200MB)
277
+ if (heapUsedMB > 200) {
278
+ this.logger.warn(`⚠️ High memory usage: ${heapUsedMB}MB heap used, ${heapTotalMB}MB total`);
279
+ }
280
+ // Trigger garbage collection if memory usage is very high (>500MB)
281
+ if (heapUsedMB > 500 && global.gc) {
282
+ this.logger.info('🧹 Triggering garbage collection due to high memory usage');
283
+ global.gc();
284
+ }
285
+ // Fail fast if memory usage is critical (>800MB)
286
+ if (heapUsedMB > 800) {
287
+ throw new Error(`Critical memory usage: ${heapUsedMB}MB. Operation aborted to prevent system instability.`);
288
+ }
289
+ }
290
+ catch (error) {
291
+ this.logger.warn('Could not check memory usage:', error);
292
+ }
293
+ }
294
+ /**
295
+ * 🔴 SNOW-003 FIX: Enhanced error message with troubleshooting guidance
296
+ */
297
+ createEnhancedErrorMessage(toolName, error, attempts, maxRetries) {
298
+ const baseMessage = `Tool '${toolName}' failed after ${attempts}/${maxRetries} attempts`;
299
+ const errorDetail = error instanceof Error ? error.message : String(error);
300
+ let troubleshooting = '';
301
+ // Add specific troubleshooting based on error type
302
+ if (error.response?.status === 401) {
303
+ troubleshooting = '\n💡 Authentication issue: Run "snow-flow auth login" to re-authenticate';
304
+ }
305
+ else if (error.response?.status === 403) {
306
+ troubleshooting = '\n💡 Permission issue: Check ServiceNow user permissions and OAuth scopes';
307
+ }
308
+ else if (error.response?.status >= 500) {
309
+ troubleshooting = '\n💡 ServiceNow server issue: Try again later or contact ServiceNow administrator';
310
+ }
311
+ else if (errorDetail.includes('timeout')) {
312
+ troubleshooting = '\n💡 Timeout issue: ServiceNow instance may be slow - try again later';
313
+ }
314
+ else if (errorDetail.includes('network') || errorDetail.includes('connection')) {
315
+ troubleshooting = '\n💡 Network issue: Check internet connection and ServiceNow instance availability';
316
+ }
317
+ return `${baseMessage}: ${errorDetail}${troubleshooting}`;
318
+ }
319
+ /**
320
+ * 🔴 SNOW-003 FIX: Enhanced error classification for ServiceNow specific errors
321
+ * Addresses the 19% failure rate by properly categorizing retryable errors
195
322
  */
196
323
  isRetryableError(error) {
197
324
  if (error instanceof Error) {
198
325
  const message = error.message.toLowerCase();
199
- return (message.includes('timeout') ||
326
+ // 🔴 CRITICAL: ServiceNow specific retryable errors
327
+ const serviceNowRetryable = (message.includes('timeout') ||
200
328
  message.includes('econnreset') ||
201
329
  message.includes('socket hang up') ||
202
330
  message.includes('enotfound') ||
203
- message.includes('rate limit'));
331
+ message.includes('rate limit') ||
332
+ message.includes('service unavailable') ||
333
+ message.includes('bad gateway') ||
334
+ message.includes('gateway timeout') ||
335
+ message.includes('connection refused') ||
336
+ message.includes('network error') ||
337
+ message.includes('dns lookup failed') ||
338
+ message.includes('connect etimedout') ||
339
+ message.includes('index not available') ||
340
+ message.includes('search index updating') ||
341
+ message.includes('temporary failure') ||
342
+ message.includes('server is busy') ||
343
+ message.includes('database lock') ||
344
+ message.includes('deadlock detected'));
345
+ // 🔴 CRITICAL: HTTP status code based retry logic
346
+ if (error.response?.status) {
347
+ const status = error.response.status;
348
+ const httpRetryable = (status === 429 || // Rate limit
349
+ status === 502 || // Bad Gateway
350
+ status === 503 || // Service Unavailable
351
+ status === 504 || // Gateway Timeout
352
+ status === 507 || // Insufficient Storage
353
+ status === 520 || // CloudFlare unknown error
354
+ status === 521 || // Web server is down
355
+ status === 522 || // Connection timed out
356
+ status === 523 || // Origin is unreachable
357
+ status === 524 // A timeout occurred
358
+ );
359
+ // 401 is retryable only once (for token refresh)
360
+ const authRetryable = status === 401 && !error.config?._retry;
361
+ return httpRetryable || authRetryable;
362
+ }
363
+ return serviceNowRetryable;
364
+ }
365
+ // Handle specific error types
366
+ if (error.code) {
367
+ const retryableCodes = [
368
+ 'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT',
369
+ 'ESOCKETTIMEDOUT', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN'
370
+ ];
371
+ return retryableCodes.includes(error.code);
204
372
  }
205
373
  return false;
206
374
  }
@@ -789,6 +789,56 @@ class ServiceNowDeploymentMCP {
789
789
  };
790
790
  let troubleshootingSteps = '';
791
791
  if (is403Error(directError) || is403Error(tableError)) {
792
+ // CRITICAL FIX: Check if widget was actually created despite 403 error
793
+ this.logger.info('403 error detected, verifying if widget was actually created...');
794
+ const verificationResult = await this.verifyWidgetInServiceNow(args.name);
795
+ if (verificationResult.exists) {
796
+ // Widget was created successfully despite 403 error!
797
+ this.logger.info('🎉 Widget verification SUCCESS: Widget exists despite 403 error', {
798
+ widgetName: args.name,
799
+ sys_id: verificationResult.sys_id,
800
+ completenessScore: verificationResult.completenessScore
801
+ });
802
+ // Format successful response similar to normal deployment
803
+ const credentials = await this.oauth.loadCredentials();
804
+ const widgetUrl = credentials?.instance ?
805
+ `https://${credentials.instance}/sp_config?id=widget_editor&sys_id=${verificationResult.sys_id}` :
806
+ 'ServiceNow instance URL not available';
807
+ return {
808
+ content: [
809
+ {
810
+ type: 'text',
811
+ text: `✅ Widget deployed successfully! (Despite 403 error)
812
+
813
+ 🎯 Widget Details:
814
+ - Name: ${args.name}
815
+ - Title: ${verificationResult.title || args.title}
816
+ - Sys ID: ${verificationResult.sys_id}
817
+ - Deployment Method: ${deploymentMethod} (with error recovery)
818
+ - Verification: ✅ Confirmed (${verificationResult.completenessScore}/100 complete)
819
+
820
+ 📦 Update Set:
821
+ - Name: ${updateSetName}
822
+ - ID: ${updateSetId || 'None'}
823
+ - Status: ${updateSetId ? '✅ Tracked' : '⚠️ Not tracked'}
824
+
825
+ 🔗 Direct Links:
826
+ - Widget Editor: ${widgetUrl}
827
+ - Service Portal Designer: https://${credentials?.instance}/sp_config?id=designer
828
+
829
+ 🔧 Note: Widget was created successfully despite receiving a 403 error. This is a known issue with ServiceNow permissions that has been automatically resolved.
830
+
831
+ ⚡ **Ready for Testing**
832
+ Your widget has been deployed and is ready for testing in Service Portal.`
833
+ }
834
+ ]
835
+ };
836
+ }
837
+ // Widget was NOT created, continue with error handling
838
+ this.logger.warn('Widget verification failed: Widget does not exist after deployment attempts', {
839
+ widgetName: args.name,
840
+ verificationDetails: verificationResult.debugInfo
841
+ });
792
842
  // Run authentication diagnostics automatically on 403 errors
793
843
  this.logger.info('403 error detected, running automatic authentication diagnostics...');
794
844
  let diagnosticsResult = '';
@@ -2079,15 +2129,44 @@ ${sessionSummary.statusCounts.pending > 0 ? '- 📋 Complete pending deployments
2079
2129
  if (!data || typeof data !== 'object') {
2080
2130
  throw new Error('Invalid diagnostics data received from ServiceNow');
2081
2131
  }
2082
- // Format test results
2083
- const testResults = Object.entries(tests).map(([name, result]) => `**${name}:** ${result?.status || 'Unknown'}
2084
- - ${result?.description || 'No description'}
2085
- ${result?.error ? `- Error: ${result.error}` : ''}
2086
- ${result?.http_status ? `- HTTP Status: ${result.http_status}` : ''}`).join('\n\n');
2087
- // Format recommendations
2088
- const recommendationText = args.include_recommendations !== false && recommendations.length > 0
2089
- ? `\n\n**🔧 Troubleshooting Recommendations:**\n${recommendations.map((rec) => `- ${rec}`).join('\n')}`
2090
- : '';
2132
+ // Format test results with enhanced null safety
2133
+ let testResults = 'No test results available';
2134
+ if (tests && typeof tests === 'object') {
2135
+ try {
2136
+ const entries = Object.entries(tests);
2137
+ if (entries.length > 0) {
2138
+ testResults = entries.map(([name, result]) => {
2139
+ // CRITICAL: Extra null checks for each property
2140
+ const status = result?.status || 'Unknown';
2141
+ const description = result?.description || 'No description';
2142
+ const error = result?.error && typeof result.error === 'string' ? `- Error: ${result.error}` : '';
2143
+ const httpStatus = result?.http_status && typeof result.http_status === 'number' ? `- HTTP Status: ${result.http_status}` : '';
2144
+ return `**${name}:** ${status}
2145
+ - ${description}
2146
+ ${error}
2147
+ ${httpStatus}`;
2148
+ }).join('\n\n');
2149
+ }
2150
+ }
2151
+ catch (testFormatError) {
2152
+ this.logger.error('Error formatting test results:', testFormatError);
2153
+ testResults = '❌ Error formatting test results - check ServiceNow connection';
2154
+ }
2155
+ }
2156
+ // Format recommendations with null safety
2157
+ let recommendationText = '';
2158
+ if (args.include_recommendations !== false && Array.isArray(recommendations) && recommendations.length > 0) {
2159
+ try {
2160
+ const validRecommendations = recommendations.filter(rec => rec && typeof rec === 'string');
2161
+ if (validRecommendations.length > 0) {
2162
+ recommendationText = `\n\n**🔧 Troubleshooting Recommendations:**\n${validRecommendations.map((rec) => `- ${rec}`).join('\n')}`;
2163
+ }
2164
+ }
2165
+ catch (recFormatError) {
2166
+ this.logger.error('Error formatting recommendations:', recFormatError);
2167
+ recommendationText = '\n\n**🔧 Troubleshooting Recommendations:**\n- Unable to format recommendations due to error';
2168
+ }
2169
+ }
2091
2170
  // Generate URL fix recommendation if we detect the trailing slash issue
2092
2171
  const urlFixRecommendation = data.instance_url && typeof data.instance_url === 'string' && data.instance_url.includes('//')
2093
2172
  ? '\n\n**🚨 CRITICAL URL ISSUE DETECTED:**\n- Your SNOW_INSTANCE in .env has a trailing slash\n- This causes malformed URLs like https://instance.com//api/\n- Remove the trailing slash from SNOW_INSTANCE=your-instance.com/'
@@ -5506,6 +5585,176 @@ Use individual deployment tools like \`snow_deploy_${args.type}\` with manual co
5506
5585
 
5507
5586
  **Error Details**: ${error.message || error}`;
5508
5587
  }
5588
+ /**
5589
+ * Verify widget exists in ServiceNow with comprehensive retry logic
5590
+ * Addresses the critical false negative bug where widgets show 403 errors but are actually created
5591
+ */
5592
+ async verifyWidgetInServiceNow(widgetName) {
5593
+ const maxRetries = 5;
5594
+ const baseDelay = 2000; // Start with 2 seconds
5595
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
5596
+ try {
5597
+ // Progressive delay - ServiceNow needs time to process
5598
+ if (attempt > 1) {
5599
+ const delay = baseDelay * attempt; // 2s, 4s, 6s, 8s, 10s
5600
+ this.logger.info(`Waiting ${delay}ms before widget verification attempt ${attempt}/${maxRetries}`);
5601
+ await this.sleep(delay);
5602
+ }
5603
+ this.logger.info(`Widget verification attempt ${attempt}/${maxRetries}`, { widgetName });
5604
+ // Multi-table verification approach (similar to flow verification)
5605
+ const [mainWidgetCheck, widgetSearchCheck] = await Promise.allSettled([
5606
+ // Check 1: Direct sp_widget table search by name
5607
+ this.client.searchRecords('sp_widget', `name=${widgetName}`, 1),
5608
+ // Check 2: Broader search with ID field
5609
+ this.client.searchRecords('sp_widget', `name=${widgetName}^ORid=${widgetName}`, 5)
5610
+ ]);
5611
+ let mainWidget = null;
5612
+ let searchResults = null;
5613
+ let verificationDetails = {
5614
+ attempt,
5615
+ mainWidgetCheck: 'pending',
5616
+ widgetSearchCheck: 'pending',
5617
+ totalFound: 0
5618
+ };
5619
+ // Process main widget check
5620
+ if (mainWidgetCheck.status === 'fulfilled' && mainWidgetCheck.value.success) {
5621
+ mainWidget = mainWidgetCheck.value.data?.[0];
5622
+ verificationDetails.mainWidgetCheck = mainWidget ? 'found' : 'not_found';
5623
+ verificationDetails.totalFound = mainWidgetCheck.value.data?.length || 0;
5624
+ }
5625
+ else {
5626
+ verificationDetails.mainWidgetCheck = `failed: ${mainWidgetCheck.status === 'rejected' ? mainWidgetCheck.reason : 'unknown error'}`;
5627
+ }
5628
+ // Process widget search check
5629
+ if (widgetSearchCheck.status === 'fulfilled' && widgetSearchCheck.value.success) {
5630
+ searchResults = widgetSearchCheck.value.data || [];
5631
+ verificationDetails.widgetSearchCheck = `found_${searchResults.length}`;
5632
+ // Use search results if main check didn't find anything
5633
+ if (!mainWidget && searchResults.length > 0) {
5634
+ mainWidget = searchResults[0];
5635
+ verificationDetails.totalFound = searchResults.length;
5636
+ }
5637
+ }
5638
+ else {
5639
+ verificationDetails.widgetSearchCheck = `failed: ${widgetSearchCheck.status === 'rejected' ? widgetSearchCheck.reason : 'unknown error'}`;
5640
+ }
5641
+ // Calculate completeness score
5642
+ let completenessScore = 0;
5643
+ if (mainWidget) {
5644
+ completenessScore += mainWidget.sys_id ? 25 : 0;
5645
+ completenessScore += mainWidget.name ? 25 : 0;
5646
+ completenessScore += mainWidget.title ? 25 : 0;
5647
+ completenessScore += mainWidget.template ? 25 : 0;
5648
+ }
5649
+ if (mainWidget && completenessScore >= 75) {
5650
+ // Widget found and appears complete
5651
+ this.logger.info(`✅ Widget verification SUCCESS on attempt ${attempt}`, {
5652
+ widgetName,
5653
+ sys_id: mainWidget.sys_id,
5654
+ completenessScore,
5655
+ totalRetries: attempt
5656
+ });
5657
+ return {
5658
+ exists: true,
5659
+ sys_id: mainWidget.sys_id,
5660
+ name: mainWidget.name,
5661
+ title: mainWidget.title,
5662
+ completenessScore,
5663
+ attempt,
5664
+ verificationDetails,
5665
+ debugInfo: {
5666
+ foundVia: verificationDetails.mainWidgetCheck === 'found' ? 'main_check' : 'search_check',
5667
+ totalFound: verificationDetails.totalFound,
5668
+ retriesNeeded: attempt
5669
+ }
5670
+ };
5671
+ }
5672
+ else if (mainWidget && completenessScore < 75) {
5673
+ // Widget found but incomplete - might still be processing
5674
+ this.logger.warn(`⚠️ Widget found but incomplete on attempt ${attempt}`, {
5675
+ widgetName,
5676
+ sys_id: mainWidget.sys_id,
5677
+ completenessScore,
5678
+ remainingRetries: maxRetries - attempt
5679
+ });
5680
+ if (attempt === maxRetries) {
5681
+ // Last attempt - return what we have
5682
+ return {
5683
+ exists: true,
5684
+ sys_id: mainWidget.sys_id,
5685
+ name: mainWidget.name,
5686
+ title: mainWidget.title,
5687
+ completenessScore,
5688
+ attempt,
5689
+ verificationDetails,
5690
+ debugInfo: {
5691
+ warning: 'Widget exists but appears incomplete',
5692
+ foundVia: verificationDetails.mainWidgetCheck === 'found' ? 'main_check' : 'search_check',
5693
+ totalFound: verificationDetails.totalFound,
5694
+ retriesNeeded: attempt
5695
+ }
5696
+ };
5697
+ }
5698
+ }
5699
+ else {
5700
+ // Widget not found
5701
+ this.logger.warn(`❌ Widget not found on attempt ${attempt}`, {
5702
+ widgetName,
5703
+ verificationDetails,
5704
+ remainingRetries: maxRetries - attempt
5705
+ });
5706
+ if (attempt === maxRetries) {
5707
+ // Final attempt failed
5708
+ return {
5709
+ exists: false,
5710
+ attempt,
5711
+ verificationDetails,
5712
+ debugInfo: {
5713
+ finalAttempt: true,
5714
+ allChecks: {
5715
+ mainWidgetCheck: verificationDetails.mainWidgetCheck,
5716
+ widgetSearchCheck: verificationDetails.widgetSearchCheck
5717
+ },
5718
+ totalRetries: maxRetries
5719
+ }
5720
+ };
5721
+ }
5722
+ }
5723
+ }
5724
+ catch (verificationError) {
5725
+ this.logger.error(`Widget verification attempt ${attempt} failed`, {
5726
+ widgetName,
5727
+ error: verificationError instanceof Error ? verificationError.message : String(verificationError),
5728
+ remainingRetries: maxRetries - attempt
5729
+ });
5730
+ if (attempt === maxRetries) {
5731
+ // Final attempt - return failure with error details
5732
+ return {
5733
+ exists: false,
5734
+ attempt,
5735
+ error: verificationError instanceof Error ? verificationError.message : String(verificationError),
5736
+ debugInfo: {
5737
+ finalAttempt: true,
5738
+ verificationError: true,
5739
+ totalRetries: maxRetries
5740
+ }
5741
+ };
5742
+ }
5743
+ }
5744
+ }
5745
+ // Should not reach here, but safety fallback
5746
+ return {
5747
+ exists: false,
5748
+ attempt: maxRetries,
5749
+ debugInfo: { unexpectedFallback: true }
5750
+ };
5751
+ }
5752
+ /**
5753
+ * Sleep utility for retry delays
5754
+ */
5755
+ sleep(ms) {
5756
+ return new Promise(resolve => setTimeout(resolve, ms));
5757
+ }
5509
5758
  /**
5510
5759
  * Format successful deployment response
5511
5760
  */