@link-assistant/hive-mind 1.50.6 → 1.50.7

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,14 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.50.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 84b9853: fix: make all long sleeps interruptible so CTRL+C responds immediately (#1574)
8
+ - Replace raw `setTimeout` sleeps with an interruptible sleep utility that listens for SIGINT
9
+ - Ensure CTRL+C during CI polling, auto-merge waits, and auto-continue delays terminates the process immediately
10
+ - Add `interruptible-sleep.lib.mjs` with full test coverage
11
+
3
12
  ## 1.50.6
4
13
 
5
14
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.50.6",
3
+ "version": "1.50.7",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Interruptible sleep utility for long-running wait loops.
3
+ *
4
+ * Replaces raw `await new Promise(r => setTimeout(r, ms))` with a sleep
5
+ * that resolves immediately on SIGINT, so the process exit handler chain
6
+ * is not blocked by a lingering timer.
7
+ *
8
+ * @see https://github.com/link-assistant/hive-mind/issues/1574
9
+ */
10
+
11
+ /**
12
+ * Sleep for `ms` milliseconds, but resolve early if SIGINT is received.
13
+ *
14
+ * When SIGINT fires during the sleep, the timer is cleared and the promise
15
+ * resolves with `{ interrupted: true }`. The existing SIGINT handler (from
16
+ * exit-handler.lib.mjs) continues to run normally — this function does NOT
17
+ * consume or re-emit the signal, it only ensures its own timer doesn't
18
+ * block the event loop.
19
+ *
20
+ * @param {number} ms - Duration in milliseconds
21
+ * @returns {Promise<{interrupted: boolean}>}
22
+ */
23
+ export function interruptibleSleep(ms) {
24
+ return new Promise(resolve => {
25
+ let timer;
26
+
27
+ const onInterrupt = () => {
28
+ clearTimeout(timer);
29
+ process.removeListener('SIGINT', onInterrupt);
30
+ resolve({ interrupted: true });
31
+ };
32
+
33
+ timer = setTimeout(() => {
34
+ process.removeListener('SIGINT', onInterrupt);
35
+ resolve({ interrupted: false });
36
+ }, ms);
37
+
38
+ process.on('SIGINT', onInterrupt);
39
+ });
40
+ }
41
+
42
+ export default { interruptibleSleep };
@@ -49,6 +49,9 @@ const { extractLinkedIssueNumber } = githubLinking;
49
49
  // Import configuration
50
50
  import { autoContinue, limitReset } from './config.lib.mjs';
51
51
 
52
+ // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
53
+ const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
54
+
52
55
  const { calculateWaitTime } = validation;
53
56
 
54
57
  /**
@@ -116,7 +119,7 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
116
119
  }, countdownInterval);
117
120
 
118
121
  // Wait until reset time
119
- await new Promise(resolve => setTimeout(resolve, waitMs));
122
+ await interruptibleSleep(waitMs);
120
123
  clearInterval(countdownTimer);
121
124
 
122
125
  const actionType = isRestart ? 'Restarting' : 'Resuming';
@@ -54,6 +54,9 @@ import { limitReset } from './config.lib.mjs';
54
54
  const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
55
55
  const { checkForExistingComment, checkForNonBotComments, getMergeBlockers } = autoMergeHelpers;
56
56
 
57
+ // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
58
+ const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
59
+
57
60
  /**
58
61
  * Main function: Watch and restart until PR becomes mergeable
59
62
  * This implements --auto-restart-until-mergeable functionality
@@ -104,7 +107,7 @@ export const watchUntilMergeable = async params => {
104
107
  // Issue #1567: Wait for initial cooldown before first check.
105
108
  // This gives CI/CD time to start and solution logs time to be posted.
106
109
  await log(formatAligned('⏳', 'Initial cooldown:', `Waiting ${INITIAL_COOLDOWN_SECONDS}s before first check...`));
107
- await new Promise(resolve => setTimeout(resolve, INITIAL_COOLDOWN_SECONDS * 1000));
110
+ await interruptibleSleep(INITIAL_COOLDOWN_SECONDS * 1000);
108
111
  await log(formatAligned('✅', 'Cooldown complete:', 'Starting monitoring loop'));
109
112
  await log('');
110
113
 
@@ -200,7 +203,7 @@ export const watchUntilMergeable = async params => {
200
203
  if (!noCiConfigured) {
201
204
  const DOUBLE_CHECK_DELAY_MS = 10000; // 10 seconds
202
205
  await log(formatAligned('🔍', 'Multi-mechanism CI consensus check:', `Waiting ${DOUBLE_CHECK_DELAY_MS / 1000}s then verifying...`, 2));
203
- await new Promise(resolve => setTimeout(resolve, DOUBLE_CHECK_DELAY_MS));
206
+ await interruptibleSleep(DOUBLE_CHECK_DELAY_MS);
204
207
 
205
208
  // Run multi-mechanism consensus: Check Runs API + Workflow Runs API + Repo-wide actions
206
209
  const consensus = await checkCIConsensus({
@@ -223,7 +226,7 @@ export const watchUntilMergeable = async params => {
223
226
  const actualWaitSeconds = currentBackoffSeconds;
224
227
  await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
225
228
  await log('');
226
- await new Promise(resolve => setTimeout(resolve, actualWaitSeconds * 1000));
229
+ await interruptibleSleep(actualWaitSeconds * 1000);
227
230
  continue;
228
231
  }
229
232
  await log(formatAligned('✅', 'All CI mechanisms agree:', `CheckRuns=${consensus.mechanisms.checkRunsAPI.status}, WorkflowRuns=complete(${consensus.mechanisms.workflowRunsAPI.total}), RepoActions=${consensus.mechanisms.repoActions.skipped ? 'skipped' : 'clear'}`, 2));
@@ -236,7 +239,7 @@ export const watchUntilMergeable = async params => {
236
239
  const actualWaitSeconds = currentBackoffSeconds;
237
240
  await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
238
241
  await log('');
239
- await new Promise(resolve => setTimeout(resolve, actualWaitSeconds * 1000));
242
+ await interruptibleSleep(actualWaitSeconds * 1000);
240
243
  continue;
241
244
  }
242
245
  }
@@ -606,7 +609,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
606
609
  }
607
610
 
608
611
  // Wait until the limit resets
609
- await new Promise(resolve => setTimeout(resolve, waitMs));
612
+ await interruptibleSleep(waitMs);
610
613
 
611
614
  await log(formatAligned('✅', 'Usage limit wait complete', 'Resuming session...'));
612
615
  await log('');
@@ -841,7 +844,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
841
844
  const actualWaitSeconds = currentBackoffSeconds;
842
845
  await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
843
846
  await log('');
844
- await new Promise(resolve => setTimeout(resolve, actualWaitSeconds * 1000));
847
+ await interruptibleSleep(actualWaitSeconds * 1000);
845
848
  }
846
849
  };
847
850
 
@@ -37,6 +37,9 @@ const { detectAndCountFeedback } = feedbackLib;
37
37
  const restartShared = await import('./solve.restart-shared.lib.mjs');
38
38
  const { checkPRMerged, checkForUncommittedChanges, getUncommittedChangesDetails, executeToolIteration, buildUncommittedChangesFeedback, isApiError } = restartShared;
39
39
 
40
+ // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
41
+ const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
42
+
40
43
  /**
41
44
  * Monitor for feedback in a loop and trigger restart when detected
42
45
  */
@@ -446,7 +449,7 @@ export const watchForFeedback = async params => {
446
449
  const actualWaitMs = actualWaitSeconds * 1000;
447
450
  await log(formatAligned('⏱️', 'Next check in:', `${actualWaitSeconds} seconds...`, 2));
448
451
  await log(''); // Blank line for readability
449
- await new Promise(resolve => setTimeout(resolve, actualWaitMs));
452
+ await interruptibleSleep(actualWaitMs);
450
453
  } else if (isTemporaryWatch && !firstIterationInTemporaryMode) {
451
454
  // In auto-restart mode, check immediately without waiting
452
455
  await log(formatAligned('', 'Checking immediately for uncommitted changes...', '', 2));