snow-flow 1.3.21 → 1.3.23

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.
@@ -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
  */
@@ -352,17 +352,36 @@ class ServiceNowFlowComposerMCP {
352
352
  console.log('✅ XML generated:', xmlResult.filePath);
353
353
  // Auto-deploy with fallback strategies
354
354
  const deployResult = await this.deployWithFallback(xmlResult.filePath, flowDefinition);
355
- // Verify deployment was successful
356
- const flowExists = await this.verifyFlowInServiceNow(parsedIntent.flowName);
357
- if (!flowExists) {
358
- throw new Error('Flow deployment claimed success but flow not found in ServiceNow');
355
+ // Comprehensive deployment verification
356
+ const verification = await this.verifyFlowInServiceNow(parsedIntent.flowName);
357
+ if (!verification.verified) {
358
+ // Deployment claimed success but flow not found - this is the critical bug!
359
+ const errorMsg = `Flow deployment reported success but verification failed: ${verification.reason}`;
360
+ // Log detailed verification results for debugging
361
+ this.logger.error('CRITICAL: False positive deployment detected', {
362
+ flowName: parsedIntent.flowName,
363
+ verificationAttempts: verification.attempts,
364
+ partialResults: verification.partial_results,
365
+ searchQuery: verification.search_query,
366
+ error: verification.error
367
+ });
368
+ throw new Error(errorMsg);
359
369
  }
370
+ // Success with comprehensive verification
360
371
  deploymentResult = {
361
372
  success: true,
362
373
  method: deployResult.strategy,
363
374
  xml_file: xmlResult.filePath,
364
375
  message: `✅ Flow deployed via ${deployResult.strategy} and verified in ServiceNow!`,
365
- flow_sys_id: flowExists.sys_id
376
+ flow_sys_id: verification.sys_id,
377
+ flow_url: verification.url,
378
+ verification_score: verification.completeness_score,
379
+ verification_details: {
380
+ has_flow: true,
381
+ has_snapshot: verification.has_snapshot,
382
+ has_trigger: verification.has_trigger,
383
+ attempts_needed: verification.verification_attempt
384
+ }
366
385
  };
367
386
  }
368
387
  catch (xmlError) {
@@ -1987,30 +2006,136 @@ ${categoryFilteredResults.length === 0 ? `🔍 **No templates found matching you
1987
2006
  return mapping[activityType.toLowerCase()] || 'script';
1988
2007
  }
1989
2008
  /**
1990
- * Verify flow exists in ServiceNow after deployment
2009
+ * Comprehensive flow verification with retry logic
1991
2010
  */
1992
2011
  async verifyFlowInServiceNow(flowName) {
1993
- try {
1994
- const ServiceNowClient = (await Promise.resolve().then(() => __importStar(require('../utils/servicenow-client.js')))).ServiceNowClient;
1995
- const client = new ServiceNowClient();
1996
- // Check if flow exists in sys_hub_flow
1997
- const flowCheck = await client.makeRequest({
1998
- method: 'GET',
1999
- url: '/api/now/table/sys_hub_flow',
2000
- params: {
2001
- sysparm_query: `name=${flowName}`,
2002
- sysparm_limit: 1
2012
+ const maxRetries = 5;
2013
+ const baseDelay = 2000; // Start with 2 seconds
2014
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
2015
+ try {
2016
+ // Progressive delay - ServiceNow needs time to process
2017
+ if (attempt > 1) {
2018
+ const delay = baseDelay * attempt; // 2s, 4s, 6s, 8s, 10s
2019
+ this.logger.info(`Waiting ${delay}ms before verification attempt ${attempt}/${maxRetries}`);
2020
+ await this.sleep(delay);
2021
+ }
2022
+ const ServiceNowClient = (await Promise.resolve().then(() => __importStar(require('../utils/servicenow-client.js')))).ServiceNowClient;
2023
+ const client = new ServiceNowClient();
2024
+ // Multi-table verification for comprehensive check
2025
+ const verificationPromises = [
2026
+ // Check main flow table
2027
+ client.makeRequest({
2028
+ method: 'GET',
2029
+ url: '/api/now/table/sys_hub_flow',
2030
+ params: {
2031
+ sysparm_query: `name=${flowName}^ORsys_name=${flowName}`,
2032
+ sysparm_fields: 'sys_id,name,sys_name,active,table,description',
2033
+ sysparm_limit: 5
2034
+ }
2035
+ }),
2036
+ // Check flow snapshots
2037
+ client.makeRequest({
2038
+ method: 'GET',
2039
+ url: '/api/now/table/sys_hub_flow_snapshot',
2040
+ params: {
2041
+ sysparm_query: `flow.name=${flowName}^ORflow.sys_name=${flowName}`,
2042
+ sysparm_fields: 'sys_id,flow,name,active',
2043
+ sysparm_limit: 5
2044
+ }
2045
+ }),
2046
+ // Check trigger instances
2047
+ client.makeRequest({
2048
+ method: 'GET',
2049
+ url: '/api/now/table/sys_hub_trigger_instance',
2050
+ params: {
2051
+ sysparm_query: `flow.name=${flowName}^ORflow.sys_name=${flowName}`,
2052
+ sysparm_fields: 'sys_id,flow,trigger_type',
2053
+ sysparm_limit: 5
2054
+ }
2055
+ })
2056
+ ];
2057
+ const [flowCheck, snapshotCheck, triggerCheck] = await Promise.allSettled(verificationPromises);
2058
+ // Analyze results
2059
+ const flowExists = flowCheck.status === 'fulfilled' &&
2060
+ flowCheck.value.result &&
2061
+ flowCheck.value.result.length > 0;
2062
+ const snapshotExists = snapshotCheck.status === 'fulfilled' &&
2063
+ snapshotCheck.value.result &&
2064
+ snapshotCheck.value.result.length > 0;
2065
+ const triggerExists = triggerCheck.status === 'fulfilled' &&
2066
+ triggerCheck.value.result &&
2067
+ triggerCheck.value.result.length > 0;
2068
+ // Comprehensive verification result
2069
+ if (flowExists) {
2070
+ const flowData = flowCheck.value.result[0];
2071
+ return {
2072
+ verified: true,
2073
+ sys_id: flowData.sys_id,
2074
+ name: flowData.name || flowData.sys_name,
2075
+ active: flowData.active,
2076
+ table: flowData.table,
2077
+ has_snapshot: snapshotExists,
2078
+ has_trigger: triggerExists,
2079
+ verification_attempt: attempt,
2080
+ verification_method: 'multi_table_comprehensive',
2081
+ completeness_score: (flowExists ? 1 : 0) + (snapshotExists ? 1 : 0) + (triggerExists ? 1 : 0),
2082
+ url: `https://${await this.getInstanceUrl()}/flow-designer/flow/${flowData.sys_id}`
2083
+ };
2084
+ }
2085
+ // Log partial results for debugging
2086
+ this.logger.warn(`Verification attempt ${attempt}: Flow=${flowExists}, Snapshot=${snapshotExists}, Trigger=${triggerExists}`);
2087
+ // If this is the last attempt, return detailed failure info
2088
+ if (attempt === maxRetries) {
2089
+ return {
2090
+ verified: false,
2091
+ reason: 'Flow not found after comprehensive verification',
2092
+ attempts: maxRetries,
2093
+ partial_results: {
2094
+ flow_found: flowExists,
2095
+ snapshot_found: snapshotExists,
2096
+ trigger_found: triggerExists
2097
+ },
2098
+ search_query: `name=${flowName}`,
2099
+ recommendation: 'Check ServiceNow Flow Designer manually or verify deployment was successful'
2100
+ };
2101
+ }
2102
+ }
2103
+ catch (error) {
2104
+ this.logger.warn(`Verification attempt ${attempt} failed:`, error);
2105
+ // If this is the last attempt, include error details
2106
+ if (attempt === maxRetries) {
2107
+ return {
2108
+ verified: false,
2109
+ reason: 'Verification failed due to error',
2110
+ error: error instanceof Error ? error.message : String(error),
2111
+ attempts: maxRetries
2112
+ };
2003
2113
  }
2004
- });
2005
- if (flowCheck.result && flowCheck.result.length > 0) {
2006
- return flowCheck.result[0];
2007
2114
  }
2008
- return null;
2009
2115
  }
2010
- catch (error) {
2011
- this.logger.error('Failed to verify flow:', error);
2012
- return null;
2116
+ return {
2117
+ verified: false,
2118
+ reason: 'Max verification attempts exceeded',
2119
+ attempts: maxRetries
2120
+ };
2121
+ }
2122
+ /**
2123
+ * Helper to get instance URL for flow links
2124
+ */
2125
+ async getInstanceUrl() {
2126
+ try {
2127
+ const credentials = await this.oauth.loadCredentials();
2128
+ return credentials?.instance || 'your-instance.service-now.com';
2013
2129
  }
2130
+ catch {
2131
+ return 'your-instance.service-now.com';
2132
+ }
2133
+ }
2134
+ /**
2135
+ * Sleep utility
2136
+ */
2137
+ sleep(ms) {
2138
+ return new Promise(resolve => setTimeout(resolve, ms));
2014
2139
  }
2015
2140
  /**
2016
2141
  * Generate manual import guide for failed deployments
@@ -43,15 +43,43 @@ class MemoryClient {
43
43
  await this.operations.setContext(this.sessionId, options.key, options.value, this.agentId, options.expires, options.permissions);
44
44
  }
45
45
  /**
46
- * Retrieve data from shared context
46
+ * Retrieve data from shared context with agent isolation
47
47
  */
48
48
  async retrieve(options) {
49
- const context = await this.operations.getContext(this.sessionId, options.key);
49
+ // CRITICAL FIX: Pass agent ID for proper memory isolation
50
+ const context = await this.operations.getContext(this.sessionId, options.key, this.agentId);
50
51
  if (!context) {
51
52
  return options.defaultValue !== undefined ? options.defaultValue : null;
52
53
  }
53
54
  return context.context_value;
54
55
  }
56
+ /**
57
+ * Retrieve data from truly shared context (no agent isolation)
58
+ * Use this when agents need to access shared coordination data
59
+ */
60
+ async retrieveShared(options) {
61
+ // Try shared prefix first
62
+ const sharedKey = `__shared__::${options.key}`;
63
+ let context = await this.operations.getContext(this.sessionId, sharedKey);
64
+ // Fallback to original key for backward compatibility
65
+ if (!context) {
66
+ context = await this.operations.getContext(this.sessionId, options.key);
67
+ }
68
+ if (!context) {
69
+ return options.defaultValue !== undefined ? options.defaultValue : null;
70
+ }
71
+ return context.context_value;
72
+ }
73
+ /**
74
+ * Store data in truly shared context (no agent isolation)
75
+ * Use this when you want all agents to access the same data
76
+ */
77
+ async storeShared(options) {
78
+ // Use a special shared prefix to avoid conflicts with namespaced keys
79
+ const sharedKey = `__shared__::${options.key}`;
80
+ await this.operations.setContext(this.sessionId, sharedKey, options.value, this.agentId, // Still track who created it
81
+ options.expires, options.permissions);
82
+ }
55
83
  /**
56
84
  * Get all context for current session
57
85
  */
@@ -278,23 +278,49 @@ class MemoryOperations {
278
278
  }
279
279
  // ==================== Shared Context Operations ====================
280
280
  /**
281
- * Store shared context
281
+ * Create agent-specific namespaced key for memory isolation
282
+ * This prevents agents from overwriting each other's memory within the same session
283
+ */
284
+ createNamespacedKey(context_key, agent_id) {
285
+ // Use a delimiter that's unlikely to conflict with normal keys
286
+ return `${agent_id}::${context_key}`;
287
+ }
288
+ /**
289
+ * Parse namespaced key to extract original key and agent ID
290
+ */
291
+ parseNamespacedKey(namespaced_key) {
292
+ const parts = namespaced_key.split('::');
293
+ if (parts.length === 2) {
294
+ return { agent_id: parts[0], original_key: parts[1] };
295
+ }
296
+ return { agent_id: null, original_key: namespaced_key };
297
+ }
298
+ /**
299
+ * Store shared context with agent isolation
282
300
  */
283
301
  async setContext(session_id, context_key, context_value, created_by_agent, expires_at, access_permissions) {
284
302
  try {
303
+ // CRITICAL FIX: Create agent-specific namespace for the context key
304
+ // This ensures agents don't overwrite each other's memory
305
+ const namespacedKey = this.createNamespacedKey(context_key, created_by_agent);
285
306
  this.memory.run(`
286
307
  INSERT OR REPLACE INTO shared_context
287
308
  (session_id, context_key, context_value, created_by_agent, expires_at, access_permissions)
288
309
  VALUES (?, ?, ?, ?, ?, ?)
289
310
  `, [
290
311
  session_id,
291
- context_key,
312
+ namespacedKey,
292
313
  typeof context_value === 'string' ? context_value : JSON.stringify(context_value),
293
314
  created_by_agent,
294
315
  expires_at ? expires_at.toISOString() : null,
295
316
  access_permissions ? JSON.stringify(access_permissions) : null
296
317
  ]);
297
- this.logger.debug('Context stored', { session_id, context_key, created_by_agent });
318
+ this.logger.debug('Context stored with agent isolation', {
319
+ session_id,
320
+ original_key: context_key,
321
+ namespaced_key: namespacedKey,
322
+ created_by_agent
323
+ });
298
324
  }
299
325
  catch (error) {
300
326
  this.logger.error('Failed to store context', error);
@@ -302,18 +328,43 @@ class MemoryOperations {
302
328
  }
303
329
  }
304
330
  /**
305
- * Get shared context
331
+ * Get shared context with agent isolation support
306
332
  */
307
- async getContext(session_id, context_key) {
308
- const result = this.memory.get(`
309
- SELECT * FROM shared_context
310
- WHERE session_id = ? AND context_key = ?
311
- AND (expires_at IS NULL OR expires_at > datetime('now'))
312
- `, [session_id, context_key]);
333
+ async getContext(session_id, context_key, requesting_agent) {
334
+ // CRITICAL FIX: Try to get agent-specific namespaced key first
335
+ let result = null;
336
+ if (requesting_agent) {
337
+ const namespacedKey = this.createNamespacedKey(context_key, requesting_agent);
338
+ result = this.memory.get(`
339
+ SELECT * FROM shared_context
340
+ WHERE session_id = ? AND context_key = ?
341
+ AND (expires_at IS NULL OR expires_at > datetime('now'))
342
+ `, [session_id, namespacedKey]);
343
+ this.logger.debug('Attempting namespaced context retrieval', {
344
+ session_id,
345
+ original_key: context_key,
346
+ namespaced_key: namespacedKey,
347
+ requesting_agent,
348
+ found: !!result
349
+ });
350
+ }
351
+ // Fallback: Try to get the original key for backward compatibility or shared data
352
+ if (!result) {
353
+ result = this.memory.get(`
354
+ SELECT * FROM shared_context
355
+ WHERE session_id = ? AND context_key = ?
356
+ AND (expires_at IS NULL OR expires_at > datetime('now'))
357
+ `, [session_id, context_key]);
358
+ this.logger.debug('Fallback context retrieval', {
359
+ session_id,
360
+ context_key,
361
+ found: !!result
362
+ });
363
+ }
313
364
  if (result) {
314
365
  try {
315
366
  // Try to parse JSON values
316
- if (result.context_value && result.context_value.startsWith('{') || result.context_value.startsWith('[')) {
367
+ if (result.context_value && (result.context_value.startsWith('{') || result.context_value.startsWith('['))) {
317
368
  result.context_value = JSON.parse(result.context_value);
318
369
  }
319
370
  if (result.access_permissions) {
@@ -322,6 +373,7 @@ class MemoryOperations {
322
373
  }
323
374
  catch (e) {
324
375
  // If parsing fails, return as-is
376
+ this.logger.warn('Failed to parse stored JSON context', { context_key, error: e });
325
377
  }
326
378
  }
327
379
  return result;
@@ -38,17 +38,27 @@ class ServiceNowClient {
38
38
  const method = (config.method || 'GET').toLowerCase();
39
39
  const url = config.url || config.endpoint;
40
40
  const data = config.data || config.body;
41
+ // CRITICAL FIX: Properly merge headers to allow content-type overrides for XML requests
42
+ const requestConfig = {
43
+ ...config,
44
+ headers: {
45
+ ...this.client.defaults.headers.common,
46
+ ...this.client.defaults.headers[method],
47
+ ...config.headers // This ensures custom headers (like Content-Type: application/xml) override defaults
48
+ }
49
+ };
50
+ this.logger.debug('🔧 Final request config headers:', requestConfig.headers);
41
51
  switch (method) {
42
52
  case 'get':
43
- return this.client.get(url, { params: config.params, ...config });
53
+ return this.client.get(url, { params: config.params, ...requestConfig });
44
54
  case 'post':
45
- return this.client.post(url, data, config);
55
+ return this.client.post(url, data, requestConfig);
46
56
  case 'put':
47
- return this.client.put(url, data, config);
57
+ return this.client.put(url, data, requestConfig);
48
58
  case 'patch':
49
- return this.client.patch(url, data, config);
59
+ return this.client.patch(url, data, requestConfig);
50
60
  case 'delete':
51
- return this.client.delete(url, config);
61
+ return this.client.delete(url, requestConfig);
52
62
  default:
53
63
  throw new Error(`Unsupported HTTP method: ${method}`);
54
64
  }
@@ -289,9 +299,19 @@ class ServiceNowClient {
289
299
  };
290
300
  try {
291
301
  const createResponse = await this.client.post(`${this.getBaseUrl()}/api/now/table/sp_widget`, testWidget);
302
+ // CRITICAL FIX: Add null safety for response processing
303
+ if (!createResponse || !createResponse.data || !createResponse.data.result || !createResponse.data.result.sys_id) {
304
+ throw new Error('Widget creation succeeded but response structure is unexpected - unable to verify sys_id');
305
+ }
292
306
  const sys_id = createResponse.data.result.sys_id;
293
- // Immediately delete the test widget
294
- await this.client.delete(`${this.getBaseUrl()}/api/now/table/sp_widget/${sys_id}`);
307
+ // Immediately delete the test widget with error handling
308
+ try {
309
+ await this.client.delete(`${this.getBaseUrl()}/api/now/table/sp_widget/${sys_id}`);
310
+ }
311
+ catch (deleteError) {
312
+ this.logger.warn(`Test widget created but cleanup failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`);
313
+ // Don't fail the test just because cleanup failed
314
+ }
295
315
  return { success: true, data: { message: 'Widget write permissions confirmed' } };
296
316
  }
297
317
  catch (error) {
@@ -304,7 +324,19 @@ class ServiceNowClient {
304
324
  description: 'Check user roles and permissions',
305
325
  test: async () => {
306
326
  const response = await this.client.get(`${this.getBaseUrl()}/api/now/table/sys_user_role?sysparm_query=user=javascript:gs.getUserID()`);
307
- return { success: true, data: { roles: response.data.result.map((r) => r.role?.display_value || r.role) } };
327
+ // CRITICAL FIX: Add null safety for response processing
328
+ if (!response || !response.data || !response.data.result) {
329
+ return { success: true, data: { roles: [], warning: 'Unable to retrieve user roles - response structure unexpected' } };
330
+ }
331
+ // Safely process roles with null checks
332
+ const roles = Array.isArray(response.data.result)
333
+ ? response.data.result.map((r) => {
334
+ if (!r || typeof r !== 'object')
335
+ return 'Unknown Role';
336
+ return r.role?.display_value || r.role || 'Unknown Role';
337
+ }).filter(role => role && role !== 'Unknown Role')
338
+ : [];
339
+ return { success: true, data: { roles } };
308
340
  }
309
341
  }
310
342
  ];
@@ -330,17 +362,27 @@ class ServiceNowClient {
330
362
  };
331
363
  }
332
364
  }
333
- // Analyze results
334
- const failedTests = Object.values(diagnostics.tests).filter((test) => test.status.includes('FAIL'));
335
- const passedTests = Object.values(diagnostics.tests).filter((test) => test.status.includes('PASS'));
365
+ // Analyze results with null safety
366
+ const failedTests = diagnostics.tests && typeof diagnostics.tests === 'object'
367
+ ? Object.values(diagnostics.tests).filter((test) => test && test.status && test.status.includes('FAIL'))
368
+ : [];
369
+ const passedTests = diagnostics.tests && typeof diagnostics.tests === 'object'
370
+ ? Object.values(diagnostics.tests).filter((test) => test && test.status && test.status.includes('PASS'))
371
+ : [];
336
372
  diagnostics.summary = {
337
- total_tests: tests.length,
338
- passed: passedTests.length,
339
- failed: failedTests.length,
340
- overall_status: failedTests.length === 0 ? '✅ ALL SYSTEMS GO' : '⚠️ ISSUES DETECTED'
373
+ total_tests: tests.length || 0,
374
+ passed: passedTests.length || 0,
375
+ failed: failedTests.length || 0,
376
+ overall_status: failedTests.length === 0 && passedTests.length > 0 ? '✅ ALL SYSTEMS GO' : '⚠️ ISSUES DETECTED'
341
377
  };
342
- // Generate recommendations
343
- diagnostics.recommendations = this.generateAuthRecommendations(diagnostics.tests);
378
+ // Generate recommendations with null safety
379
+ try {
380
+ diagnostics.recommendations = this.generateAuthRecommendations(diagnostics.tests);
381
+ }
382
+ catch (recError) {
383
+ this.logger.error('Error generating recommendations:', recError);
384
+ diagnostics.recommendations = ['⚠️ Unable to generate recommendations due to error'];
385
+ }
344
386
  return {
345
387
  success: failedTests.length === 0,
346
388
  data: diagnostics,
@@ -359,24 +401,51 @@ class ServiceNowClient {
359
401
  */
360
402
  generateAuthRecommendations(tests) {
361
403
  const recommendations = [];
362
- if (tests['Widget Write Test']?.status.includes('FAIL')) {
363
- const error = tests['Widget Write Test'].error;
364
- if (error.includes('403')) {
365
- recommendations.push('🔐 Widget deployment failed with 403 Forbidden. Check OAuth scopes: ensure \'useraccount\' and \'glide_system_administration\' scopes are enabled');
366
- recommendations.push('👤 Verify user has sp_portal_manager or admin role in ServiceNow');
367
- recommendations.push('🛡️ Check if instance has deployment restrictions for external applications');
404
+ // CRITICAL FIX: Add comprehensive null safety checks
405
+ if (!tests || typeof tests !== 'object') {
406
+ recommendations.push('⚠️ Unable to analyze test results due to missing test data');
407
+ return recommendations;
408
+ }
409
+ // Safe check for Widget Write Test
410
+ const widgetTest = tests['Widget Write Test'];
411
+ if (widgetTest?.status && typeof widgetTest.status === 'string' && widgetTest.status.includes('FAIL')) {
412
+ const error = widgetTest.error;
413
+ if (error && typeof error === 'string') {
414
+ if (error.includes('403')) {
415
+ recommendations.push('🔐 Widget deployment failed with 403 Forbidden. Check OAuth scopes: ensure \'useraccount\' and \'glide_system_administration\' scopes are enabled');
416
+ recommendations.push('👤 Verify user has sp_portal_manager or admin role in ServiceNow');
417
+ recommendations.push('🛡️ Check if instance has deployment restrictions for external applications');
418
+ }
419
+ if (error.includes('401')) {
420
+ recommendations.push('🔑 Authentication failed. Re-run: snow-flow auth login');
421
+ }
368
422
  }
369
- if (error.includes('401')) {
370
- recommendations.push('🔑 Authentication failed. Re-run: snow-flow auth login');
423
+ else {
424
+ recommendations.push('🔐 Widget write test failed with unknown error. Check ServiceNow permissions');
371
425
  }
372
426
  }
373
- if (tests['User Role Check']?.status.includes('PASS')) {
374
- const roles = tests['User Role Check'].result?.roles || [];
375
- if (!roles.some((role) => role.includes('admin') || role.includes('sp_portal'))) {
376
- recommendations.push('⚠️ User lacks admin or portal management roles. Contact ServiceNow admin to assign appropriate roles');
427
+ // Safe check for User Role Check
428
+ const roleTest = tests['User Role Check'];
429
+ if (roleTest?.status && typeof roleTest.status === 'string' && roleTest.status.includes('PASS')) {
430
+ const roles = roleTest.result?.roles;
431
+ if (Array.isArray(roles)) {
432
+ const hasRequiredRole = roles.some((role) => {
433
+ if (typeof role === 'string') {
434
+ return role.includes('admin') || role.includes('sp_portal');
435
+ }
436
+ return false;
437
+ });
438
+ if (!hasRequiredRole) {
439
+ recommendations.push('⚠️ User lacks admin or portal management roles. Contact ServiceNow admin to assign appropriate roles');
440
+ }
441
+ }
442
+ else {
443
+ recommendations.push('⚠️ Unable to verify user roles. Check if user has admin or portal management permissions');
377
444
  }
378
445
  }
379
- if (tests['Update Set Access']?.status.includes('FAIL')) {
446
+ // Safe check for Update Set Access
447
+ const updateSetTest = tests['Update Set Access'];
448
+ if (updateSetTest?.status && typeof updateSetTest.status === 'string' && updateSetTest.status.includes('FAIL')) {
380
449
  recommendations.push('📦 Update Set access failed. Ensure user has update_set_manager or admin role');
381
450
  }
382
451
  if (recommendations.length === 0) {
@@ -102,6 +102,28 @@ class XMLFirstFlowGenerator {
102
102
  .replace(/"/g, '&quot;')
103
103
  .replace(/'/g, '&apos;');
104
104
  }
105
+ /**
106
+ * Escape JSON content for XML serialization (no CDATA needed)
107
+ * CRITICAL FIX: Properly handle JSON content in XML to avoid parsing errors
108
+ */
109
+ escapeForXml(jsonContent) {
110
+ if (!jsonContent)
111
+ return '';
112
+ // First escape XML special characters
113
+ let escaped = jsonContent
114
+ .replace(/&/g, '&amp;')
115
+ .replace(/</g, '&lt;')
116
+ .replace(/>/g, '&gt;')
117
+ .replace(/"/g, '&quot;')
118
+ .replace(/'/g, '&apos;');
119
+ // Additional escaping for problematic sequences that could break XML parsing
120
+ escaped = escaped
121
+ .replace(/]]>/g, ']]&gt;') // Escape CDATA end sequences
122
+ .replace(/\r?\n/g, '&#10;') // Preserve line breaks as XML entities
123
+ .replace(/\r/g, '&#13;') // Preserve carriage returns
124
+ .replace(/\t/g, '&#9;'); // Preserve tabs
125
+ return escaped;
126
+ }
105
127
  /**
106
128
  * Generate complete Update Set XML for a flow
107
129
  * Based on REAL ServiceNow XML structure research
@@ -157,7 +179,7 @@ class XMLFirstFlowGenerator {
157
179
  <action>INSERT_OR_UPDATE</action>
158
180
  <application>global</application>
159
181
  <name>sys_hub_flow_snapshot_${snapshotSysId}</name>
160
- <payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_flow_snapshot"><sys_hub_flow_snapshot action="INSERT_OR_UPDATE"><sys_id>${snapshotSysId}</sys_id><name>${this.escapeXml(flowDef.name)}</name><flow>${this.flowSysId}</flow><note>Initial version</note><snapshot><![CDATA[${JSON.stringify(flowDefinitionJson, null, 2)}]]]]><![CDATA[></snapshot><sys_created_by>admin</sys_created_by><sys_created_on>${timestamp}</sys_created_on></sys_hub_flow_snapshot></record_update>]]></payload>
182
+ <payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_flow_snapshot"><sys_hub_flow_snapshot action="INSERT_OR_UPDATE"><sys_id>${snapshotSysId}</sys_id><name>${this.escapeXml(flowDef.name)}</name><flow>${this.flowSysId}</flow><note>Initial version</note><snapshot>${this.escapeForXml(JSON.stringify(flowDefinitionJson, null, 2))}</snapshot><sys_created_by>admin</sys_created_by><sys_created_on>${timestamp}</sys_created_on></sys_hub_flow_snapshot></record_update>]]></payload>
161
183
  <remote_update_set>${updateSetSysId}</remote_update_set>
162
184
  <source_table>sys_hub_flow_snapshot</source_table>
163
185
  <type>Flow Designer Snapshot</type>
@@ -339,7 +361,7 @@ class XMLFirstFlowGenerator {
339
361
  <action>INSERT_OR_UPDATE</action>
340
362
  <application>global</application>
341
363
  <name>sys_hub_action_instance_${sysIds[index]}</name>
342
- <payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_action_instance"><sys_hub_action_instance action="INSERT_OR_UPDATE"><sys_id>${sysIds[index]}</sys_id><flow>${this.flowSysId}</flow><action_type display_value="">${this.getActionTypeId(activity.type)}</action_type><name>${this.escapeXml(activity.name)}</name><order>${activity.order || (index + 1) * 100}</order><active>true</active><inputs><![CDATA[${JSON.stringify(activity.inputs)}]]]]><![CDATA[></inputs><outputs><![CDATA[${JSON.stringify(activity.outputs || {})}]]]]><![CDATA[></outputs><condition>${this.escapeXml(activity.condition || '')}</condition><comment_text/><sys_class_name>sys_hub_action_instance</sys_class_name><sys_created_by>admin</sys_created_by><sys_created_on>${timestamp}</sys_created_on><sys_domain>global</sys_domain><sys_domain_path>/</sys_domain_path></sys_hub_action_instance></record_update>]]></payload>
364
+ <payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_action_instance"><sys_hub_action_instance action="INSERT_OR_UPDATE"><sys_id>${sysIds[index]}</sys_id><flow>${this.flowSysId}</flow><action_type display_value="">${this.getActionTypeId(activity.type)}</action_type><name>${this.escapeXml(activity.name)}</name><order>${activity.order || (index + 1) * 100}</order><active>true</active><inputs>${this.escapeForXml(JSON.stringify(activity.inputs))}</inputs><outputs>${this.escapeForXml(JSON.stringify(activity.outputs || {}))}</outputs><condition>${this.escapeXml(activity.condition || '')}</condition><comment_text/><sys_class_name>sys_hub_action_instance</sys_class_name><sys_created_by>admin</sys_created_by><sys_created_on>${timestamp}</sys_created_on><sys_domain>global</sys_domain><sys_domain_path>/</sys_domain_path></sys_hub_action_instance></record_update>]]></payload>
343
365
  <remote_update_set>${updateSetSysId}</remote_update_set>
344
366
  <source_table>sys_hub_action_instance</source_table>
345
367
  <type>Flow Designer Action</type>
package/dist/version.js CHANGED
@@ -7,7 +7,7 @@ exports.VERSION_INFO = exports.VERSION = void 0;
7
7
  exports.getVersionString = getVersionString;
8
8
  exports.getLatestFeatures = getLatestFeatures;
9
9
  exports.isLatestVersion = isLatestVersion;
10
- exports.VERSION = '1.3.16';
10
+ exports.VERSION = '1.3.23';
11
11
  exports.VERSION_INFO = {
12
12
  version: exports.VERSION,
13
13
  name: 'Snow-Flow',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-flow",
3
- "version": "1.3.21",
3
+ "version": "1.3.23",
4
4
  "description": "ServiceNow Queen Agent - Hive-Mind Intelligence for ServiceNow Development inspired by claude-flow. Transform complex workflows into elegant one-command orchestration.",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",