@link-assistant/hive-mind 1.50.8 ā 1.50.10
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 +12 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/agent.prompts.lib.mjs +25 -37
- package/src/architecture-care.prompts.lib.mjs +11 -11
- package/src/claude.prompts.lib.mjs +31 -46
- package/src/codex.lib.mjs +481 -100
- package/src/codex.options.lib.mjs +52 -0
- package/src/codex.prompts.lib.mjs +84 -39
- package/src/experiments-examples.prompts.lib.mjs +7 -7
- package/src/hive.bootstrap.lib.mjs +32 -0
- package/src/hive.config.lib.mjs +3 -3
- package/src/hive.mjs +13 -20
- package/src/interactive-mode.lib.mjs +200 -265
- package/src/interactive-mode.shared.lib.mjs +133 -0
- package/src/lib.mjs +101 -4
- package/src/limits.lib.mjs +339 -2
- package/src/log-upload.lib.mjs +46 -3
- package/src/models/index.mjs +21 -12
- package/src/opencode.prompts.lib.mjs +26 -38
- package/src/queue-config.lib.mjs +6 -0
- package/src/solve.auto-continue.lib.mjs +1 -0
- package/src/solve.bootstrap.lib.mjs +39 -0
- package/src/solve.config.lib.mjs +11 -11
- package/src/solve.mjs +35 -40
- package/src/solve.progress-monitoring.lib.mjs +10 -2
- package/src/solve.restart-shared.lib.mjs +13 -1
- package/src/solve.results.lib.mjs +43 -5
- package/src/solve.validation.lib.mjs +1 -1
- package/src/telegram-bot.mjs +4 -2
- package/src/telegram-solve-queue.helpers.lib.mjs +151 -0
- package/src/telegram-solve-queue.lib.mjs +82 -181
- package/src/version-info.lib.mjs +8 -5
|
@@ -37,6 +37,41 @@ const { autoContinueWhenLimitResets } = autoContinue;
|
|
|
37
37
|
const claudeCommandBuilder = await import('./claude.command-builder.lib.mjs');
|
|
38
38
|
export const { buildClaudeResumeCommand, buildClaudeInitialCommand } = claudeCommandBuilder;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Build a solve.mjs resume command for tools that do not have a first-party interactive
|
|
42
|
+
* resume CLI flow like Claude Code. This keeps the invocation within hive-mind so the
|
|
43
|
+
* original tool selection and working directory can be preserved.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} options
|
|
46
|
+
* @param {string} options.issueUrl - The issue URL passed to solve.mjs
|
|
47
|
+
* @param {string} options.sessionId - The session ID to resume
|
|
48
|
+
* @param {string|null} [options.tool] - Tool name (codex, opencode, agent)
|
|
49
|
+
* @param {string|null} [options.model] - Model name to preserve
|
|
50
|
+
* @param {string|null} [options.tempDir] - Working directory to preserve
|
|
51
|
+
* @param {string} [options.nodePath] - Node binary path
|
|
52
|
+
* @param {string} [options.scriptPath] - solve.mjs path
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export const buildSolveResumeCommand = ({ issueUrl, sessionId, tool = null, model = null, tempDir = null, nodePath = process.argv[0], scriptPath = process.argv[1] }) => {
|
|
56
|
+
const shellQuote = value => `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
57
|
+
|
|
58
|
+
const args = [shellQuote(scriptPath), shellQuote(issueUrl), '--resume', shellQuote(sessionId)];
|
|
59
|
+
|
|
60
|
+
if (tool && tool !== 'claude') {
|
|
61
|
+
args.push('--tool', shellQuote(tool));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (model) {
|
|
65
|
+
args.push('--model', shellQuote(model));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (tempDir) {
|
|
69
|
+
args.push('--working-directory', shellQuote(tempDir));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `${shellQuote(nodePath)} ${args.join(' ')}`;
|
|
73
|
+
};
|
|
74
|
+
|
|
40
75
|
// Import error handling functions
|
|
41
76
|
// const errorHandlers = await import('./solve.error-handlers.lib.mjs'); // Not currently used
|
|
42
77
|
// Import Sentry integration
|
|
@@ -444,12 +479,8 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
444
479
|
const absoluteLogPath = path.resolve(getLogFile());
|
|
445
480
|
await log(`ā
Complete log file: ${absoluteLogPath}`);
|
|
446
481
|
|
|
447
|
-
// Show claude resume command only for --tool claude (or default)
|
|
448
|
-
// This allows users to investigate, resume, see context, and more
|
|
449
|
-
// Uses the (cd ... && claude --resume ...) pattern for a fully copyable, executable command
|
|
450
482
|
const tool = argv.tool || 'claude';
|
|
451
483
|
if (tool === 'claude') {
|
|
452
|
-
// Build the Claude CLI resume command using the command builder
|
|
453
484
|
const claudeResumeCmd = buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model });
|
|
454
485
|
|
|
455
486
|
await log('');
|
|
@@ -457,6 +488,13 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
457
488
|
await log('');
|
|
458
489
|
await log(` ${claudeResumeCmd}`);
|
|
459
490
|
await log('');
|
|
491
|
+
} else if (issueUrl) {
|
|
492
|
+
const solveResumeCmd = buildSolveResumeCommand({ issueUrl, sessionId, tool, model: argv.model, tempDir });
|
|
493
|
+
await log('');
|
|
494
|
+
await log(`š” To continue this ${tool} session with solve:`);
|
|
495
|
+
await log('');
|
|
496
|
+
await log(` ${solveResumeCmd}`);
|
|
497
|
+
await log('');
|
|
460
498
|
}
|
|
461
499
|
|
|
462
500
|
if (limitReached) {
|
|
@@ -472,7 +510,7 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
472
510
|
await log(`\nā° Limit resets at: ${global.limitResetTime}`);
|
|
473
511
|
}
|
|
474
512
|
|
|
475
|
-
await log('\nš” After the limit resets, resume using the
|
|
513
|
+
await log('\nš” After the limit resets, resume using the command above.');
|
|
476
514
|
|
|
477
515
|
if (argv.autoCleanup !== false) {
|
|
478
516
|
await log('');
|
|
@@ -306,7 +306,7 @@ export const performSystemChecks = async (minDiskSpace = 2048, skipToolConnectio
|
|
|
306
306
|
} else if (argv.tool === 'codex') {
|
|
307
307
|
// Validate Codex connection
|
|
308
308
|
const codexLib = await import('./codex.lib.mjs');
|
|
309
|
-
isToolConnected = await codexLib.validateCodexConnection(model);
|
|
309
|
+
isToolConnected = await codexLib.validateCodexConnection(model, argv.verbose);
|
|
310
310
|
if (!isToolConnected) {
|
|
311
311
|
await log('ā Cannot proceed without Codex connection', { level: 'error' });
|
|
312
312
|
return false;
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -41,7 +41,7 @@ const { parseGitHubUrl, validateGitHubEntityExistence } = await import('./github
|
|
|
41
41
|
const { validateModelName, buildModelOptionDescription } = await import('./models/index.mjs');
|
|
42
42
|
const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
|
|
43
43
|
const { extractIsolationFromArgs, isValidPerCommandIsolation, resolveIsolation, createIsolationAwareQueueCallback } = await import('./telegram-isolation.lib.mjs');
|
|
44
|
-
const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
|
|
44
|
+
const { formatUsageMessage, formatCodexLimitsSection, getAllCachedLimits } = await import('./limits.lib.mjs');
|
|
45
45
|
const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
|
|
46
46
|
const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
|
|
47
47
|
const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
|
|
@@ -758,9 +758,11 @@ bot.command('limits', async ctx => {
|
|
|
758
758
|
|
|
759
759
|
// Format message with usage limits and queue status (issues #1343, #1267)
|
|
760
760
|
const claudeError = limits.claude.success ? null : limits.claude.error;
|
|
761
|
+
const codexError = limits.codex.success ? null : limits.codex.error;
|
|
761
762
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
762
763
|
const queueStatus = await solveQueue.formatStatus();
|
|
763
|
-
const
|
|
764
|
+
const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError);
|
|
765
|
+
const message = 'š *Usage Limits*\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus]);
|
|
764
766
|
await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
|
|
765
767
|
});
|
|
766
768
|
bot.command('version', async ctx => {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Count running processes by name.
|
|
10
|
+
* @param {string} processName - Process name to search for (e.g., 'claude', 'agent')
|
|
11
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
12
|
+
* @returns {Promise<{count: number, processes: string[]}>}
|
|
13
|
+
*/
|
|
14
|
+
export async function getRunningProcesses(processName, verbose = false) {
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await execAsync(`pgrep -l -x ${processName} 2>/dev/null || true`);
|
|
17
|
+
const lines = stdout
|
|
18
|
+
.trim()
|
|
19
|
+
.split('\n')
|
|
20
|
+
.filter(line => line.trim());
|
|
21
|
+
|
|
22
|
+
const processes = lines
|
|
23
|
+
.map(line => {
|
|
24
|
+
const parts = line.trim().split(/\s+/);
|
|
25
|
+
return {
|
|
26
|
+
pid: parts[0],
|
|
27
|
+
name: parts.slice(1).join(' ') || processName,
|
|
28
|
+
};
|
|
29
|
+
})
|
|
30
|
+
.filter(p => p.pid);
|
|
31
|
+
|
|
32
|
+
if (verbose) {
|
|
33
|
+
console.log(`[VERBOSE] /solve_queue found ${processes.length} running ${processName} processes`);
|
|
34
|
+
if (processes.length > 0) {
|
|
35
|
+
console.log(`[VERBOSE] /solve_queue processes: ${JSON.stringify(processes)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
count: processes.length,
|
|
41
|
+
processes: processes.map(p => `${p.pid}:${p.name}`),
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (verbose) {
|
|
45
|
+
console.error(`[VERBOSE] /solve_queue error counting ${processName} processes:`, error.message);
|
|
46
|
+
}
|
|
47
|
+
return { count: 0, processes: [] };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Count running claude processes.
|
|
53
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
54
|
+
* @returns {Promise<{count: number, processes: string[]}>}
|
|
55
|
+
*/
|
|
56
|
+
export async function getRunningClaudeProcesses(verbose = false) {
|
|
57
|
+
return getRunningProcesses('claude', verbose);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Count running agent processes.
|
|
62
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
63
|
+
* @returns {Promise<{count: number, processes: string[]}>}
|
|
64
|
+
*/
|
|
65
|
+
export async function getRunningAgentProcesses(verbose = false) {
|
|
66
|
+
return getRunningProcesses('agent', verbose);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Count running codex processes.
|
|
71
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
72
|
+
* @returns {Promise<{count: number, processes: string[]}>}
|
|
73
|
+
*/
|
|
74
|
+
export async function getRunningCodexProcesses(verbose = false) {
|
|
75
|
+
return getRunningProcesses('codex', verbose);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Format a threshold as percentage for display.
|
|
80
|
+
* @param {number} ratio - Ratio (0.0 - 1.0)
|
|
81
|
+
* @returns {string} Formatted percentage
|
|
82
|
+
*/
|
|
83
|
+
export function formatThresholdPercent(ratio) {
|
|
84
|
+
return `${Math.round(ratio * 100)}%`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format milliseconds into human-readable duration.
|
|
89
|
+
* Shows days, hours, minutes, and seconds as appropriate.
|
|
90
|
+
* Examples: "5h 43m 23s", "2m 15s", "45s", "1d 3h 12m 5s"
|
|
91
|
+
*
|
|
92
|
+
* @param {number} ms - Duration in milliseconds
|
|
93
|
+
* @returns {string} Human-readable duration
|
|
94
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1267
|
|
95
|
+
*/
|
|
96
|
+
export function formatDuration(ms) {
|
|
97
|
+
if (ms < 0) ms = 0;
|
|
98
|
+
|
|
99
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
100
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
101
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
102
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
103
|
+
const seconds = totalSeconds % 60;
|
|
104
|
+
|
|
105
|
+
const parts = [];
|
|
106
|
+
if (days > 0) parts.push(`${days}d`);
|
|
107
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
108
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
109
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
110
|
+
|
|
111
|
+
return parts.join(' ');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate human-readable waiting reason based on threshold violation.
|
|
116
|
+
* @param {string} metric - The metric name (ram, cpu, disk, etc.)
|
|
117
|
+
* @param {number} currentValue - Current value (as percentage 0-100)
|
|
118
|
+
* @param {number} threshold - Threshold ratio (0.0 - 1.0)
|
|
119
|
+
* @returns {string} Human-readable reason
|
|
120
|
+
*/
|
|
121
|
+
export function formatWaitingReason(metric, currentValue, threshold) {
|
|
122
|
+
const thresholdPercent = formatThresholdPercent(threshold);
|
|
123
|
+
const currentPercent = Math.round(currentValue);
|
|
124
|
+
|
|
125
|
+
switch (metric) {
|
|
126
|
+
case 'ram':
|
|
127
|
+
return `RAM usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
128
|
+
case 'cpu':
|
|
129
|
+
return `CPU usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
130
|
+
case 'disk':
|
|
131
|
+
return `Disk usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
132
|
+
case 'claude_5_hour_session':
|
|
133
|
+
return `Claude 5 hour session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
134
|
+
case 'claude_weekly':
|
|
135
|
+
return `Claude weekly limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
136
|
+
case 'codex_5_hour_session':
|
|
137
|
+
return `Codex 5 hour session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
138
|
+
case 'codex_weekly':
|
|
139
|
+
return `Codex weekly limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
140
|
+
case 'github':
|
|
141
|
+
return `GitHub API usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
|
|
142
|
+
case 'min_interval':
|
|
143
|
+
return 'Minimum interval between commands not reached';
|
|
144
|
+
case 'claude_running':
|
|
145
|
+
return 'Claude process is already running';
|
|
146
|
+
case 'codex_running':
|
|
147
|
+
return 'Codex process is already running';
|
|
148
|
+
default:
|
|
149
|
+
return `${metric} threshold exceeded`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -15,24 +15,12 @@
|
|
|
15
15
|
* @see https://github.com/link-assistant/hive-mind/issues/1041
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
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
|
|
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}
|
|
872
|
-
* @param {number}
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|