@link-assistant/hive-mind 0.54.0 → 0.54.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,27 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.54.2
4
+
5
+ ### Patch Changes
6
+
7
+ - c5f5194: Fix Telegram message getting stuck at "Starting solve command..."
8
+ - Add error handling to `executeAndUpdateMessage` function to catch Telegram API errors
9
+ - Fix critical bug where `messageInfo` was being cleared before the final message update
10
+ - Add proper error logging for message edit failures in both immediate and queued execution paths
11
+
12
+ ## 0.54.1
13
+
14
+ ### Patch Changes
15
+
16
+ - 55576af: fix: allow parallel queue execution when no limits exceeded
17
+
18
+ Previously, "Claude process is already running" was treated as a blocking reason on its own, preventing parallel execution even when all system and API limits were within thresholds.
19
+
20
+ Changes:
21
+ - `claude_running` is now tracked as a metric, not a blocking reason
22
+ - Commands can run in parallel as long as actual limits are not exceeded
23
+ - When any limit >= threshold, allow exactly one claude command to pass
24
+
3
25
  ## 0.54.0
4
26
 
5
27
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.54.0",
3
+ "version": "0.54.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",
@@ -673,20 +673,25 @@ function validateGitHubUrl(args, options = {}) {
673
673
  */
674
674
  async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock) {
675
675
  const result = await executeStartScreen(commandName, args);
676
+ const { chat, message_id } = startingMessage;
676
677
 
677
- if (result.warning) {
678
- await ctx.telegram.editMessageText(startingMessage.chat.id, startingMessage.message_id, undefined, `⚠️ ${result.warning}`, { parse_mode: 'Markdown' });
679
- return;
680
- }
678
+ // Safely edit message - catch errors to prevent stuck "Starting..." messages (issue #1062)
679
+ const safeEdit = async text => {
680
+ try {
681
+ await ctx.telegram.editMessageText(chat.id, message_id, undefined, text, { parse_mode: 'Markdown' });
682
+ } catch (e) {
683
+ console.error(`[telegram-bot] Failed to update message for ${commandName}: ${e.message}`);
684
+ }
685
+ };
686
+
687
+ if (result.warning) return safeEdit(`⚠️ ${result.warning}`);
681
688
 
682
689
  if (result.success) {
683
- const sessionNameMatch = result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -r\s+(\S+)/);
684
- const sessionName = sessionNameMatch ? sessionNameMatch[1] : 'unknown';
685
- const response = `✅ ${commandName.charAt(0).toUpperCase() + commandName.slice(1)} command started successfully!\n\n📊 Session: \`${sessionName}\`\n\n${infoBlock}`;
686
- await ctx.telegram.editMessageText(startingMessage.chat.id, startingMessage.message_id, undefined, response, { parse_mode: 'Markdown' });
690
+ const match = result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -r\s+(\S+)/);
691
+ const session = match ? match[1] : 'unknown';
692
+ await safeEdit(`✅ ${commandName.charAt(0).toUpperCase() + commandName.slice(1)} command started successfully!\n\n📊 Session: \`${session}\`\n\n${infoBlock}`);
687
693
  } else {
688
- const response = `❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``;
689
- await ctx.telegram.editMessageText(startingMessage.chat.id, startingMessage.message_id, undefined, response, { parse_mode: 'Markdown' });
694
+ await safeEdit(`❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``);
690
695
  }
691
696
  }
692
697
 
@@ -357,6 +357,12 @@ export class SolveQueue {
357
357
 
358
358
  /**
359
359
  * Check if a new command can start
360
+ *
361
+ * Logic per issue #1061:
362
+ * 1. "Claude process is already running" is NOT a limit by itself - it's a metric
363
+ * 2. Commands can run in parallel as long as actual limits are not exceeded
364
+ * 3. When any limit >= threshold, allow exactly one claude command to pass
365
+ *
360
366
  * @returns {Promise<{canStart: boolean, reason?: string, reasons?: string[], oneAtATime?: boolean}>}
361
367
  */
362
368
  async canStartCommand() {
@@ -373,10 +379,12 @@ export class SolveQueue {
373
379
  }
374
380
  }
375
381
 
376
- // Check running claude processes
382
+ // Check running claude processes (this is a metric, not a blocking reason by itself)
377
383
  const claudeProcs = await getRunningClaudeProcesses(this.verbose);
378
- if (claudeProcs.count > 0) {
379
- reasons.push(formatWaitingReason('claude_running', claudeProcs.count, 0) + ` (${claudeProcs.count} processes)`);
384
+ const hasRunningClaude = claudeProcs.count > 0;
385
+
386
+ // Track claude_running as a metric (but don't add to reasons yet)
387
+ if (hasRunningClaude) {
380
388
  this.recordThrottle('claude_running');
381
389
  }
382
390
 
@@ -389,8 +397,8 @@ export class SolveQueue {
389
397
  oneAtATime = true;
390
398
  }
391
399
 
392
- // Check API limits
393
- const limitCheck = await this.checkApiLimits(claudeProcs.count > 0);
400
+ // Check API limits (pass hasRunningClaude to enable special handling)
401
+ const limitCheck = await this.checkApiLimits(hasRunningClaude);
394
402
  if (!limitCheck.ok) {
395
403
  reasons.push(...limitCheck.reasons);
396
404
  }
@@ -398,6 +406,13 @@ export class SolveQueue {
398
406
  oneAtATime = true;
399
407
  }
400
408
 
409
+ // "Claude process running" only blocks if there are OTHER reasons too
410
+ // This allows parallel execution when limits are not exceeded
411
+ if (hasRunningClaude && reasons.length > 0) {
412
+ // Add claude_running info only when combined with actual limit reasons
413
+ reasons.unshift(formatWaitingReason('claude_running', claudeProcs.count, 0) + ` (${claudeProcs.count} processes)`);
414
+ }
415
+
401
416
  const canStart = reasons.length === 0;
402
417
 
403
418
  if (!canStart && this.verbose) {
@@ -461,6 +476,12 @@ export class SolveQueue {
461
476
 
462
477
  /**
463
478
  * Check API limits (Claude, GitHub) using cached values
479
+ *
480
+ * Simplified logic per issue #1061:
481
+ * - When any limit >= threshold, allow exactly one claude command to pass
482
+ * - Only block if there's already a command in progress
483
+ * - Running claude is the ultimate test of whether limits are really exhausted
484
+ *
464
485
  * @param {boolean} hasRunningClaude - Whether claude processes are running
465
486
  * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean}>}
466
487
  */
@@ -475,29 +496,26 @@ export class SolveQueue {
475
496
  const weeklyPercent = claudeResult.usage.allModels.percentage;
476
497
 
477
498
  // Session limit (5-hour)
499
+ // When above threshold: allow exactly one command, block if one is running
478
500
  if (sessionPercent !== null) {
479
501
  const sessionRatio = sessionPercent / 100;
480
- if (sessionRatio >= 1.0) {
481
- reasons.push('Claude session limit is 100% (waiting for reset)');
482
- this.recordThrottle('claude_session_100');
483
- } else if (sessionRatio >= QUEUE_CONFIG.CLAUDE_SESSION_THRESHOLD) {
484
- reasons.push(formatWaitingReason('claude_session', sessionPercent, QUEUE_CONFIG.CLAUDE_SESSION_THRESHOLD));
485
- this.recordThrottle('claude_session_high');
502
+ if (sessionRatio >= QUEUE_CONFIG.CLAUDE_SESSION_THRESHOLD) {
503
+ // Only block if Claude is already running
504
+ if (hasRunningClaude) {
505
+ reasons.push(formatWaitingReason('claude_session', sessionPercent, QUEUE_CONFIG.CLAUDE_SESSION_THRESHOLD));
506
+ }
507
+ this.recordThrottle(sessionRatio >= 1.0 ? 'claude_session_100' : 'claude_session_high');
486
508
  }
487
509
  }
488
510
 
489
511
  // Weekly limit
512
+ // When above threshold: allow exactly one command, block if one is in progress
490
513
  if (weeklyPercent !== null) {
491
514
  const weeklyRatio = weeklyPercent / 100;
492
- if (weeklyRatio >= 1.0) {
515
+ if (weeklyRatio >= QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) {
493
516
  oneAtATime = true;
494
- this.recordThrottle('claude_weekly_100');
495
- if (this.processing.size > 0) {
496
- reasons.push('Claude weekly limit is 100% (waiting for current command)');
497
- }
498
- } else if (weeklyRatio >= QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) {
499
- oneAtATime = true;
500
- this.recordThrottle('claude_weekly_high');
517
+ this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : 'claude_weekly_high');
518
+ // Only block if command is already in progress
501
519
  if (this.processing.size > 0) {
502
520
  reasons.push(formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) + ' (waiting for current command)');
503
521
  }
@@ -511,12 +529,9 @@ export class SolveQueue {
511
529
  if (githubResult.success) {
512
530
  const usedPercent = githubResult.githubRateLimit.usedPercentage;
513
531
  const usedRatio = usedPercent / 100;
514
- if (usedRatio >= 1.0) {
515
- reasons.push('GitHub API limit is 100% (waiting for reset)');
516
- this.recordThrottle('github_100');
517
- } else if (usedRatio >= QUEUE_CONFIG.GITHUB_API_THRESHOLD) {
532
+ if (usedRatio >= QUEUE_CONFIG.GITHUB_API_THRESHOLD) {
518
533
  reasons.push(formatWaitingReason('github', usedPercent, QUEUE_CONFIG.GITHUB_API_THRESHOLD));
519
- this.recordThrottle('github_high');
534
+ this.recordThrottle(usedRatio >= 1.0 ? 'github_100' : 'github_high');
520
535
  }
521
536
  }
522
537
  }
@@ -652,13 +667,18 @@ export class SolveQueue {
652
667
  if (sessionMatch) sessionName = sessionMatch[1];
653
668
  }
654
669
 
670
+ // IMPORTANT: Save messageInfo BEFORE calling setStarted, because setStarted clears it
671
+ // This was a bug where the final message update never happened because messageInfo was null
672
+ // See: https://github.com/link-assistant/hive-mind/issues/1062
673
+ const savedMessageInfo = item.messageInfo;
674
+
655
675
  // Update to Started status (terminal - forgets message tracking)
656
676
  item.setStarted(sessionName);
657
677
  this.stats.totalCompleted++;
658
678
 
659
- // Final message update before forgetting
660
- if (item.ctx && result) {
661
- const { chatId, messageId } = item.messageInfo || {};
679
+ // Final message update using saved messageInfo
680
+ if (item.ctx && result && savedMessageInfo) {
681
+ const { chatId, messageId } = savedMessageInfo;
662
682
  if (chatId && messageId) {
663
683
  try {
664
684
  if (result.warning) {
@@ -670,8 +690,10 @@ export class SolveQueue {
670
690
  const response = `❌ Error executing solve command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``;
671
691
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, response, { parse_mode: 'Markdown' });
672
692
  }
673
- } catch {
674
- // Ignore message edit failures
693
+ } catch (error) {
694
+ // Log message edit failures for debugging
695
+ // See: https://github.com/link-assistant/hive-mind/issues/1062
696
+ console.error(`[solve-queue] Failed to update message for item ${item.id}: ${error.message}`);
675
697
  }
676
698
  }
677
699
  }
@@ -689,8 +711,10 @@ export class SolveQueue {
689
711
  if (chatId && messageId && item.ctx) {
690
712
  try {
691
713
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, `❌ Error: ${error.message}`, { parse_mode: 'Markdown' });
692
- } catch {
693
- // Ignore
714
+ } catch (editError) {
715
+ // Log the edit failure for debugging
716
+ // See: https://github.com/link-assistant/hive-mind/issues/1062
717
+ console.error(`[solve-queue] Failed to update error message for item ${item.id}: ${editError.message}`);
694
718
  }
695
719
  }
696
720
  } finally {