@link-assistant/hive-mind 1.25.5 → 1.25.6

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,18 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.25.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 5200c2a: Fix auto-restart spamming PR with comments when usage limit is reached (#1356)
8
+
9
+ When the AI tool's usage limit is reached during --auto-restart-until-mergeable mode, the loop now:
10
+ 1. Detects the `limitReached` flag from the tool result
11
+ 2. Silently waits for the limit reset time plus a 10-minute buffer (no GitHub comment posted)
12
+ 3. Resumes the session using `--resume <sessionId>` with a "Continue" prompt, preserving context
13
+
14
+ For non-limit tool failures, the loop now stops immediately instead of retrying, preventing infinite loops on unrecoverable errors.
15
+
3
16
  ## 1.25.5
4
17
 
5
18
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.25.5",
3
+ "version": "1.25.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -41,7 +41,14 @@ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
41
41
 
42
42
  // Import shared utilities from the restart-shared module
43
43
  const restartShared = await import('./solve.restart-shared.lib.mjs');
44
- const { checkPRMerged, checkPRClosed, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions, isApiError } = restartShared;
44
+ const { checkPRMerged, checkPRClosed, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildAutoRestartInstructions, isUsageLimitReached } = restartShared;
45
+
46
+ // Import validation functions for time parsing (used for usage limit wait)
47
+ const validation = await import('./solve.validation.lib.mjs');
48
+ const { calculateWaitTime } = validation;
49
+
50
+ // Import configuration (used for limit reset buffer and jitter)
51
+ import { limitReset } from './config.lib.mjs';
45
52
 
46
53
  /**
47
54
  * Issue #1323: Check if a comment with specific content already exists on the PR
@@ -338,9 +345,6 @@ export const watchUntilMergeable = async params => {
338
345
  // `restartCount` counts actual AI tool executions (when we actually restart the AI)
339
346
  let restartCount = 0;
340
347
 
341
- // Track consecutive API errors for retry limit
342
- const MAX_API_ERROR_RETRIES = 3;
343
- let consecutiveApiErrors = 0;
344
348
  let currentBackoffSeconds = watchInterval;
345
349
 
346
350
  await log('');
@@ -620,8 +624,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
620
624
  await log('');
621
625
 
622
626
  // Post a comment to PR about the restart
627
+ // Issue #1356: Include restart count for tracking and add deduplication
623
628
  try {
624
- const commentBody = `## 🔄 Auto-restart triggered\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
629
+ const commentBody = `## 🔄 Auto-restart triggered (attempt ${restartCount})\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
625
630
  await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
626
631
  await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
627
632
  } catch (commentError) {
@@ -656,33 +661,90 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
656
661
  });
657
662
 
658
663
  if (!toolResult.success) {
659
- // Check if this is an API error using shared utility
660
- if (isApiError(toolResult)) {
661
- consecutiveApiErrors++;
662
- await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, `API error detected (${consecutiveApiErrors}/${MAX_API_ERROR_RETRIES})`, 2));
663
-
664
- if (consecutiveApiErrors >= MAX_API_ERROR_RETRIES) {
665
- await log('');
666
- await log(formatAligned('❌', 'MAXIMUM API ERROR RETRIES REACHED', ''));
667
- await log(formatAligned('', 'Error details:', toolResult.result || 'Unknown API error', 2));
668
- await log(formatAligned('', 'Action:', 'Exiting to prevent infinite loop', 2));
669
- return { success: false, reason: 'api_error', latestSessionId, latestAnthropicCost };
664
+ // Issue #1356: Check for usage limit errors FIRST (most specific)
665
+ // When usage limit is reached, silently wait for limitResetTime + buffer + jitter,
666
+ // then resume the session using --resume <sessionId> with a "Continue" prompt.
667
+ // No GitHub comment is posted only log output.
668
+ if (isUsageLimitReached(toolResult)) {
669
+ const resumeSessionId = toolResult.sessionId;
670
+ const resetTime = toolResult.limitResetTime;
671
+ const baseWaitMs = resetTime ? calculateWaitTime(resetTime) : 0;
672
+ const bufferMs = limitReset.bufferMs;
673
+ const jitterMs = Math.floor(Math.random() * limitReset.jitterMs);
674
+ const waitMs = baseWaitMs + bufferMs + jitterMs;
675
+ const bufferMinutes = Math.round(bufferMs / 60000);
676
+ const jitterSeconds = Math.round(jitterMs / 1000);
677
+ const waitMinutes = Math.round(waitMs / 60000);
678
+
679
+ await log('');
680
+ await log(formatAligned('⏳', 'USAGE LIMIT REACHED', ''));
681
+ await log(formatAligned('', 'Reset time:', resetTime || 'Unknown', 2));
682
+ await log(formatAligned('', 'Waiting:', `${waitMinutes} min (reset + ${bufferMinutes} min buffer + ${jitterSeconds}s jitter)`, 2));
683
+ await log(formatAligned('', 'Action:', 'Silently waiting then resuming — no GitHub comment posted', 2));
684
+ if (resumeSessionId) {
685
+ await log(formatAligned('', 'Session ID:', resumeSessionId, 2));
670
686
  }
687
+ await log('');
671
688
 
672
- // Apply exponential backoff
673
- currentBackoffSeconds = Math.min(currentBackoffSeconds * 2, 300);
674
- await log(formatAligned('', 'Backing off:', `Will retry after ${currentBackoffSeconds} seconds`, 2));
675
- } else {
676
- consecutiveApiErrors = 0;
677
- currentBackoffSeconds = watchInterval;
678
- await log(formatAligned('⚠️', `${argv.tool.toUpperCase()} execution failed`, 'Will retry in next check', 2));
689
+ // Wait silently until the limit resets (no GitHub comment)
690
+ await new Promise(resolve => setTimeout(resolve, waitMs));
691
+
692
+ await log(formatAligned('✅', 'Usage limit wait complete', 'Resuming session...'));
693
+ await log('');
694
+
695
+ // Resume the session: execute with --resume <sessionId> and a "Continue" prompt
696
+ // This preserves context and the system message from the original session
697
+ if (resumeSessionId) {
698
+ const resumeArgv = { ...argv, resume: resumeSessionId };
699
+ const resumeResult = await executeToolIteration({
700
+ issueUrl,
701
+ owner,
702
+ repo,
703
+ issueNumber,
704
+ prNumber,
705
+ branchName: prBranch || branchName,
706
+ tempDir,
707
+ mergeStateStatus,
708
+ feedbackLines: ['Continue'],
709
+ argv: resumeArgv,
710
+ });
711
+
712
+ if (resumeResult.success) {
713
+ // Resume succeeded - capture session data
714
+ currentBackoffSeconds = watchInterval;
715
+ if (resumeResult.sessionId) {
716
+ latestSessionId = resumeResult.sessionId;
717
+ latestAnthropicCost = resumeResult.anthropicTotalCostUSD;
718
+ }
719
+ await log(formatAligned('✅', `${argv.tool.toUpperCase()} resume completed:`, 'Checking if PR is now mergeable...'));
720
+ } else if (isUsageLimitReached(resumeResult)) {
721
+ // Hit the limit again immediately after resume — store for next outer iteration
722
+ await log(formatAligned('⚠️', 'Usage limit hit again after resume', 'Will retry in next check cycle', 2));
723
+ } else {
724
+ // Resume failed for a non-limit reason — stop the loop
725
+ await log('');
726
+ await log(formatAligned('❌', `${argv.tool.toUpperCase()} RESUME FAILED`, ''));
727
+ await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed after limit reset', 2));
728
+ return { success: false, reason: 'tool_failure_after_resume', latestSessionId, latestAnthropicCost };
729
+ }
730
+ } else {
731
+ // No session ID available — cannot resume, restart fresh in next iteration
732
+ await log(formatAligned('⚠️', 'No session ID for resume', 'Will restart fresh in next check cycle', 2));
733
+ }
734
+
735
+ lastCheckTime = new Date();
736
+ continue;
679
737
  }
738
+
739
+ // Any other failure (not usage limit): stop the auto-restart loop
740
+ // Per reviewer feedback: non-limit failures should fail and stop attempts
741
+ await log('');
742
+ await log(formatAligned('❌', `${argv.tool.toUpperCase()} EXECUTION FAILED`, ''));
743
+ await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed', 2));
744
+ return { success: false, reason: 'tool_failure', latestSessionId, latestAnthropicCost };
680
745
  } else {
681
- // Success - reset error counters
682
- consecutiveApiErrors = 0;
746
+ // Success - capture latest session data
683
747
  currentBackoffSeconds = watchInterval;
684
-
685
- // Capture latest session data
686
748
  if (toolResult.sessionId) {
687
749
  latestSessionId = toolResult.sessionId;
688
750
  latestAnthropicCost = toolResult.anthropicTotalCostUSD;
@@ -768,7 +830,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
768
830
  }
769
831
 
770
832
  // Wait for next interval
771
- const actualWaitSeconds = consecutiveApiErrors > 0 ? currentBackoffSeconds : watchInterval;
833
+ const actualWaitSeconds = currentBackoffSeconds;
772
834
  await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
773
835
  await log('');
774
836
  await new Promise(resolve => setTimeout(resolve, actualWaitSeconds * 1000));
@@ -359,6 +359,18 @@ export const isApiError = toolResult => {
359
359
  return errorPatterns.some(pattern => toolResult.result.includes(pattern));
360
360
  };
361
361
 
362
+ /**
363
+ * Issue #1356: Check if a tool result indicates a usage limit was reached
364
+ * This is separate from isApiError because usage limits return different fields
365
+ * (limitReached, limitResetTime) and require different handling (exit loop, not retry).
366
+ * @param {Object} toolResult - Tool execution result
367
+ * @returns {boolean}
368
+ */
369
+ export const isUsageLimitReached = toolResult => {
370
+ if (!toolResult) return false;
371
+ return toolResult.limitReached === true;
372
+ };
373
+
362
374
  export default {
363
375
  checkPRMerged,
364
376
  checkPRClosed,
@@ -369,4 +381,5 @@ export default {
369
381
  buildAutoRestartInstructions,
370
382
  buildUncommittedChangesFeedback,
371
383
  isApiError,
384
+ isUsageLimitReached,
372
385
  };