@link-assistant/hive-mind 2.5.3 → 2.5.4

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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.5.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 2ebdb3d: Add dequeue-decision diagnostics to the Telegram solve queue so global FIFO ordering across the per-tool queues can be audited in production (issue #2051). The oldest startable task still wins the single, globally-paced startup slot; when an older task is skipped because it cannot start, the queue now logs a concise, deduplicated "FIFO queue-jump" line naming the older task and the exact reason it is blocked (Claude/Codex limits, RAM/CPU/disk, min-interval, or one-at-a-time), and records it on `stats.lastQueueJump`. Verbose mode additionally prints a per-tool head snapshot each cycle. No change to ordering, pacing, or the minimum start interval.
8
+
3
9
  ## 2.5.3
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -492,3 +492,61 @@ export function formatWaitingReason(metric, currentValue, threshold, options = {
492
492
  return `${metric} threshold exceeded`;
493
493
  }
494
494
  }
495
+
496
+ /**
497
+ * Report the dequeue decision across all tool queues for a SolveQueue.
498
+ *
499
+ * Provides the observability that issue #2051 needs to confirm whether a
500
+ * long-waiting task is blocked legitimately (limits/resources/pacing) or by an
501
+ * ordering defect:
502
+ * - In verbose mode: prints a per-tool head snapshot (age + startable +
503
+ * blocking reasons) and the selected item each cycle.
504
+ * - Always-on: emits a concise "FIFO queue-jump" line whenever the
505
+ * globally-oldest queued head is skipped in favor of a younger startable head,
506
+ * naming the older task and the exact reason it is blocked. Deduplicated by
507
+ * (task id + block reasons) so a persistently-blocked task is only reported
508
+ * when its situation changes.
509
+ *
510
+ * @param {Object} queue - SolveQueue-like instance (uses verbose, log, recordThrottle, stats).
511
+ * @param {Array<{tool: string, item: Object, ageMs: number, startable: boolean, blockReasons: string[]}>} headDiagnostics
512
+ * @param {{item: Object, tool: string}|undefined} selected
513
+ * @see https://github.com/link-assistant/hive-mind/issues/2051
514
+ */
515
+ export function reportDequeueDecision(queue, headDiagnostics, selected) {
516
+ if (!headDiagnostics || headDiagnostics.length === 0) return;
517
+
518
+ if (queue.verbose) {
519
+ queue.log('Dequeue decision (global FIFO across tool queues):');
520
+ for (const d of headDiagnostics) {
521
+ const detail = d.startable ? 'STARTABLE' : `blocked by [${d.blockReasons.join('; ') || 'unknown'}]`;
522
+ queue.log(` ${d.tool}: waited ${formatDuration(d.ageMs)} - ${detail}`);
523
+ }
524
+ queue.log(selected ? ` -> selected ${selected.tool} (${selected.item.url})` : ' -> nothing startable this cycle');
525
+ }
526
+
527
+ if (!selected) return;
528
+
529
+ // Identify the globally-oldest queued head regardless of startability.
530
+ const oldest = headDiagnostics.reduce((a, b) => (a.item.createdAt <= b.item.createdAt ? a : b));
531
+ if (oldest.item.id === selected.item.id) return; // Strict FIFO honored - nothing to report.
532
+
533
+ const waitedMs = Date.now() - oldest.item.createdAt;
534
+ const blockedBy = oldest.blockReasons.join('; ') || 'unknown';
535
+ queue.recordThrottle('fifo_queue_jump');
536
+ queue.stats.lastQueueJump = {
537
+ skippedTool: oldest.tool,
538
+ skippedUrl: oldest.item.url,
539
+ waitedMs,
540
+ blockedBy: oldest.blockReasons,
541
+ startedTool: selected.tool,
542
+ startedUrl: selected.item.url,
543
+ };
544
+
545
+ // Deduplicate the always-on notice so a persistently-blocked task does not
546
+ // spam the log on every poll; only report when the reason changes.
547
+ const signature = `${oldest.item.id}|${blockedBy}`;
548
+ if (queue._lastQueueJumpSignature !== signature) {
549
+ queue._lastQueueJumpSignature = signature;
550
+ console.log(`[solve_queue] FIFO queue-jump: ${selected.tool} task started ahead of an older ${oldest.tool} task waiting ${formatDuration(waitedMs)} (${oldest.item.url}) — older task blocked by: ${blockedBy}`);
551
+ }
552
+ }
@@ -2,22 +2,17 @@
2
2
  /**
3
3
  * Telegram Solve Queue Library
4
4
  *
5
- * Producer/consumer queue for /solve commands in the Telegram bot.
6
- * Implements resource-aware throttling to prevent system overload.
7
- *
8
- * Features:
9
- * - Resource checking (RAM, CPU, disk)
10
- * - API limit checking (Claude, GitHub)
11
- * - Minimum interval between command starts
12
- * - Running process detection
13
- * - Status tracking: Queued -> Waiting -> Starting -> Started
5
+ * Producer/consumer queue for /solve commands in the Telegram bot. Implements
6
+ * resource-aware throttling (RAM, CPU, disk), API limit checking (Claude,
7
+ * GitHub), a minimum interval between command starts, running-process
8
+ * detection, and status tracking (Queued -> Waiting -> Starting -> Started).
14
9
  *
15
10
  * @see https://github.com/link-assistant/hive-mind/issues/1041
16
11
  */
17
12
 
18
13
  import { getCachedClaudeLimits, getCachedCodexLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
19
14
  export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
20
- import { collectExecutingItems, formatDuration, formatQueueToolSection, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses, getRunningSessionItems, groupQueueItemsByTool } from './telegram-solve-queue.helpers.lib.mjs';
15
+ import { collectExecutingItems, formatDuration, formatQueueToolSection, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses, getRunningSessionItems, groupQueueItemsByTool, reportDequeueDecision } from './telegram-solve-queue.helpers.lib.mjs';
21
16
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
17
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
18
  import { reserveStartSlotForQueue } from './queue-start-reservation.lib.mjs';
@@ -190,8 +185,7 @@ export class SolveQueue {
190
185
  qwen: null,
191
186
  gemini: null,
192
187
  };
193
- // Legacy: keep for compatibility with existing code that uses lastStartTime
194
- this.lastStartTime = null;
188
+ this.lastStartTime = null; // Legacy: global last-start timestamp
195
189
 
196
190
  // Consumer task reference
197
191
  this.consumerTask = null;
@@ -385,14 +379,12 @@ export class SolveQueue {
385
379
  }
386
380
 
387
381
  /**
388
- * Find the next startable item across all tool queues.
389
- * With separate queues, each tool is checked independently so tool-specific
390
- * limits do not block unrelated tools. Issue #2015 adds a global startup
391
- * interval, so even when multiple tools are startable this returns only the
392
- * oldest startable item to prevent burst launches.
393
- *
394
- * Also immediately rejects all queued items when a 'reject' strategy threshold
395
- * is exceeded, instead of leaving them waiting indefinitely.
382
+ * Find the next startable item across all tool queues. Each tool is checked
383
+ * independently so tool-specific limits do not block unrelated tools; issue
384
+ * #2015 adds a global startup interval, so even when multiple tools are
385
+ * startable this returns only the oldest startable item (global FIFO) to
386
+ * prevent burst launches. Queued items are rejected immediately when a
387
+ * 'reject' strategy threshold is exceeded rather than left waiting.
396
388
  *
397
389
  * @returns {Promise<Array<{item: SolveQueueItem, tool: string, index: number, check: Object}>>}
398
390
  * @see https://github.com/link-assistant/hive-mind/issues/1159
@@ -400,6 +392,9 @@ export class SolveQueue {
400
392
  */
401
393
  async findStartableItems() {
402
394
  const startableItems = [];
395
+ // Per-tool head diagnostics: why each queue head is/isn't startable, so
396
+ // global FIFO ordering can be audited in production (issue #2051).
397
+ const headDiagnostics = [];
403
398
 
404
399
  for (const [tool, toolQueue] of Object.entries(this.queues)) {
405
400
  if (toolQueue.length === 0) continue;
@@ -415,23 +410,36 @@ export class SolveQueue {
415
410
  continue;
416
411
  }
417
412
 
413
+ const item = toolQueue[0];
414
+ if (!item) continue;
415
+
416
+ // Determine startability and capture the blocking reason(s) for diagnostics.
417
+ // For tool-specific one-at-a-time, only count that tool's processing items.
418
+ const toolProcessingCount = this.getProcessingCountByTool(tool);
419
+ let startable = false;
420
+ const blockReasons = Array.isArray(check.reasons) ? [...check.reasons] : [];
418
421
  if (check.canStart) {
419
- const item = toolQueue[0];
420
- if (!item) continue;
421
- // Also check one-at-a-time mode for this specific tool
422
- // For tool-specific one-at-a-time, only count that tool's processing items
423
- const toolProcessingCount = this.getProcessingCountByTool(tool);
424
422
  if (check.oneAtATime && toolProcessingCount > 0) {
425
- // This tool is in one-at-a-time mode and has items processing
426
- // Skip but don't block other tools
427
- continue;
423
+ // One-at-a-time for this tool with a task already processing: skip it
424
+ // but don't block other tools.
425
+ blockReasons.push(`one-at-a-time: ${toolProcessingCount} ${tool} task(s) already processing`);
426
+ } else {
427
+ startable = true;
428
+ startableItems.push({ item, tool, index: 0, check });
428
429
  }
429
- startableItems.push({ item, tool, index: 0, check });
430
430
  }
431
+
432
+ headDiagnostics.push({ tool, item, ageMs: Date.now() - item.createdAt, startable, blockReasons });
431
433
  }
432
434
 
435
+ // Global FIFO: the oldest startable head wins the (globally paced) startup slot.
433
436
  startableItems.sort((a, b) => a.item.createdAt - b.item.createdAt);
434
- return startableItems.slice(0, 1);
437
+ const selected = startableItems.slice(0, 1);
438
+
439
+ // Observe the dequeue decision (issue #2051): see reportDequeueDecision().
440
+ reportDequeueDecision(this, headDiagnostics, selected[0]);
441
+
442
+ return selected;
435
443
  }
436
444
 
437
445
  /**
@@ -555,23 +563,16 @@ export class SolveQueue {
555
563
  }
556
564
 
557
565
  /**
558
- * Check if a new command can start
559
- *
560
- * Logic per issue #1061:
561
- * 1. "Claude process is already running" is NOT a limit by itself - it's a metric
562
- * 2. Commands can run in parallel as long as actual limits are not exceeded
563
- * 3. When any limit >= threshold, allow exactly one claude command to pass
564
- *
565
- * Logic per issue #1159:
566
- * - Different tools have different limits. Claude limits only apply to 'claude' tool.
567
- * - Processing count for Claude limits only includes Claude items, not agent/codex/gemini/qwen items.
568
- * - This allows non-Claude tasks to run in parallel when Claude limits are reached.
566
+ * Check if a new command can start.
569
567
  *
570
- * Logic per issue #1253:
571
- * - All thresholds now support configurable strategies (reject, enqueue, dequeue-one-at-a-time)
572
- * - 'reject' strategy immediately rejects the command without queueing
573
- * - 'enqueue' blocks and waits in queue until metric drops
574
- * - 'dequeue-one-at-a-time' allows one command while blocking subsequent
568
+ * - #1061: a running Claude process is a metric, not a limit by itself;
569
+ * commands run in parallel while actual limits are not exceeded.
570
+ * - #1159: tools have independent limits Claude limits (and Claude
571
+ * processing counts) apply only to the 'claude' tool, so non-Claude tasks
572
+ * run in parallel when Claude limits are reached.
573
+ * - #1253: every threshold supports a configurable strategy — 'reject'
574
+ * (reject without queueing), 'enqueue' (block until the metric drops), or
575
+ * 'dequeue-one-at-a-time' (allow one, block subsequent).
575
576
  *
576
577
  * @param {Object} options - Options for the check
577
578
  * @param {string} options.tool - The tool being used ('claude', 'agent', 'codex', 'gemini', 'qwen', etc.)