@link-assistant/hive-mind 1.23.9 → 1.23.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.23.10
4
+
5
+ ### Patch Changes
6
+
7
+ - cc57624: Add retry logic for fork validation network errors (Issue #1311). The validateForkParent function now retries up to 3 times with exponential backoff for transient network errors like TCP timeouts. Network errors now show a distinct error message with helpful retry suggestions instead of incorrectly reporting a fork parent mismatch.
8
+
3
9
  ## 1.23.9
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.23.9",
3
+ "version": "1.23.10",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/lib.mjs CHANGED
@@ -243,6 +243,22 @@ export const retry = async (fn, options = {}) => {
243
243
  }
244
244
  };
245
245
 
246
+ /**
247
+ * Check if an error is a transient network error that can be retried.
248
+ * Used by validateForkParent to detect network timeouts (Issue #1311).
249
+ * @param {Error|string} error - The error to check
250
+ * @returns {boolean} True if the error is transient and retryable
251
+ */
252
+ export const isTransientNetworkError = error => {
253
+ const msg = (error?.message || error?.toString() || '').toLowerCase();
254
+ const output = (error?.stderr?.toString() || error?.stdout?.toString() || '').toLowerCase();
255
+ const combined = msg + ' ' + output;
256
+
257
+ const transientPatterns = ['i/o timeout', 'dial tcp', 'connection refused', 'connection reset', 'econnreset', 'etimedout', 'enotfound', 'ehostunreach', 'enetunreach', 'network is unreachable', 'temporary failure', 'http 502', 'http 503', 'http 504', 'bad gateway', 'service unavailable', 'gateway timeout', 'tls handshake timeout', 'ssl_error', 'socket hang up'];
258
+
259
+ return transientPatterns.some(pattern => combined.includes(pattern));
260
+ };
261
+
246
262
  /**
247
263
  * Format bytes to human readable string
248
264
  * @param {number} bytes - Number of bytes
@@ -110,93 +110,82 @@ export const checkExistingForkOfRoot = async rootRepo => {
110
110
  * This prevents issues where a fork was created from an intermediate fork (fork of a fork)
111
111
  * instead of directly from the intended upstream repository.
112
112
  *
113
+ * Issue #1311: Added retry logic for transient network errors (TCP timeouts, etc.)
114
+ *
113
115
  * @param {string} forkRepo - The fork repository to validate (e.g., "user/repo")
114
116
  * @param {string} expectedUpstream - The expected upstream repository (e.g., "owner/repo")
115
- * @returns {Promise<{isValid: boolean, isFork: boolean, parent: string|null, source: string|null, error: string|null}>}
117
+ * @returns {Promise<{isValid: boolean, isFork: boolean, parent: string|null, source: string|null, error: string|null, isNetworkError?: boolean}>}
116
118
  */
117
119
  export const validateForkParent = async (forkRepo, expectedUpstream) => {
118
- try {
119
- const forkInfoResult = await $`gh api repos/${forkRepo} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}'`;
120
-
121
- if (forkInfoResult.code !== 0) {
122
- return {
123
- isValid: false,
124
- isFork: false,
125
- parent: null,
126
- source: null,
127
- error: `Failed to get fork info for ${forkRepo}`,
128
- };
129
- }
120
+ // Issue #1311: Retry configuration for transient network errors
121
+ const maxAttempts = 3;
122
+ const baseDelay = 2000;
123
+ const networkErr = msg => ({ isValid: false, isFork: false, parent: null, source: null, error: msg, isNetworkError: true });
130
124
 
131
- const forkInfo = JSON.parse(forkInfoResult.stdout.toString().trim());
132
- const isFork = forkInfo.fork === true;
133
- const parent = forkInfo.parent || null;
134
- const source = forkInfo.source || null;
135
-
136
- // If not a fork at all, it's invalid for our purposes
137
- if (!isFork) {
138
- return {
139
- isValid: false,
140
- isFork: false,
141
- parent: null,
142
- source: null,
143
- error: `Repository ${forkRepo} is not a GitHub fork`,
144
- };
145
- }
125
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
126
+ try {
127
+ const forkInfoResult = await $`gh api repos/${forkRepo} --jq '{fork: .fork, parent: .parent.full_name, source: .source.full_name}'`;
128
+
129
+ // Check for network errors in non-zero exit code
130
+ if (forkInfoResult.code !== 0) {
131
+ const errorOutput = (forkInfoResult.stderr?.toString() || '') + (forkInfoResult.stdout?.toString() || '');
132
+ // Issue #1311: Retry on transient network errors
133
+ if (lib.isTransientNetworkError({ message: errorOutput })) {
134
+ if (attempt < maxAttempts) {
135
+ const delay = baseDelay * Math.pow(2, attempt - 1);
136
+ await log(` ⚠️ Network error, retrying in ${delay / 1000}s... (${attempt}/${maxAttempts})`, { level: 'warning' });
137
+ await lib.sleep(delay);
138
+ continue;
139
+ }
140
+ return networkErr(`Network error after ${maxAttempts} attempts: ${errorOutput.substring(0, 200)}`);
141
+ }
142
+ return { isValid: false, isFork: false, parent: null, source: null, error: `Failed to get fork info for ${forkRepo}` };
143
+ }
146
144
 
147
- // The fork's PARENT (immediate upstream) should match expectedUpstream
148
- // The SOURCE (ultimate root) is also acceptable as it indicates the fork
149
- // is part of the correct hierarchy, just at a different level
150
- const parentMatches = parent === expectedUpstream;
151
- const sourceMatches = source === expectedUpstream;
152
-
153
- // Ideal case: parent matches directly (fork was made from expected upstream)
154
- if (parentMatches) {
155
- return {
156
- isValid: true,
157
- isFork: true,
158
- parent,
159
- source,
160
- error: null,
161
- };
162
- }
145
+ const forkInfo = JSON.parse(forkInfoResult.stdout.toString().trim());
146
+ const isFork = forkInfo.fork === true;
147
+ const parent = forkInfo.parent || null;
148
+ const source = forkInfo.source || null;
163
149
 
164
- // Special case: source matches but parent doesn't
165
- // This means the fork was made from an intermediate fork
166
- // For issue #967, this is the problematic case we want to catch
167
- if (sourceMatches && !parentMatches) {
168
- return {
169
- isValid: false,
170
- isFork: true,
171
- parent,
172
- source,
173
- error: `Fork ${forkRepo} was created from ${parent} (intermediate fork), not directly from ${expectedUpstream}. ` + `This can cause pull requests to include unexpected commits from the intermediate fork.`,
174
- };
175
- }
150
+ // If not a fork at all, it's invalid for our purposes
151
+ if (!isFork) {
152
+ return { isValid: false, isFork: false, parent: null, source: null, error: `Repository ${forkRepo} is not a GitHub fork` };
153
+ }
176
154
 
177
- // Neither parent nor source matches - completely different repository tree
178
- return {
179
- isValid: false,
180
- isFork: true,
181
- parent,
182
- source,
183
- error: `Fork ${forkRepo} is from a different repository tree (parent: ${parent}, source: ${source}) and cannot be used with ${expectedUpstream}`,
184
- };
185
- } catch (error) {
186
- reportError(error, {
187
- context: 'validate_fork_parent',
188
- forkRepo,
189
- expectedUpstream,
190
- operation: 'check_fork_hierarchy',
191
- });
192
- return {
193
- isValid: false,
194
- isFork: false,
195
- parent: null,
196
- source: null,
197
- error: `Error validating fork parent: ${error.message}`,
198
- };
155
+ // The fork's PARENT (immediate upstream) should match expectedUpstream
156
+ // The SOURCE (ultimate root) is also acceptable as it indicates the fork is part of the correct hierarchy
157
+ const parentMatches = parent === expectedUpstream;
158
+ const sourceMatches = source === expectedUpstream;
159
+
160
+ if (parentMatches) {
161
+ return { isValid: true, isFork: true, parent, source, error: null };
162
+ }
163
+
164
+ // Special case: source matches but parent doesn't - fork was made from an intermediate fork
165
+ // For issue #967, this is the problematic case we want to catch
166
+ if (sourceMatches && !parentMatches) {
167
+ return { isValid: false, isFork: true, parent, source, error: `Fork ${forkRepo} was created from ${parent} (intermediate fork), not directly from ${expectedUpstream}. This can cause pull requests to include unexpected commits from the intermediate fork.` };
168
+ }
169
+
170
+ // Neither parent nor source matches - completely different repository tree
171
+ return { isValid: false, isFork: true, parent, source, error: `Fork ${forkRepo} is from a different repository tree (parent: ${parent}, source: ${source}) and cannot be used with ${expectedUpstream}` };
172
+ } catch (error) {
173
+ // Issue #1311: Retry on transient network errors
174
+ if (lib.isTransientNetworkError(error)) {
175
+ if (attempt < maxAttempts) {
176
+ const delay = baseDelay * Math.pow(2, attempt - 1);
177
+ await log(` ⚠️ Network error, retrying in ${delay / 1000}s... (${attempt}/${maxAttempts})`, { level: 'warning' });
178
+ await lib.sleep(delay);
179
+ continue;
180
+ }
181
+ reportError(error, { context: 'validate_fork_parent', forkRepo, expectedUpstream, operation: 'check_fork_hierarchy', attempt, maxAttempts, isNetworkError: true });
182
+ return networkErr(`Network error after ${maxAttempts} attempts: ${error.message}`);
183
+ }
184
+ reportError(error, { context: 'validate_fork_parent', forkRepo, expectedUpstream, operation: 'check_fork_hierarchy' });
185
+ return { isValid: false, isFork: false, parent: null, source: null, error: `Error validating fork parent: ${error.message}` };
186
+ }
199
187
  }
188
+ return networkErr(`Failed to validate fork after ${maxAttempts} attempts`);
200
189
  };
201
190
 
202
191
  /**
@@ -513,6 +502,25 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
513
502
  const forkValidation = await validateForkParent(existingForkName, `${owner}/${repo}`);
514
503
 
515
504
  if (!forkValidation.isValid) {
505
+ // Issue #1311: Handle network errors separately from fork mismatch errors
506
+ if (forkValidation.isNetworkError) {
507
+ await log('');
508
+ await log(`${formatAligned('❌', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'error' });
509
+ await log('');
510
+ await log(' 🔍 What happened:');
511
+ await log(` Failed to connect to GitHub API while validating fork.`);
512
+ await log(` Error: ${forkValidation.error}`);
513
+ await log('');
514
+ await log(' 💡 This is likely a temporary network issue. You can:');
515
+ await log(' 1. Wait a moment and try again');
516
+ await log(' 2. Check your internet connection');
517
+ await log(' 3. Check GitHub status: https://www.githubstatus.com/');
518
+ await log('');
519
+ await log(' Or use --no-fork to skip fork validation if you have write access.');
520
+ await log('');
521
+ await safeExit(1, 'Network error during fork validation - please retry');
522
+ }
523
+
516
524
  // Fork parent mismatch detected - this prevents issue #967
517
525
  await log('');
518
526
  await log(`${formatAligned('❌', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'error' });
@@ -842,29 +850,44 @@ Thank you!`;
842
850
  const forkValidation = await validateForkParent(actualForkName, `${owner}/${repo}`);
843
851
 
844
852
  if (!forkValidation.isValid) {
845
- // Fork parent mismatch detected
846
- await log('');
847
- await log(`${formatAligned('⚠️', 'FORK PARENT MISMATCH WARNING', '')}`, { level: 'warning' });
848
- await log('');
849
- await log(' 🔍 Issue detected:');
850
- if (!forkValidation.isFork) {
851
- await log(` The repository ${actualForkName} is NOT a GitHub fork.`);
853
+ // Issue #1311: Handle network errors separately from fork mismatch errors
854
+ if (forkValidation.isNetworkError) {
855
+ await log('');
856
+ await log(`${formatAligned('⚠️', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'warning' });
857
+ await log('');
858
+ await log(' 🔍 What happened:');
859
+ await log(` Failed to connect to GitHub API while validating fork.`);
860
+ await log(` Error: ${forkValidation.error}`);
861
+ await log('');
862
+ await log(' 💡 This is likely a temporary network issue.');
863
+ await log(' Continuing with the fork, but validation was skipped.');
864
+ await log('');
865
+ // Note: We continue here since this is someone else's fork and we can't verify it
852
866
  } else {
853
- await log(` The fork ${actualForkName} was created from ${forkValidation.parent},`);
854
- await log(` not directly from the target repository ${owner}/${repo}.`);
867
+ // Fork parent mismatch detected
868
+ await log('');
869
+ await log(`${formatAligned('⚠️', 'FORK PARENT MISMATCH WARNING', '')}`, { level: 'warning' });
870
+ await log('');
871
+ await log(' 🔍 Issue detected:');
872
+ if (!forkValidation.isFork) {
873
+ await log(` The repository ${actualForkName} is NOT a GitHub fork.`);
874
+ } else {
875
+ await log(` The fork ${actualForkName} was created from ${forkValidation.parent},`);
876
+ await log(` not directly from the target repository ${owner}/${repo}.`);
877
+ }
878
+ await log('');
879
+ await log(' 📦 Fork relationship:');
880
+ await log(` • Fork: ${actualForkName}`);
881
+ await log(` • Fork parent: ${forkValidation.parent || 'N/A'}`);
882
+ await log(` • Fork source (root): ${forkValidation.source || 'N/A'}`);
883
+ await log(` • Expected parent: ${owner}/${repo}`);
884
+ await log('');
885
+ await log(' ⚠️ This may cause pull requests to include unexpected commits.');
886
+ await log(' Consider using --fork to create your own fork instead.');
887
+ await log('');
888
+ // Note: We don't exit here since this is someone else's fork and we're just using it
889
+ // The user should be aware but can proceed (they didn't create this fork)
855
890
  }
856
- await log('');
857
- await log(' 📦 Fork relationship:');
858
- await log(` • Fork: ${actualForkName}`);
859
- await log(` • Fork parent: ${forkValidation.parent || 'N/A'}`);
860
- await log(` • Fork source (root): ${forkValidation.source || 'N/A'}`);
861
- await log(` • Expected parent: ${owner}/${repo}`);
862
- await log('');
863
- await log(' ⚠️ This may cause pull requests to include unexpected commits.');
864
- await log(' Consider using --fork to create your own fork instead.');
865
- await log('');
866
- // Note: We don't exit here since this is someone else's fork and we're just using it
867
- // The user should be aware but can proceed (they didn't create this fork)
868
891
  } else {
869
892
  await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
870
893
  }