snow-flow 1.3.0 → 1.3.2
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.
- package/.claude-flow/queen/queen-memory.db +0 -0
- package/.env.example +249 -14
- package/CLAUDE.md +185 -24
- package/README.md +55 -5
- package/dist/api/error-handling.js +898 -4
- package/dist/api/performance-optimizer.js +3 -2
- package/dist/cli.js +590 -200
- package/dist/config/snow-flow-config.js +320 -8
- package/dist/health/system-health.js +288 -21
- package/dist/mcp/base-mcp-server.js +2 -0
- package/dist/mcp/http-transport-wrapper.js +65 -4
- package/dist/mcp/servicenow-automation-mcp-refactored.js +90 -9
- package/dist/mcp/servicenow-deployment-mcp-refactored.js +151 -2
- package/dist/mcp/servicenow-deployment-mcp.js +828 -15
- package/dist/mcp/servicenow-integration-mcp-refactored.js +100 -9
- package/dist/mcp/servicenow-intelligent-mcp.js +198 -7
- package/dist/mcp/servicenow-memory-mcp.js +2 -1
- package/dist/mcp/servicenow-operations-mcp-refactored.js +3 -2
- package/dist/mcp/servicenow-xml-flow-mcp.js +671 -0
- package/dist/mcp/shared/base-mcp-server.js +1356 -8
- package/dist/mcp/shared/mcp-resource-manager.js +304 -0
- package/dist/queen/parallel-agent-engine.js +26 -8
- package/dist/queen/servicenow-queen.js +47 -44
- package/dist/sparc/sparc-help.js +37 -26
- package/dist/sparc/team-sparc.js +60 -16
- package/dist/utils/action-type-cache.js +2 -1
- package/dist/utils/mcp-config-manager.js +7 -3
- package/dist/utils/mcp-server-manager.js +7 -3
- package/dist/utils/servicenow-client.js +134 -107
- package/dist/utils/servicenow-id-generator.js +171 -0
- package/dist/utils/snow-oauth.js +13 -8
- package/dist/utils/widget-template-generator.js +1690 -0
- package/dist/utils/xml-first-flow-generator.js +473 -0
- package/dist/version.js +7 -1
- package/flow-update-sets/flow_build_automated_approval_flow_with_notifications_flow.xml +203 -0
- package/flow-update-sets/flow_create_approval_flow_for_equipment_requests_flow.xml +206 -0
- package/flow-update-sets/flow_create_equipment_approval_flow_with_manager_approv_flow.xml +254 -0
- package/flow-update-sets/iphone_15_pro_approval_flow.xml +203 -0
- package/flow-update-sets/test_iphone_approval_flow_flow.xml +303 -0
- package/package.json +2 -1
- package/test-config.js +55 -0
- package/test-real-monitoring.js +184 -0
|
@@ -447,12 +447,594 @@ class FlowErrorHandler {
|
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
/**
|
|
450
|
-
* Retry original operation
|
|
450
|
+
* Retry original operation with exponential backoff
|
|
451
451
|
*/
|
|
452
452
|
async retryOriginalOperation(error) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
453
|
+
const maxRetries = error.max_retries || 3;
|
|
454
|
+
const currentRetryCount = error.retry_count || 0;
|
|
455
|
+
this.logger.info(`Attempting retry ${currentRetryCount + 1}/${maxRetries} for operation`, {
|
|
456
|
+
operation: error.context.operation,
|
|
457
|
+
error_type: error.type,
|
|
458
|
+
previous_attempts: currentRetryCount
|
|
459
|
+
});
|
|
460
|
+
// Check if we've exceeded max retries
|
|
461
|
+
if (currentRetryCount >= maxRetries) {
|
|
462
|
+
this.logger.error('Maximum retry attempts exceeded', {
|
|
463
|
+
operation: error.context.operation,
|
|
464
|
+
total_attempts: currentRetryCount,
|
|
465
|
+
final_error: error.message
|
|
466
|
+
});
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
final_state: null,
|
|
470
|
+
retry_exhausted: true,
|
|
471
|
+
total_attempts: currentRetryCount,
|
|
472
|
+
last_error: error.message,
|
|
473
|
+
suggested_actions: [
|
|
474
|
+
'Review the operation configuration',
|
|
475
|
+
'Check ServiceNow system status',
|
|
476
|
+
'Contact administrator for manual intervention',
|
|
477
|
+
'Consider alternative approach'
|
|
478
|
+
]
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
// Calculate retry delay using exponential backoff
|
|
482
|
+
const baseDelay = this.retryDelays[Math.min(currentRetryCount, this.retryDelays.length - 1)];
|
|
483
|
+
const jitterDelay = baseDelay + Math.random() * 1000; // Add jitter to prevent thundering herd
|
|
484
|
+
this.logger.info(`Waiting ${Math.round(jitterDelay)}ms before retry attempt`, {
|
|
485
|
+
retry_count: currentRetryCount + 1,
|
|
486
|
+
base_delay: baseDelay,
|
|
487
|
+
actual_delay: Math.round(jitterDelay)
|
|
488
|
+
});
|
|
489
|
+
// Wait before retry
|
|
490
|
+
await this.delay(jitterDelay);
|
|
491
|
+
try {
|
|
492
|
+
// Determine retry strategy based on error type
|
|
493
|
+
const retryResult = await this.executeRetryBasedOnErrorType(error);
|
|
494
|
+
if (retryResult.success) {
|
|
495
|
+
this.logger.info('Retry attempt successful', {
|
|
496
|
+
operation: error.context.operation,
|
|
497
|
+
retry_count: currentRetryCount + 1,
|
|
498
|
+
success_after_attempts: currentRetryCount + 1
|
|
499
|
+
});
|
|
500
|
+
return {
|
|
501
|
+
success: true,
|
|
502
|
+
final_state: retryResult.data,
|
|
503
|
+
retry_count: currentRetryCount + 1,
|
|
504
|
+
recovery_method: 'retry_with_backoff'
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
// Update error with new retry count and continue retry cycle
|
|
509
|
+
const updatedError = {
|
|
510
|
+
...error,
|
|
511
|
+
retry_count: currentRetryCount + 1,
|
|
512
|
+
message: retryResult.error || error.message
|
|
513
|
+
};
|
|
514
|
+
// Recursively retry if not at max attempts
|
|
515
|
+
return await this.retryOriginalOperation(updatedError);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch (retryError) {
|
|
519
|
+
this.logger.error('Error during retry attempt', {
|
|
520
|
+
retry_count: currentRetryCount + 1,
|
|
521
|
+
error: retryError instanceof Error ? retryError.message : String(retryError)
|
|
522
|
+
});
|
|
523
|
+
// Create updated error for next retry or final failure
|
|
524
|
+
const updatedError = {
|
|
525
|
+
...error,
|
|
526
|
+
retry_count: currentRetryCount + 1,
|
|
527
|
+
message: retryError instanceof Error ? retryError.message : String(retryError),
|
|
528
|
+
original_error: retryError instanceof Error ? retryError : new Error(String(retryError))
|
|
529
|
+
};
|
|
530
|
+
// Continue retry cycle
|
|
531
|
+
return await this.retryOriginalOperation(updatedError);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Execute retry based on specific error type
|
|
536
|
+
*/
|
|
537
|
+
async executeRetryBasedOnErrorType(error) {
|
|
538
|
+
try {
|
|
539
|
+
switch (error.type) {
|
|
540
|
+
case ErrorType.AUTHENTICATION_ERROR:
|
|
541
|
+
return await this.retryAuthenticationOperation(error);
|
|
542
|
+
case ErrorType.NETWORK_ERROR:
|
|
543
|
+
case ErrorType.TIMEOUT_ERROR:
|
|
544
|
+
return await this.retryNetworkOperation(error);
|
|
545
|
+
case ErrorType.RATE_LIMIT_ERROR:
|
|
546
|
+
return await this.retryRateLimitedOperation(error);
|
|
547
|
+
case ErrorType.SERVICE_UNAVAILABLE:
|
|
548
|
+
return await this.retryServiceUnavailableOperation(error);
|
|
549
|
+
case ErrorType.API_ERROR:
|
|
550
|
+
return await this.retryAPIOperation(error);
|
|
551
|
+
case ErrorType.CONFLICT_ERROR:
|
|
552
|
+
return await this.retryConflictOperation(error);
|
|
553
|
+
case ErrorType.RESOURCE_NOT_FOUND:
|
|
554
|
+
return await this.retryResourceOperation(error);
|
|
555
|
+
default:
|
|
556
|
+
return await this.retryGenericOperation(error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (executeError) {
|
|
560
|
+
return {
|
|
561
|
+
success: false,
|
|
562
|
+
error: executeError instanceof Error ? executeError.message : String(executeError)
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Retry authentication operations
|
|
568
|
+
*/
|
|
569
|
+
async retryAuthenticationOperation(error) {
|
|
570
|
+
try {
|
|
571
|
+
this.logger.info('Retrying authentication operation');
|
|
572
|
+
// Try to refresh authentication
|
|
573
|
+
const credentials = this.client.credentialsInstance;
|
|
574
|
+
if (credentials?.refreshToken) {
|
|
575
|
+
// Attempt token refresh
|
|
576
|
+
const refreshResult = await this.client.oauth?.refreshAccessToken();
|
|
577
|
+
if (refreshResult?.success) {
|
|
578
|
+
// Retry the original operation with new token
|
|
579
|
+
return await this.retryWithNewAuthentication(error);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: 'Authentication refresh failed - manual re-authentication required'
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
catch (authError) {
|
|
588
|
+
return {
|
|
589
|
+
success: false,
|
|
590
|
+
error: authError instanceof Error ? authError.message : String(authError)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Retry network operations
|
|
596
|
+
*/
|
|
597
|
+
async retryNetworkOperation(error) {
|
|
598
|
+
try {
|
|
599
|
+
this.logger.info('Retrying network operation');
|
|
600
|
+
// For network errors, simply retry the original request
|
|
601
|
+
return await this.executeOriginalOperation(error);
|
|
602
|
+
}
|
|
603
|
+
catch (networkError) {
|
|
604
|
+
return {
|
|
605
|
+
success: false,
|
|
606
|
+
error: networkError instanceof Error ? networkError.message : String(networkError)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Retry rate limited operations with longer delay
|
|
612
|
+
*/
|
|
613
|
+
async retryRateLimitedOperation(error) {
|
|
614
|
+
try {
|
|
615
|
+
this.logger.info('Retrying rate limited operation with extended delay');
|
|
616
|
+
// Rate limit errors need longer delays
|
|
617
|
+
const rateLimitDelay = 30000 + Math.random() * 10000; // 30-40 seconds
|
|
618
|
+
await this.delay(rateLimitDelay);
|
|
619
|
+
return await this.executeOriginalOperation(error);
|
|
620
|
+
}
|
|
621
|
+
catch (rateLimitError) {
|
|
622
|
+
return {
|
|
623
|
+
success: false,
|
|
624
|
+
error: rateLimitError instanceof Error ? rateLimitError.message : String(rateLimitError)
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Retry service unavailable operations
|
|
630
|
+
*/
|
|
631
|
+
async retryServiceUnavailableOperation(error) {
|
|
632
|
+
try {
|
|
633
|
+
this.logger.info('Retrying operation after service unavailability');
|
|
634
|
+
// Check service health before retry
|
|
635
|
+
const healthCheck = await this.performServiceHealthCheck();
|
|
636
|
+
if (!healthCheck.healthy) {
|
|
637
|
+
return {
|
|
638
|
+
success: false,
|
|
639
|
+
error: `Service still unavailable: ${healthCheck.reason}`
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
return await this.executeOriginalOperation(error);
|
|
643
|
+
}
|
|
644
|
+
catch (serviceError) {
|
|
645
|
+
return {
|
|
646
|
+
success: false,
|
|
647
|
+
error: serviceError instanceof Error ? serviceError.message : String(serviceError)
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Retry API operations
|
|
653
|
+
*/
|
|
654
|
+
async retryAPIOperation(error) {
|
|
655
|
+
try {
|
|
656
|
+
this.logger.info('Retrying API operation');
|
|
657
|
+
// API errors might be transient, retry with same parameters
|
|
658
|
+
return await this.executeOriginalOperation(error);
|
|
659
|
+
}
|
|
660
|
+
catch (apiError) {
|
|
661
|
+
return {
|
|
662
|
+
success: false,
|
|
663
|
+
error: apiError instanceof Error ? apiError.message : String(apiError)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Retry conflict operations with modified data
|
|
669
|
+
*/
|
|
670
|
+
async retryConflictOperation(error) {
|
|
671
|
+
try {
|
|
672
|
+
this.logger.info('Retrying conflict operation with modifications');
|
|
673
|
+
// For conflicts, try to modify the operation slightly
|
|
674
|
+
const modifiedOperation = await this.modifyOperationForConflictResolution(error);
|
|
675
|
+
return await this.executeModifiedOperation(error, modifiedOperation);
|
|
676
|
+
}
|
|
677
|
+
catch (conflictError) {
|
|
678
|
+
return {
|
|
679
|
+
success: false,
|
|
680
|
+
error: conflictError instanceof Error ? conflictError.message : String(conflictError)
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Retry resource operations
|
|
686
|
+
*/
|
|
687
|
+
async retryResourceOperation(error) {
|
|
688
|
+
try {
|
|
689
|
+
this.logger.info('Retrying resource operation');
|
|
690
|
+
// For resource not found, try to create the resource first
|
|
691
|
+
const resourceCreated = await this.ensureResourceExists(error);
|
|
692
|
+
if (resourceCreated.success) {
|
|
693
|
+
return await this.executeOriginalOperation(error);
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
success: false,
|
|
697
|
+
error: `Failed to create required resource: ${resourceCreated.error}`
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch (resourceError) {
|
|
701
|
+
return {
|
|
702
|
+
success: false,
|
|
703
|
+
error: resourceError instanceof Error ? resourceError.message : String(resourceError)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Generic retry operation
|
|
709
|
+
*/
|
|
710
|
+
async retryGenericOperation(error) {
|
|
711
|
+
try {
|
|
712
|
+
this.logger.info('Performing generic retry operation');
|
|
713
|
+
// Generic retry - just execute the original operation again
|
|
714
|
+
return await this.executeOriginalOperation(error);
|
|
715
|
+
}
|
|
716
|
+
catch (genericError) {
|
|
717
|
+
return {
|
|
718
|
+
success: false,
|
|
719
|
+
error: genericError instanceof Error ? genericError.message : String(genericError)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Execute the original operation that failed
|
|
725
|
+
*/
|
|
726
|
+
async executeOriginalOperation(error) {
|
|
727
|
+
try {
|
|
728
|
+
// This would reconstruct and execute the original operation
|
|
729
|
+
// For now, we'll simulate the operation based on context
|
|
730
|
+
const operation = error.context.operation;
|
|
731
|
+
const operationType = this.extractOperationType(operation);
|
|
732
|
+
switch (operationType) {
|
|
733
|
+
case 'deploy':
|
|
734
|
+
return await this.retryDeployOperation(error);
|
|
735
|
+
case 'search':
|
|
736
|
+
return await this.retrySearchOperation(error);
|
|
737
|
+
case 'create':
|
|
738
|
+
return await this.retryCreateOperation(error);
|
|
739
|
+
case 'update':
|
|
740
|
+
return await this.retryUpdateOperation(error);
|
|
741
|
+
default:
|
|
742
|
+
// Generic HTTP request retry
|
|
743
|
+
return await this.retryHTTPRequest(error);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch (executeError) {
|
|
747
|
+
return {
|
|
748
|
+
success: false,
|
|
749
|
+
error: executeError instanceof Error ? executeError.message : String(executeError)
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// Helper methods for specific operation types
|
|
754
|
+
async retryDeployOperation(error) {
|
|
755
|
+
try {
|
|
756
|
+
this.logger.info('Retrying deployment operation', {
|
|
757
|
+
component_type: error.context.component_type,
|
|
758
|
+
component_id: error.context.component_id
|
|
759
|
+
});
|
|
760
|
+
// Extract deployment parameters from context
|
|
761
|
+
const deploymentData = error.context.user_input;
|
|
762
|
+
if (!deploymentData) {
|
|
763
|
+
return { success: false, error: 'No deployment data available for retry' };
|
|
764
|
+
}
|
|
765
|
+
// Retry based on component type
|
|
766
|
+
switch (error.context.component_type) {
|
|
767
|
+
case 'widget':
|
|
768
|
+
return await this.retryWidgetDeployment(deploymentData, error);
|
|
769
|
+
case 'flow':
|
|
770
|
+
return await this.retryFlowDeployment(deploymentData, error);
|
|
771
|
+
case 'application':
|
|
772
|
+
return await this.retryApplicationDeployment(deploymentData, error);
|
|
773
|
+
case 'script':
|
|
774
|
+
return await this.retryScriptDeployment(deploymentData, error);
|
|
775
|
+
default:
|
|
776
|
+
return await this.retryGenericDeployment(deploymentData, error);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch (deployError) {
|
|
780
|
+
return {
|
|
781
|
+
success: false,
|
|
782
|
+
error: deployError instanceof Error ? deployError.message : String(deployError)
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async retrySearchOperation(error) {
|
|
787
|
+
try {
|
|
788
|
+
this.logger.info('Retrying search operation', { operation: error.context.operation });
|
|
789
|
+
const searchParams = error.context.user_input;
|
|
790
|
+
if (!searchParams) {
|
|
791
|
+
return { success: false, error: 'No search parameters available for retry' };
|
|
792
|
+
}
|
|
793
|
+
// Modify search parameters if this is a retry to avoid same failure
|
|
794
|
+
const modifiedParams = {
|
|
795
|
+
...searchParams,
|
|
796
|
+
sysparm_limit: Math.min(searchParams.sysparm_limit || 100, 50), // Reduce limit
|
|
797
|
+
sysparm_offset: searchParams.sysparm_offset || 0,
|
|
798
|
+
retry_attempt: (error.retry_count || 0) + 1
|
|
799
|
+
};
|
|
800
|
+
// Perform the search request
|
|
801
|
+
const result = await this.client.makeRequest({
|
|
802
|
+
method: 'GET',
|
|
803
|
+
endpoint: searchParams.endpoint || '/api/now/table/sys_metadata',
|
|
804
|
+
params: modifiedParams
|
|
805
|
+
});
|
|
806
|
+
if (result.status === 200) {
|
|
807
|
+
return { success: true, data: result.data };
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
return { success: false, error: `Search failed with status: ${result.status}` };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch (searchError) {
|
|
814
|
+
return {
|
|
815
|
+
success: false,
|
|
816
|
+
error: searchError instanceof Error ? searchError.message : String(searchError)
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async retryCreateOperation(error) {
|
|
821
|
+
try {
|
|
822
|
+
this.logger.info('Retrying create operation', {
|
|
823
|
+
table: error.context.component_type,
|
|
824
|
+
retry_count: error.retry_count
|
|
825
|
+
});
|
|
826
|
+
const createData = error.context.user_input;
|
|
827
|
+
if (!createData || !createData.table) {
|
|
828
|
+
return { success: false, error: 'No create data or table specified for retry' };
|
|
829
|
+
}
|
|
830
|
+
// Modify data for conflict resolution if needed
|
|
831
|
+
let retryData = { ...createData.data };
|
|
832
|
+
// If this is a conflict retry, modify unique fields
|
|
833
|
+
if (error.type === ErrorType.CONFLICT_ERROR && error.retry_count > 0) {
|
|
834
|
+
const timestamp = Date.now();
|
|
835
|
+
if (retryData.name) {
|
|
836
|
+
retryData.name = `${retryData.name}_retry_${timestamp}`;
|
|
837
|
+
}
|
|
838
|
+
if (retryData.sys_name) {
|
|
839
|
+
retryData.sys_name = `${retryData.sys_name}_retry_${timestamp}`;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Perform the create request
|
|
843
|
+
const result = await this.client.createRecord(createData.table, retryData);
|
|
844
|
+
if (result && result.sys_id) {
|
|
845
|
+
this.logger.info('Create operation retry successful', { sys_id: result.sys_id });
|
|
846
|
+
return { success: true, data: result };
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
return { success: false, error: 'Create operation failed - no sys_id returned' };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch (createError) {
|
|
853
|
+
return {
|
|
854
|
+
success: false,
|
|
855
|
+
error: createError instanceof Error ? createError.message : String(createError)
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async retryUpdateOperation(error) {
|
|
860
|
+
try {
|
|
861
|
+
this.logger.info('Retrying update operation', {
|
|
862
|
+
table: error.context.component_type,
|
|
863
|
+
record_id: error.context.component_id
|
|
864
|
+
});
|
|
865
|
+
const updateData = error.context.user_input;
|
|
866
|
+
if (!updateData || !updateData.table || !updateData.record_id) {
|
|
867
|
+
return { success: false, error: 'No update data, table, or record ID available for retry' };
|
|
868
|
+
}
|
|
869
|
+
// First, verify the record still exists
|
|
870
|
+
try {
|
|
871
|
+
const existingRecord = await this.client.getRecord(updateData.table, updateData.record_id);
|
|
872
|
+
if (!existingRecord) {
|
|
873
|
+
return { success: false, error: 'Record no longer exists for update' };
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
catch (getError) {
|
|
877
|
+
return { success: false, error: 'Cannot verify record existence for update retry' };
|
|
878
|
+
}
|
|
879
|
+
// Perform the update request
|
|
880
|
+
const result = await this.client.updateRecord(updateData.table, updateData.record_id, updateData.data);
|
|
881
|
+
if (result) {
|
|
882
|
+
this.logger.info('Update operation retry successful', { sys_id: updateData.record_id });
|
|
883
|
+
return { success: true, data: result };
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
return { success: false, error: 'Update operation failed' };
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
catch (updateError) {
|
|
890
|
+
return {
|
|
891
|
+
success: false,
|
|
892
|
+
error: updateError instanceof Error ? updateError.message : String(updateError)
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async retryHTTPRequest(error) {
|
|
897
|
+
try {
|
|
898
|
+
this.logger.info('Retrying HTTP request', { operation: error.context.operation });
|
|
899
|
+
const requestData = error.context.user_input;
|
|
900
|
+
if (!requestData) {
|
|
901
|
+
return { success: false, error: 'No request data available for HTTP retry' };
|
|
902
|
+
}
|
|
903
|
+
// Construct request with retry-safe parameters
|
|
904
|
+
const requestConfig = {
|
|
905
|
+
method: requestData.method || 'GET',
|
|
906
|
+
endpoint: requestData.endpoint || requestData.url,
|
|
907
|
+
params: requestData.params,
|
|
908
|
+
data: requestData.data,
|
|
909
|
+
timeout: Math.min((requestData.timeout || 30000) * 1.5, 60000), // Increase timeout
|
|
910
|
+
retry_attempt: (error.retry_count || 0) + 1
|
|
911
|
+
};
|
|
912
|
+
// Add retry headers
|
|
913
|
+
const headers = {
|
|
914
|
+
...requestData.headers,
|
|
915
|
+
'X-Retry-Attempt': String(requestConfig.retry_attempt),
|
|
916
|
+
'X-Request-ID': `retry_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
917
|
+
};
|
|
918
|
+
const result = await this.client.makeRequest({
|
|
919
|
+
...requestConfig,
|
|
920
|
+
headers
|
|
921
|
+
});
|
|
922
|
+
if (result.status >= 200 && result.status < 300) {
|
|
923
|
+
return { success: true, data: result.data };
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
return {
|
|
927
|
+
success: false,
|
|
928
|
+
error: `HTTP request failed with status: ${result.status} - ${result.statusText}`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch (httpError) {
|
|
933
|
+
return {
|
|
934
|
+
success: false,
|
|
935
|
+
error: httpError instanceof Error ? httpError.message : String(httpError)
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
// Additional helper methods
|
|
940
|
+
async retryWithNewAuthentication(error) {
|
|
941
|
+
// Retry with refreshed authentication
|
|
942
|
+
return await this.executeOriginalOperation(error);
|
|
943
|
+
}
|
|
944
|
+
async performServiceHealthCheck() {
|
|
945
|
+
try {
|
|
946
|
+
// Basic health check - try to access ServiceNow instance
|
|
947
|
+
const response = await this.client.makeRequest({
|
|
948
|
+
method: 'GET',
|
|
949
|
+
endpoint: '/api/now/v2/table/sys_user',
|
|
950
|
+
params: { sysparm_limit: 1 }
|
|
951
|
+
});
|
|
952
|
+
return { healthy: response.status === 200 };
|
|
953
|
+
}
|
|
954
|
+
catch (healthError) {
|
|
955
|
+
return {
|
|
956
|
+
healthy: false,
|
|
957
|
+
reason: healthError instanceof Error ? healthError.message : 'Unknown health check error'
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async modifyOperationForConflictResolution(error) {
|
|
962
|
+
// Modify operation to resolve conflicts (e.g., add timestamp to name)
|
|
963
|
+
const timestamp = Date.now();
|
|
964
|
+
return {
|
|
965
|
+
...error.context.system_state,
|
|
966
|
+
name: `${error.context.system_state?.name || 'item'}_${timestamp}`,
|
|
967
|
+
modified_for_conflict: true
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async executeModifiedOperation(error, modifiedData) {
|
|
971
|
+
try {
|
|
972
|
+
this.logger.info('Executing modified operation for conflict resolution', {
|
|
973
|
+
original_operation: error.context.operation,
|
|
974
|
+
modifications: modifiedData.modified_for_conflict ? 'conflict_resolution' : 'standard'
|
|
975
|
+
});
|
|
976
|
+
// Execute the operation with modified data based on operation type
|
|
977
|
+
const operationType = this.extractOperationType(error.context.operation);
|
|
978
|
+
switch (operationType) {
|
|
979
|
+
case 'create':
|
|
980
|
+
const table = error.context.component_type || 'sys_metadata';
|
|
981
|
+
const result = await this.client.createRecord(table, modifiedData);
|
|
982
|
+
return { success: true, data: result };
|
|
983
|
+
case 'update':
|
|
984
|
+
if (!error.context.component_id) {
|
|
985
|
+
return { success: false, error: 'No component ID for update operation' };
|
|
986
|
+
}
|
|
987
|
+
const updateResult = await this.client.updateRecord(error.context.component_type || 'sys_metadata', error.context.component_id, modifiedData);
|
|
988
|
+
return { success: true, data: updateResult };
|
|
989
|
+
case 'deploy':
|
|
990
|
+
// For deployment operations, use the deployment data structure
|
|
991
|
+
return await this.retryGenericDeployment(modifiedData, error);
|
|
992
|
+
default:
|
|
993
|
+
return { success: false, error: 'Unsupported operation type for modification' };
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
catch (executeError) {
|
|
997
|
+
return {
|
|
998
|
+
success: false,
|
|
999
|
+
error: executeError instanceof Error ? executeError.message : String(executeError)
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async ensureResourceExists(error) {
|
|
1004
|
+
try {
|
|
1005
|
+
this.logger.info('Attempting to create missing resource', {
|
|
1006
|
+
resource_type: error.context.component_type,
|
|
1007
|
+
resource_id: error.context.component_id
|
|
1008
|
+
});
|
|
1009
|
+
// Extract resource information from context
|
|
1010
|
+
const resourceData = error.context.user_input;
|
|
1011
|
+
if (!resourceData) {
|
|
1012
|
+
return { success: false, error: 'No resource data available for creation' };
|
|
1013
|
+
}
|
|
1014
|
+
// Determine resource type and create accordingly
|
|
1015
|
+
switch (error.context.component_type) {
|
|
1016
|
+
case 'table':
|
|
1017
|
+
return await this.createMissingTable(resourceData);
|
|
1018
|
+
case 'field':
|
|
1019
|
+
return await this.createMissingField(resourceData);
|
|
1020
|
+
case 'user':
|
|
1021
|
+
return await this.createMissingUser(resourceData);
|
|
1022
|
+
case 'group':
|
|
1023
|
+
return await this.createMissingGroup(resourceData);
|
|
1024
|
+
default:
|
|
1025
|
+
// Generic resource creation
|
|
1026
|
+
return await this.createGenericResource(resourceData);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
catch (resourceError) {
|
|
1030
|
+
return {
|
|
1031
|
+
success: false,
|
|
1032
|
+
error: resourceError instanceof Error ? resourceError.message : String(resourceError)
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
async delay(ms) {
|
|
1037
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
456
1038
|
}
|
|
457
1039
|
/**
|
|
458
1040
|
* Create missing resource
|
|
@@ -589,5 +1171,317 @@ class FlowErrorHandler {
|
|
|
589
1171
|
return Array.from(this.transactions.values())
|
|
590
1172
|
.filter(t => t.status === 'active');
|
|
591
1173
|
}
|
|
1174
|
+
// =====================================
|
|
1175
|
+
// DEPLOYMENT RETRY METHODS
|
|
1176
|
+
// =====================================
|
|
1177
|
+
/**
|
|
1178
|
+
* Retry widget deployment
|
|
1179
|
+
*/
|
|
1180
|
+
async retryWidgetDeployment(deploymentData, error) {
|
|
1181
|
+
try {
|
|
1182
|
+
this.logger.info('Retrying widget deployment', { widget_name: deploymentData.name });
|
|
1183
|
+
// Construct widget data with retry modifications
|
|
1184
|
+
const widgetData = {
|
|
1185
|
+
name: deploymentData.name,
|
|
1186
|
+
title: deploymentData.title,
|
|
1187
|
+
template: deploymentData.template,
|
|
1188
|
+
server_script: deploymentData.server_script,
|
|
1189
|
+
client_script: deploymentData.client_script,
|
|
1190
|
+
css: deploymentData.css,
|
|
1191
|
+
category: deploymentData.category || 'custom',
|
|
1192
|
+
roles: deploymentData.roles || '',
|
|
1193
|
+
public: deploymentData.public !== false
|
|
1194
|
+
};
|
|
1195
|
+
// If this is a conflict retry, modify the name
|
|
1196
|
+
if (error.type === ErrorType.CONFLICT_ERROR && (error.retry_count || 0) > 0) {
|
|
1197
|
+
widgetData.name = `${widgetData.name}_retry_${Date.now()}`;
|
|
1198
|
+
}
|
|
1199
|
+
const result = await this.client.createRecord('sp_widget', widgetData);
|
|
1200
|
+
return { success: true, data: result };
|
|
1201
|
+
}
|
|
1202
|
+
catch (widgetError) {
|
|
1203
|
+
return {
|
|
1204
|
+
success: false,
|
|
1205
|
+
error: widgetError instanceof Error ? widgetError.message : String(widgetError)
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Retry flow deployment
|
|
1211
|
+
*/
|
|
1212
|
+
async retryFlowDeployment(deploymentData, error) {
|
|
1213
|
+
try {
|
|
1214
|
+
this.logger.info('Retrying flow deployment', { flow_name: deploymentData.name });
|
|
1215
|
+
const flowData = {
|
|
1216
|
+
name: deploymentData.name,
|
|
1217
|
+
description: deploymentData.description,
|
|
1218
|
+
active: deploymentData.active !== false,
|
|
1219
|
+
flow_designer: deploymentData.flow_definition || '{}',
|
|
1220
|
+
trigger_conditions: deploymentData.trigger_conditions,
|
|
1221
|
+
table: deploymentData.table
|
|
1222
|
+
};
|
|
1223
|
+
// Handle conflict retry
|
|
1224
|
+
if (error.type === ErrorType.CONFLICT_ERROR && (error.retry_count || 0) > 0) {
|
|
1225
|
+
flowData.name = `${flowData.name}_retry_${Date.now()}`;
|
|
1226
|
+
}
|
|
1227
|
+
const result = await this.client.createRecord('sys_hub_flow', flowData);
|
|
1228
|
+
return { success: true, data: result };
|
|
1229
|
+
}
|
|
1230
|
+
catch (flowError) {
|
|
1231
|
+
return {
|
|
1232
|
+
success: false,
|
|
1233
|
+
error: flowError instanceof Error ? flowError.message : String(flowError)
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Retry application deployment
|
|
1239
|
+
*/
|
|
1240
|
+
async retryApplicationDeployment(deploymentData, error) {
|
|
1241
|
+
try {
|
|
1242
|
+
this.logger.info('Retrying application deployment', { app_name: deploymentData.name });
|
|
1243
|
+
const appData = {
|
|
1244
|
+
name: deploymentData.name,
|
|
1245
|
+
short_description: deploymentData.short_description,
|
|
1246
|
+
description: deploymentData.description,
|
|
1247
|
+
version: deploymentData.version || '1.0.0',
|
|
1248
|
+
vendor: deploymentData.vendor || 'Custom',
|
|
1249
|
+
vendor_prefix: deploymentData.vendor_prefix,
|
|
1250
|
+
active: deploymentData.active !== false
|
|
1251
|
+
};
|
|
1252
|
+
// Handle conflict retry
|
|
1253
|
+
if (error.type === ErrorType.CONFLICT_ERROR && (error.retry_count || 0) > 0) {
|
|
1254
|
+
appData.name = `${appData.name}_retry_${Date.now()}`;
|
|
1255
|
+
}
|
|
1256
|
+
const result = await this.client.createRecord('sys_app', appData);
|
|
1257
|
+
return { success: true, data: result };
|
|
1258
|
+
}
|
|
1259
|
+
catch (appError) {
|
|
1260
|
+
return {
|
|
1261
|
+
success: false,
|
|
1262
|
+
error: appError instanceof Error ? appError.message : String(appError)
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Retry script deployment
|
|
1268
|
+
*/
|
|
1269
|
+
async retryScriptDeployment(deploymentData, error) {
|
|
1270
|
+
try {
|
|
1271
|
+
this.logger.info('Retrying script deployment', { script_name: deploymentData.name });
|
|
1272
|
+
const scriptData = {
|
|
1273
|
+
name: deploymentData.name,
|
|
1274
|
+
script: deploymentData.script,
|
|
1275
|
+
description: deploymentData.description,
|
|
1276
|
+
api_name: deploymentData.api_name,
|
|
1277
|
+
client_callable: deploymentData.client_callable || false,
|
|
1278
|
+
active: deploymentData.active !== false
|
|
1279
|
+
};
|
|
1280
|
+
// Handle conflict retry
|
|
1281
|
+
if (error.type === ErrorType.CONFLICT_ERROR && (error.retry_count || 0) > 0) {
|
|
1282
|
+
scriptData.name = `${scriptData.name}_retry_${Date.now()}`;
|
|
1283
|
+
}
|
|
1284
|
+
const result = await this.client.createRecord('sys_script_include', scriptData);
|
|
1285
|
+
return { success: true, data: result };
|
|
1286
|
+
}
|
|
1287
|
+
catch (scriptError) {
|
|
1288
|
+
return {
|
|
1289
|
+
success: false,
|
|
1290
|
+
error: scriptError instanceof Error ? scriptError.message : String(scriptError)
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Generic deployment retry
|
|
1296
|
+
*/
|
|
1297
|
+
async retryGenericDeployment(deploymentData, error) {
|
|
1298
|
+
try {
|
|
1299
|
+
this.logger.info('Retrying generic deployment', {
|
|
1300
|
+
component_type: error.context.component_type,
|
|
1301
|
+
data_keys: Object.keys(deploymentData)
|
|
1302
|
+
});
|
|
1303
|
+
// Determine table from component type or data
|
|
1304
|
+
const table = deploymentData.table ||
|
|
1305
|
+
this.getTableFromComponentType(error.context.component_type) ||
|
|
1306
|
+
'sys_metadata';
|
|
1307
|
+
const result = await this.client.createRecord(table, deploymentData);
|
|
1308
|
+
return { success: true, data: result };
|
|
1309
|
+
}
|
|
1310
|
+
catch (genericError) {
|
|
1311
|
+
return {
|
|
1312
|
+
success: false,
|
|
1313
|
+
error: genericError instanceof Error ? genericError.message : String(genericError)
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
// =====================================
|
|
1318
|
+
// RESOURCE CREATION METHODS
|
|
1319
|
+
// =====================================
|
|
1320
|
+
/**
|
|
1321
|
+
* Create missing table
|
|
1322
|
+
*/
|
|
1323
|
+
async createMissingTable(resourceData) {
|
|
1324
|
+
try {
|
|
1325
|
+
const tableData = {
|
|
1326
|
+
name: resourceData.table_name || resourceData.name,
|
|
1327
|
+
label: resourceData.label || resourceData.table_name,
|
|
1328
|
+
super_class: resourceData.super_class || 'sys_metadata',
|
|
1329
|
+
is_extendable: resourceData.is_extendable !== false
|
|
1330
|
+
};
|
|
1331
|
+
await this.client.createRecord('sys_db_object', tableData);
|
|
1332
|
+
return { success: true };
|
|
1333
|
+
}
|
|
1334
|
+
catch (error) {
|
|
1335
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Create missing field
|
|
1340
|
+
*/
|
|
1341
|
+
async createMissingField(resourceData) {
|
|
1342
|
+
try {
|
|
1343
|
+
const fieldData = {
|
|
1344
|
+
table: resourceData.table,
|
|
1345
|
+
column_name: resourceData.field_name,
|
|
1346
|
+
column_label: resourceData.label || resourceData.field_name,
|
|
1347
|
+
internal_type: resourceData.type || 'string',
|
|
1348
|
+
max_length: resourceData.max_length || 40
|
|
1349
|
+
};
|
|
1350
|
+
await this.client.createRecord('sys_dictionary', fieldData);
|
|
1351
|
+
return { success: true };
|
|
1352
|
+
}
|
|
1353
|
+
catch (error) {
|
|
1354
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Create missing user
|
|
1359
|
+
*/
|
|
1360
|
+
async createMissingUser(resourceData) {
|
|
1361
|
+
try {
|
|
1362
|
+
const userData = {
|
|
1363
|
+
user_name: resourceData.user_name,
|
|
1364
|
+
first_name: resourceData.first_name || 'Unknown',
|
|
1365
|
+
last_name: resourceData.last_name || 'User',
|
|
1366
|
+
email: resourceData.email,
|
|
1367
|
+
active: resourceData.active !== false
|
|
1368
|
+
};
|
|
1369
|
+
await this.client.createRecord('sys_user', userData);
|
|
1370
|
+
return { success: true };
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Create missing group
|
|
1378
|
+
*/
|
|
1379
|
+
async createMissingGroup(resourceData) {
|
|
1380
|
+
try {
|
|
1381
|
+
const groupData = {
|
|
1382
|
+
name: resourceData.group_name || resourceData.name,
|
|
1383
|
+
description: resourceData.description || `Group ${resourceData.name}`,
|
|
1384
|
+
active: resourceData.active !== false
|
|
1385
|
+
};
|
|
1386
|
+
await this.client.createRecord('sys_user_group', groupData);
|
|
1387
|
+
return { success: true };
|
|
1388
|
+
}
|
|
1389
|
+
catch (error) {
|
|
1390
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Create generic resource
|
|
1395
|
+
*/
|
|
1396
|
+
async createGenericResource(resourceData) {
|
|
1397
|
+
try {
|
|
1398
|
+
const table = resourceData.table || 'sys_metadata';
|
|
1399
|
+
await this.client.createRecord(table, resourceData);
|
|
1400
|
+
return { success: true };
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
// =====================================
|
|
1407
|
+
// UTILITY METHODS
|
|
1408
|
+
// =====================================
|
|
1409
|
+
/**
|
|
1410
|
+
* Get ServiceNow table name from component type
|
|
1411
|
+
*/
|
|
1412
|
+
getTableFromComponentType(componentType) {
|
|
1413
|
+
if (!componentType)
|
|
1414
|
+
return null;
|
|
1415
|
+
const tableMap = {
|
|
1416
|
+
'widget': 'sp_widget',
|
|
1417
|
+
'flow': 'sys_hub_flow',
|
|
1418
|
+
'application': 'sys_app',
|
|
1419
|
+
'script': 'sys_script_include',
|
|
1420
|
+
'business_rule': 'sys_script',
|
|
1421
|
+
'client_script': 'sys_script_client',
|
|
1422
|
+
'ui_policy': 'sys_ui_policy',
|
|
1423
|
+
'ui_action': 'sys_ui_action',
|
|
1424
|
+
'table': 'sys_db_object',
|
|
1425
|
+
'field': 'sys_dictionary',
|
|
1426
|
+
'user': 'sys_user',
|
|
1427
|
+
'group': 'sys_user_group'
|
|
1428
|
+
};
|
|
1429
|
+
return tableMap[componentType.toLowerCase()] || null;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Enhanced operation type extraction
|
|
1433
|
+
*/
|
|
1434
|
+
extractOperationType(operation) {
|
|
1435
|
+
const op = operation.toLowerCase();
|
|
1436
|
+
// More specific operation detection
|
|
1437
|
+
if (op.includes('deploy') || op.includes('create_') || op.includes('snow_deploy'))
|
|
1438
|
+
return 'deploy';
|
|
1439
|
+
if (op.includes('search') || op.includes('find') || op.includes('query') || op.includes('discover'))
|
|
1440
|
+
return 'search';
|
|
1441
|
+
if (op.includes('create') || op.includes('insert') || op.includes('add'))
|
|
1442
|
+
return 'create';
|
|
1443
|
+
if (op.includes('update') || op.includes('modify') || op.includes('edit') || op.includes('patch'))
|
|
1444
|
+
return 'update';
|
|
1445
|
+
if (op.includes('delete') || op.includes('remove'))
|
|
1446
|
+
return 'delete';
|
|
1447
|
+
if (op.includes('get') || op.includes('fetch') || op.includes('retrieve'))
|
|
1448
|
+
return 'read';
|
|
1449
|
+
if (op.includes('authenticate') || op.includes('login') || op.includes('auth'))
|
|
1450
|
+
return 'auth';
|
|
1451
|
+
if (op.includes('http') || op.includes('request') || op.includes('api'))
|
|
1452
|
+
return 'http';
|
|
1453
|
+
return 'generic';
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Check if error type is retryable
|
|
1457
|
+
*/
|
|
1458
|
+
isRetryableError(errorType) {
|
|
1459
|
+
const retryableErrors = [
|
|
1460
|
+
ErrorType.NETWORK_ERROR,
|
|
1461
|
+
ErrorType.TIMEOUT_ERROR,
|
|
1462
|
+
ErrorType.RATE_LIMIT_ERROR,
|
|
1463
|
+
ErrorType.SERVICE_UNAVAILABLE,
|
|
1464
|
+
ErrorType.API_ERROR,
|
|
1465
|
+
ErrorType.CONFLICT_ERROR
|
|
1466
|
+
];
|
|
1467
|
+
return retryableErrors.includes(errorType);
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Get circuit breaker status for operation
|
|
1471
|
+
*/
|
|
1472
|
+
getCircuitBreakerStatus(operation) {
|
|
1473
|
+
// Simple circuit breaker implementation
|
|
1474
|
+
const recentErrors = this.errorHistory
|
|
1475
|
+
.filter(error => error.context.operation === operation &&
|
|
1476
|
+
error.context.timestamp.getTime() > Date.now() - 300000 // Last 5 minutes
|
|
1477
|
+
);
|
|
1478
|
+
const failureCount = recentErrors.length;
|
|
1479
|
+
const isOpen = failureCount >= 5; // Open circuit after 5 failures
|
|
1480
|
+
return {
|
|
1481
|
+
isOpen,
|
|
1482
|
+
failureCount,
|
|
1483
|
+
lastFailureTime: recentErrors.length > 0 ? recentErrors[recentErrors.length - 1].context.timestamp : undefined
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
592
1486
|
}
|
|
593
1487
|
exports.FlowErrorHandler = FlowErrorHandler;
|