@link-assistant/hive-mind 1.6.0 → 1.6.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,22 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.6.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 4ccbbd7: Fix CLAUDE_WEEKLY_THRESHOLD not enforcing one-at-a-time mode when external Claude processes are running
8
+ - Fixed oneAtATime mode to also consider externally running Claude processes (detected via pgrep), not just queue-internal processing
9
+ - Standardized all threshold comparisons to use >= (inclusive) instead of mixed > and >= operators
10
+ - Updated documentation comments to accurately reflect inclusive threshold behavior
11
+ - Added README recommendation to capture bot logs using tee for post-incident analysis
12
+ - Added case study documentation for issue #1133
13
+
14
+ ## 1.6.1
15
+
16
+ ### Patch Changes
17
+
18
+ - b07fa91: Improve /limits output format for better clarity and consistency: use 5m load average for CPU calculation (matching /solve queue), show CPU cores as "X.XX/Y CPU cores used" format consistent with RAM and Disk display
19
+
3
20
  ## 1.6.0
4
21
 
5
22
  ### Minor Changes
@@ -455,7 +472,7 @@
455
472
  - `RAM_THRESHOLD: 0.5` - Stop new commands if RAM usage > 50%
456
473
  - `CPU_THRESHOLD: 0.5` - Stop new commands if CPU usage > 50%
457
474
  - `DISK_THRESHOLD: 0.95` - One-at-a-time mode if disk usage > 95%
458
- - `CLAUDE_SESSION_THRESHOLD: 0.9` - Stop if Claude 5-hour limit > 90%
475
+ - `CLAUDE_5_HOUR_SESSION_THRESHOLD: 0.9` - Stop if Claude 5-hour limit > 90%
459
476
  - `CLAUDE_WEEKLY_THRESHOLD: 0.99` - One-at-a-time mode if weekly limit > 99%
460
477
  - `GITHUB_API_THRESHOLD: 0.8` - Stop if GitHub API > 80% with parallel claude commands
461
478
  - 1-minute minimum interval between command starts
package/README.md CHANGED
@@ -409,10 +409,26 @@ Want to see the Hive Mind in action? Join our Telegram channel where you can exe
409
409
  ```
410
410
 
411
411
  3. **Start the Bot**
412
+
412
413
  ```bash
413
414
  hive-telegram-bot
414
415
  ```
415
416
 
417
+ **Recommended: Capture logs with tee**
418
+
419
+ When running the bot for extended periods, it's recommended to capture logs to a file using `tee`. This ensures you can review logs later even if the terminal buffer overflows:
420
+
421
+ ```bash
422
+ hive-telegram-bot 2>&1 | tee -a logs/bot-$(date +%Y%m%d).log
423
+ ```
424
+
425
+ Or create a logs directory and start with automatic log rotation:
426
+
427
+ ```bash
428
+ mkdir -p logs
429
+ hive-telegram-bot 2>&1 | tee -a "logs/bot-$(date +%Y%m%d-%H%M%S).log"
430
+ ```
431
+
416
432
  ### Bot Commands
417
433
 
418
434
  All commands work in **group chats only** (not in private messages with the bot):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.6.0",
3
+ "version": "1.6.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",
@@ -13,8 +13,9 @@
13
13
  "hive-telegram-bot": "./src/telegram-bot.mjs"
14
14
  },
15
15
  "scripts": {
16
- "test": "node tests/solve-queue.test.mjs && node tests/test-usage-limit.mjs",
16
+ "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs",
17
17
  "test:queue": "node tests/solve-queue.test.mjs",
18
+ "test:limits-display": "node tests/limits-display.test.mjs",
18
19
  "test:usage-limit": "node tests/test-usage-limit.mjs",
19
20
  "lint": "eslint 'src/**/*.{js,mjs,cjs}'",
20
21
  "lint:fix": "eslint 'src/**/*.{js,mjs,cjs}' --fix",
@@ -139,6 +139,25 @@ function formatBytes(bytes) {
139
139
  return `${value.toFixed(decimals)} ${sizes[i]}`;
140
140
  }
141
141
 
142
+ /**
143
+ * Format two byte values into a combined "used/total UNIT used" format
144
+ * @param {number} usedBytes - Used size in bytes
145
+ * @param {number} totalBytes - Total size in bytes
146
+ * @returns {string} Formatted string (e.g., "2.8/11.7 GB used")
147
+ */
148
+ function formatBytesRange(usedBytes, totalBytes) {
149
+ if (totalBytes === 0) return '0/0 B used';
150
+ const k = 1024;
151
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
152
+ // Determine unit based on total (larger value)
153
+ const i = Math.floor(Math.log(totalBytes) / Math.log(k));
154
+ const usedValue = usedBytes / Math.pow(k, i);
155
+ const totalValue = totalBytes / Math.pow(k, i);
156
+ // Use 1 decimal place for GB and above, none for smaller units
157
+ const decimals = i >= 3 ? 1 : 0;
158
+ return `${usedValue.toFixed(decimals)}/${totalValue.toFixed(decimals)} ${sizes[i]} used`;
159
+ }
160
+
142
161
  /**
143
162
  * Get GitHub API rate limits by calling gh api rate_limit
144
163
  * Returns rate limit info for core, search, graphql, and other resources
@@ -266,9 +285,10 @@ export async function getCpuLoadInfo(verbose = false) {
266
285
  };
267
286
  }
268
287
 
269
- // Calculate usage percentage based on load average vs CPU count
288
+ // Calculate usage percentage based on 5-minute load average vs CPU count
270
289
  // Load average of 1.0 per CPU = 100% utilization
271
- const usagePercentage = Math.min(100, Math.round((loadAvg1 / cpuCount) * 100));
290
+ // Using 5m average for consistency with solve queue (see issue #1137)
291
+ const usagePercentage = Math.min(100, Math.round((loadAvg5 / cpuCount) * 100));
272
292
 
273
293
  if (verbose) {
274
294
  console.log(`[VERBOSE] /limits CPU load: ${loadAvg1.toFixed(2)} (1m), ${loadAvg5.toFixed(2)} (5m), ${loadAvg15.toFixed(2)} (15m), ${cpuCount} CPUs, ${usagePercentage}% used`);
@@ -663,8 +683,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
663
683
  message += 'CPU\n';
664
684
  const usedBar = getProgressBar(cpuLoad.usagePercentage);
665
685
  message += `${usedBar} ${cpuLoad.usagePercentage}% used\n`;
666
- message += `Load avg: ${cpuLoad.loadAvg1.toFixed(2)} (1m) ${cpuLoad.loadAvg5.toFixed(2)} (5m) ${cpuLoad.loadAvg15.toFixed(2)} (15m)\n`;
667
- message += `${cpuLoad.cpuCount} CPU core${cpuLoad.cpuCount > 1 ? 's' : ''}\n\n`;
686
+ // Show cores used based on 5m load average (e.g., "0.04/6 CPU cores used" or "3/6 CPU cores used")
687
+ // Use parseFloat to strip unnecessary trailing zeros (3.00 -> 3, 0.10 -> 0.1, 0.04 -> 0.04)
688
+ message += `${parseFloat(cpuLoad.loadAvg5.toFixed(2))}/${cpuLoad.cpuCount} CPU cores used\n\n`;
668
689
  }
669
690
 
670
691
  // Memory section (if provided)
@@ -672,7 +693,7 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
672
693
  message += 'RAM\n';
673
694
  const usedBar = getProgressBar(memory.usedPercentage);
674
695
  message += `${usedBar} ${memory.usedPercentage}% used\n`;
675
- message += `${memory.usedFormatted} used of ${memory.totalFormatted}\n\n`;
696
+ message += `${formatBytesRange(memory.usedBytes, memory.totalBytes)}\n\n`;
676
697
  }
677
698
 
678
699
  // Disk space section (if provided)
@@ -681,7 +702,7 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
681
702
  // Show used percentage with progress bar
682
703
  const usedBar = getProgressBar(diskSpace.usedPercentage);
683
704
  message += `${usedBar} ${diskSpace.usedPercentage}% used\n`;
684
- message += `${diskSpace.usedFormatted} used of ${diskSpace.totalFormatted}\n\n`;
705
+ message += `${formatBytesRange(diskSpace.usedBytes, diskSpace.totalBytes)}\n\n`;
685
706
  }
686
707
 
687
708
  // GitHub API rate limits section (if provided)
@@ -699,8 +720,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
699
720
  message += '\n';
700
721
  }
701
722
 
702
- // Current session (five_hour)
703
- message += 'Current session\n';
723
+ // Claude 5 hour session (five_hour)
724
+ message += 'Claude 5 hour session\n';
704
725
  if (usage.currentSession.percentage !== null) {
705
726
  // Add time passed progress bar first
706
727
  const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
@@ -710,7 +731,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
710
731
  }
711
732
 
712
733
  // Add usage progress bar second
713
- const pct = usage.currentSession.percentage;
734
+ // Use Math.floor so 100% only appears when usage is exactly 100%
735
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
736
+ const pct = Math.floor(usage.currentSession.percentage);
714
737
  const bar = getProgressBar(pct);
715
738
  message += `${bar} ${pct}% used\n`;
716
739
 
@@ -738,7 +761,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
738
761
  }
739
762
 
740
763
  // Add usage progress bar second
741
- const pct = usage.allModels.percentage;
764
+ // Use Math.floor so 100% only appears when usage is exactly 100%
765
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
766
+ const pct = Math.floor(usage.allModels.percentage);
742
767
  const bar = getProgressBar(pct);
743
768
  message += `${bar} ${pct}% used\n`;
744
769
 
@@ -766,7 +791,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
766
791
  }
767
792
 
768
793
  // Add usage progress bar second
769
- const pct = usage.sonnetOnly.percentage;
794
+ // Use Math.floor so 100% only appears when usage is exactly 100%
795
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
796
+ const pct = Math.floor(usage.sonnetOnly.percentage);
770
797
  const bar = getProgressBar(pct);
771
798
  message += `${bar} ${pct}% used\n`;
772
799
 
@@ -858,9 +858,13 @@ bot.command('limits', async ctx => {
858
858
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
859
859
  const queueStats = solveQueue.getStats();
860
860
  const claudeProcs = await getRunningClaudeProcesses(VERBOSE);
861
+ // Calculate total processing: queue-internal + external claude processes
862
+ // This provides a uniform view of all processing happening
863
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
864
+ const totalProcessing = queueStats.processing + claudeProcs.count;
861
865
  const codeBlockEnd = message.lastIndexOf('```');
862
866
  if (codeBlockEnd !== -1) {
863
- const queueStatus = queueStats.queued > 0 || queueStats.processing > 0 ? `Pending: ${queueStats.queued}, Processing: ${queueStats.processing}` : 'Empty (no pending commands)';
867
+ const queueStatus = queueStats.queued > 0 || totalProcessing > 0 ? `Pending: ${queueStats.queued}, Processing: ${totalProcessing}` : 'Empty (no pending commands)';
864
868
  message = message.slice(0, codeBlockEnd) + `\nSolve Queue\n${queueStatus}\nClaude processes: ${claudeProcs.count}\n` + message.slice(codeBlockEnd);
865
869
  }
866
870
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
@@ -33,15 +33,17 @@ import { getCachedClaudeLimits, getCachedGitHubLimits, getCachedMemoryInfo, getC
33
33
  */
34
34
  export const QUEUE_CONFIG = {
35
35
  // Resource thresholds (usage ratios: 0.0 - 1.0)
36
- RAM_THRESHOLD: 0.5, // Stop if RAM usage > 50%
36
+ // All thresholds use >= comparison (inclusive)
37
+ RAM_THRESHOLD: 0.5, // Stop if RAM usage >= 50%
37
38
  // CPU threshold uses 5-minute load average, not instantaneous CPU usage
38
- CPU_THRESHOLD: 0.5, // Stop if 5-minute load average > 50% of CPU count
39
- DISK_THRESHOLD: 0.95, // One-at-a-time if disk usage > 95%
39
+ CPU_THRESHOLD: 0.5, // Stop if 5-minute load average >= 50% of CPU count
40
+ DISK_THRESHOLD: 0.95, // One-at-a-time if disk usage >= 95%
40
41
 
41
42
  // API limit thresholds (usage ratios: 0.0 - 1.0)
42
- CLAUDE_SESSION_THRESHOLD: 0.9, // Stop if 5-hour limit > 90%
43
- CLAUDE_WEEKLY_THRESHOLD: 0.99, // One-at-a-time if weekly limit > 99%
44
- GITHUB_API_THRESHOLD: 0.8, // Stop if GitHub > 80% with parallel claude
43
+ // All thresholds use >= comparison (inclusive)
44
+ CLAUDE_5_HOUR_SESSION_THRESHOLD: 0.9, // Stop if 5-hour limit >= 90%
45
+ CLAUDE_WEEKLY_THRESHOLD: 0.99, // One-at-a-time if weekly limit >= 99%
46
+ GITHUB_API_THRESHOLD: 0.8, // Stop if GitHub >= 80% with parallel claude
45
47
 
46
48
  // Timing
47
49
  // MIN_START_INTERVAL_MS: Time to allow solve command to start actual claude process
@@ -135,8 +137,8 @@ function formatWaitingReason(metric, currentValue, threshold) {
135
137
  return `CPU usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
136
138
  case 'disk':
137
139
  return `Disk usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
138
- case 'claude_session':
139
- return `Claude session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
140
+ case 'claude_5_hour_session':
141
+ return `Claude 5 hour session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
140
142
  case 'claude_weekly':
141
143
  return `Claude weekly limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
142
144
  case 'github':
@@ -394,22 +396,25 @@ export class SolveQueue {
394
396
  const claudeProcs = await getRunningClaudeProcesses(this.verbose);
395
397
  const hasRunningClaude = claudeProcs.count > 0;
396
398
 
399
+ // Calculate total processing count: queue-internal + external claude processes
400
+ // This is used for CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD
401
+ // to allow exactly one command at a time when threshold is reached
402
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
403
+ const totalProcessing = this.processing.size + claudeProcs.count;
404
+
397
405
  // Track claude_running as a metric (but don't add to reasons yet)
398
406
  if (hasRunningClaude) {
399
407
  this.recordThrottle('claude_running');
400
408
  }
401
409
 
402
- // Check system resources
410
+ // Check system resources (ultimate restrictions - block unconditionally)
403
411
  const resourceCheck = await this.checkSystemResources();
404
412
  if (!resourceCheck.ok) {
405
413
  reasons.push(...resourceCheck.reasons);
406
414
  }
407
- if (resourceCheck.oneAtATime) {
408
- oneAtATime = true;
409
- }
410
415
 
411
- // Check API limits (pass hasRunningClaude to enable special handling)
412
- const limitCheck = await this.checkApiLimits(hasRunningClaude);
416
+ // Check API limits (pass hasRunningClaude and totalProcessing for uniform checking)
417
+ const limitCheck = await this.checkApiLimits(hasRunningClaude, totalProcessing);
413
418
  if (!limitCheck.ok) {
414
419
  reasons.push(...limitCheck.reasons);
415
420
  }
@@ -438,6 +443,7 @@ export class SolveQueue {
438
443
  reasons,
439
444
  oneAtATime,
440
445
  claudeProcesses: claudeProcs.count,
446
+ totalProcessing,
441
447
  };
442
448
  }
443
449
 
@@ -448,17 +454,21 @@ export class SolveQueue {
448
454
  * This provides a more stable metric that isn't affected by brief spikes
449
455
  * during claude process startup.
450
456
  *
451
- * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean}>}
457
+ * Note: System resource thresholds are ultimate restrictions - they block unconditionally
458
+ * when triggered. Unlike Claude API thresholds which allow one command at a time via
459
+ * totalProcessing check, system resources block all new commands immediately.
460
+ * See: https://github.com/link-assistant/hive-mind/issues/1133
461
+ *
462
+ * @returns {Promise<{ok: boolean, reasons: string[]}>}
452
463
  */
453
464
  async checkSystemResources() {
454
465
  const reasons = [];
455
- let oneAtATime = false;
456
466
 
457
467
  // Check RAM (using cached value)
458
468
  const memResult = await getCachedMemoryInfo(this.verbose);
459
469
  if (memResult.success) {
460
470
  const usedRatio = memResult.memory.usedPercentage / 100;
461
- if (usedRatio > QUEUE_CONFIG.RAM_THRESHOLD) {
471
+ if (usedRatio >= QUEUE_CONFIG.RAM_THRESHOLD) {
462
472
  reasons.push(formatWaitingReason('ram', memResult.memory.usedPercentage, QUEUE_CONFIG.RAM_THRESHOLD));
463
473
  this.recordThrottle('ram_high');
464
474
  }
@@ -481,42 +491,45 @@ export class SolveQueue {
481
491
  this.log(`CPU 5m load avg: ${loadAvg5.toFixed(2)}, cpus: ${cpuCount}, usage: ${usagePercent}%`);
482
492
  }
483
493
 
484
- if (usageRatio > QUEUE_CONFIG.CPU_THRESHOLD) {
494
+ if (usageRatio >= QUEUE_CONFIG.CPU_THRESHOLD) {
485
495
  reasons.push(formatWaitingReason('cpu', usagePercent, QUEUE_CONFIG.CPU_THRESHOLD));
486
496
  this.recordThrottle('cpu_high');
487
497
  }
488
498
  }
489
499
 
490
500
  // Check disk space (using cached value)
501
+ // Note: Disk threshold is an ultimate restriction - it blocks unconditionally when triggered
502
+ // Unlike CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD which use totalProcessing
503
+ // to allow one command at a time, disk threshold blocks all new commands immediately
504
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
491
505
  const diskResult = await getCachedDiskInfo(this.verbose);
492
506
  if (diskResult.success) {
493
507
  // Calculate usage from free percentage
494
508
  const usedPercent = 100 - diskResult.diskSpace.freePercentage;
495
509
  const usedRatio = usedPercent / 100;
496
- if (usedRatio > QUEUE_CONFIG.DISK_THRESHOLD) {
497
- oneAtATime = true;
510
+ if (usedRatio >= QUEUE_CONFIG.DISK_THRESHOLD) {
511
+ reasons.push(formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.DISK_THRESHOLD));
498
512
  this.recordThrottle('disk_high');
499
- if (this.processing.size > 0) {
500
- reasons.push(formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.DISK_THRESHOLD) + ' (waiting for current command)');
501
- }
502
513
  }
503
514
  }
504
515
 
505
- return { ok: reasons.length === 0, reasons, oneAtATime };
516
+ return { ok: reasons.length === 0, reasons };
506
517
  }
507
518
 
508
519
  /**
509
520
  * Check API limits (Claude, GitHub) using cached values
510
521
  *
511
- * Simplified logic per issue #1061:
512
- * - When any limit >= threshold, allow exactly one claude command to pass
513
- * - Only block if there's already a command in progress
514
- * - Running claude is the ultimate test of whether limits are really exhausted
522
+ * Logic per issue #1133:
523
+ * - CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD use one-at-a-time mode:
524
+ * when above threshold, allow exactly one command, block if totalProcessing > 0
525
+ * - GitHub threshold blocks unconditionally when exceeded (ultimate restriction)
526
+ * - totalProcessing = queue-internal count + external claude processes (pgrep)
515
527
  *
516
528
  * @param {boolean} hasRunningClaude - Whether claude processes are running
529
+ * @param {number} totalProcessing - Total processing count (queue + external claude processes)
517
530
  * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean}>}
518
531
  */
519
- async checkApiLimits(hasRunningClaude = false) {
532
+ async checkApiLimits(hasRunningClaude = false, totalProcessing = 0) {
520
533
  const reasons = [];
521
534
  let oneAtATime = false;
522
535
 
@@ -527,15 +540,18 @@ export class SolveQueue {
527
540
  const weeklyPercent = claudeResult.usage.allModels.percentage;
528
541
 
529
542
  // Session limit (5-hour)
530
- // When above threshold: allow exactly one command, block if one is running
543
+ // When above threshold: allow exactly one command, block if any processing is happening
544
+ // Uses totalProcessing (queue + external claude) for uniform checking
545
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
531
546
  if (sessionPercent !== null) {
532
547
  const sessionRatio = sessionPercent / 100;
533
- if (sessionRatio >= QUEUE_CONFIG.CLAUDE_SESSION_THRESHOLD) {
534
- // Only block if Claude is already running
535
- if (hasRunningClaude) {
536
- reasons.push(formatWaitingReason('claude_session', sessionPercent, QUEUE_CONFIG.CLAUDE_SESSION_THRESHOLD));
548
+ if (sessionRatio >= QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) {
549
+ oneAtATime = true;
550
+ this.recordThrottle(sessionRatio >= 1.0 ? 'claude_5_hour_session_100' : 'claude_5_hour_session_high');
551
+ // Use totalProcessing (queue + external claude) for uniform checking
552
+ if (totalProcessing > 0) {
553
+ reasons.push(formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) + ' (waiting for current command)');
537
554
  }
538
- this.recordThrottle(sessionRatio >= 1.0 ? 'claude_session_100' : 'claude_session_high');
539
555
  }
540
556
  }
541
557
 
@@ -546,8 +562,9 @@ export class SolveQueue {
546
562
  if (weeklyRatio >= QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) {
547
563
  oneAtATime = true;
548
564
  this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : 'claude_weekly_high');
549
- // Only block if command is already in progress
550
- if (this.processing.size > 0) {
565
+ // Use totalProcessing (queue + external claude) for uniform checking
566
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
567
+ if (totalProcessing > 0) {
551
568
  reasons.push(formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) + ' (waiting for current command)');
552
569
  }
553
570
  }
@@ -664,8 +681,13 @@ export class SolveQueue {
664
681
  }
665
682
 
666
683
  // Check one-at-a-time mode
667
- if (check.oneAtATime && this.processing.size > 0) {
668
- this.log('One-at-a-time mode: waiting for current command to finish');
684
+ // When oneAtATime is true (e.g., weekly limit >= 99%), block if any processing is happening
685
+ // totalProcessing = queue-internal (this.processing.size) + external claude processes (pgrep)
686
+ // This ensures uniform checking across all threshold conditions
687
+ // See: https://github.com/link-assistant/hive-mind/issues/1133
688
+ if (check.oneAtATime && check.totalProcessing > 0) {
689
+ const processInfo = check.claudeProcesses > 0 ? ` (${check.claudeProcesses} claude process${check.claudeProcesses > 1 ? 'es' : ''} running)` : '';
690
+ this.log(`One-at-a-time mode: waiting for current command to finish${processInfo}`);
669
691
  await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
670
692
  continue;
671
693
  }