@link-assistant/hive-mind 1.50.7 → 1.50.9

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.
@@ -15,24 +15,12 @@
15
15
  * @see https://github.com/link-assistant/hive-mind/issues/1041
16
16
  */
17
17
 
18
- import { exec } from 'node:child_process';
19
- import { promisify } from 'node:util';
20
-
21
- const execAsync = promisify(exec);
22
-
23
- // Import centralized limits and caching
24
- import { getCachedClaudeLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
25
-
26
- // Import centralized queue configuration
27
- // This ensures thresholds are consistent between queue logic and display formatting
28
- // See: https://github.com/link-assistant/hive-mind/issues/1242
29
- // See: https://github.com/link-assistant/hive-mind/issues/1253 (configurable strategies)
18
+ import { getCachedClaudeLimits, getCachedCodexLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
19
+ export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
20
+ import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
30
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
31
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
32
23
 
33
- /**
34
- * Status enum for queue items
35
- */
36
24
  export const QueueItemStatus = {
37
25
  QUEUED: 'queued',
38
26
  WAITING: 'waiting',
@@ -42,136 +30,6 @@ export const QueueItemStatus = {
42
30
  CANCELLED: 'cancelled',
43
31
  };
44
32
 
45
- /**
46
- * Count running processes by name
47
- * @param {string} processName - Process name to search for (e.g., 'claude', 'agent')
48
- * @param {boolean} verbose - Whether to log verbose output
49
- * @returns {Promise<{count: number, processes: string[]}>}
50
- */
51
- export async function getRunningProcesses(processName, verbose = false) {
52
- try {
53
- const { stdout } = await execAsync(`pgrep -l -x ${processName} 2>/dev/null || true`);
54
- const lines = stdout
55
- .trim()
56
- .split('\n')
57
- .filter(line => line.trim());
58
-
59
- const processes = lines
60
- .map(line => {
61
- const parts = line.trim().split(/\s+/);
62
- return {
63
- pid: parts[0],
64
- name: parts.slice(1).join(' ') || processName,
65
- };
66
- })
67
- .filter(p => p.pid);
68
-
69
- if (verbose) {
70
- console.log(`[VERBOSE] /solve_queue found ${processes.length} running ${processName} processes`);
71
- if (processes.length > 0) {
72
- console.log(`[VERBOSE] /solve_queue processes: ${JSON.stringify(processes)}`);
73
- }
74
- }
75
-
76
- return {
77
- count: processes.length,
78
- processes: processes.map(p => `${p.pid}:${p.name}`),
79
- };
80
- } catch (error) {
81
- if (verbose) {
82
- console.error(`[VERBOSE] /solve_queue error counting ${processName} processes:`, error.message);
83
- }
84
- return { count: 0, processes: [] };
85
- }
86
- }
87
-
88
- /**
89
- * Count running claude processes
90
- * @param {boolean} verbose - Whether to log verbose output
91
- * @returns {Promise<{count: number, processes: string[]}>}
92
- */
93
- export async function getRunningClaudeProcesses(verbose = false) {
94
- return getRunningProcesses('claude', verbose);
95
- }
96
-
97
- /**
98
- * Count running agent processes
99
- * @param {boolean} verbose - Whether to log verbose output
100
- * @returns {Promise<{count: number, processes: string[]}>}
101
- */
102
- export async function getRunningAgentProcesses(verbose = false) {
103
- return getRunningProcesses('agent', verbose);
104
- }
105
-
106
- /**
107
- * Format a threshold as percentage for display
108
- * @param {number} ratio - Ratio (0.0 - 1.0)
109
- * @returns {string} Formatted percentage
110
- */
111
- function formatThresholdPercent(ratio) {
112
- return `${Math.round(ratio * 100)}%`;
113
- }
114
-
115
- /**
116
- * Format milliseconds into human-readable duration
117
- * Shows days, hours, minutes, and seconds as appropriate.
118
- * Examples: "5h 43m 23s", "2m 15s", "45s", "1d 3h 12m 5s"
119
- *
120
- * @param {number} ms - Duration in milliseconds
121
- * @returns {string} Human-readable duration
122
- * @see https://github.com/link-assistant/hive-mind/issues/1267
123
- */
124
- export function formatDuration(ms) {
125
- if (ms < 0) ms = 0;
126
-
127
- const totalSeconds = Math.floor(ms / 1000);
128
- const days = Math.floor(totalSeconds / 86400);
129
- const hours = Math.floor((totalSeconds % 86400) / 3600);
130
- const minutes = Math.floor((totalSeconds % 3600) / 60);
131
- const seconds = totalSeconds % 60;
132
-
133
- const parts = [];
134
- if (days > 0) parts.push(`${days}d`);
135
- if (hours > 0) parts.push(`${hours}h`);
136
- if (minutes > 0) parts.push(`${minutes}m`);
137
- if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
138
-
139
- return parts.join(' ');
140
- }
141
-
142
- /**
143
- * Generate human-readable waiting reason based on threshold violation
144
- * @param {string} metric - The metric name (ram, cpu, disk, etc.)
145
- * @param {number} currentValue - Current value (as percentage 0-100)
146
- * @param {number} threshold - Threshold ratio (0.0 - 1.0)
147
- * @returns {string} Human-readable reason
148
- */
149
- function formatWaitingReason(metric, currentValue, threshold) {
150
- const thresholdPercent = formatThresholdPercent(threshold);
151
- const currentPercent = Math.round(currentValue);
152
-
153
- switch (metric) {
154
- case 'ram':
155
- return `RAM usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
156
- case 'cpu':
157
- return `CPU usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
158
- case 'disk':
159
- return `Disk usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
160
- case 'claude_5_hour_session':
161
- return `Claude 5 hour session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
162
- case 'claude_weekly':
163
- return `Claude weekly limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
164
- case 'github':
165
- return `GitHub API usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
166
- case 'min_interval':
167
- return `Minimum interval between commands not reached`;
168
- case 'claude_running':
169
- return `Claude process is already running`;
170
- default:
171
- return `${metric} threshold exceeded`;
172
- }
173
- }
174
-
175
33
  /**
176
34
  * Queue item representing a /solve command request
177
35
  */
@@ -281,6 +139,7 @@ export class SolveQueue {
281
139
  this.queues = {
282
140
  claude: [],
283
141
  agent: [],
142
+ codex: [],
284
143
  };
285
144
  this.processing = new Map();
286
145
  this.completed = [];
@@ -291,6 +150,7 @@ export class SolveQueue {
291
150
  this.lastStartTimeByTool = {
292
151
  claude: null,
293
152
  agent: null,
153
+ codex: null,
294
154
  };
295
155
  // Legacy: keep for compatibility with existing code that uses lastStartTime
296
156
  this.lastStartTime = null;
@@ -647,22 +507,29 @@ export class SolveQueue {
647
507
 
648
508
  // Check running claude processes (this is a metric, not a blocking reason by itself)
649
509
  const claudeProcs = await getRunningClaudeProcesses(this.verbose);
510
+ const codexProcs = await getRunningCodexProcesses(this.verbose);
511
+ const agentProcs = await getRunningAgentProcesses(this.verbose);
650
512
  const hasRunningClaude = claudeProcs.count > 0;
513
+ const hasRunningCodex = codexProcs.count > 0;
651
514
 
652
515
  // Calculate total processing count for system resources (all tools)
653
516
  // System resources (RAM, CPU, disk) apply to all tools
654
- const totalProcessing = this.processing.size + claudeProcs.count;
517
+ const totalProcessing = this.processing.size + claudeProcs.count + codexProcs.count + agentProcs.count;
655
518
 
656
519
  // Calculate Claude-specific processing count for Claude API limits
657
520
  // Only counts Claude items in queue + external claude processes
658
521
  // Agent items don't count against Claude's one-at-a-time limit
659
522
  // See: https://github.com/link-assistant/hive-mind/issues/1159
660
523
  const claudeProcessingCount = this.getProcessingCountByTool('claude');
524
+ const codexProcessingCount = this.getProcessingCountByTool('codex');
661
525
 
662
526
  // Track claude_running as a metric (but don't add to reasons yet)
663
527
  if (hasRunningClaude) {
664
528
  this.recordThrottle('claude_running');
665
529
  }
530
+ if (hasRunningCodex) {
531
+ this.recordThrottle('codex_running');
532
+ }
666
533
 
667
534
  // Check system resources with strategy support
668
535
  // System resources apply to ALL tools, not just Claude
@@ -685,7 +552,9 @@ export class SolveQueue {
685
552
  // This allows agent tasks to proceed when Claude limits are reached
686
553
  // See: https://github.com/link-assistant/hive-mind/issues/1159
687
554
  // See: https://github.com/link-assistant/hive-mind/issues/1253 (strategies)
688
- const limitCheck = await this.checkApiLimits(hasRunningClaude, claudeProcessingCount, tool);
555
+ const hasRunningToolProcess = tool === 'codex' ? hasRunningCodex : hasRunningClaude;
556
+ const toolProcessingCount = tool === 'codex' ? codexProcessingCount : claudeProcessingCount;
557
+ const limitCheck = await this.checkApiLimits(hasRunningToolProcess, toolProcessingCount, tool);
689
558
  if (limitCheck.rejected) {
690
559
  rejected = true;
691
560
  rejectReason = limitCheck.rejectReason;
@@ -705,6 +574,9 @@ export class SolveQueue {
705
574
  // See: https://github.com/link-assistant/hive-mind/issues/1078
706
575
  reasons.push(formatWaitingReason('claude_running', claudeProcs.count, 0) + ` (${claudeProcs.count} processes)`);
707
576
  }
577
+ if (tool === 'codex' && hasRunningCodex && reasons.length > 0) {
578
+ reasons.push(formatWaitingReason('codex_running', codexProcs.count, 0) + ` (${codexProcs.count} processes)`);
579
+ }
708
580
 
709
581
  const canStart = reasons.length === 0 && !rejected;
710
582
 
@@ -724,8 +596,10 @@ export class SolveQueue {
724
596
  reasons,
725
597
  oneAtATime,
726
598
  claudeProcesses: claudeProcs.count,
599
+ codexProcesses: codexProcs.count,
727
600
  totalProcessing,
728
601
  claudeProcessingCount,
602
+ codexProcessingCount,
729
603
  };
730
604
  }
731
605
 
@@ -868,12 +742,12 @@ export class SolveQueue {
868
742
  * - All thresholds now support configurable strategies (reject, enqueue, dequeue-one-at-a-time)
869
743
  * - Configuration via HIVE_MIND_QUEUE_CONFIG or individual env vars
870
744
  *
871
- * @param {boolean} hasRunningClaude - Whether claude processes are running (from pgrep)
872
- * @param {number} claudeProcessingCount - Count of 'claude' tool items being processed in queue
745
+ * @param {boolean} hasRunningToolProcess - Whether matching tool processes are running (from pgrep)
746
+ * @param {number} toolProcessingCount - Count of matching tool items being processed in queue
873
747
  * @param {string} tool - The tool being used ('claude', 'agent', etc.)
874
748
  * @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean, rejected: boolean, rejectReason: string|null}>}
875
749
  */
876
- async checkApiLimits(hasRunningClaude = false, claudeProcessingCount = 0, tool = 'claude') {
750
+ async checkApiLimits(hasRunningToolProcess = false, toolProcessingCount = 0, tool = 'claude') {
877
751
  const reasons = [];
878
752
  let oneAtATime = false;
879
753
  let rejected = false;
@@ -884,12 +758,9 @@ export class SolveQueue {
884
758
  // affected by Claude API limits (5-hour session, weekly limits)
885
759
  // See: https://github.com/link-assistant/hive-mind/issues/1159
886
760
  const applyClaudeLimits = tool === 'claude';
761
+ const applyCodexLimits = tool === 'codex';
887
762
 
888
- // Calculate total Claude processing: queue-internal claude items + external claude processes
889
- // This is used for Claude limits one-at-a-time mode - only counts Claude-related processing
890
- // Agent items in the queue don't count against Claude's one-at-a-time limit
891
- // See: https://github.com/link-assistant/hive-mind/issues/1159
892
- const totalClaudeProcessing = claudeProcessingCount + (hasRunningClaude ? 1 : 0);
763
+ const totalToolProcessing = toolProcessingCount + (hasRunningToolProcess ? 1 : 0);
893
764
 
894
765
  // Check Claude limits (using cached value)
895
766
  // Only applied when tool is 'claude'
@@ -914,7 +785,7 @@ export class SolveQueue {
914
785
  rejectReason = reason;
915
786
  } else if (strategy === 'dequeue-one-at-a-time') {
916
787
  oneAtATime = true;
917
- if (totalClaudeProcessing > 0) {
788
+ if (totalToolProcessing > 0) {
918
789
  reasons.push(reason + ' (waiting for current command)');
919
790
  }
920
791
  } else {
@@ -939,7 +810,7 @@ export class SolveQueue {
939
810
  rejectReason = reason;
940
811
  } else if (strategy === 'dequeue-one-at-a-time') {
941
812
  oneAtATime = true;
942
- if (totalClaudeProcessing > 0) {
813
+ if (totalToolProcessing > 0) {
943
814
  reasons.push(reason + ' (waiting for current command)');
944
815
  }
945
816
  } else {
@@ -949,13 +820,62 @@ export class SolveQueue {
949
820
  }
950
821
  }
951
822
  }
823
+ } else if (applyCodexLimits) {
824
+ const codexResult = await getCachedCodexLimits(this.verbose);
825
+ if (codexResult.success) {
826
+ const sessionPercent = codexResult.usage.currentSession.percentage;
827
+ const weeklyPercent = codexResult.usage.allModels.percentage;
828
+
829
+ if (sessionPercent !== null) {
830
+ const sessionRatio = sessionPercent / 100;
831
+ if (sessionRatio >= QUEUE_CONFIG.thresholds.codex5Hour.value) {
832
+ const reason = formatWaitingReason('codex_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.codex5Hour.value);
833
+ const strategy = QUEUE_CONFIG.thresholds.codex5Hour.strategy;
834
+ this.recordThrottle(sessionRatio >= 1.0 ? 'codex_5_hour_session_100' : `codex_5_hour_session_${strategy}`);
835
+
836
+ if (strategy === 'reject') {
837
+ rejected = true;
838
+ rejectReason = reason;
839
+ } else if (strategy === 'dequeue-one-at-a-time') {
840
+ oneAtATime = true;
841
+ if (totalToolProcessing > 0) {
842
+ reasons.push(reason + ' (waiting for current command)');
843
+ }
844
+ } else {
845
+ reasons.push(reason);
846
+ }
847
+ }
848
+ }
849
+
850
+ if (weeklyPercent !== null) {
851
+ const weeklyRatio = weeklyPercent / 100;
852
+ if (weeklyRatio >= QUEUE_CONFIG.thresholds.codexWeekly.value) {
853
+ const reason = formatWaitingReason('codex_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.codexWeekly.value);
854
+ const strategy = QUEUE_CONFIG.thresholds.codexWeekly.strategy;
855
+ this.recordThrottle(weeklyRatio >= 1.0 ? 'codex_weekly_100' : `codex_weekly_${strategy}`);
856
+
857
+ if (strategy === 'reject') {
858
+ rejected = true;
859
+ rejectReason = reason;
860
+ } else if (strategy === 'dequeue-one-at-a-time') {
861
+ oneAtATime = true;
862
+ if (totalToolProcessing > 0) {
863
+ reasons.push(reason + ' (waiting for current command)');
864
+ }
865
+ } else {
866
+ reasons.push(reason);
867
+ }
868
+ }
869
+ }
870
+ }
952
871
  } else if (this.verbose) {
953
872
  this.log(`Claude limits not applied for --tool ${tool}`);
954
873
  }
955
874
 
956
- // Check GitHub limits (only relevant if claude processes running)
875
+ // Check GitHub limits when the active tool already has a running process.
876
+ // This keeps the queue behavior aligned with the existing one-at-a-time throttling model.
957
877
  // Configurable strategy via HIVE_MIND_QUEUE_CONFIG or HIVE_MIND_GITHUB_API_STRATEGY
958
- if (hasRunningClaude) {
878
+ if (hasRunningToolProcess) {
959
879
  const githubResult = await getCachedGitHubLimits(this.verbose);
960
880
  if (githubResult.success) {
961
881
  const usedPercent = githubResult.githubRateLimit.usedPercentage;
@@ -970,7 +890,7 @@ export class SolveQueue {
970
890
  rejectReason = reason;
971
891
  } else if (strategy === 'dequeue-one-at-a-time') {
972
892
  oneAtATime = true;
973
- if (totalClaudeProcessing > 0) {
893
+ if (totalToolProcessing > 0) {
974
894
  reasons.push(reason + ' (waiting for current command)');
975
895
  }
976
896
  } else {
@@ -1273,22 +1193,11 @@ export class SolveQueue {
1273
1193
  * @see https://github.com/link-assistant/hive-mind/issues/1267
1274
1194
  */
1275
1195
  async formatStatus() {
1276
- // Get actual process counts for each tool queue
1277
- // The "processing" count is the number of running system processes, not queue internal state
1278
- // This ensures users see accurate counts of what's actually running
1279
- const claudeProcs = await getRunningClaudeProcesses(this.verbose);
1280
- const agentProcs = await getRunningAgentProcesses(this.verbose);
1281
-
1282
- const processCounts = {
1283
- claude: claudeProcs.count,
1284
- agent: agentProcs.count,
1285
- };
1286
-
1287
1196
  // Always show per-tool breakdown for all known queues
1288
1197
  let message = 'Queues\n';
1289
1198
  for (const [tool, toolQueue] of Object.entries(this.queues)) {
1290
1199
  const pending = toolQueue.length;
1291
- const processing = processCounts[tool] || 0;
1200
+ const processing = (await getRunningProcesses(tool, this.verbose)).count;
1292
1201
  message += `${tool} (pending: ${pending}, processing: ${processing})\n`;
1293
1202
  }
1294
1203
 
@@ -1326,20 +1235,12 @@ export class SolveQueue {
1326
1235
  // Get actual process counts for each tool queue
1327
1236
  // The "processing" count is the number of running system processes, not queue internal state
1328
1237
  // This ensures users see accurate counts of what's actually running
1329
- const claudeProcs = await getRunningClaudeProcesses(this.verbose);
1330
- const agentProcs = await getRunningAgentProcesses(this.verbose);
1331
-
1332
- const processCounts = {
1333
- claude: claudeProcs.count,
1334
- agent: agentProcs.count,
1335
- };
1336
-
1337
1238
  let message = '📋 *Solve Queue Status*\n\n';
1338
1239
 
1339
1240
  // Show per-tool queue breakdown with items grouped by queue
1340
1241
  for (const [tool, toolQueue] of Object.entries(this.queues)) {
1341
1242
  const pending = toolQueue.length;
1342
- const processing = processCounts[tool] || 0;
1243
+ const processing = (await getRunningProcesses(tool, this.verbose)).count;
1343
1244
  message += `*${tool}* (pending: ${pending}, processing: ${processing})\n`;
1344
1245
 
1345
1246
  // Show first 5 queued items for this tool
@@ -562,7 +562,8 @@ const VERSION_COMMANDS = [
562
562
  { key: 'playwright', command: 'playwright --version 2>&1' },
563
563
  { key: 'playwrightTest', command: "npm list -g @playwright/test --depth=0 2>&1 | grep @playwright/test | awk '{print $2}'" },
564
564
  { key: 'playwrightMcp', command: "npm list -g @playwright/mcp --depth=0 2>&1 | grep @playwright/mcp | awk '{print $2}'" },
565
- { key: 'playwrightMcpStatus', command: 'timeout 5 claude mcp list 2>&1 | grep -i playwright | head -1' },
565
+ { key: 'playwrightMcpClaudeStatus', command: 'timeout 5 claude mcp list 2>&1 | grep -i playwright | head -1' },
566
+ { key: 'playwrightMcpCodexStatus', command: 'timeout 5 codex mcp list 2>&1 | grep -i playwright | head -1' },
566
567
  { key: 'puppeteerBrowsers', command: "npm list -g @puppeteer/browsers --depth=0 2>&1 | grep @puppeteer/browsers | awk '{print $2}'" },
567
568
 
568
569
  // Browsers (installed via Playwright)
@@ -840,7 +841,8 @@ export async function getVersionInfo(verbose = false, processVersion = null) {
840
841
  playwright: versions.playwright,
841
842
  playwrightTest: versions.playwrightTest,
842
843
  playwrightMcp: versions.playwrightMcp,
843
- playwrightMcpStatus: versions.playwrightMcpStatus,
844
+ playwrightMcpClaudeStatus: versions.playwrightMcpClaudeStatus,
845
+ playwrightMcpCodexStatus: versions.playwrightMcpCodexStatus,
844
846
  puppeteerBrowsers: versions.puppeteerBrowsers,
845
847
 
846
848
  // Browsers
@@ -1179,11 +1181,12 @@ export function formatVersionMessage(versions) {
1179
1181
  const browserAutoLines = [];
1180
1182
  addVersionLine(browserAutoLines, 'Playwright', versions.playwright, 'playwright');
1181
1183
  addVersionLine(browserAutoLines, 'Playwright Test', versions.playwrightTest, 'playwrightTest');
1182
- // Playwright MCP: show version with Claude Code connection status inline
1184
+ // Playwright MCP: show version with Claude Code and Codex connection status inline
1183
1185
  if (versions.playwrightMcp) {
1184
1186
  const mcpVersion = parseVersion('playwrightMcp', versions.playwrightMcp);
1185
- const claudeStatus = versions.playwrightMcpStatus ? 'connected' : 'not connected';
1186
- browserAutoLines.push(`• Playwright MCP: \`${mcpVersion} | Claude Code: ${claudeStatus}\``);
1187
+ const claudeStatus = versions.playwrightMcpClaudeStatus ? 'connected' : 'not connected';
1188
+ const codexStatus = versions.playwrightMcpCodexStatus ? 'connected' : 'not connected';
1189
+ browserAutoLines.push(`• Playwright MCP: \`${mcpVersion} | Claude Code: ${claudeStatus} | Codex: ${codexStatus}\``);
1187
1190
  }
1188
1191
  addVersionLine(browserAutoLines, 'Puppeteer Browsers', versions.puppeteerBrowsers, 'puppeteerBrowsers');
1189
1192