@link-assistant/hive-mind 1.30.3 → 1.30.5

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,31 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.30.5
4
+
5
+ ### Patch Changes
6
+
7
+ - a9a58ab: Switch Docker builds to registry cache for faster arm64 builds
8
+ - Changed from GitHub Actions cache to Docker Hub registry cache backend
9
+ - Use architecture-specific cache tags (buildcache-amd64, buildcache-arm64) to prevent cross-platform cache overwriting
10
+ - Increased Docker job timeout from 45 to 60 minutes for safety margin
11
+ - Added comprehensive case study documentation for issue #1415
12
+
13
+ ## 1.30.4
14
+
15
+ ### Patch Changes
16
+
17
+ - 65eefd9: fix: prevent solve command from hanging after PR is merged (Issue #1346)
18
+
19
+ Previously, after the solve command detected a merged PR and printed "PR MERGED! Stopping auto-restart-until-mergeable mode", the process would hang indefinitely instead of exiting.
20
+
21
+ Root cause: The `finally` block in `src/solve.mjs` completed all async work but never called `process.exit(0)`. Active handles on the Node.js event loop (from libraries like `command-stream` and network connections) prevent natural process exit. When Sentry is enabled (`--sentry`), `@sentry/profiling-node` native handles also contribute.
22
+
23
+ The fix:
24
+ 1. Restores explicit `šŸ“ Complete log file:` display in the `finally` block (matching original behavior)
25
+ 2. Calls `closeSentry()` from `sentry.lib.mjs` to properly flush Sentry events and release profiling handles when Sentry is enabled (no-op when disabled)
26
+ 3. Calls `process.exit(0)` as a required safety net to prevent hanging from any remaining active handles
27
+ 4. Adds a hard `Promise.race` timeout (3s) around `sentry.close()` in `exit-handler.lib.mjs` to prevent it from hanging if Sentry's transport stalls
28
+
3
29
  ## 1.30.3
4
30
 
5
31
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.30.3",
3
+ "version": "1.30.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -944,7 +944,9 @@ export const executeClaudeCommand = async params => {
944
944
  if (execCommand.kill) {
945
945
  await log(` Sending SIGTERM to process...`, { verbose: true });
946
946
  execCommand.kill('SIGTERM');
947
- setTimeout(() => {
947
+ // Issue #1346: Capture timer handle so it can be cleared after use to avoid
948
+ // leaking an active event loop reference when the process exits before 2s
949
+ const sigkillTimerId = setTimeout(() => {
948
950
  try {
949
951
  if (!execCommand.result?.code) {
950
952
  log(` Process still alive after 2s, sending SIGKILL`, { verbose: true });
@@ -954,6 +956,7 @@ export const executeClaudeCommand = async params => {
954
956
  /* process may have exited */
955
957
  }
956
958
  }, 2000);
959
+ sigkillTimerId.unref();
957
960
  }
958
961
  } catch (e) {
959
962
  await log(` Warning: Could not kill process: ${e.message}`, { verbose: true });
@@ -67,11 +67,17 @@ const showExitMessage = async (reason = 'Process exiting', code = 0) => {
67
67
  export const safeExit = async (code = 0, reason = 'Process completed') => {
68
68
  await showExitMessage(reason, code);
69
69
 
70
- // Close Sentry to flush any pending events and allow the process to exit cleanly
70
+ // Close Sentry to flush any pending events and allow the process to exit cleanly.
71
+ // Use Promise.race with a hard timeout to guarantee sentry.close() never hangs
72
+ // indefinitely — the 2000ms hint passed to sentry.close() is forwarded to internal
73
+ // flush logic, but the outer Promise itself has no built-in deadline, so we enforce one.
71
74
  try {
72
75
  const sentry = await getSentry();
73
76
  if (sentry && sentry.close) {
74
- await sentry.close(2000); // Wait up to 2 seconds for pending events to be sent
77
+ await Promise.race([
78
+ sentry.close(2000),
79
+ new Promise(resolve => setTimeout(resolve, 3000)), // hard 3s deadline
80
+ ]);
75
81
  }
76
82
  } catch {
77
83
  // Ignore Sentry.close() errors - exit anyway
@@ -119,7 +125,7 @@ export const installGlobalExitHandlers = () => {
119
125
  try {
120
126
  const sentry = await getSentry();
121
127
  if (sentry && sentry.close) {
122
- await sentry.close(2000);
128
+ await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
123
129
  }
124
130
  } catch {
125
131
  // Ignore Sentry.close() errors
@@ -140,7 +146,7 @@ export const installGlobalExitHandlers = () => {
140
146
  try {
141
147
  const sentry = await getSentry();
142
148
  if (sentry && sentry.close) {
143
- await sentry.close(2000);
149
+ await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
144
150
  }
145
151
  } catch {
146
152
  // Ignore Sentry.close() errors
@@ -164,7 +170,7 @@ export const installGlobalExitHandlers = () => {
164
170
  try {
165
171
  const sentry = await getSentry();
166
172
  if (sentry && sentry.close) {
167
- await sentry.close(2000);
173
+ await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
168
174
  }
169
175
  } catch {
170
176
  // Ignore Sentry.close() errors
@@ -188,7 +194,7 @@ export const installGlobalExitHandlers = () => {
188
194
  try {
189
195
  const sentry = await getSentry();
190
196
  if (sentry && sentry.close) {
191
- await sentry.close(2000);
197
+ await Promise.race([sentry.close(2000), new Promise(resolve => setTimeout(resolve, 3000))]);
192
198
  }
193
199
  } catch {
194
200
  // Ignore Sentry.close() errors
@@ -769,9 +769,15 @@ export async function waitForCI(owner, repo, prNumber, options = {}, verbose = f
769
769
  }
770
770
 
771
771
  if (onStatusUpdate) {
772
- // Issue #1269: Wrap callback with timeout to prevent infinite blocking
772
+ // Issue #1269: Wrap callback with timeout to prevent infinite blocking; #1346: capture and clear timeout handle to prevent dangling timer
773
773
  try {
774
- await Promise.race([onStatusUpdate(ciStatus), new Promise((_, reject) => setTimeout(() => reject(new Error(`Callback timeout after ${callbackTimeout}ms`)), callbackTimeout))]);
774
+ let callbackTimeoutId;
775
+ await Promise.race([
776
+ onStatusUpdate(ciStatus),
777
+ new Promise((_, reject) => {
778
+ callbackTimeoutId = setTimeout(() => reject(new Error(`Callback timeout after ${callbackTimeout}ms`)), callbackTimeout);
779
+ }),
780
+ ]).finally(() => clearTimeout(callbackTimeoutId));
775
781
  } catch (callbackError) {
776
782
  // Issue #1269: Log callback errors but continue processing
777
783
  console.error(`[ERROR] /merge: Status update callback failed for PR #${prNumber}: ${callbackError.message}`);
package/src/hive.mjs CHANGED
@@ -52,7 +52,13 @@ if (isDirectExecution) {
52
52
  console.log(' Loading dependencies (this may take a moment)...');
53
53
  // Helper function to add timeout to async operations
54
54
  const withTimeout = (promise, timeoutMs, operation) => {
55
- return Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`Operation '${operation}' timed out after ${timeoutMs}ms. This might be due to slow network or npm configuration issues.`)), timeoutMs))]);
55
+ let timeoutId;
56
+ return Promise.race([
57
+ promise,
58
+ new Promise((_, reject) => {
59
+ timeoutId = setTimeout(() => reject(new Error(`Operation '${operation}' timed out after ${timeoutMs}ms. This might be due to slow network or npm configuration issues.`)), timeoutMs);
60
+ }),
61
+ ]).finally(() => clearTimeout(timeoutId));
56
62
  };
57
63
  // Use use-m to dynamically import modules for cross-runtime compatibility
58
64
  if (typeof use === 'undefined') {
@@ -273,8 +273,16 @@ export const closeSentry = async (timeout = 2000) => {
273
273
  return;
274
274
  }
275
275
 
276
+ // Issue #1346: Use Promise.race with a hard deadline so a hung sentry.close()
277
+ // (e.g. from @sentry/profiling-node native thread) cannot block the caller forever.
276
278
  try {
277
- await sentry.close(timeout);
279
+ let hardDeadlineId;
280
+ await Promise.race([
281
+ sentry.close(timeout),
282
+ new Promise(resolve => {
283
+ hardDeadlineId = setTimeout(resolve, timeout + 1000);
284
+ }),
285
+ ]).finally(() => clearTimeout(hardDeadlineId));
278
286
  } catch (error) {
279
287
  // Silently fail if close fails
280
288
  if (process.env.DEBUG === 'true') {
package/src/solve.mjs CHANGED
@@ -41,7 +41,7 @@ const config = await import('./solve.config.lib.mjs');
41
41
  const { initializeConfig, parseArguments } = config;
42
42
  // Import Sentry integration
43
43
  const sentryLib = await import('./sentry.lib.mjs');
44
- const { initializeSentry, addBreadcrumb, reportError } = sentryLib;
44
+ const { initializeSentry, addBreadcrumb, reportError, closeSentry } = sentryLib;
45
45
  const { yargs, hideBin } = await initializeConfig(use);
46
46
  const path = (await use('path')).default;
47
47
  const fs = (await use('fs')).promises;
@@ -1485,14 +1485,15 @@ try {
1485
1485
  $,
1486
1486
  });
1487
1487
  } finally {
1488
- // Clean up temporary directory using repository module
1489
1488
  await cleanupTempDirectory(tempDir, argv, limitReached);
1490
1489
 
1491
- // Show final log file reference (similar to Claude and other agents)
1492
- // This ensures users always know where to find the complete log file
1490
+ // Show final log file reference so users always know where to find the complete log
1493
1491
  if (getLogFile()) {
1494
- const path = await use('path');
1495
- const absoluteLogPath = path.resolve(getLogFile());
1496
- await log(`\nšŸ“ Complete log file: ${absoluteLogPath}`);
1492
+ const finalLogPath = path.resolve(getLogFile());
1493
+ await log(`\nšŸ“ Complete log file: ${finalLogPath}`);
1497
1494
  }
1495
+
1496
+ // Issue #1346: Flush Sentry events before exit.
1497
+ // closeSentry() uses a hard Promise.race deadline so it cannot block indefinitely.
1498
+ await closeSentry();
1498
1499
  }