@link-assistant/hive-mind 1.49.1 → 1.49.2

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,15 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.49.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 026c95c: fix: non-consistent auto-restart logic on comments (#1567)
8
+ - Reduce CI check interval from 5 minutes to 2 minutes for faster response times
9
+ - Prevent concurrent sessions on the same PR/issue via active session URL checking
10
+ - Add cross-process deduplication for "Ready to merge" comments
11
+ - Add initial 2-minute cooldown before first mergeable check to ensure proper ordering
12
+
3
13
  ## 1.49.1
4
14
 
5
15
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.49.1",
3
+ "version": "1.49.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -243,6 +243,39 @@ export function startSessionMonitoring(bot, verbose = false, intervalMs = 30000)
243
243
  return timer;
244
244
  }
245
245
 
246
+ /**
247
+ * Issue #1567: Check if there's an active session for a given URL.
248
+ * This prevents concurrent sessions on the same PR/issue, which causes
249
+ * iteration number jumps, duplicate "Ready to merge" comments, and other
250
+ * inconsistencies when two auto-restart-until-mergeable processes run
251
+ * simultaneously.
252
+ *
253
+ * @param {string} url - The GitHub URL to check (issue or PR URL)
254
+ * @param {boolean} verbose - Whether to log verbose output
255
+ * @returns {{isActive: boolean, sessionName: string|null}} Whether an active session exists for this URL
256
+ */
257
+ export function hasActiveSessionForUrl(url, verbose = false) {
258
+ if (!url) return { isActive: false, sessionName: null };
259
+
260
+ // Normalize the URL for comparison (remove trailing slashes, fragments, etc.)
261
+ const normalizeUrl = u => u.replace(/\/+$/, '').replace(/#.*$/, '').toLowerCase();
262
+ const normalizedUrl = normalizeUrl(url);
263
+
264
+ for (const [sessionName, sessionInfo] of activeSessions.entries()) {
265
+ if (sessionInfo.url && normalizeUrl(sessionInfo.url) === normalizedUrl) {
266
+ if (verbose) {
267
+ console.log(`[VERBOSE] Found active session for URL ${url}: ${sessionName}`);
268
+ }
269
+ return { isActive: true, sessionName };
270
+ }
271
+ }
272
+
273
+ if (verbose) {
274
+ console.log(`[VERBOSE] No active session found for URL ${url}`);
275
+ }
276
+ return { isActive: false, sessionName: null };
277
+ }
278
+
246
279
  /**
247
280
  * Get statistics about session tracking
248
281
  * @param {boolean} verbose - Whether to log verbose output
@@ -510,9 +510,11 @@ export const watchUntilMergeable = async params => {
510
510
  const { issueUrl, owner, repo, issueNumber, prNumber, prBranch, branchName, tempDir, argv } = params;
511
511
 
512
512
  const rawWatchInterval = argv.watchInterval || 60; // seconds
513
- // Issue #1503: Enforce minimum 5-minute (300s) CI check interval to conserve GitHub API rate limits.
514
- // This prevents excessive API calls during long-running CI pipelines.
515
- const MIN_CI_CHECK_INTERVAL_SECONDS = 300;
513
+ // Issue #1503: Enforce minimum CI check interval to conserve GitHub API rate limits.
514
+ // Issue #1567: Reduced from 5 minutes (300s) to 2 minutes (120s) to decrease wait times
515
+ // between working session finish and "Ready to merge" / next action detection.
516
+ // This also applies uniformly whether CI/CD is configured or not.
517
+ const MIN_CI_CHECK_INTERVAL_SECONDS = 120;
516
518
  const watchInterval = Math.max(rawWatchInterval, MIN_CI_CHECK_INTERVAL_SECONDS);
517
519
  const isAutoMerge = argv.autoMerge || false;
518
520
  // Issue #1503: --wait-for-all-actions-in-repository-before-mergable (default: true)
@@ -549,11 +551,20 @@ export const watchUntilMergeable = async params => {
549
551
  let consecutiveNoRunsChecks = 0;
550
552
  let lastKnownHeadSha = null;
551
553
 
554
+ // Issue #1567: Initial cooldown before first check.
555
+ // Wait at least MIN_CI_CHECK_INTERVAL_SECONDS after working session finishes before
556
+ // starting to check. This ensures:
557
+ // 1. Solution Draft Log is fully posted before any "Ready to merge" can appear
558
+ // 2. CI/CD checks have time to register with GitHub (avoids false "no CI" detection)
559
+ // 3. Consistent behavior whether CI/CD is configured or not
560
+ const INITIAL_COOLDOWN_SECONDS = MIN_CI_CHECK_INTERVAL_SECONDS;
561
+
552
562
  await log('');
553
563
  await log(formatAligned('šŸ”„', 'AUTO-RESTART-UNTIL-MERGEABLE MODE ACTIVE', ''));
554
564
  await log(formatAligned('', 'Monitoring PR:', `#${prNumber}`, 2));
555
565
  await log(formatAligned('', 'Mode:', isAutoMerge ? 'Auto-merge (will merge when ready)' : 'Auto-restart-until-mergeable (will NOT auto-merge)', 2));
556
566
  await log(formatAligned('', 'Checking interval:', `${watchInterval} seconds (minimum: ${MIN_CI_CHECK_INTERVAL_SECONDS}s)`, 2));
567
+ await log(formatAligned('', 'Initial cooldown:', `${INITIAL_COOLDOWN_SECONDS} seconds`, 2));
557
568
  await log(formatAligned('', 'Wait for all repo actions:', waitForAllRepoActionsFlag ? 'Yes (absolute safety)' : 'No', 2));
558
569
  await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
559
570
  await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
@@ -561,6 +572,13 @@ export const watchUntilMergeable = async params => {
561
572
  await log('Press Ctrl+C to stop watching manually');
562
573
  await log('');
563
574
 
575
+ // Issue #1567: Wait for initial cooldown before first check.
576
+ // This gives CI/CD time to start and solution logs time to be posted.
577
+ await log(formatAligned('ā³', 'Initial cooldown:', `Waiting ${INITIAL_COOLDOWN_SECONDS}s before first check...`));
578
+ await new Promise(resolve => setTimeout(resolve, INITIAL_COOLDOWN_SECONDS * 1000));
579
+ await log(formatAligned('āœ…', 'Cooldown complete:', 'Starting monitoring loop'));
580
+ await log('');
581
+
564
582
  let iteration = 0;
565
583
  let lastCheckTime = new Date();
566
584
 
@@ -732,16 +750,28 @@ export const watchUntilMergeable = async params => {
732
750
  await log(formatAligned('', 'Exiting auto-restart-until-mergeable mode', '', 2));
733
751
 
734
752
  // Issue #1371: Post success comment only if not already posted in this session.
735
- // Use in-memory flag instead of checking all PR comment history (issue #1323),
736
- // since the historical check incorrectly suppressed notifications when a
737
- // previous solve run had already posted a "Ready to merge" comment.
753
+ // Issue #1567: Also check PR comment history as a cross-process guard.
754
+ // Two layers of deduplication:
755
+ // 1. In-memory flag (readyToMergeCommentPosted) — prevents duplicates within this process
756
+ // 2. checkForExistingComment — prevents duplicates from concurrent processes
757
+ // The in-memory flag is reset when HEAD SHA changes (line 614), so a new commit
758
+ // will allow a fresh "Ready to merge" comment.
738
759
  try {
739
760
  if (!readyToMergeCommentPosted) {
740
- // Issue #1345: Differentiate message when no CI is configured
741
- const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
742
- const commentBody = `## āœ… Ready to merge\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
743
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
744
- readyToMergeCommentPosted = true;
761
+ // Issue #1567: Cross-process deduplication — check if another process already
762
+ // posted a "Ready to merge" comment. This catches the case where two concurrent
763
+ // watchUntilMergeable processes both detect mergeability simultaneously.
764
+ const hasExistingReadyComment = await checkForExistingComment(owner, repo, prNumber, '## āœ… Ready to merge', argv.verbose);
765
+ if (hasExistingReadyComment) {
766
+ await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment (already posted by another process)', '', 2));
767
+ readyToMergeCommentPosted = true;
768
+ } else {
769
+ // Issue #1345: Differentiate message when no CI is configured
770
+ const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
771
+ const commentBody = `## āœ… Ready to merge\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
772
+ await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
773
+ readyToMergeCommentPosted = true;
774
+ }
745
775
  } else {
746
776
  await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment (already posted this session)', '', 2));
747
777
  }
@@ -48,7 +48,7 @@ const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-s
48
48
  const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
49
49
  const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
50
50
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
51
- const { trackSession, startSessionMonitoring } = await import('./session-monitor.lib.mjs');
51
+ const { trackSession, startSessionMonitoring, hasActiveSessionForUrl } = await import('./session-monitor.lib.mjs');
52
52
 
53
53
  const config = yargs(hideBin(process.argv))
54
54
  .usage('Usage: hive-telegram-bot [options]')
@@ -238,11 +238,8 @@ if (hiveEnabled && hiveOverrides.length > 0) {
238
238
  throw new Error(msg);
239
239
  });
240
240
  await testYargs.parse(testArgs);
241
- // Issue #1482: Validate --base-branch/--target-branch in overrides early
242
- const overrideBranchError = validateBranchInArgs(hiveOverrides);
243
- if (overrideBranchError) {
244
- throw new Error(overrideBranchError);
245
- }
241
+ const overrideBranchError = validateBranchInArgs(hiveOverrides); // Issue #1482
242
+ if (overrideBranchError) throw new Error(overrideBranchError);
246
243
  console.log('āœ… Hive overrides validated successfully');
247
244
  } finally {
248
245
  // Restore stderr
@@ -755,17 +752,9 @@ bot.command('limits', async ctx => {
755
752
  // Get all limits using shared cache (3min for API, 2min for system)
756
753
  const limits = await getAllCachedLimits(VERBOSE);
757
754
 
758
- // Format the message with usage limits and queue status
759
- // If Claude auth failed, pass the error to formatUsageMessage to show it in the Claude sections
760
- // while still displaying all other limits sections (disk, GitHub, CPU, memory)
761
- // See: https://github.com/link-assistant/hive-mind/issues/1343
755
+ // Format message with usage limits and queue status (issues #1343, #1267)
762
756
  const claudeError = limits.claude.success ? null : limits.claude.error;
763
757
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
764
- // Fetch queue status and pass it as an extra section to formatUsageMessage so that all
765
- // sections are assembled before the code block is formed — no fragile string-searching needed.
766
- // Shows each queue (claude, agent) with pending/processing counts.
767
- // Processing counts are actual running system processes (via pgrep).
768
- // See: https://github.com/link-assistant/hive-mind/issues/1267
769
758
  const queueStatus = await solveQueue.formatStatus();
770
759
  const message = 'šŸ“Š *Usage Limits*\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [queueStatus]);
771
760
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
@@ -1007,22 +996,22 @@ async function handleSolveCommand(ctx) {
1007
996
  if (solveOverrides.length > 0) infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}šŸ”’ Locked options: ${escapeMarkdown(solveOverrides.join(' '))}`;
1008
997
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
1009
998
 
1010
- // Check for duplicate URL in queue
1011
- // See: https://github.com/link-assistant/hive-mind/issues/1080
999
+ // Check for duplicate URL in queue (issue #1080)
1012
1000
  const existingItem = solveQueue.findByUrl(normalizedUrl);
1013
1001
  if (existingItem) {
1014
1002
  const statusText = existingItem.status === 'starting' || existingItem.status === 'started' ? 'being processed' : 'already in the queue';
1015
1003
  await safeReply(ctx, `āŒ This URL is ${statusText}.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nStatus: ${existingItem.status}\n\nšŸ’” Use /solve_queue to check the queue status.`, { reply_to_message_id: ctx.message.message_id });
1016
1004
  return;
1017
1005
  }
1018
-
1006
+ // Issue #1567: Prevent concurrent sessions on the same PR/issue
1007
+ const activeSession = hasActiveSessionForUrl(normalizedUrl, VERBOSE);
1008
+ if (activeSession.isActive) {
1009
+ await safeReply(ctx, `āŒ A working session is already running for this URL.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nSession: \`${activeSession.sessionName}\`\n\nšŸ’” Wait for the current session to complete, or use /solve\\_stop to cancel it.`, { reply_to_message_id: ctx.message.message_id });
1010
+ return;
1011
+ }
1019
1012
  const check = await solveQueue.canStartCommand({ tool: solveTool }); // Skip Claude limits for agent (#1159)
1020
1013
  const queueStats = solveQueue.getStats();
1021
-
1022
- // Handle rejection: when a threshold strategy is 'reject', the command should fail immediately
1023
- // without being placed in the queue. This ensures users get clear feedback about why
1024
- // their command cannot be processed (e.g., disk full, server maintenance pending).
1025
- // See: https://github.com/link-assistant/hive-mind/issues/1267
1014
+ // Handle rejection: threshold strategy is 'reject' — fail immediately (issue #1267)
1026
1015
  if (check.rejected) {
1027
1016
  await safeReply(ctx, `āŒ Solve command rejected.\n\n${infoBlock}\n\n🚫 Reason: ${escapeMarkdown(check.rejectReason || 'Unknown')}`, { reply_to_message_id: ctx.message.message_id });
1028
1017
  return;
@@ -1288,13 +1277,11 @@ bot.on('message', async (ctx, next) => {
1288
1277
  bot.catch((error, ctx) => {
1289
1278
  console.error('Unhandled error while processing update', ctx.update.update_id);
1290
1279
  console.error('Error:', error);
1291
- // Log detailed error information
1292
1280
  console.error('Error details:', {
1293
1281
  name: error.name,
1294
1282
  message: error.message,
1295
1283
  stack: error.stack?.split('\n').slice(0, 10).join('\n'),
1296
1284
  });
1297
- // Log context information for debugging
1298
1285
  if (VERBOSE) {
1299
1286
  console.log('[VERBOSE] Error context:', {
1300
1287
  chatId: ctx.chat?.id,
@@ -1319,7 +1306,6 @@ bot.catch((error, ctx) => {
1319
1306
 
1320
1307
  // Try to notify the user about the error with more details
1321
1308
  if (ctx?.reply) {
1322
- // Detect if this is a Telegram API parsing error
1323
1309
  const isTelegramParsingError = error.message && (error.message.includes("can't parse entities") || error.message.includes("Can't parse entities") || error.message.includes("can't find end of") || (error.message.includes('Bad Request') && error.message.includes('400')));
1324
1310
 
1325
1311
  let errorMessage;
@@ -1342,7 +1328,6 @@ bot.catch((error, ctx) => {
1342
1328
  // Issue #1460: Show user a simple, non-confusing message — all details are in the logs
1343
1329
  errorMessage = `āŒ Failed to send formatted message. Please try your command again.\n\nIf the issue persists, contact support with Update ID: ${ctx.update.update_id}`;
1344
1330
  } else {
1345
- // Build informative error message for other errors
1346
1331
  errorMessage = 'āŒ An error occurred while processing your request.\n\n';
1347
1332
  if (error.message) {
1348
1333
  // Filter out sensitive info and escape markdown
@@ -1358,8 +1343,7 @@ bot.catch((error, ctx) => {
1358
1343
  if (VERBOSE) errorMessage += `\n\nšŸ” Debug info: Update ID: ${ctx.update.update_id}`;
1359
1344
  }
1360
1345
 
1361
- // Issue #1460: For parsing errors, always send as plain text (we already know Markdown is the problem)
1362
- // For other errors, try Markdown first, then fall back to plain text
1346
+ // Issue #1460: For parsing errors send plain text; otherwise try Markdown first
1363
1347
  if (isTelegramParsingError) {
1364
1348
  ctx.reply(errorMessage).catch(fallbackError => {
1365
1349
  console.error('Failed to send plain text error message:', fallbackError);