@link-assistant/hive-mind 1.11.4 → 1.11.6

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
+ ## 1.11.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 5eef9e4: Skip Claude API limits for --tool agent tasks in queue
8
+ - Agent tools (Grok Code, OpenCode Zen) use different backends with their own rate limits
9
+ - Add tool parameter to canStartCommand() and checkApiLimits() functions
10
+ - Skip Claude-specific limits (5-hour session, weekly) when tool is 'agent'
11
+ - Consumer loop now passes next queue item's tool to limit checks
12
+ - Add 7 new tests for tool-specific limit handling
13
+ - Add case study documentation
14
+
15
+ Fixes #1159
16
+
17
+ ## 1.11.5
18
+
19
+ ### Patch Changes
20
+
21
+ - 7d3387c: Fix duplicate Solution Draft Log comments on GitHub PRs
22
+
23
+ When a Claude session ends with uncommitted changes and --attach-logs is enabled, the solution draft log was being uploaded twice - once by verifyResults() during normal completion, and again after temporary watch mode completes. This fix tracks whether logs were already uploaded and skips the duplicate upload.
24
+
3
25
  ## 1.11.4
4
26
 
5
27
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -194,7 +194,7 @@ export const cacheTtl = {
194
194
  api: parseIntWithDefault('HIVE_MIND_API_CACHE_TTL_MS', 3 * 60 * 1000), // 3 minutes
195
195
  // Claude Usage API cache TTL - must be at least 20 minutes to avoid rate limiting
196
196
  // The API returns null values when called too frequently
197
- usageApi: parseIntWithDefault('HIVE_MIND_USAGE_API_CACHE_TTL_MS', 20 * 60 * 1000), // 20 minutes
197
+ usageApi: parseIntWithDefault('HIVE_MIND_USAGE_API_CACHE_TTL_MS', 10 * 60 * 1000), // 10 minutes
198
198
  // System metrics cache TTL (RAM, CPU, disk)
199
199
  system: parseIntWithDefault('HIVE_MIND_SYSTEM_CACHE_TTL_MS', 2 * 60 * 1000), // 2 minutes
200
200
  };
package/src/solve.mjs CHANGED
@@ -1159,7 +1159,9 @@ try {
1159
1159
  // Pass shouldRestart to prevent early exit when auto-restart is needed
1160
1160
  // Include agent tool pricing data when available (publicPricingEstimate, pricingInfo)
1161
1161
  // Issue #1088: Pass errorDuringExecution for "Finished with errors" state
1162
- await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution);
1162
+ // Issue #1154: Track if logs were already uploaded to prevent duplicates
1163
+ const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution);
1164
+ const logsAlreadyUploaded = verifyResult?.logUploadSuccess || false;
1163
1165
 
1164
1166
  // Start watch mode if enabled OR if we need to handle uncommitted changes
1165
1167
  if (argv.verbose) {
@@ -1246,7 +1248,8 @@ try {
1246
1248
  }
1247
1249
 
1248
1250
  // Attach updated logs to PR after auto-restart completes
1249
- if (shouldAttachLogs && prNumber) {
1251
+ // Issue #1154: Skip if logs were already uploaded by verifyResults() to prevent duplicates
1252
+ if (shouldAttachLogs && prNumber && !logsAlreadyUploaded) {
1250
1253
  await log('📎 Uploading working session logs to Pull Request...');
1251
1254
  try {
1252
1255
  const logUploadSuccess = await attachLogToGitHub({
@@ -1273,6 +1276,9 @@ try {
1273
1276
  } catch (uploadError) {
1274
1277
  await log(`âš ī¸ Error uploading logs: ${uploadError.message}`, { level: 'warning' });
1275
1278
  }
1279
+ } else if (logsAlreadyUploaded) {
1280
+ await log('â„šī¸ Logs already uploaded by verifyResults, skipping duplicate upload', { verbose: true });
1281
+ logsAttached = true;
1276
1282
  }
1277
1283
  }
1278
1284
 
@@ -564,7 +564,8 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
564
564
  if (!argv.watch && !shouldRestart) {
565
565
  await safeExit(0, 'Process completed successfully');
566
566
  }
567
- return; // Return normally for watch mode or auto-restart
567
+ // Issue #1154: Return logUploadSuccess to prevent duplicate log uploads
568
+ return { logUploadSuccess }; // Return for watch mode or auto-restart
568
569
  } else {
569
570
  await log(` â„šī¸ Found pull request #${pr.number} but it appears to be from a different session`);
570
571
  }
@@ -627,7 +628,8 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
627
628
  if (!argv.watch && !shouldRestart) {
628
629
  await safeExit(0, 'Process completed successfully');
629
630
  }
630
- return; // Return normally for watch mode or auto-restart
631
+ // Issue #1154: Return logUploadSuccess to prevent duplicate log uploads
632
+ return { logUploadSuccess: true }; // Return for watch mode or auto-restart
631
633
  } else if (allComments.length > 0) {
632
634
  await log(` â„šī¸ Issue has ${allComments.length} existing comment(s)`);
633
635
  } else {
@@ -645,7 +647,8 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
645
647
  if (!argv.watch) {
646
648
  await safeExit(0, 'Process completed successfully');
647
649
  }
648
- return; // Return normally for watch mode
650
+ // Issue #1154: Return logUploadSuccess to prevent duplicate log uploads
651
+ return { logUploadSuccess: false }; // Return for watch mode
649
652
  } catch (searchError) {
650
653
  reportError(searchError, {
651
654
  context: 'verify_pr_creation',
@@ -661,7 +664,8 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
661
664
  if (!argv.watch) {
662
665
  await safeExit(0, 'Process completed successfully');
663
666
  }
664
- return; // Return normally for watch mode
667
+ // Issue #1154: Return logUploadSuccess to prevent duplicate log uploads
668
+ return { logUploadSuccess: false }; // Return for watch mode
665
669
  }
666
670
  };
667
671
 
@@ -1102,7 +1102,7 @@ bot.command(/^solve$/i, async ctx => {
1102
1102
  return;
1103
1103
  }
1104
1104
 
1105
- const check = await solveQueue.canStartCommand();
1105
+ const check = await solveQueue.canStartCommand({ tool: solveTool }); // Skip Claude limits for agent (#1159)
1106
1106
  const queueStats = solveQueue.getStats();
1107
1107
  if (check.canStart && queueStats.queued === 0) {
1108
1108
  const startingMessage = await ctx.reply(`🚀 Starting solve command...\n\n${infoBlock}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
@@ -41,9 +41,9 @@ export const QUEUE_CONFIG = {
41
41
 
42
42
  // API limit thresholds (usage ratios: 0.0 - 1.0)
43
43
  // All thresholds use >= comparison (inclusive)
44
- CLAUDE_5_HOUR_SESSION_THRESHOLD: 0.85, // One-at-a-time if 5-hour limit >= 85%
45
- CLAUDE_WEEKLY_THRESHOLD: 0.98, // One-at-a-time if weekly limit >= 98%
46
- GITHUB_API_THRESHOLD: 0.8, // Enqueue if GitHub >= 80% with parallel claude
44
+ CLAUDE_5_HOUR_SESSION_THRESHOLD: 0.75, // One-at-a-time if 5-hour limit >= 75%
45
+ CLAUDE_WEEKLY_THRESHOLD: 0.97, // One-at-a-time if weekly limit >= 97%
46
+ GITHUB_API_THRESHOLD: 0.75, // Enqueue if GitHub >= 75% with parallel claude
47
47
 
48
48
  // Timing
49
49
  // MIN_START_INTERVAL_MS: Time to allow solve command to start actual claude process
@@ -242,6 +242,13 @@ class SolveQueueItem {
242
242
 
243
243
  /**
244
244
  * Solve Queue - Producer/Consumer queue for /solve commands
245
+ *
246
+ * Uses separate queues for each tool type to ensure:
247
+ * - Claude tasks never block agent tasks (and vice versa)
248
+ * - Each tool queue maintains FIFO order
249
+ * - Each tool has independent rate limiting
250
+ *
251
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
245
252
  */
246
253
  export class SolveQueue {
247
254
  constructor(options = {}) {
@@ -249,14 +256,23 @@ export class SolveQueue {
249
256
  this.executeCallback = options.executeCallback || null;
250
257
  this.messageUpdateCallback = options.messageUpdateCallback || null;
251
258
 
252
- // Queue state
253
- this.queue = [];
259
+ // Separate queues per tool type - claude tasks never block agent tasks
260
+ // See: https://github.com/link-assistant/hive-mind/issues/1159
261
+ this.queues = {
262
+ claude: [],
263
+ agent: [],
264
+ };
254
265
  this.processing = new Map();
255
266
  this.completed = [];
256
267
  this.failed = [];
257
268
  this.isRunning = true;
258
269
 
259
- // Timing
270
+ // Timing - separate per tool to ensure independent processing
271
+ this.lastStartTimeByTool = {
272
+ claude: null,
273
+ agent: null,
274
+ };
275
+ // Legacy: keep for compatibility with existing code that uses lastStartTime
260
276
  this.lastStartTime = null;
261
277
 
262
278
  // Consumer task reference
@@ -272,7 +288,43 @@ export class SolveQueue {
272
288
  throttleReasons: {},
273
289
  };
274
290
 
275
- this.log('SolveQueue initialized');
291
+ this.log('SolveQueue initialized with separate tool queues');
292
+ }
293
+
294
+ /**
295
+ * Get the queue array for a specific tool, creating it if needed
296
+ * @param {string} tool - Tool type ('claude', 'agent', etc.)
297
+ * @returns {Array} The queue array for this tool
298
+ */
299
+ getToolQueue(tool) {
300
+ if (!this.queues[tool]) {
301
+ this.queues[tool] = [];
302
+ }
303
+ return this.queues[tool];
304
+ }
305
+
306
+ /**
307
+ * Get combined queue length across all tools (for backwards compatibility)
308
+ * @returns {number} Total queue length
309
+ */
310
+ get queue() {
311
+ let total = [];
312
+ for (const toolQueue of Object.values(this.queues)) {
313
+ total = total.concat(toolQueue);
314
+ }
315
+ return total;
316
+ }
317
+
318
+ /**
319
+ * Get total pending count across all tool queues
320
+ * @returns {number} Total pending items
321
+ */
322
+ getTotalQueueLength() {
323
+ let total = 0;
324
+ for (const toolQueue of Object.values(this.queues)) {
325
+ total += toolQueue.length;
326
+ }
327
+ return total;
276
328
  }
277
329
 
278
330
  /**
@@ -286,16 +338,19 @@ export class SolveQueue {
286
338
  }
287
339
 
288
340
  /**
289
- * Add a solve command to the queue
341
+ * Add a solve command to the appropriate tool queue
342
+ * Items are added to the queue for their specific tool type.
290
343
  * @param {Object} options - Queue item options
291
344
  * @returns {SolveQueueItem} The queued item
345
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
292
346
  */
293
347
  enqueue(options) {
294
348
  const item = new SolveQueueItem(options);
295
- this.queue.push(item);
349
+ const toolQueue = this.getToolQueue(item.tool);
350
+ toolQueue.push(item);
296
351
  this.stats.totalEnqueued++;
297
352
 
298
- this.log(`Enqueued: ${item.toString()}, queue length: ${this.queue.length}`);
353
+ this.log(`Enqueued: ${item.toString()} to ${item.tool} queue, queue length: ${toolQueue.length}`);
299
354
 
300
355
  // Start consumer if not already running
301
356
  this.ensureConsumerRunning();
@@ -304,17 +359,19 @@ export class SolveQueue {
304
359
  }
305
360
 
306
361
  /**
307
- * Find an item by URL in the queue or processing items
362
+ * Find an item by URL in any queue or processing items
308
363
  * Used to prevent duplicate URLs from being added to the queue
309
364
  * @param {string} url - The URL to search for
310
365
  * @returns {SolveQueueItem|null} The found item or null
311
366
  * @see https://github.com/link-assistant/hive-mind/issues/1080
312
367
  */
313
368
  findByUrl(url) {
314
- // Check queued items
315
- const queuedItem = this.queue.find(item => item.url === url);
316
- if (queuedItem) {
317
- return queuedItem;
369
+ // Check all tool queues
370
+ for (const toolQueue of Object.values(this.queues)) {
371
+ const queuedItem = toolQueue.find(item => item.url === url);
372
+ if (queuedItem) {
373
+ return queuedItem;
374
+ }
318
375
  }
319
376
 
320
377
  // Check processing items
@@ -329,17 +386,22 @@ export class SolveQueue {
329
386
 
330
387
  /**
331
388
  * Cancel a queued item by ID
389
+ * Searches all tool queues to find the item.
332
390
  * @param {string} id - Item ID
333
391
  * @returns {boolean} True if cancelled
392
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
334
393
  */
335
394
  cancel(id) {
336
- const queueIndex = this.queue.findIndex(item => item.id === id);
337
- if (queueIndex !== -1) {
338
- const item = this.queue.splice(queueIndex, 1)[0];
339
- item.setCancelled();
340
- this.stats.totalCancelled++;
341
- this.log(`Cancelled queued item: ${item.toString()}`);
342
- return true;
395
+ // Search all tool queues
396
+ for (const [tool, toolQueue] of Object.entries(this.queues)) {
397
+ const queueIndex = toolQueue.findIndex(item => item.id === id);
398
+ if (queueIndex !== -1) {
399
+ const item = toolQueue.splice(queueIndex, 1)[0];
400
+ item.setCancelled();
401
+ this.stats.totalCancelled++;
402
+ this.log(`Cancelled queued item: ${item.toString()} from ${tool} queue`);
403
+ return true;
404
+ }
343
405
  }
344
406
 
345
407
  if (this.processing.has(id)) {
@@ -355,39 +417,130 @@ export class SolveQueue {
355
417
  * @returns {Object}
356
418
  */
357
419
  getStats() {
420
+ // Calculate per-tool queue stats
421
+ const queuedByTool = {};
422
+ let totalQueued = 0;
423
+ for (const [tool, toolQueue] of Object.entries(this.queues)) {
424
+ queuedByTool[tool] = toolQueue.length;
425
+ totalQueued += toolQueue.length;
426
+ }
427
+
358
428
  return {
359
- queued: this.queue.length,
429
+ queued: totalQueued,
430
+ queuedByTool,
360
431
  processing: this.processing.size,
361
432
  completed: this.completed.length,
362
433
  failed: this.failed.length,
363
434
  ...this.stats,
364
435
  cacheStats: getLimitCache().getStats(),
365
436
  lastStartTime: this.lastStartTime,
437
+ lastStartTimeByTool: this.lastStartTimeByTool,
366
438
  isRunning: this.isRunning,
367
439
  };
368
440
  }
369
441
 
442
+ /**
443
+ * Count processing items by tool type
444
+ * Used for tool-specific limit checking - e.g., Claude limits only count Claude processing items
445
+ * @param {string} tool - Tool type to count ('claude', 'agent', etc.)
446
+ * @returns {number} Count of processing items with the specified tool
447
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
448
+ */
449
+ getProcessingCountByTool(tool) {
450
+ let count = 0;
451
+ for (const item of this.processing.values()) {
452
+ if (item.tool === tool) {
453
+ count++;
454
+ }
455
+ }
456
+ return count;
457
+ }
458
+
459
+ /**
460
+ * Find startable items from each tool queue
461
+ * Returns the first item from each tool queue that can start.
462
+ * With separate queues, each tool is checked independently so they don't block each other.
463
+ * @returns {Promise<Array<{item: SolveQueueItem, tool: string, index: number, check: Object}>>}
464
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
465
+ */
466
+ async findStartableItems() {
467
+ const startableItems = [];
468
+
469
+ for (const [tool, toolQueue] of Object.entries(this.queues)) {
470
+ if (toolQueue.length === 0) continue;
471
+
472
+ // Check if first item in this tool's queue can start
473
+ const item = toolQueue[0];
474
+ const check = await this.canStartCommand({ tool });
475
+
476
+ if (check.canStart) {
477
+ // Also check one-at-a-time mode for this specific tool
478
+ // For tool-specific one-at-a-time, only count that tool's processing items
479
+ const toolProcessingCount = this.getProcessingCountByTool(tool);
480
+ if (check.oneAtATime && toolProcessingCount > 0) {
481
+ // This tool is in one-at-a-time mode and has items processing
482
+ // Skip but don't block other tools
483
+ continue;
484
+ }
485
+ startableItems.push({ item, tool, index: 0, check });
486
+ }
487
+ }
488
+
489
+ return startableItems;
490
+ }
491
+
492
+ /**
493
+ * Find first queue item that can start based on its tool's limits (legacy compatibility)
494
+ * With separate queues, returns the first startable item from any tool queue.
495
+ * @returns {Promise<{item: SolveQueueItem|null, index: number, check: Object}>}
496
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
497
+ */
498
+ async findStartableItem() {
499
+ const startableItems = await this.findStartableItems();
500
+ if (startableItems.length > 0) {
501
+ // Return the first startable item (arbitrary order among tools)
502
+ const first = startableItems[0];
503
+ return { item: first.item, index: first.index, check: first.check };
504
+ }
505
+ return { item: null, index: -1, check: null };
506
+ }
507
+
370
508
  /**
371
509
  * Get queue items summary for display
510
+ * Combines items from all tool queues into a single pending list.
372
511
  * @returns {Object}
512
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
373
513
  */
374
514
  getQueueSummary() {
515
+ // Collect pending items from all tool queues
516
+ const pending = [];
517
+ for (const [tool, toolQueue] of Object.entries(this.queues)) {
518
+ for (const item of toolQueue) {
519
+ pending.push({
520
+ id: item.id,
521
+ url: item.url,
522
+ requester: item.requester,
523
+ waitTime: item.getWaitTime(),
524
+ createdAt: item.createdAt,
525
+ status: item.status,
526
+ waitingReason: item.waitingReason,
527
+ tool,
528
+ });
529
+ }
530
+ }
531
+
532
+ // Sort by createdAt to show oldest first (global order)
533
+ pending.sort((a, b) => a.createdAt - b.createdAt);
534
+
375
535
  return {
376
- pending: this.queue.map(item => ({
377
- id: item.id,
378
- url: item.url,
379
- requester: item.requester,
380
- waitTime: item.getWaitTime(),
381
- createdAt: item.createdAt,
382
- status: item.status,
383
- waitingReason: item.waitingReason,
384
- })),
536
+ pending,
385
537
  processing: Array.from(this.processing.values()).map(item => ({
386
538
  id: item.id,
387
539
  url: item.url,
388
540
  requester: item.requester,
389
541
  startedAt: item.startedAt,
390
542
  status: item.status,
543
+ tool: item.tool,
391
544
  })),
392
545
  };
393
546
  }
@@ -400,15 +553,26 @@ export class SolveQueue {
400
553
  * 2. Commands can run in parallel as long as actual limits are not exceeded
401
554
  * 3. When any limit >= threshold, allow exactly one claude command to pass
402
555
  *
556
+ * Logic per issue #1159:
557
+ * - Different tools have different limits. Claude limits only apply to 'claude' tool.
558
+ * - Processing count for Claude limits only includes Claude items, not agent items.
559
+ * - This allows agent tasks to run in parallel when Claude limits are reached.
560
+ *
561
+ * @param {Object} options - Options for the check
562
+ * @param {string} options.tool - The tool being used ('claude', 'agent', etc.)
403
563
  * @returns {Promise<{canStart: boolean, reason?: string, reasons?: string[], oneAtATime?: boolean}>}
404
564
  */
405
- async canStartCommand() {
565
+ async canStartCommand(options = {}) {
566
+ const tool = options.tool || 'claude';
406
567
  const reasons = [];
407
568
  let oneAtATime = false;
408
569
 
409
- // Check minimum interval since last start
410
- if (this.lastStartTime) {
411
- const timeSinceLastStart = Date.now() - this.lastStartTime;
570
+ // Check minimum interval since last start FOR THIS TOOL
571
+ // Each tool queue has independent timing to prevent cross-blocking
572
+ // See: https://github.com/link-assistant/hive-mind/issues/1159
573
+ const lastStartTime = this.lastStartTimeByTool[tool] || null;
574
+ if (lastStartTime) {
575
+ const timeSinceLastStart = Date.now() - lastStartTime;
412
576
  if (timeSinceLastStart < QUEUE_CONFIG.MIN_START_INTERVAL_MS) {
413
577
  const waitSeconds = Math.ceil((QUEUE_CONFIG.MIN_START_INTERVAL_MS - timeSinceLastStart) / 1000);
414
578
  reasons.push(formatWaitingReason('min_interval', 0, 0) + ` (${waitSeconds}s remaining)`);
@@ -420,18 +584,23 @@ export class SolveQueue {
420
584
  const claudeProcs = await getRunningClaudeProcesses(this.verbose);
421
585
  const hasRunningClaude = claudeProcs.count > 0;
422
586
 
423
- // Calculate total processing count: queue-internal + external claude processes
424
- // This is used for CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD
425
- // to allow exactly one command at a time when threshold is reached
426
- // See: https://github.com/link-assistant/hive-mind/issues/1133
587
+ // Calculate total processing count for system resources (all tools)
588
+ // System resources (RAM, CPU, disk) apply to all tools
427
589
  const totalProcessing = this.processing.size + claudeProcs.count;
428
590
 
591
+ // Calculate Claude-specific processing count for Claude API limits
592
+ // Only counts Claude items in queue + external claude processes
593
+ // Agent items don't count against Claude's one-at-a-time limit
594
+ // See: https://github.com/link-assistant/hive-mind/issues/1159
595
+ const claudeProcessingCount = this.getProcessingCountByTool('claude');
596
+
429
597
  // Track claude_running as a metric (but don't add to reasons yet)
430
598
  if (hasRunningClaude) {
431
599
  this.recordThrottle('claude_running');
432
600
  }
433
601
 
434
602
  // Check system resources (RAM, CPU block unconditionally; disk uses one-at-a-time mode)
603
+ // System resources apply to ALL tools, not just Claude
435
604
  // See: https://github.com/link-assistant/hive-mind/issues/1155
436
605
  const resourceCheck = await this.checkSystemResources(totalProcessing);
437
606
  if (!resourceCheck.ok) {
@@ -441,8 +610,11 @@ export class SolveQueue {
441
610
  oneAtATime = true;
442
611
  }
443
612
 
444
- // Check API limits (pass hasRunningClaude and totalProcessing for uniform checking)
445
- const limitCheck = await this.checkApiLimits(hasRunningClaude, totalProcessing);
613
+ // Check API limits (pass hasRunningClaude, claudeProcessingCount, and tool)
614
+ // Claude limits use claudeProcessingCount (only Claude items), not totalProcessing
615
+ // This allows agent tasks to proceed when Claude limits are reached
616
+ // See: https://github.com/link-assistant/hive-mind/issues/1159
617
+ const limitCheck = await this.checkApiLimits(hasRunningClaude, claudeProcessingCount, tool);
446
618
  if (!limitCheck.ok) {
447
619
  reasons.push(...limitCheck.reasons);
448
620
  }
@@ -472,6 +644,7 @@ export class SolveQueue {
472
644
  oneAtATime,
473
645
  claudeProcesses: claudeProcs.count,
474
646
  totalProcessing,
647
+ claudeProcessingCount,
475
648
  };
476
649
  }
477
650
 
@@ -559,54 +732,77 @@ export class SolveQueue {
559
732
  *
560
733
  * Logic per issue #1133:
561
734
  * - CLAUDE_5_HOUR_SESSION_THRESHOLD and CLAUDE_WEEKLY_THRESHOLD use one-at-a-time mode:
562
- * when above threshold, allow exactly one command, block if totalProcessing > 0
735
+ * when above threshold, allow exactly one command, block if claudeProcessing > 0
563
736
  * - GitHub threshold blocks unconditionally when exceeded (ultimate restriction)
564
- * - totalProcessing = queue-internal count + external claude processes (pgrep)
565
737
  *
566
- * @param {boolean} hasRunningClaude - Whether claude processes are running
567
- * @param {number} totalProcessing - Total processing count (queue + external claude processes)
738
+ * Logic per issue #1159:
739
+ * - When tool is 'agent', skip Claude-specific limits entirely since agent uses different
740
+ * rate limits (Grok Code or similar). Only system resources and GitHub limits apply.
741
+ * - For Claude limits, only count Claude-specific processing items, not agent items.
742
+ * This allows agent tasks to run in parallel even when Claude limits are reached.
743
+ *
744
+ * @param {boolean} hasRunningClaude - Whether claude processes are running (from pgrep)
745
+ * @param {number} claudeProcessingCount - Count of 'claude' tool items being processed in queue
746
+ * @param {string} tool - The tool being used ('claude', 'agent', etc.)
568
747
  * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean}>}
569
748
  */
570
- async checkApiLimits(hasRunningClaude = false, totalProcessing = 0) {
749
+ async checkApiLimits(hasRunningClaude = false, claudeProcessingCount = 0, tool = 'claude') {
571
750
  const reasons = [];
572
751
  let oneAtATime = false;
573
752
 
753
+ // Apply Claude-specific limits only when tool is 'claude'
754
+ // Other tools (like 'agent') use different rate limiting backends and are not
755
+ // affected by Claude API limits (5-hour session, weekly limits)
756
+ // See: https://github.com/link-assistant/hive-mind/issues/1159
757
+ const applyClaudeLimits = tool === 'claude';
758
+
759
+ // Calculate total Claude processing: queue-internal claude items + external claude processes
760
+ // This is used for Claude limits one-at-a-time mode - only counts Claude-related processing
761
+ // Agent items in the queue don't count against Claude's one-at-a-time limit
762
+ // See: https://github.com/link-assistant/hive-mind/issues/1159
763
+ const totalClaudeProcessing = claudeProcessingCount + (hasRunningClaude ? 1 : 0);
764
+
574
765
  // Check Claude limits (using cached value)
575
- const claudeResult = await getCachedClaudeLimits(this.verbose);
576
- if (claudeResult.success) {
577
- const sessionPercent = claudeResult.usage.currentSession.percentage;
578
- const weeklyPercent = claudeResult.usage.allModels.percentage;
579
-
580
- // Session limit (5-hour)
581
- // When above threshold: allow exactly one command, block if any processing is happening
582
- // Uses totalProcessing (queue + external claude) for uniform checking
583
- // See: https://github.com/link-assistant/hive-mind/issues/1133
584
- if (sessionPercent !== null) {
585
- const sessionRatio = sessionPercent / 100;
586
- if (sessionRatio >= QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) {
587
- oneAtATime = true;
588
- this.recordThrottle(sessionRatio >= 1.0 ? 'claude_5_hour_session_100' : 'claude_5_hour_session_high');
589
- // Use totalProcessing (queue + external claude) for uniform checking
590
- if (totalProcessing > 0) {
591
- reasons.push(formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) + ' (waiting for current command)');
766
+ // Only applied when tool is 'claude'
767
+ if (applyClaudeLimits) {
768
+ const claudeResult = await getCachedClaudeLimits(this.verbose);
769
+ if (claudeResult.success) {
770
+ const sessionPercent = claudeResult.usage.currentSession.percentage;
771
+ const weeklyPercent = claudeResult.usage.allModels.percentage;
772
+
773
+ // Session limit (5-hour)
774
+ // When above threshold: allow exactly one Claude command, block if any Claude processing
775
+ // Only counts Claude-specific processing, not agent items
776
+ // See: https://github.com/link-assistant/hive-mind/issues/1133, #1159
777
+ if (sessionPercent !== null) {
778
+ const sessionRatio = sessionPercent / 100;
779
+ if (sessionRatio >= QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) {
780
+ oneAtATime = true;
781
+ this.recordThrottle(sessionRatio >= 1.0 ? 'claude_5_hour_session_100' : 'claude_5_hour_session_high');
782
+ // Use totalClaudeProcessing for Claude-specific one-at-a-time checking
783
+ if (totalClaudeProcessing > 0) {
784
+ reasons.push(formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.CLAUDE_5_HOUR_SESSION_THRESHOLD) + ' (waiting for current command)');
785
+ }
592
786
  }
593
787
  }
594
- }
595
788
 
596
- // Weekly limit
597
- // When above threshold: allow exactly one command, block if one is in progress
598
- if (weeklyPercent !== null) {
599
- const weeklyRatio = weeklyPercent / 100;
600
- if (weeklyRatio >= QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) {
601
- oneAtATime = true;
602
- this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : 'claude_weekly_high');
603
- // Use totalProcessing (queue + external claude) for uniform checking
604
- // See: https://github.com/link-assistant/hive-mind/issues/1133
605
- if (totalProcessing > 0) {
606
- reasons.push(formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) + ' (waiting for current command)');
789
+ // Weekly limit
790
+ // When above threshold: allow exactly one Claude command, block if one is in progress
791
+ if (weeklyPercent !== null) {
792
+ const weeklyRatio = weeklyPercent / 100;
793
+ if (weeklyRatio >= QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) {
794
+ oneAtATime = true;
795
+ this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : 'claude_weekly_high');
796
+ // Use totalClaudeProcessing for Claude-specific one-at-a-time checking
797
+ // See: https://github.com/link-assistant/hive-mind/issues/1133, #1159
798
+ if (totalClaudeProcessing > 0) {
799
+ reasons.push(formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.CLAUDE_WEEKLY_THRESHOLD) + ' (waiting for current command)');
800
+ }
607
801
  }
608
802
  }
609
803
  }
804
+ } else if (this.verbose) {
805
+ this.log(`Claude limits not applied for --tool ${tool}`);
610
806
  }
611
807
 
612
808
  // Check GitHub limits (only relevant if claude processes running)
@@ -678,88 +874,101 @@ export class SolveQueue {
678
874
  }
679
875
 
680
876
  /**
681
- * Consumer loop - processes items from the queue
877
+ * Consumer loop - processes items from all tool queues
878
+ *
879
+ * With separate queues per tool:
880
+ * - Each tool queue is checked independently
881
+ * - Claude limits only affect Claude queue
882
+ * - Agent queue can proceed even when Claude is blocked (and vice versa)
883
+ * - Multiple items can start in the same cycle (one per tool)
884
+ *
885
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
682
886
  */
683
887
  async runConsumer() {
684
- this.log('Consumer started');
888
+ this.log('Consumer started with separate tool queues');
685
889
 
686
890
  while (this.isRunning) {
687
- if (this.queue.length === 0) {
891
+ // Check if all queues are empty
892
+ if (this.getTotalQueueLength() === 0) {
688
893
  await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
689
894
  continue;
690
895
  }
691
896
 
692
- const check = await this.canStartCommand();
693
-
694
- if (!check.canStart) {
695
- // Update all queued items to waiting status with reason
696
- // Also periodically refresh messages to show current status
697
- // See: https://github.com/link-assistant/hive-mind/issues/1078
698
- for (const item of this.queue) {
699
- if (item.status === QueueItemStatus.QUEUED || item.status === QueueItemStatus.WAITING) {
700
- const previousStatus = item.status;
701
- const previousReason = item.waitingReason;
702
- item.setWaiting(check.reason);
703
-
704
- // Update message if:
705
- // 1. Status or reason changed
706
- // 2. OR it's time for a periodic update (every MESSAGE_UPDATE_INTERVAL_MS)
707
- const shouldUpdate = previousStatus !== item.status || previousReason !== item.waitingReason || this.shouldUpdateMessage(item);
708
-
709
- if (shouldUpdate) {
710
- const position = this.queue.indexOf(item) + 1;
711
- await this.updateItemMessage(item, `âŗ Waiting (position #${position})\n\n${item.infoBlock}\n\n*Reason:*\n${check.reason}`);
712
- }
713
- }
714
- }
897
+ // Find startable items from each tool queue
898
+ // Each tool is checked independently so they don't block each other
899
+ // See: https://github.com/link-assistant/hive-mind/issues/1159
900
+ const startableItems = await this.findStartableItems();
715
901
 
716
- this.log(`Throttled: ${check.reason}`);
902
+ if (startableItems.length === 0) {
903
+ // No items can start - update all queued items with their tool-specific waiting reasons
904
+ await this.updateAllWaitingItems();
905
+ this.log(`Throttled: no items can start from any tool queue`);
717
906
  await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
718
907
  continue;
719
908
  }
720
909
 
721
- // Check one-at-a-time mode
722
- // When oneAtATime is true (e.g., weekly limit >= 99%), block if any processing is happening
723
- // totalProcessing = queue-internal (this.processing.size) + external claude processes (pgrep)
724
- // This ensures uniform checking across all threshold conditions
725
- // See: https://github.com/link-assistant/hive-mind/issues/1133
726
- if (check.oneAtATime && check.totalProcessing > 0) {
727
- const processInfo = check.claudeProcesses > 0 ? ` (${check.claudeProcesses} claude process${check.claudeProcesses > 1 ? 'es' : ''} running)` : '';
728
- this.log(`One-at-a-time mode: waiting for current command to finish${processInfo}`);
729
- await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
730
- continue;
731
- }
910
+ // Start items from each tool that can proceed
911
+ // This allows parallel starts from different tool queues
912
+ for (const startable of startableItems) {
913
+ const { tool } = startable;
914
+ const toolQueue = this.getToolQueue(tool);
732
915
 
733
- // Get next item from queue
734
- const item = this.queue.shift();
735
- if (!item) continue;
916
+ // Remove the first item from this tool's queue
917
+ const item = toolQueue.shift();
918
+ if (!item) continue;
736
919
 
737
- // NOTE: Running claude processes is NOT a blocking limit by itself
738
- // Commands can run in parallel as long as actual limits (CPU, API, etc.) are not exceeded
739
- // The MIN_START_INTERVAL_MS ensures enough time for processes to be counted
740
- // See: https://github.com/link-assistant/hive-mind/issues/1078
920
+ // Update status to Starting
921
+ item.setStarting();
922
+ this.processing.set(item.id, item);
741
923
 
742
- // Update status to Starting
743
- item.setStarting();
744
- this.processing.set(item.id, item);
745
- this.lastStartTime = Date.now();
746
- this.stats.totalStarted++;
924
+ // Update tool-specific last start time
925
+ this.lastStartTimeByTool[tool] = Date.now();
926
+ this.lastStartTime = Date.now(); // Legacy compatibility
927
+ this.stats.totalStarted++;
747
928
 
748
- // Update message to show Starting status
749
- await this.updateItemMessage(item, `🚀 Starting solve command...\n\n${item.infoBlock}`);
929
+ // Update message to show Starting status
930
+ await this.updateItemMessage(item, `🚀 Starting solve command...\n\n${item.infoBlock}`);
750
931
 
751
- this.log(`Starting: ${item.toString()}`);
932
+ this.log(`Starting: ${item.toString()} from ${tool} queue`);
752
933
 
753
- // Execute in background
754
- this.executeItem(item).catch(error => {
755
- console.error(`[solve-queue] Execution error for ${item.id}:`, error);
756
- });
934
+ // Execute in background
935
+ this.executeItem(item).catch(error => {
936
+ console.error(`[solve-queue] Execution error for ${item.id}:`, error);
937
+ });
938
+ }
757
939
  }
758
940
 
759
941
  this.log('Consumer stopped');
760
942
  this.consumerTask = null;
761
943
  }
762
944
 
945
+ /**
946
+ * Update all waiting items with their tool-specific waiting reasons
947
+ * @see https://github.com/link-assistant/hive-mind/issues/1078
948
+ */
949
+ async updateAllWaitingItems() {
950
+ for (const [tool, toolQueue] of Object.entries(this.queues)) {
951
+ for (let i = 0; i < toolQueue.length; i++) {
952
+ const item = toolQueue[i];
953
+ if (item.status === QueueItemStatus.QUEUED || item.status === QueueItemStatus.WAITING) {
954
+ // Get the specific reason for this item's tool
955
+ const itemCheck = await this.canStartCommand({ tool: item.tool });
956
+ const previousStatus = item.status;
957
+ const previousReason = item.waitingReason;
958
+ item.setWaiting(itemCheck.reason || 'Waiting in queue');
959
+
960
+ // Update message if status/reason changed or it's time for periodic update
961
+ const shouldUpdate = previousStatus !== item.status || previousReason !== item.waitingReason || this.shouldUpdateMessage(item);
962
+
963
+ if (shouldUpdate) {
964
+ const position = i + 1; // Position within this tool's queue
965
+ await this.updateItemMessage(item, `âŗ Waiting (${tool} queue #${position})\n\n${item.infoBlock}\n\n*Reason:*\n${item.waitingReason}`);
966
+ }
967
+ }
968
+ }
969
+ }
970
+ }
971
+
763
972
  /**
764
973
  * Execute a queue item
765
974
  * @param {SolveQueueItem} item
@@ -870,26 +1079,47 @@ export class SolveQueue {
870
1079
 
871
1080
  /**
872
1081
  * Format queue status for display
1082
+ * Shows per-tool queue counts.
873
1083
  * @returns {string}
1084
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
874
1085
  */
875
1086
  formatStatus() {
876
1087
  const stats = this.getStats();
877
1088
  if (stats.queued > 0 || stats.processing > 0) {
878
- return `Solve Queue: ${stats.queued} pending, ${stats.processing} processing\n`;
1089
+ // Show per-tool breakdown if there are items
1090
+ const toolBreakdown = Object.entries(stats.queuedByTool)
1091
+ .filter(entry => entry[1] > 0)
1092
+ .map(([tool, count]) => `${tool}: ${count}`)
1093
+ .join(', ');
1094
+ const queueInfo = toolBreakdown ? ` (${toolBreakdown})` : '';
1095
+ return `Solve Queue: ${stats.queued} pending${queueInfo}, ${stats.processing} processing\n`;
879
1096
  }
880
1097
  return 'Solve Queue: empty\n';
881
1098
  }
882
1099
 
883
1100
  /**
884
1101
  * Format detailed queue status for Telegram message
1102
+ * Shows per-tool queue breakdown.
885
1103
  * @returns {string}
1104
+ * @see https://github.com/link-assistant/hive-mind/issues/1159
886
1105
  */
887
1106
  formatDetailedStatus() {
888
1107
  const stats = this.getStats();
889
1108
  const summary = this.getQueueSummary();
890
1109
 
891
1110
  let message = '📋 *Solve Queue Status*\n\n';
892
- message += `Pending: ${stats.queued}\n`;
1111
+ message += `Pending: ${stats.queued}`;
1112
+
1113
+ // Add per-tool breakdown
1114
+ const toolBreakdown = Object.entries(stats.queuedByTool)
1115
+ .filter(entry => entry[1] > 0)
1116
+ .map(([tool, count]) => `${tool}: ${count}`)
1117
+ .join(', ');
1118
+ if (toolBreakdown) {
1119
+ message += ` (${toolBreakdown})`;
1120
+ }
1121
+ message += '\n';
1122
+
893
1123
  message += `Processing: ${stats.processing}\n`;
894
1124
  message += `Completed: ${stats.completed}\n`;
895
1125
  message += `Failed: ${stats.failed}\n\n`;
@@ -897,7 +1127,7 @@ export class SolveQueue {
897
1127
  if (summary.processing.length > 0) {
898
1128
  message += '*Currently Processing:*\n';
899
1129
  for (const item of summary.processing) {
900
- message += `â€ĸ ${item.url}\n`;
1130
+ message += `â€ĸ ${item.url} [${item.tool}]\n`;
901
1131
  }
902
1132
  message += '\n';
903
1133
  }
@@ -906,7 +1136,7 @@ export class SolveQueue {
906
1136
  message += '*Waiting in Queue:*\n';
907
1137
  for (const item of summary.pending.slice(0, 5)) {
908
1138
  const waitSeconds = Math.floor(item.waitTime / 1000);
909
- message += `â€ĸ ${item.url} (${item.status}, ${waitSeconds}s)\n`;
1139
+ message += `â€ĸ ${item.url} [${item.tool}] (${item.status}, ${waitSeconds}s)\n`;
910
1140
  if (item.waitingReason) {
911
1141
  message += ` └ ${item.waitingReason}\n`;
912
1142
  }