@link-assistant/hive-mind 2.11.13 → 2.12.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -1
  3. package/src/agent-command.lib.mjs +74 -0
  4. package/src/agent.lib.mjs +59 -34
  5. package/src/agentic-cli-updater.lib.mjs +241 -0
  6. package/src/claude.connection.lib.mjs +209 -0
  7. package/src/claude.lib.mjs +6 -202
  8. package/src/codex.lib.mjs +0 -128
  9. package/src/formal-ai-isolation.lib.mjs +62 -0
  10. package/src/formal-ai-maintenance.lib.mjs +106 -0
  11. package/src/formal-ai-model.lib.mjs +25 -0
  12. package/src/formal-ai-runtime.lib.mjs +10 -0
  13. package/src/formal-ai-sidecar.lib.mjs +565 -0
  14. package/src/formal-ai-updater.lib.mjs +294 -0
  15. package/src/formal-ai-version.lib.mjs +100 -0
  16. package/src/formal-ai.lib.mjs +11 -16
  17. package/src/github-rate-limit.lib.mjs +3 -0
  18. package/src/github-url-parser.lib.mjs +255 -0
  19. package/src/github.lib.mjs +22 -343
  20. package/src/hive.mjs +0 -152
  21. package/src/interactive-mode.lib.mjs +0 -43
  22. package/src/isolation-runner.lib.mjs +44 -173
  23. package/src/limits.lib.mjs +0 -89
  24. package/src/model-args.lib.mjs +32 -0
  25. package/src/models/index.mjs +5 -19
  26. package/src/session-monitor.lib.mjs +14 -172
  27. package/src/solve.auto-merge.lib.mjs +70 -164
  28. package/src/solve.mjs +31 -193
  29. package/src/solve.repository.lib.mjs +0 -83
  30. package/src/solve.results.lib.mjs +2 -92
  31. package/src/solve.session.lib.mjs +52 -19
  32. package/src/solve.tool-uncommitted.lib.mjs +22 -0
  33. package/src/state-lock.lib.mjs +82 -0
  34. package/src/telegram-bot.mjs +17 -65
  35. package/src/telegram-fix-command.lib.mjs +1 -8
  36. package/src/telegram-merge-queue.lib.mjs +3 -155
  37. package/src/telegram-solve-queue.lib.mjs +9 -168
  38. package/src/telegram-task-command.lib.mjs +1 -8
  39. package/src/use-m-bootstrap.lib.mjs +6 -5
  40. package/src/use-with-retry.lib.mjs +128 -2
  41. package/src/working-session-summary.lib.mjs +47 -1
@@ -9,7 +9,6 @@
9
9
  *
10
10
  * @see https://github.com/link-assistant/hive-mind/issues/1041
11
11
  */
12
-
13
12
  import { getCachedClaudeLimits, getCachedCodexLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
14
13
  export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
15
14
  import { collectExecutingItems, formatDuration, formatQueueToolSection, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses, getRunningSessionItems, groupQueueItemsByTool, reportDequeueDecision } from './telegram-solve-queue.helpers.lib.mjs';
@@ -19,7 +18,6 @@ import { reserveStartSlotForQueue } from './queue-start-reservation.lib.mjs';
19
18
  import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
20
19
  import { t } from './i18n.lib.mjs';
21
20
  import { lt } from './limits-i18n.lib.mjs';
22
-
23
21
  export const QueueItemStatus = {
24
22
  QUEUED: 'queued',
25
23
  WAITING: 'waiting',
@@ -28,20 +26,16 @@ export const QueueItemStatus = {
28
26
  FAILED: 'failed',
29
27
  CANCELLED: 'cancelled',
30
28
  };
31
-
32
29
  function getLocale(options = {}) {
33
30
  if (typeof options === 'string') return options;
34
31
  return options?.locale || null;
35
32
  }
36
-
37
33
  function appendWaitingForCurrentCommand(reason, locale) {
38
34
  return `${reason} (${lt('queue_waiting_current_command', {}, { locale })})`;
39
35
  }
40
-
41
36
  function appendRemainingDuration(reason, ms, locale) {
42
37
  return `${reason} (${lt('remaining', { duration: formatDuration(ms, { locale }) }, { locale })})`;
43
38
  }
44
-
45
39
  /**
46
40
  * Queue item representing a /solve command request
47
41
  */
@@ -57,8 +51,7 @@ class SolveQueueItem {
57
51
  this.tool = options.tool || 'claude';
58
52
  // Issue #1983: preserve per-command isolation through queued execution.
59
53
  this.perCommandIsolation = options.perCommandIsolation || null;
60
- // Issue #1688: keep parsed URL context (owner/repo/number/type) so completion
61
- // notifications can look up linked PRs for issue URLs.
54
+ // Issue #1688: keep parsed URL context (owner/repo/number/type) so completion notifications can look up linked PRs for issue URLs.
62
55
  this.urlContext = options.urlContext || null;
63
56
  // Issue #1688: requester user ID for /subscribe duplicate-suppression.
64
57
  this.requesterUserId = options.ctx?.from?.id ?? null;
@@ -75,11 +68,9 @@ class SolveQueueItem {
75
68
  this.sessionName = null;
76
69
  // Message tracking - forget after STARTED
77
70
  this.messageInfo = null; // { chatId, messageId }
78
- // Track when we last updated the Telegram message
79
- // See: https://github.com/link-assistant/hive-mind/issues/1078
71
+ // Track when we last updated the Telegram message See: https://github.com/link-assistant/hive-mind/issues/1078
80
72
  this.lastMessageUpdateTime = null;
81
73
  }
82
-
83
74
  /**
84
75
  * Update status to waiting with reason
85
76
  * @param {string} reason - Waiting reason
@@ -88,7 +79,6 @@ class SolveQueueItem {
88
79
  this.status = QueueItemStatus.WAITING;
89
80
  this.waitingReason = reason;
90
81
  }
91
-
92
82
  /**
93
83
  * Update status to starting
94
84
  */
@@ -97,7 +87,6 @@ class SolveQueueItem {
97
87
  this.startedAt = new Date();
98
88
  this.waitingReason = null;
99
89
  }
100
-
101
90
  /**
102
91
  * Update status to started and clear message tracking
103
92
  * @param {string} sessionName - Session name for debugging
@@ -108,7 +97,6 @@ class SolveQueueItem {
108
97
  // Terminal status - forget message tracking
109
98
  this.messageInfo = null;
110
99
  }
111
-
112
100
  /**
113
101
  * Mark item as failed
114
102
  * @param {Error|string} error - Error that occurred
@@ -117,14 +105,12 @@ class SolveQueueItem {
117
105
  this.status = QueueItemStatus.FAILED;
118
106
  this.error = error instanceof Error ? error.message : error;
119
107
  }
120
-
121
108
  /**
122
109
  * Mark item as cancelled
123
110
  */
124
111
  setCancelled() {
125
112
  this.status = QueueItemStatus.CANCELLED;
126
113
  }
127
-
128
114
  /**
129
115
  * Get wait time in queue (ms)
130
116
  */
@@ -132,7 +118,6 @@ class SolveQueueItem {
132
118
  const endTime = this.startedAt || new Date();
133
119
  return endTime - this.createdAt;
134
120
  }
135
-
136
121
  /**
137
122
  * Format for display
138
123
  * @returns {string}
@@ -141,7 +126,6 @@ class SolveQueueItem {
141
126
  return `[${this.id}] ${this.url} (${this.status})`;
142
127
  }
143
128
  }
144
-
145
129
  /**
146
130
  * Solve Queue - Producer/Consumer queue for /solve commands
147
131
  *
@@ -159,13 +143,10 @@ export class SolveQueue {
159
143
  this.messageUpdateCallback = options.messageUpdateCallback || null;
160
144
  this.getRunningProcessesFn = options.getRunningProcesses || getRunningProcesses;
161
145
  this.getRunningIsolatedSessionsFn = options.getRunningIsolatedSessions || getRunningIsolatedSessions;
162
- // Source of currently-executing detached sessions (with issue/PR URLs) used
163
- // to list executing tasks in the detailed status (issue #1837).
146
+ // Source of currently-executing detached sessions (with issue/PR URLs) used to list executing tasks in the detailed status (issue #1837).
164
147
  this.getRunningSessionItemsFn = options.getRunningSessionItems || getRunningSessionItems;
165
148
  this.autoStart = options.autoStart !== false;
166
-
167
- // Separate queues per tool type - claude tasks never block other tool tasks
168
- // See: https://github.com/link-assistant/hive-mind/issues/1159
149
+ // Separate queues per tool type - claude tasks never block other tool tasks See: https://github.com/link-assistant/hive-mind/issues/1159
169
150
  this.queues = {
170
151
  claude: [],
171
152
  agent: [],
@@ -177,7 +158,6 @@ export class SolveQueue {
177
158
  this.completed = [];
178
159
  this.failed = [];
179
160
  this.isRunning = true;
180
-
181
161
  // Timing - separate per tool to ensure independent processing
182
162
  this.lastStartTimeByTool = {
183
163
  claude: null,
@@ -187,10 +167,8 @@ export class SolveQueue {
187
167
  gemini: null,
188
168
  };
189
169
  this.lastStartTime = null; // Legacy: global last-start timestamp
190
-
191
170
  // Consumer task reference
192
171
  this.consumerTask = null;
193
-
194
172
  // Statistics
195
173
  this.stats = {
196
174
  totalEnqueued: 0,
@@ -200,10 +178,8 @@ export class SolveQueue {
200
178
  totalCancelled: 0,
201
179
  throttleReasons: {},
202
180
  };
203
-
204
181
  this.log('SolveQueue initialized with separate tool queues');
205
182
  }
206
-
207
183
  /**
208
184
  * Get the queue array for a specific tool, creating it if needed
209
185
  * @param {string} tool - Tool type ('claude', 'agent', 'codex', 'gemini', etc.)
@@ -215,7 +191,6 @@ export class SolveQueue {
215
191
  }
216
192
  return this.queues[tool];
217
193
  }
218
-
219
194
  /**
220
195
  * Get combined queue length across all tools (for backwards compatibility)
221
196
  * @returns {number} Total queue length
@@ -227,7 +202,6 @@ export class SolveQueue {
227
202
  }
228
203
  return total;
229
204
  }
230
-
231
205
  /**
232
206
  * Get total pending count across all tool queues
233
207
  * @returns {number} Total pending items
@@ -239,7 +213,6 @@ export class SolveQueue {
239
213
  }
240
214
  return total;
241
215
  }
242
-
243
216
  /**
244
217
  * Log message if verbose mode is enabled
245
218
  * @param {string} message
@@ -249,7 +222,6 @@ export class SolveQueue {
249
222
  console.log(`[VERBOSE] /queue: ${message}`);
250
223
  }
251
224
  }
252
-
253
225
  /**
254
226
  * Add a solve command to the appropriate tool queue
255
227
  * Items are added to the queue for their specific tool type.
@@ -262,15 +234,11 @@ export class SolveQueue {
262
234
  const toolQueue = this.getToolQueue(item.tool);
263
235
  toolQueue.push(item);
264
236
  this.stats.totalEnqueued++;
265
-
266
237
  this.log(`Enqueued: ${item.toString()} to ${item.tool} queue, queue length: ${toolQueue.length}`);
267
-
268
238
  // Start consumer if not already running
269
239
  if (this.autoStart) this.ensureConsumerRunning();
270
-
271
240
  return item;
272
241
  }
273
-
274
242
  /**
275
243
  * Find an item by URL in any queue or processing items
276
244
  * Used to prevent duplicate URLs from being added to the queue
@@ -286,17 +254,14 @@ export class SolveQueue {
286
254
  return queuedItem;
287
255
  }
288
256
  }
289
-
290
257
  // Check processing items
291
258
  for (const item of this.processing.values()) {
292
259
  if (item.url === url) {
293
260
  return item;
294
261
  }
295
262
  }
296
-
297
263
  return null;
298
264
  }
299
-
300
265
  /**
301
266
  * Cancel a queued item by ID
302
267
  * Searches all tool queues to find the item.
@@ -316,15 +281,12 @@ export class SolveQueue {
316
281
  return true;
317
282
  }
318
283
  }
319
-
320
284
  if (this.processing.has(id)) {
321
285
  this.log(`Cannot cancel processing item: ${id}`);
322
286
  return false;
323
287
  }
324
-
325
288
  return false;
326
289
  }
327
-
328
290
  /**
329
291
  * Get queue statistics
330
292
  * @returns {Object}
@@ -337,7 +299,6 @@ export class SolveQueue {
337
299
  queuedByTool[tool] = toolQueue.length;
338
300
  totalQueued += toolQueue.length;
339
301
  }
340
-
341
302
  return {
342
303
  queued: totalQueued,
343
304
  queuedByTool,
@@ -351,7 +312,6 @@ export class SolveQueue {
351
312
  isRunning: this.isRunning,
352
313
  };
353
314
  }
354
-
355
315
  /**
356
316
  * Count processing items by tool type
357
317
  * Used for tool-specific limit checking - e.g., Claude limits only count Claude processing items
@@ -368,17 +328,14 @@ export class SolveQueue {
368
328
  }
369
329
  return count;
370
330
  }
371
-
372
331
  recordStart(tool = 'claude', startTime = Date.now()) {
373
332
  this.lastStartTimeByTool[tool] = startTime;
374
333
  this.lastStartTime = startTime;
375
334
  return startTime;
376
335
  }
377
-
378
336
  reserveStartSlot(options = {}) {
379
337
  return reserveStartSlotForQueue(this, options);
380
338
  }
381
-
382
339
  /**
383
340
  * Find the next startable item across all tool queues. Each tool is checked
384
341
  * independently so tool-specific limits do not block unrelated tools; issue
@@ -393,56 +350,41 @@ export class SolveQueue {
393
350
  */
394
351
  async findStartableItems() {
395
352
  const startableItems = [];
396
- // Per-tool head diagnostics: why each queue head is/isn't startable, so
397
- // global FIFO ordering can be audited in production (issue #2051).
353
+ // Per-tool head diagnostics: why each queue head is/isn't startable, so global FIFO ordering can be audited in production (issue #2051).
398
354
  const headDiagnostics = [];
399
-
400
355
  for (const [tool, toolQueue] of Object.entries(this.queues)) {
401
356
  if (toolQueue.length === 0) continue;
402
-
403
357
  // Check if first item in this tool's queue can start
404
358
  const check = await this.canStartCommand({ tool, locale: toolQueue[0]?.locale || null });
405
-
406
- // When a 'reject' strategy threshold is exceeded, immediately reject
407
- // all items in this tool's queue instead of leaving them waiting.
408
- // See: https://github.com/link-assistant/hive-mind/issues/1555
359
+ // When a 'reject' strategy threshold is exceeded, immediately reject all items in this tool's queue instead of leaving them waiting. See: https://github.com/link-assistant/hive-mind/issues/1555
409
360
  if (check.rejected) {
410
361
  await this.rejectAllItemsInQueue(tool, toolQueue, check.rejectReason);
411
362
  continue;
412
363
  }
413
-
414
364
  const item = toolQueue[0];
415
365
  if (!item) continue;
416
-
417
- // Determine startability and capture the blocking reason(s) for diagnostics.
418
- // For tool-specific one-at-a-time, only count that tool's processing items.
366
+ // Determine startability and capture the blocking reason(s) for diagnostics. For tool-specific one-at-a-time, only count that tool's processing items.
419
367
  const toolProcessingCount = this.getProcessingCountByTool(tool);
420
368
  let startable = false;
421
369
  const blockReasons = Array.isArray(check.reasons) ? [...check.reasons] : [];
422
370
  if (check.canStart) {
423
371
  if (check.oneAtATime && toolProcessingCount > 0) {
424
- // One-at-a-time for this tool with a task already processing: skip it
425
- // but don't block other tools.
372
+ // One-at-a-time for this tool with a task already processing: skip it but don't block other tools.
426
373
  blockReasons.push(`one-at-a-time: ${toolProcessingCount} ${tool} task(s) already processing`);
427
374
  } else {
428
375
  startable = true;
429
376
  startableItems.push({ item, tool, index: 0, check });
430
377
  }
431
378
  }
432
-
433
379
  headDiagnostics.push({ tool, item, ageMs: Date.now() - item.createdAt, startable, blockReasons });
434
380
  }
435
-
436
381
  // Global FIFO: the oldest startable head wins the (globally paced) startup slot.
437
382
  startableItems.sort((a, b) => a.item.createdAt - b.item.createdAt);
438
383
  const selected = startableItems.slice(0, 1);
439
-
440
384
  // Observe the dequeue decision (issue #2051): see reportDequeueDecision().
441
385
  reportDequeueDecision(this, headDiagnostics, selected[0]);
442
-
443
386
  return selected;
444
387
  }
445
-
446
388
  /**
447
389
  * Reject all items in a tool queue and notify users.
448
390
  * Called when a 'reject' strategy threshold is exceeded for queued items.
@@ -459,14 +401,11 @@ export class SolveQueue {
459
401
  item.setFailed(reason);
460
402
  this.failed.push(item);
461
403
  this.stats.totalFailed++;
462
-
463
404
  this.log(`Rejected queued item: ${item.toString()} from ${tool} queue - ${reason}`);
464
-
465
405
  await this.updateItemMessage(item, t('telegram.solve_rejected', { infoBlock: item.infoBlock, reason }, { locale: item.locale }));
466
406
  }
467
407
  while (this.failed.length > 100) this.failed.shift();
468
408
  }
469
-
470
409
  /**
471
410
  * Find first queue item that can start based on its tool's limits (legacy compatibility)
472
411
  * With separate queues, returns the first startable item from any tool queue.
@@ -482,7 +421,6 @@ export class SolveQueue {
482
421
  }
483
422
  return { item: null, index: -1, check: null };
484
423
  }
485
-
486
424
  /**
487
425
  * Get queue items summary for display
488
426
  * Combines items from all tool queues into a single pending list.
@@ -506,10 +444,8 @@ export class SolveQueue {
506
444
  });
507
445
  }
508
446
  }
509
-
510
447
  // Sort by createdAt to show oldest first (global order)
511
448
  pending.sort((a, b) => a.createdAt - b.createdAt);
512
-
513
449
  return {
514
450
  pending,
515
451
  processing: Array.from(this.processing.values()).map(item => ({
@@ -522,7 +458,6 @@ export class SolveQueue {
522
458
  })),
523
459
  };
524
460
  }
525
-
526
461
  /**
527
462
  * Get external processing counts from both process scanning and tracked
528
463
  * isolated sessions. The displayed/accounted value is the maximum of the two
@@ -539,7 +474,6 @@ export class SolveQueue {
539
474
  const isolatedByTool = isolated.byTool || {};
540
475
  const processByTool = {};
541
476
  const byTool = {};
542
-
543
477
  await Promise.all(
544
478
  uniqueTools.map(async tool => {
545
479
  const result = await this.getRunningProcessesFn(tool, this.verbose);
@@ -549,10 +483,8 @@ export class SolveQueue {
549
483
  byTool[tool] = Math.max(processCount, isolatedCount);
550
484
  })
551
485
  );
552
-
553
486
  const processTotal = Object.values(processByTool).reduce((sum, count) => sum + count, 0);
554
487
  const isolatedTotal = isolated.count || Object.values(isolatedByTool).reduce((sum, count) => sum + count, 0);
555
-
556
488
  return {
557
489
  byTool,
558
490
  processByTool,
@@ -562,7 +494,6 @@ export class SolveQueue {
562
494
  processTotal,
563
495
  };
564
496
  }
565
-
566
497
  /**
567
498
  * Check if a new command can start.
568
499
  *
@@ -586,10 +517,7 @@ export class SolveQueue {
586
517
  let oneAtATime = false;
587
518
  let rejected = false;
588
519
  let rejectReason = null;
589
-
590
- // Check minimum interval since the last task start globally.
591
- // Issue #2015: do not let another tool queue bypass startup pacing; host
592
- // CPU/RAM/disk metrics need time to settle before any next task starts.
520
+ // Check minimum interval since the last task start globally. Issue #2015: do not let another tool queue bypass startup pacing; host CPU/RAM/disk metrics need time to settle before any next task starts.
593
521
  const lastStartTime = this.lastStartTime || null;
594
522
  if (lastStartTime) {
595
523
  const timeSinceLastStart = Date.now() - lastStartTime;
@@ -599,7 +527,6 @@ export class SolveQueue {
599
527
  this.recordThrottle('min_interval');
600
528
  }
601
529
  }
602
-
603
530
  // Check running tool processes (this is a metric, not a blocking reason by itself).
604
531
  // For screen-isolated sessions, use the maximum of `$ --status` executing
605
532
  // counts and pgrep counts so detached sessions remain visible.
@@ -613,11 +540,9 @@ export class SolveQueue {
613
540
  const hasRunningCodex = codexProcessCount > 0;
614
541
  const hasRunningQwen = qwenProcessCount > 0;
615
542
  const hasRunningGemini = geminiProcessCount > 0;
616
-
617
543
  // Calculate total processing count for system resources (all tools)
618
544
  // System resources (RAM, CPU, disk) apply to all tools
619
545
  const totalProcessing = this.processing.size + externalProcessing.total;
620
-
621
546
  // Calculate Claude-specific processing count for Claude API limits
622
547
  // Only counts Claude items in queue + external claude processes
623
548
  // Non-Claude items don't count against Claude's one-at-a-time limit
@@ -626,7 +551,6 @@ export class SolveQueue {
626
551
  const codexProcessingCount = this.getProcessingCountByTool('codex');
627
552
  const qwenProcessingCount = this.getProcessingCountByTool('qwen');
628
553
  const geminiProcessingCount = this.getProcessingCountByTool('gemini');
629
-
630
554
  // Track claude_running as a metric (but don't add to reasons yet)
631
555
  if (hasRunningClaude) {
632
556
  this.recordThrottle('claude_running');
@@ -640,7 +564,6 @@ export class SolveQueue {
640
564
  if (hasRunningGemini) {
641
565
  this.recordThrottle('gemini_running');
642
566
  }
643
-
644
567
  // Check system resources with strategy support
645
568
  // System resources apply to ALL tools, not just Claude
646
569
  // See: https://github.com/link-assistant/hive-mind/issues/1155
@@ -656,7 +579,6 @@ export class SolveQueue {
656
579
  if (resourceCheck.oneAtATime) {
657
580
  oneAtATime = true;
658
581
  }
659
-
660
582
  // Check API limits with strategy support (pass hasRunningClaude, claudeProcessingCount, and tool)
661
583
  // Claude limits use claudeProcessingCount (only Claude items), not totalProcessing
662
584
  // This allows non-Claude tasks to proceed when Claude limits are reached
@@ -675,7 +597,6 @@ export class SolveQueue {
675
597
  if (limitCheck.oneAtATime) {
676
598
  oneAtATime = true;
677
599
  }
678
-
679
600
  // "Claude process running" only blocks if there are OTHER reasons too
680
601
  // This allows parallel execution when limits are not exceeded
681
602
  if (hasRunningClaude && reasons.length > 0) {
@@ -693,9 +614,7 @@ export class SolveQueue {
693
614
  if (tool === 'gemini' && hasRunningGemini && reasons.length > 0) {
694
615
  reasons.push(`${formatWaitingReason('gemini_running', geminiProcessCount, 0, { locale })} (${lt('queue_processes', { count: geminiProcessCount }, { locale })})`);
695
616
  }
696
-
697
617
  const canStart = reasons.length === 0 && !rejected;
698
-
699
618
  if (!canStart && this.verbose) {
700
619
  if (rejected) {
701
620
  this.log(`Rejected: ${rejectReason}`);
@@ -703,7 +622,6 @@ export class SolveQueue {
703
622
  this.log(`Cannot start: ${reasons.join(', ')}`);
704
623
  }
705
624
  }
706
-
707
625
  return {
708
626
  canStart,
709
627
  rejected,
@@ -724,7 +642,6 @@ export class SolveQueue {
724
642
  geminiProcessingCount,
725
643
  };
726
644
  }
727
-
728
645
  /**
729
646
  * Check system resources (RAM, CPU, disk) using cached values
730
647
  *
@@ -755,7 +672,6 @@ export class SolveQueue {
755
672
  let oneAtATime = false;
756
673
  let rejected = false;
757
674
  let rejectReason = null;
758
-
759
675
  // Check RAM (using cached value)
760
676
  const memResult = await getCachedMemoryInfo(this.verbose);
761
677
  if (memResult.success) {
@@ -764,7 +680,6 @@ export class SolveQueue {
764
680
  const reason = formatWaitingReason('ram', memResult.memory.usedPercentage, QUEUE_CONFIG.thresholds.ram.value, { locale });
765
681
  const strategy = QUEUE_CONFIG.thresholds.ram.strategy;
766
682
  this.recordThrottle(`ram_${strategy}`);
767
-
768
683
  if (strategy === 'reject') {
769
684
  rejected = true;
770
685
  rejectReason = reason;
@@ -779,7 +694,6 @@ export class SolveQueue {
779
694
  }
780
695
  }
781
696
  }
782
-
783
697
  // Check CPU using 5-minute load average (more stable than 1-minute)
784
698
  const cpuResult = await getCachedCpuInfo(this.verbose);
785
699
  if (cpuResult.success) {
@@ -791,16 +705,13 @@ export class SolveQueue {
791
705
  // Load average of 1.0 per CPU = 100% utilization
792
706
  const usageRatio = loadAvg5 / cpuCount;
793
707
  const usagePercent = Math.min(100, Math.round(usageRatio * 100));
794
-
795
708
  if (this.verbose) {
796
709
  this.log(`CPU 5m load avg: ${loadAvg5.toFixed(2)}, cpus: ${cpuCount}, usage: ${usagePercent}%`);
797
710
  }
798
-
799
711
  if (usageRatio >= QUEUE_CONFIG.thresholds.cpu.value) {
800
712
  const reason = formatWaitingReason('cpu', usagePercent, QUEUE_CONFIG.thresholds.cpu.value, { locale });
801
713
  const strategy = QUEUE_CONFIG.thresholds.cpu.strategy;
802
714
  this.recordThrottle(`cpu_${strategy}`);
803
-
804
715
  if (strategy === 'reject') {
805
716
  rejected = true;
806
717
  rejectReason = reason;
@@ -815,7 +726,6 @@ export class SolveQueue {
815
726
  }
816
727
  }
817
728
  }
818
-
819
729
  // Check disk space (using cached value)
820
730
  // Default strategy changed to 'reject' because queue is lost on restart anyway
821
731
  // See: https://github.com/link-assistant/hive-mind/issues/1253
@@ -828,7 +738,6 @@ export class SolveQueue {
828
738
  const reason = formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.thresholds.disk.value, { locale });
829
739
  const strategy = QUEUE_CONFIG.thresholds.disk.strategy;
830
740
  this.recordThrottle(`disk_${strategy}`);
831
-
832
741
  if (strategy === 'reject') {
833
742
  rejected = true;
834
743
  rejectReason = reason;
@@ -843,10 +752,8 @@ export class SolveQueue {
843
752
  }
844
753
  }
845
754
  }
846
-
847
755
  return { ok: reasons.length === 0 && !rejected, reasons, oneAtATime, rejected, rejectReason };
848
756
  }
849
-
850
757
  /**
851
758
  * Check API limits (Claude, GitHub) using cached values
852
759
  *
@@ -876,16 +783,13 @@ export class SolveQueue {
876
783
  let oneAtATime = false;
877
784
  let rejected = false;
878
785
  let rejectReason = null;
879
-
880
786
  // Apply Claude-specific limits only when tool is 'claude'
881
787
  // Other tools (like 'agent', 'gemini', and 'qwen') use different rate limiting backends and are not
882
788
  // affected by Claude API limits (5-hour session, weekly limits)
883
789
  // See: https://github.com/link-assistant/hive-mind/issues/1159
884
790
  const applyClaudeLimits = tool === 'claude';
885
791
  const applyCodexLimits = tool === 'codex';
886
-
887
792
  const totalToolProcessing = toolProcessingCount + (hasRunningToolProcess ? 1 : 0);
888
-
889
793
  // Check Claude limits (using cached value)
890
794
  // Only applied when tool is 'claude'
891
795
  if (applyClaudeLimits) {
@@ -893,7 +797,6 @@ export class SolveQueue {
893
797
  if (claudeResult.success) {
894
798
  const sessionPercent = claudeResult.usage.currentSession.percentage;
895
799
  const weeklyPercent = claudeResult.usage.allModels.percentage;
896
-
897
800
  // Session limit (5-hour)
898
801
  // Configurable strategy via HIVE_MIND_QUEUE_CONFIG or HIVE_MIND_CLAUDE_5_HOUR_SESSION_STRATEGY
899
802
  // See: https://github.com/link-assistant/hive-mind/issues/1133, #1159, #1253
@@ -903,7 +806,6 @@ export class SolveQueue {
903
806
  const reason = formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.claude5Hour.value, { locale });
904
807
  const strategy = QUEUE_CONFIG.thresholds.claude5Hour.strategy;
905
808
  this.recordThrottle(sessionRatio >= 1.0 ? 'claude_5_hour_session_100' : `claude_5_hour_session_${strategy}`);
906
-
907
809
  if (strategy === 'reject') {
908
810
  rejected = true;
909
811
  rejectReason = reason;
@@ -918,7 +820,6 @@ export class SolveQueue {
918
820
  }
919
821
  }
920
822
  }
921
-
922
823
  // Weekly limit
923
824
  // Configurable strategy via HIVE_MIND_QUEUE_CONFIG or HIVE_MIND_CLAUDE_WEEKLY_STRATEGY
924
825
  // See: https://github.com/link-assistant/hive-mind/issues/1133, #1159, #1253
@@ -928,7 +829,6 @@ export class SolveQueue {
928
829
  const reason = formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.claudeWeekly.value, { locale });
929
830
  const strategy = QUEUE_CONFIG.thresholds.claudeWeekly.strategy;
930
831
  this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : `claude_weekly_${strategy}`);
931
-
932
832
  if (strategy === 'reject') {
933
833
  rejected = true;
934
834
  rejectReason = reason;
@@ -949,14 +849,12 @@ export class SolveQueue {
949
849
  if (codexResult.success) {
950
850
  const sessionPercent = codexResult.usage.currentSession.percentage;
951
851
  const weeklyPercent = codexResult.usage.allModels.percentage;
952
-
953
852
  if (sessionPercent !== null) {
954
853
  const sessionRatio = sessionPercent / 100;
955
854
  if (sessionRatio >= QUEUE_CONFIG.thresholds.codex5Hour.value) {
956
855
  const reason = formatWaitingReason('codex_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.codex5Hour.value, { locale });
957
856
  const strategy = QUEUE_CONFIG.thresholds.codex5Hour.strategy;
958
857
  this.recordThrottle(sessionRatio >= 1.0 ? 'codex_5_hour_session_100' : `codex_5_hour_session_${strategy}`);
959
-
960
858
  if (strategy === 'reject') {
961
859
  rejected = true;
962
860
  rejectReason = reason;
@@ -970,14 +868,12 @@ export class SolveQueue {
970
868
  }
971
869
  }
972
870
  }
973
-
974
871
  if (weeklyPercent !== null) {
975
872
  const weeklyRatio = weeklyPercent / 100;
976
873
  if (weeklyRatio >= QUEUE_CONFIG.thresholds.codexWeekly.value) {
977
874
  const reason = formatWaitingReason('codex_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.codexWeekly.value, { locale });
978
875
  const strategy = QUEUE_CONFIG.thresholds.codexWeekly.strategy;
979
876
  this.recordThrottle(weeklyRatio >= 1.0 ? 'codex_weekly_100' : `codex_weekly_${strategy}`);
980
-
981
877
  if (strategy === 'reject') {
982
878
  rejected = true;
983
879
  rejectReason = reason;
@@ -995,7 +891,6 @@ export class SolveQueue {
995
891
  } else if (this.verbose) {
996
892
  this.log(`Claude limits not applied for --tool ${tool}`);
997
893
  }
998
-
999
894
  // Check GitHub limits when the active tool already has a running process.
1000
895
  // This keeps the queue behavior aligned with the existing one-at-a-time throttling model.
1001
896
  // Configurable strategy via HIVE_MIND_QUEUE_CONFIG or HIVE_MIND_GITHUB_API_STRATEGY
@@ -1008,7 +903,6 @@ export class SolveQueue {
1008
903
  const reason = formatWaitingReason('github', usedPercent, QUEUE_CONFIG.thresholds.githubApi.value, { locale });
1009
904
  const strategy = QUEUE_CONFIG.thresholds.githubApi.strategy;
1010
905
  this.recordThrottle(usedRatio >= 1.0 ? 'github_100' : `github_${strategy}`);
1011
-
1012
906
  if (strategy === 'reject') {
1013
907
  rejected = true;
1014
908
  rejectReason = reason;
@@ -1024,10 +918,8 @@ export class SolveQueue {
1024
918
  }
1025
919
  }
1026
920
  }
1027
-
1028
921
  return { ok: reasons.length === 0 && !rejected, reasons, oneAtATime, rejected, rejectReason };
1029
922
  }
1030
-
1031
923
  /**
1032
924
  * Record a throttle event for statistics
1033
925
  * @param {string} reason
@@ -1035,20 +927,17 @@ export class SolveQueue {
1035
927
  recordThrottle(reason) {
1036
928
  this.stats.throttleReasons[reason] = (this.stats.throttleReasons[reason] || 0) + 1;
1037
929
  }
1038
-
1039
930
  /**
1040
931
  * Ensure consumer task is running
1041
932
  */
1042
933
  ensureConsumerRunning() {
1043
934
  if (this.consumerTask) return;
1044
-
1045
935
  this.consumerTask = this.runConsumer();
1046
936
  this.consumerTask.catch(error => {
1047
937
  console.error('[solve_queue] Consumer error:', error);
1048
938
  this.consumerTask = null;
1049
939
  });
1050
940
  }
1051
-
1052
941
  /**
1053
942
  * Update item message in Telegram
1054
943
  * @param {SolveQueueItem} item
@@ -1057,7 +946,6 @@ export class SolveQueue {
1057
946
  */
1058
947
  async updateItemMessage(item, text, trackUpdateTime = true) {
1059
948
  if (!item.messageInfo || !item.ctx) return;
1060
-
1061
949
  try {
1062
950
  const { chatId, messageId } = item.messageInfo;
1063
951
  await item.ctx.telegram.editMessageText(chatId, messageId, undefined, text, { parse_mode: 'Markdown' });
@@ -1068,7 +956,6 @@ export class SolveQueue {
1068
956
  this.log(`Failed to update message: ${error.message}`);
1069
957
  }
1070
958
  }
1071
-
1072
959
  /**
1073
960
  * Check if an item's message should be updated periodically
1074
961
  * @param {SolveQueueItem} item
@@ -1079,7 +966,6 @@ export class SolveQueue {
1079
966
  if (!item.lastMessageUpdateTime) return true; // Never updated
1080
967
  return Date.now() - item.lastMessageUpdateTime >= QUEUE_CONFIG.MESSAGE_UPDATE_INTERVAL_MS;
1081
968
  }
1082
-
1083
969
  /**
1084
970
  * Consumer loop - processes items from all tool queues
1085
971
  *
@@ -1093,16 +979,13 @@ export class SolveQueue {
1093
979
  */
1094
980
  async runConsumer() {
1095
981
  this.log('Consumer started with separate tool queues');
1096
-
1097
982
  while (this.isRunning) {
1098
983
  // Check if all queues are empty
1099
984
  if (this.getTotalQueueLength() === 0) {
1100
985
  await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
1101
986
  continue;
1102
987
  }
1103
-
1104
988
  const startableItems = await this.findStartableItems();
1105
-
1106
989
  if (startableItems.length === 0) {
1107
990
  // No items can start - update all queued items with their tool-specific waiting reasons
1108
991
  await this.updateAllWaitingItems();
@@ -1110,37 +993,28 @@ export class SolveQueue {
1110
993
  await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
1111
994
  continue;
1112
995
  }
1113
-
1114
996
  for (const startable of startableItems) {
1115
997
  const { tool } = startable;
1116
998
  const toolQueue = this.getToolQueue(tool);
1117
-
1118
999
  // Remove the first item from this tool's queue
1119
1000
  const item = toolQueue.shift();
1120
1001
  if (!item) continue;
1121
-
1122
1002
  // Update status to Starting
1123
1003
  item.setStarting();
1124
1004
  this.processing.set(item.id, item);
1125
-
1126
1005
  this.recordStart(tool);
1127
1006
  this.stats.totalStarted++;
1128
-
1129
1007
  await this.updateItemMessage(item, formatStartingWorkSessionMessage({ infoBlock: item.infoBlock, locale: item.locale }));
1130
-
1131
1008
  this.log(`Starting: ${item.toString()} from ${tool} queue`);
1132
-
1133
1009
  // Execute in background
1134
1010
  this.executeItem(item).catch(error => {
1135
1011
  console.error(`[solve_queue] Execution error for ${item.id}:`, error);
1136
1012
  });
1137
1013
  }
1138
1014
  }
1139
-
1140
1015
  this.log('Consumer stopped');
1141
1016
  this.consumerTask = null;
1142
1017
  }
1143
-
1144
1018
  /**
1145
1019
  * Update all waiting items with their tool-specific waiting reasons.
1146
1020
  * Items blocked by a 'reject' strategy threshold are immediately rejected
@@ -1160,7 +1034,6 @@ export class SolveQueue {
1160
1034
  await this.rejectAllItemsInQueue(tool, toolQueue, toolCheck.rejectReason);
1161
1035
  continue;
1162
1036
  }
1163
-
1164
1037
  for (let i = 0; i < toolQueue.length; i++) {
1165
1038
  const item = toolQueue[i];
1166
1039
  if (item.status === QueueItemStatus.QUEUED || item.status === QueueItemStatus.WAITING) {
@@ -1169,10 +1042,8 @@ export class SolveQueue {
1169
1042
  const previousReason = item.waitingReason;
1170
1043
  const waitReason = itemCheck.reason || lt('queue_waiting_in_queue', {}, { locale: item.locale });
1171
1044
  item.setWaiting(waitReason);
1172
-
1173
1045
  // Update message if status/reason changed or it's time for periodic update
1174
1046
  const shouldUpdate = previousStatus !== item.status || previousReason !== item.waitingReason || this.shouldUpdateMessage(item);
1175
-
1176
1047
  if (shouldUpdate) {
1177
1048
  const position = i + 1; // Position within this tool's queue
1178
1049
  await this.updateItemMessage(item, `${t('telegram.solve_waiting', { tool, position }, { locale: item.locale })}\n\n${item.infoBlock}\n\n*${t('telegram.reason_label', {}, { locale: item.locale })}:*\n${item.waitingReason}`);
@@ -1181,7 +1052,6 @@ export class SolveQueue {
1181
1052
  }
1182
1053
  }
1183
1054
  }
1184
-
1185
1055
  /**
1186
1056
  * Execute a queue item
1187
1057
  * @param {SolveQueueItem} item
@@ -1190,23 +1060,19 @@ export class SolveQueue {
1190
1060
  try {
1191
1061
  if (this.executeCallback) {
1192
1062
  const result = await this.executeCallback(item);
1193
-
1194
1063
  // Extract session name from result
1195
1064
  let sessionName = result?.sessionId || 'unknown';
1196
1065
  if (result && result.output) {
1197
1066
  const sessionMatch = result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/);
1198
1067
  if (sessionMatch) sessionName = sessionMatch[1];
1199
1068
  }
1200
-
1201
1069
  // IMPORTANT: Save messageInfo BEFORE calling setStarted, because setStarted clears it
1202
1070
  // This was a bug where the final message update never happened because messageInfo was null
1203
1071
  // See: https://github.com/link-assistant/hive-mind/issues/1062
1204
1072
  const savedMessageInfo = item.messageInfo;
1205
-
1206
1073
  // Update to Started status (terminal - forgets message tracking)
1207
1074
  item.setStarted(sessionName);
1208
1075
  this.stats.totalCompleted++;
1209
-
1210
1076
  // Final message update using saved messageInfo
1211
1077
  if (item.ctx && result && savedMessageInfo) {
1212
1078
  const { chatId, messageId } = savedMessageInfo;
@@ -1241,7 +1107,6 @@ export class SolveQueue {
1241
1107
  item.setFailed(error);
1242
1108
  this.stats.totalFailed++;
1243
1109
  console.error(`[solve_queue] Item failed: ${item.id}`, error);
1244
-
1245
1110
  // Try to update message with error
1246
1111
  const { chatId, messageId } = item.messageInfo || {};
1247
1112
  if (chatId && messageId && item.ctx) {
@@ -1256,21 +1121,17 @@ export class SolveQueue {
1256
1121
  }
1257
1122
  } finally {
1258
1123
  this.processing.delete(item.id);
1259
-
1260
1124
  if (item.status === QueueItemStatus.STARTED) {
1261
1125
  this.completed.push(item);
1262
1126
  } else if (item.status === QueueItemStatus.FAILED) {
1263
1127
  this.failed.push(item);
1264
1128
  }
1265
-
1266
1129
  this.log(`Finished: ${item.toString()}`);
1267
-
1268
1130
  // Limit history size
1269
1131
  while (this.completed.length > 100) this.completed.shift();
1270
1132
  while (this.failed.length > 100) this.failed.shift();
1271
1133
  }
1272
1134
  }
1273
-
1274
1135
  /**
1275
1136
  * Sleep for specified milliseconds
1276
1137
  * @param {number} ms
@@ -1279,7 +1140,6 @@ export class SolveQueue {
1279
1140
  sleep(ms) {
1280
1141
  return new Promise(resolve => setTimeout(resolve, ms));
1281
1142
  }
1282
-
1283
1143
  /**
1284
1144
  * Stop the queue
1285
1145
  */
@@ -1287,7 +1147,6 @@ export class SolveQueue {
1287
1147
  this.log('Stopping queue...');
1288
1148
  this.isRunning = false;
1289
1149
  }
1290
-
1291
1150
  /**
1292
1151
  * Clear the limit cache
1293
1152
  */
@@ -1295,7 +1154,6 @@ export class SolveQueue {
1295
1154
  getLimitCache().clear();
1296
1155
  this.log('Limit cache cleared');
1297
1156
  }
1298
-
1299
1157
  /**
1300
1158
  * Format queue status for display in /limits command
1301
1159
  * Shows per-tool queue breakdown with processing counts.
@@ -1324,10 +1182,8 @@ export class SolveQueue {
1324
1182
  const processing = externalProcessing.byTool[tool] || 0;
1325
1183
  message += `${tool} (${lt('queue_pending', {}, { locale })}: ${pending}, ${lt('queue_processing', {}, { locale })}: ${processing})\n`;
1326
1184
  }
1327
-
1328
1185
  return message;
1329
1186
  }
1330
-
1331
1187
  /**
1332
1188
  * Format detailed queue status for Telegram message.
1333
1189
  *
@@ -1349,7 +1205,6 @@ export class SolveQueue {
1349
1205
  // real running tasks; the queue's own `processing` Map is emptied once a task
1350
1206
  // is dispatched, so without this the executing items are never listed (#1837).
1351
1207
  const runningSessionItems = await this.getRunningSessionItemsFn(this.verbose);
1352
-
1353
1208
  // Section labels: each per-tool sub-list uses a capitalized heading.
1354
1209
  const cap = s => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
1355
1210
  const labels = {
@@ -1358,19 +1213,15 @@ export class SolveQueue {
1358
1213
  completed: cap(lt('queue_completed', {}, { locale })),
1359
1214
  failed: cap(lt('queue_failed', {}, { locale })),
1360
1215
  };
1361
-
1362
1216
  // Group the (globally-capped) completed/failed history by tool so each tool
1363
1217
  // queue shows its own Completed/Failed list (issue #1891 follow-up).
1364
1218
  const completedByTool = groupQueueItemsByTool(this.completed);
1365
1219
  const failedByTool = groupQueueItemsByTool(this.failed);
1366
-
1367
1220
  let message = `📋 *${lt('solve_queue_status', {}, { locale })}*\n\n`;
1368
1221
  const max = QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE;
1369
-
1370
1222
  // Every tool with *any* activity (queued, processing, or in history) so
1371
1223
  // per-tool history shows even after a tool's live queue has drained.
1372
1224
  const tools = [...new Set([...Object.keys(this.queues), ...Object.keys(completedByTool), ...Object.keys(failedByTool)])];
1373
-
1374
1225
  for (const tool of tools) {
1375
1226
  const toolQueue = this.queues[tool] || [];
1376
1227
  const pending = toolQueue.length;
@@ -1380,26 +1231,20 @@ export class SolveQueue {
1380
1231
  const executing = collectExecutingItems({ processingItems: this.processing.values(), sessionItems: runningSessionItems, tool });
1381
1232
  const completed = completedByTool[tool] || [];
1382
1233
  const failed = failedByTool[tool] || [];
1383
-
1384
1234
  // Skip tools with nothing to show in any list.
1385
1235
  if (pending === 0 && executing.length === 0 && completed.length === 0 && failed.length === 0) continue;
1386
-
1387
1236
  const pendingItems = toolQueue.map(item => ({ url: item.url, waitMs: item.getWaitTime(), waitingReason: item.waitingReason }));
1388
1237
  message += formatQueueToolSection({ tool, executing, pendingItems, completed, failed, labels, max, locale });
1389
1238
  }
1390
-
1391
1239
  // Summary stats
1392
1240
  message += `${lt('queue_completed', {}, { locale })}: ${stats.completed}, ${lt('queue_failed', {}, { locale })}: ${stats.failed}\n`;
1393
-
1394
1241
  return message;
1395
1242
  }
1396
1243
  }
1397
-
1398
1244
  /**
1399
1245
  * Global queue instance (singleton)
1400
1246
  */
1401
1247
  let globalQueue = null;
1402
-
1403
1248
  /**
1404
1249
  * Get or create the global solve queue instance
1405
1250
  * @param {Object} options - Queue options
@@ -1413,7 +1258,6 @@ export function getSolveQueue(options = {}) {
1413
1258
  }
1414
1259
  return globalQueue;
1415
1260
  }
1416
-
1417
1261
  /**
1418
1262
  * Reset the global queue (useful for testing)
1419
1263
  */
@@ -1423,7 +1267,6 @@ export function resetSolveQueue() {
1423
1267
  globalQueue = null;
1424
1268
  }
1425
1269
  }
1426
-
1427
1270
  /**
1428
1271
  * Create an execute callback for the queue
1429
1272
  * @param {Function} executeStartScreen - Function to execute start-screen command
@@ -1460,7 +1303,6 @@ export function createQueueExecuteCallback(executeStartScreen, trackSessionFn) {
1460
1303
  return result;
1461
1304
  };
1462
1305
  }
1463
-
1464
1306
  /**
1465
1307
  * Get count of tracked isolated sessions that are still executing according
1466
1308
  * to `$ --status`. Queue display combines this with pgrep counts using max().
@@ -1479,7 +1321,6 @@ export async function getRunningIsolatedSessions(verbose = false) {
1479
1321
  return { count: 0, sessions: [], byTool: {} };
1480
1322
  }
1481
1323
  }
1482
-
1483
1324
  export default {
1484
1325
  SolveQueue,
1485
1326
  SolveQueueItem,