@link-assistant/hive-mind 2.13.4 → 2.13.5
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 +19 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +5 -1
- package/src/claude.lib.mjs +5 -158
- package/src/claude.session-tokens.lib.mjs +180 -0
- package/src/codex.diagnostics.lib.mjs +135 -0
- package/src/codex.lib.mjs +8 -121
- package/src/config.lib.mjs +9 -0
- package/src/gemini.lib.mjs +5 -1
- package/src/git-retry.lib.mjs +97 -0
- package/src/github-pr-idempotency.lib.mjs +83 -0
- package/src/github-rate-limit.lib.mjs +44 -41
- package/src/hive.mjs +8 -150
- package/src/hive.repository-fallback.lib.mjs +125 -0
- package/src/hive.startup-checks.lib.mjs +57 -0
- package/src/isolation-runner.lib.mjs +4 -277
- package/src/isolation-runner.parsers.lib.mjs +292 -0
- package/src/lib.mjs +79 -18
- package/src/opencode.lib.mjs +5 -1
- package/src/qwen.lib.mjs +5 -1
- package/src/session-monitor.lib.mjs +12 -272
- package/src/session-monitor.queries.lib.mjs +304 -0
- package/src/solve.auto-pr-push-sync.lib.mjs +176 -0
- package/src/solve.auto-pr.lib.mjs +40 -154
- package/src/solve.mjs +8 -158
- package/src/solve.mode.lib.mjs +191 -0
- package/src/telegram-solve-queue.lib.mjs +19 -272
- package/src/telegram-solve-queue.throttling.lib.mjs +323 -0
- package/src/transient-errors.lib.mjs +238 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve which mode solve.mjs is running in and collect the pull request /
|
|
3
|
+
* issue facts that mode implies.
|
|
4
|
+
*
|
|
5
|
+
* Three entry shapes converge here:
|
|
6
|
+
* - `--auto-continue` on an issue URL, which may find an existing pull
|
|
7
|
+
* request (or just a branch left behind by an earlier run);
|
|
8
|
+
* - a pull request URL, whose head branch and linked issue are read from the
|
|
9
|
+
* API;
|
|
10
|
+
* - a plain issue URL, which is the traditional mode.
|
|
11
|
+
*
|
|
12
|
+
* Fork detection is shared by the first two: a pull request whose head
|
|
13
|
+
* repository owner differs from the upstream owner puts solve in fork mode,
|
|
14
|
+
* unless the upstream is private and the user has write access (issue #1716),
|
|
15
|
+
* in which case working directly on the upstream is both possible and safer.
|
|
16
|
+
*
|
|
17
|
+
* Extracted from solve.mjs (issue #2175) so that file stays under the
|
|
18
|
+
* 1350-line early-warning threshold of the CI file-headroom check (long files
|
|
19
|
+
* cause concurrent PR merge conflicts — issue #1593). Behaviour is unchanged,
|
|
20
|
+
* including the `global.createdPR` assignments the error handlers read.
|
|
21
|
+
*
|
|
22
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2175
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} deps every collaborator solve.mjs already has in scope
|
|
27
|
+
* @returns {Promise<{issueNumber: number|undefined, prNumber: number|undefined, prBranch: string|undefined, mergeStateStatus: string|undefined, prState: string|undefined, forkOwner: string|null, forkRepoName: string|null, isContinueMode: boolean}>}
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveSolveMode({ argv, owner, repo, urlNumber, issueUrl, isIssueUrl, isPrUrl, skipForkForPrivateUpstream, shouldAttachLogs, log, safeExit, githubLib, processAutoContinueForIssue, handleMaintainerForkAccess, extractLinkedIssueNumber, reportError, cleanErrorMessage }) {
|
|
30
|
+
let issueNumber;
|
|
31
|
+
let prNumber;
|
|
32
|
+
let prBranch;
|
|
33
|
+
let mergeStateStatus;
|
|
34
|
+
let prState;
|
|
35
|
+
let forkOwner = null;
|
|
36
|
+
let forkRepoName = null;
|
|
37
|
+
let isContinueMode = false;
|
|
38
|
+
// Auto-continue logic: check for existing PRs if --auto-continue is enabled
|
|
39
|
+
const autoContinueResult = await processAutoContinueForIssue(argv, isIssueUrl, urlNumber, owner, repo);
|
|
40
|
+
if (autoContinueResult.isContinueMode) {
|
|
41
|
+
isContinueMode = true;
|
|
42
|
+
prNumber = autoContinueResult.prNumber;
|
|
43
|
+
prBranch = autoContinueResult.prBranch;
|
|
44
|
+
issueNumber = autoContinueResult.issueNumber;
|
|
45
|
+
// Only check PR details if we have a PR number
|
|
46
|
+
if (prNumber) {
|
|
47
|
+
// Store PR info globally for error handlers
|
|
48
|
+
global.createdPR = { number: prNumber };
|
|
49
|
+
// Check if PR is from a fork and get fork owner, merge status, and PR state
|
|
50
|
+
if (argv.verbose) {
|
|
51
|
+
await log(' Checking if PR is from a fork...', { verbose: true });
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
// Issue #2175: routed through githubLib.ghPrView (the same helper the
|
|
55
|
+
// pull-request-URL branch below uses) instead of a direct `$` call to
|
|
56
|
+
// `gh`, so the request goes through the rate-limit-safe wrapper.
|
|
57
|
+
const prCheckResult = await githubLib.ghPrView({ prNumber, owner, repo, jsonFields: 'headRepositoryOwner,headRepository,mergeStateStatus,state' });
|
|
58
|
+
if (prCheckResult.code === 0 && prCheckResult.data) {
|
|
59
|
+
const prCheckData = prCheckResult.data;
|
|
60
|
+
// Extract merge status and PR state
|
|
61
|
+
mergeStateStatus = prCheckData.mergeStateStatus;
|
|
62
|
+
prState = prCheckData.state;
|
|
63
|
+
if (argv.verbose) {
|
|
64
|
+
await log(` PR state: ${prState || 'UNKNOWN'}`, { verbose: true });
|
|
65
|
+
await log(` Merge status: ${mergeStateStatus || 'UNKNOWN'}`, { verbose: true });
|
|
66
|
+
}
|
|
67
|
+
if (prCheckData.headRepositoryOwner && prCheckData.headRepositoryOwner.login !== owner) {
|
|
68
|
+
const detectedForkOwner = prCheckData.headRepositoryOwner.login;
|
|
69
|
+
const detectedForkRepoName = prCheckData.headRepository && prCheckData.headRepository.name ? prCheckData.headRepository.name : null;
|
|
70
|
+
// Issue #1716: Skip fork mode for private upstream repos with write access.
|
|
71
|
+
if (skipForkForPrivateUpstream) {
|
|
72
|
+
await log(`🔒 Detected fork PR from ${detectedForkOwner}/${detectedForkRepoName || repo}, but upstream ${owner}/${repo} is private and you have write access.`);
|
|
73
|
+
await log(' Working directly on the private upstream repository (Issue #1716).');
|
|
74
|
+
} else {
|
|
75
|
+
forkOwner = detectedForkOwner;
|
|
76
|
+
// Get actual fork repository name (may be prefixed) and store for use in setupRepository
|
|
77
|
+
forkRepoName = detectedForkRepoName;
|
|
78
|
+
await log(`🍴 Detected fork PR from ${forkOwner}/${forkRepoName || repo}`);
|
|
79
|
+
if (argv.verbose) {
|
|
80
|
+
await log(` Fork owner: ${forkOwner}`, { verbose: true });
|
|
81
|
+
await log(' Will clone fork repository for continue mode', { verbose: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Check if maintainer can push to the fork when --allow-to-push-to-contributors-pull-requests-as-maintainer is enabled
|
|
85
|
+
if (forkOwner && argv.allowToPushToContributorsPullRequestsAsMaintainer && argv.autoFork) {
|
|
86
|
+
await handleMaintainerForkAccess({ owner, repo, prNumber });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (forkCheckError) {
|
|
91
|
+
if (argv.verbose) {
|
|
92
|
+
await log(` Warning: Could not check fork status: ${forkCheckError.message}`, { verbose: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// We have a branch but no PR - we'll use the existing branch and create a PR later
|
|
97
|
+
await log(`🔄 Using existing branch: ${prBranch} (no PR yet - will create one)`);
|
|
98
|
+
await log(' This branch was created by an earlier run; this run is reusing it rather than creating a fresh branch.');
|
|
99
|
+
if (argv.verbose) {
|
|
100
|
+
await log(' Branch will be checked out and PR will be created during auto-PR creation phase', {
|
|
101
|
+
verbose: true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if (isIssueUrl) {
|
|
106
|
+
issueNumber = autoContinueResult.issueNumber || urlNumber;
|
|
107
|
+
}
|
|
108
|
+
if (isPrUrl) {
|
|
109
|
+
isContinueMode = true;
|
|
110
|
+
prNumber = urlNumber;
|
|
111
|
+
// Store PR info globally for error handlers
|
|
112
|
+
global.createdPR = { number: prNumber, url: issueUrl };
|
|
113
|
+
await log(`🔄 Continue mode: Working with PR #${prNumber}`);
|
|
114
|
+
if (argv.verbose) {
|
|
115
|
+
await log(' Continue mode activated: PR URL provided directly', { verbose: true });
|
|
116
|
+
await log(` PR Number set to: ${prNumber}`, { verbose: true });
|
|
117
|
+
await log(' Will fetch PR details and linked issue', { verbose: true });
|
|
118
|
+
}
|
|
119
|
+
// Get PR details to find the linked issue and branch
|
|
120
|
+
try {
|
|
121
|
+
const prResult = await githubLib.ghPrView({
|
|
122
|
+
prNumber,
|
|
123
|
+
owner,
|
|
124
|
+
repo,
|
|
125
|
+
jsonFields: 'headRefName,body,number,mergeStateStatus,state,headRepositoryOwner,headRepository',
|
|
126
|
+
});
|
|
127
|
+
if (prResult.code !== 0 || !prResult.data) {
|
|
128
|
+
await log('Error: Failed to get PR details', { level: 'error' });
|
|
129
|
+
if (prResult.output.includes('Could not resolve to a PullRequest')) {
|
|
130
|
+
await githubLib.handlePRNotFoundError({ prNumber, owner, repo, argv, shouldAttachLogs });
|
|
131
|
+
} else {
|
|
132
|
+
await log(`Error: ${prResult.stderr || 'Unknown error'}`, { level: 'error' });
|
|
133
|
+
}
|
|
134
|
+
await safeExit(1, 'Failed to get PR details');
|
|
135
|
+
}
|
|
136
|
+
const prData = prResult.data;
|
|
137
|
+
prBranch = prData.headRefName;
|
|
138
|
+
mergeStateStatus = prData.mergeStateStatus;
|
|
139
|
+
prState = prData.state;
|
|
140
|
+
// Check if this is a fork PR
|
|
141
|
+
if (prData.headRepositoryOwner && prData.headRepositoryOwner.login !== owner) {
|
|
142
|
+
const detectedForkOwner = prData.headRepositoryOwner.login;
|
|
143
|
+
const detectedForkRepoName = prData.headRepository && prData.headRepository.name ? prData.headRepository.name : null;
|
|
144
|
+
// Issue #1716: Skip fork mode for private upstream repos with write access.
|
|
145
|
+
if (skipForkForPrivateUpstream) {
|
|
146
|
+
await log(`🔒 Detected fork PR from ${detectedForkOwner}/${detectedForkRepoName || repo}, but upstream ${owner}/${repo} is private and you have write access.`);
|
|
147
|
+
await log(' Working directly on the private upstream repository (Issue #1716).');
|
|
148
|
+
} else {
|
|
149
|
+
forkOwner = detectedForkOwner;
|
|
150
|
+
// Get actual fork repository name and store for use in setupRepository
|
|
151
|
+
forkRepoName = detectedForkRepoName;
|
|
152
|
+
await log(`🍴 Detected fork PR from ${forkOwner}/${forkRepoName || repo}`);
|
|
153
|
+
if (argv.verbose) {
|
|
154
|
+
await log(` Fork owner: ${forkOwner}`, { verbose: true });
|
|
155
|
+
await log(' Will clone fork repository for continue mode', { verbose: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Check if maintainer can push to the fork when --allow-to-push-to-contributors-pull-requests-as-maintainer is enabled
|
|
159
|
+
if (forkOwner && argv.allowToPushToContributorsPullRequestsAsMaintainer && argv.autoFork) {
|
|
160
|
+
await handleMaintainerForkAccess({ owner, repo, prNumber });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await log(`📝 PR branch: ${prBranch}`);
|
|
164
|
+
const prBody = prData.body || '';
|
|
165
|
+
const extractedIssueNumber = extractLinkedIssueNumber(prBody);
|
|
166
|
+
if (extractedIssueNumber) {
|
|
167
|
+
issueNumber = extractedIssueNumber;
|
|
168
|
+
await log(`🔗 Found linked issue #${issueNumber}`);
|
|
169
|
+
} else {
|
|
170
|
+
// If no linked issue found, we can still continue but warn
|
|
171
|
+
await log('⚠️ Warning: No linked issue found in PR body', { level: 'warning' });
|
|
172
|
+
await log(' The PR should contain "Fixes #123" or similar to link an issue', { level: 'warning' });
|
|
173
|
+
// Set issueNumber to PR number as fallback
|
|
174
|
+
issueNumber = prNumber;
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
reportError(error, {
|
|
178
|
+
context: 'pr_processing',
|
|
179
|
+
prNumber,
|
|
180
|
+
operation: 'process_pull_request',
|
|
181
|
+
});
|
|
182
|
+
await log(`Error: Failed to process PR: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
183
|
+
await safeExit(1, 'Failed to process PR');
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Traditional issue mode
|
|
187
|
+
issueNumber = urlNumber;
|
|
188
|
+
await log(`📝 Issue mode: Working with issue #${issueNumber}`);
|
|
189
|
+
}
|
|
190
|
+
return { issueNumber, prNumber, prBranch, mergeStateStatus, prState, forkOwner, forkRepoName, isContinueMode };
|
|
191
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @see https://github.com/link-assistant/hive-mind/issues/1041
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { getLimitCache } from './limits.lib.mjs';
|
|
13
13
|
export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
|
|
14
14
|
import { collectExecutingItems, formatDuration, formatQueueToolSection, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses, getRunningSessionItems, groupQueueItemsByTool, reportDequeueDecision } from './telegram-solve-queue.helpers.lib.mjs';
|
|
15
15
|
export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
|
|
@@ -20,6 +20,8 @@ import { canonicalizeGitHubUrl as canonicalizeQueueUrl } from './github-url-pars
|
|
|
20
20
|
import { t } from './i18n.lib.mjs';
|
|
21
21
|
import { safeEditMessageText } from './telegram-safe-reply.lib.mjs';
|
|
22
22
|
import { lt } from './limits-i18n.lib.mjs';
|
|
23
|
+
// Issue #2175: throttling decisions live in their own module to keep this file under the 1350-line warning threshold.
|
|
24
|
+
import { checkApiLimits as checkApiLimitsImpl, checkSystemResources as checkSystemResourcesImpl, getLocale } from './telegram-solve-queue.throttling.lib.mjs';
|
|
23
25
|
export const QueueItemStatus = {
|
|
24
26
|
QUEUED: 'queued',
|
|
25
27
|
WAITING: 'waiting',
|
|
@@ -28,13 +30,6 @@ export const QueueItemStatus = {
|
|
|
28
30
|
FAILED: 'failed',
|
|
29
31
|
CANCELLED: 'cancelled',
|
|
30
32
|
};
|
|
31
|
-
function getLocale(options = {}) {
|
|
32
|
-
if (typeof options === 'string') return options;
|
|
33
|
-
return options?.locale || null;
|
|
34
|
-
}
|
|
35
|
-
function appendWaitingForCurrentCommand(reason, locale) {
|
|
36
|
-
return `${reason} (${lt('queue_waiting_current_command', {}, { locale })})`;
|
|
37
|
-
}
|
|
38
33
|
function appendRemainingDuration(reason, ms, locale) {
|
|
39
34
|
return `${reason} (${lt('remaining', { duration: formatDuration(ms, { locale }) }, { locale })})`;
|
|
40
35
|
}
|
|
@@ -652,282 +647,34 @@ export class SolveQueue {
|
|
|
652
647
|
};
|
|
653
648
|
}
|
|
654
649
|
/**
|
|
655
|
-
* Check system resources (RAM, CPU, disk) using cached values
|
|
656
|
-
*
|
|
657
|
-
* Uses 5-minute load average for CPU instead of instantaneous usage.
|
|
658
|
-
* This provides a more stable metric that isn't affected by brief spikes
|
|
659
|
-
* during claude process startup.
|
|
660
|
-
*
|
|
661
|
-
* Resource threshold modes are now configurable via HIVE_MIND_QUEUE_CONFIG:
|
|
662
|
-
* - 'reject': Immediately reject the command, no queueing
|
|
663
|
-
* - 'enqueue': Block all commands unconditionally until metric drops
|
|
664
|
-
* - 'dequeue-one-at-a-time': Allow one command when above threshold
|
|
650
|
+
* Check system resources (RAM, CPU, disk) using cached values.
|
|
665
651
|
*
|
|
666
|
-
*
|
|
667
|
-
* -
|
|
668
|
-
* -
|
|
669
|
-
* - DISK: enqueue (waits until disk drops below the threshold)
|
|
652
|
+
* Issue #2175: the implementation lives in
|
|
653
|
+
* telegram-solve-queue.throttling.lib.mjs so this file stays under the
|
|
654
|
+
* 1350-line early-warning threshold (issue #1593).
|
|
670
655
|
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
* See: https://github.com/link-assistant/hive-mind/issues/1981
|
|
674
|
-
*
|
|
675
|
-
* @param {number} totalProcessing - Total processing count (queue + external claude processes)
|
|
656
|
+
* @param {number} totalProcessing - Total processing count (queue + external tool processes)
|
|
657
|
+
* @param {object} [options]
|
|
676
658
|
* @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean, rejected: boolean, rejectReason: string|null}>}
|
|
677
659
|
*/
|
|
678
660
|
async checkSystemResources(totalProcessing = 0, options = {}) {
|
|
679
|
-
|
|
680
|
-
const reasons = [];
|
|
681
|
-
let oneAtATime = false;
|
|
682
|
-
let rejected = false;
|
|
683
|
-
let rejectReason = null;
|
|
684
|
-
// Check RAM (using cached value)
|
|
685
|
-
const memResult = await getCachedMemoryInfo(this.verbose);
|
|
686
|
-
if (memResult.success) {
|
|
687
|
-
const usedRatio = memResult.memory.usedPercentage / 100;
|
|
688
|
-
if (usedRatio >= QUEUE_CONFIG.thresholds.ram.value) {
|
|
689
|
-
const reason = formatWaitingReason('ram', memResult.memory.usedPercentage, QUEUE_CONFIG.thresholds.ram.value, { locale });
|
|
690
|
-
const strategy = QUEUE_CONFIG.thresholds.ram.strategy;
|
|
691
|
-
this.recordThrottle(`ram_${strategy}`);
|
|
692
|
-
if (strategy === 'reject') {
|
|
693
|
-
rejected = true;
|
|
694
|
-
rejectReason = reason;
|
|
695
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
696
|
-
oneAtATime = true;
|
|
697
|
-
if (totalProcessing > 0) {
|
|
698
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
699
|
-
}
|
|
700
|
-
} else {
|
|
701
|
-
// 'enqueue' - block unconditionally
|
|
702
|
-
reasons.push(reason);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
// Check CPU using 5-minute load average (more stable than 1-minute)
|
|
707
|
-
const cpuResult = await getCachedCpuInfo(this.verbose);
|
|
708
|
-
if (cpuResult.success) {
|
|
709
|
-
// Use loadAvg5 (5-minute average) instead of usagePercentage (1-minute based)
|
|
710
|
-
// This provides a more stable metric that isn't affected by transient spikes
|
|
711
|
-
const loadAvg5 = cpuResult.cpuLoad.loadAvg5;
|
|
712
|
-
const cpuCount = cpuResult.cpuLoad.cpuCount;
|
|
713
|
-
// Calculate usage ratio: loadAvg5 / cpuCount
|
|
714
|
-
// Load average of 1.0 per CPU = 100% utilization
|
|
715
|
-
const usageRatio = loadAvg5 / cpuCount;
|
|
716
|
-
const usagePercent = Math.min(100, Math.round(usageRatio * 100));
|
|
717
|
-
if (this.verbose) {
|
|
718
|
-
this.log(`CPU 5m load avg: ${loadAvg5.toFixed(2)}, cpus: ${cpuCount}, usage: ${usagePercent}%`);
|
|
719
|
-
}
|
|
720
|
-
if (usageRatio >= QUEUE_CONFIG.thresholds.cpu.value) {
|
|
721
|
-
const reason = formatWaitingReason('cpu', usagePercent, QUEUE_CONFIG.thresholds.cpu.value, { locale });
|
|
722
|
-
const strategy = QUEUE_CONFIG.thresholds.cpu.strategy;
|
|
723
|
-
this.recordThrottle(`cpu_${strategy}`);
|
|
724
|
-
if (strategy === 'reject') {
|
|
725
|
-
rejected = true;
|
|
726
|
-
rejectReason = reason;
|
|
727
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
728
|
-
oneAtATime = true;
|
|
729
|
-
if (totalProcessing > 0) {
|
|
730
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
731
|
-
}
|
|
732
|
-
} else {
|
|
733
|
-
// 'enqueue' - block unconditionally
|
|
734
|
-
reasons.push(reason);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
// Check disk space (using cached value)
|
|
739
|
-
// Default strategy changed to 'reject' because queue is lost on restart anyway
|
|
740
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1253
|
|
741
|
-
const diskResult = await getCachedDiskInfo(this.verbose);
|
|
742
|
-
if (diskResult.success) {
|
|
743
|
-
// Calculate usage from free percentage
|
|
744
|
-
const usedPercent = 100 - diskResult.diskSpace.freePercentage;
|
|
745
|
-
const usedRatio = usedPercent / 100;
|
|
746
|
-
if (usedRatio >= QUEUE_CONFIG.thresholds.disk.value) {
|
|
747
|
-
const reason = formatWaitingReason('disk', usedPercent, QUEUE_CONFIG.thresholds.disk.value, { locale });
|
|
748
|
-
const strategy = QUEUE_CONFIG.thresholds.disk.strategy;
|
|
749
|
-
this.recordThrottle(`disk_${strategy}`);
|
|
750
|
-
if (strategy === 'reject') {
|
|
751
|
-
rejected = true;
|
|
752
|
-
rejectReason = reason;
|
|
753
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
754
|
-
oneAtATime = true;
|
|
755
|
-
if (totalProcessing > 0) {
|
|
756
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
757
|
-
}
|
|
758
|
-
} else {
|
|
759
|
-
// 'enqueue' - block unconditionally
|
|
760
|
-
reasons.push(reason);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return { ok: reasons.length === 0 && !rejected, reasons, oneAtATime, rejected, rejectReason };
|
|
661
|
+
return checkSystemResourcesImpl(this, totalProcessing, options);
|
|
765
662
|
}
|
|
766
663
|
/**
|
|
767
|
-
* Check API limits (Claude, GitHub) using cached values
|
|
664
|
+
* Check API limits (Claude, Codex, GitHub) using cached values.
|
|
768
665
|
*
|
|
769
|
-
*
|
|
770
|
-
* -
|
|
771
|
-
*
|
|
772
|
-
* - GitHub threshold blocks unconditionally when exceeded (ultimate restriction)
|
|
666
|
+
* Issue #2175: the implementation lives in
|
|
667
|
+
* telegram-solve-queue.throttling.lib.mjs so this file stays under the
|
|
668
|
+
* 1350-line early-warning threshold (issue #1593).
|
|
773
669
|
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
* This allows non-Claude tasks to run in parallel even when Claude limits are reached.
|
|
779
|
-
*
|
|
780
|
-
* Logic per issue #1253:
|
|
781
|
-
* - All thresholds now support configurable strategies (reject, enqueue, dequeue-one-at-a-time)
|
|
782
|
-
* - Configuration via HIVE_MIND_QUEUE_CONFIG or individual env vars
|
|
783
|
-
*
|
|
784
|
-
* @param {boolean} hasRunningToolProcess - Whether matching tool processes are running (from pgrep)
|
|
785
|
-
* @param {number} toolProcessingCount - Count of matching tool items being processed in queue
|
|
786
|
-
* @param {string} tool - The tool being used ('claude', 'agent', 'codex', 'gemini', 'qwen', etc.)
|
|
670
|
+
* @param {boolean} hasRunningToolProcess
|
|
671
|
+
* @param {number} toolProcessingCount
|
|
672
|
+
* @param {string} tool
|
|
673
|
+
* @param {object} [options]
|
|
787
674
|
* @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean, rejected: boolean, rejectReason: string|null}>}
|
|
788
675
|
*/
|
|
789
676
|
async checkApiLimits(hasRunningToolProcess = false, toolProcessingCount = 0, tool = 'claude', options = {}) {
|
|
790
|
-
|
|
791
|
-
const reasons = [];
|
|
792
|
-
let oneAtATime = false;
|
|
793
|
-
let rejected = false;
|
|
794
|
-
let rejectReason = null;
|
|
795
|
-
// Apply Claude-specific limits only when tool is 'claude'
|
|
796
|
-
// Other tools (like 'agent', 'gemini', and 'qwen') use different rate limiting backends and are not
|
|
797
|
-
// affected by Claude API limits (5-hour session, weekly limits)
|
|
798
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1159
|
|
799
|
-
const applyClaudeLimits = tool === 'claude';
|
|
800
|
-
const applyCodexLimits = tool === 'codex';
|
|
801
|
-
const totalToolProcessing = toolProcessingCount + (hasRunningToolProcess ? 1 : 0);
|
|
802
|
-
// Check Claude limits (using cached value)
|
|
803
|
-
// Only applied when tool is 'claude'
|
|
804
|
-
if (applyClaudeLimits) {
|
|
805
|
-
const claudeResult = await getCachedClaudeLimits(this.verbose);
|
|
806
|
-
if (claudeResult.success) {
|
|
807
|
-
const sessionPercent = claudeResult.usage.currentSession.percentage;
|
|
808
|
-
const weeklyPercent = claudeResult.usage.allModels.percentage;
|
|
809
|
-
// Session limit (5-hour)
|
|
810
|
-
// Configurable strategy via HIVE_MIND_QUEUE_CONFIG or HIVE_MIND_CLAUDE_5_HOUR_SESSION_STRATEGY
|
|
811
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1133, #1159, #1253
|
|
812
|
-
if (sessionPercent !== null) {
|
|
813
|
-
const sessionRatio = sessionPercent / 100;
|
|
814
|
-
if (sessionRatio >= QUEUE_CONFIG.thresholds.claude5Hour.value) {
|
|
815
|
-
const reason = formatWaitingReason('claude_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.claude5Hour.value, { locale });
|
|
816
|
-
const strategy = QUEUE_CONFIG.thresholds.claude5Hour.strategy;
|
|
817
|
-
this.recordThrottle(sessionRatio >= 1.0 ? 'claude_5_hour_session_100' : `claude_5_hour_session_${strategy}`);
|
|
818
|
-
if (strategy === 'reject') {
|
|
819
|
-
rejected = true;
|
|
820
|
-
rejectReason = reason;
|
|
821
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
822
|
-
oneAtATime = true;
|
|
823
|
-
if (totalToolProcessing > 0) {
|
|
824
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
825
|
-
}
|
|
826
|
-
} else {
|
|
827
|
-
// 'enqueue' - block unconditionally
|
|
828
|
-
reasons.push(reason);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
// Weekly limit
|
|
833
|
-
// Configurable strategy via HIVE_MIND_QUEUE_CONFIG or HIVE_MIND_CLAUDE_WEEKLY_STRATEGY
|
|
834
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1133, #1159, #1253
|
|
835
|
-
if (weeklyPercent !== null) {
|
|
836
|
-
const weeklyRatio = weeklyPercent / 100;
|
|
837
|
-
if (weeklyRatio >= QUEUE_CONFIG.thresholds.claudeWeekly.value) {
|
|
838
|
-
const reason = formatWaitingReason('claude_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.claudeWeekly.value, { locale });
|
|
839
|
-
const strategy = QUEUE_CONFIG.thresholds.claudeWeekly.strategy;
|
|
840
|
-
this.recordThrottle(weeklyRatio >= 1.0 ? 'claude_weekly_100' : `claude_weekly_${strategy}`);
|
|
841
|
-
if (strategy === 'reject') {
|
|
842
|
-
rejected = true;
|
|
843
|
-
rejectReason = reason;
|
|
844
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
845
|
-
oneAtATime = true;
|
|
846
|
-
if (totalToolProcessing > 0) {
|
|
847
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
848
|
-
}
|
|
849
|
-
} else {
|
|
850
|
-
// 'enqueue' - block unconditionally
|
|
851
|
-
reasons.push(reason);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
} else if (applyCodexLimits) {
|
|
857
|
-
const codexResult = await getCachedCodexLimits(this.verbose);
|
|
858
|
-
if (codexResult.success) {
|
|
859
|
-
const sessionPercent = codexResult.usage.currentSession.percentage;
|
|
860
|
-
const weeklyPercent = codexResult.usage.allModels.percentage;
|
|
861
|
-
if (sessionPercent !== null) {
|
|
862
|
-
const sessionRatio = sessionPercent / 100;
|
|
863
|
-
if (sessionRatio >= QUEUE_CONFIG.thresholds.codex5Hour.value) {
|
|
864
|
-
const reason = formatWaitingReason('codex_5_hour_session', sessionPercent, QUEUE_CONFIG.thresholds.codex5Hour.value, { locale });
|
|
865
|
-
const strategy = QUEUE_CONFIG.thresholds.codex5Hour.strategy;
|
|
866
|
-
this.recordThrottle(sessionRatio >= 1.0 ? 'codex_5_hour_session_100' : `codex_5_hour_session_${strategy}`);
|
|
867
|
-
if (strategy === 'reject') {
|
|
868
|
-
rejected = true;
|
|
869
|
-
rejectReason = reason;
|
|
870
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
871
|
-
oneAtATime = true;
|
|
872
|
-
if (totalToolProcessing > 0) {
|
|
873
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
874
|
-
}
|
|
875
|
-
} else {
|
|
876
|
-
reasons.push(reason);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (weeklyPercent !== null) {
|
|
881
|
-
const weeklyRatio = weeklyPercent / 100;
|
|
882
|
-
if (weeklyRatio >= QUEUE_CONFIG.thresholds.codexWeekly.value) {
|
|
883
|
-
const reason = formatWaitingReason('codex_weekly', weeklyPercent, QUEUE_CONFIG.thresholds.codexWeekly.value, { locale });
|
|
884
|
-
const strategy = QUEUE_CONFIG.thresholds.codexWeekly.strategy;
|
|
885
|
-
this.recordThrottle(weeklyRatio >= 1.0 ? 'codex_weekly_100' : `codex_weekly_${strategy}`);
|
|
886
|
-
if (strategy === 'reject') {
|
|
887
|
-
rejected = true;
|
|
888
|
-
rejectReason = reason;
|
|
889
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
890
|
-
oneAtATime = true;
|
|
891
|
-
if (totalToolProcessing > 0) {
|
|
892
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
893
|
-
}
|
|
894
|
-
} else {
|
|
895
|
-
reasons.push(reason);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
} else if (this.verbose) {
|
|
901
|
-
this.log(`Claude limits not applied for --tool ${tool}`);
|
|
902
|
-
}
|
|
903
|
-
// Check GitHub limits when the active tool already has a running process.
|
|
904
|
-
// This keeps the queue behavior aligned with the existing one-at-a-time throttling model.
|
|
905
|
-
// Configurable strategy via HIVE_MIND_QUEUE_CONFIG or HIVE_MIND_GITHUB_API_STRATEGY
|
|
906
|
-
if (hasRunningToolProcess) {
|
|
907
|
-
const githubResult = await getCachedGitHubLimits(this.verbose);
|
|
908
|
-
if (githubResult.success) {
|
|
909
|
-
const usedPercent = githubResult.githubRateLimit.usedPercentage;
|
|
910
|
-
const usedRatio = usedPercent / 100;
|
|
911
|
-
if (usedRatio >= QUEUE_CONFIG.thresholds.githubApi.value) {
|
|
912
|
-
const reason = formatWaitingReason('github', usedPercent, QUEUE_CONFIG.thresholds.githubApi.value, { locale });
|
|
913
|
-
const strategy = QUEUE_CONFIG.thresholds.githubApi.strategy;
|
|
914
|
-
this.recordThrottle(usedRatio >= 1.0 ? 'github_100' : `github_${strategy}`);
|
|
915
|
-
if (strategy === 'reject') {
|
|
916
|
-
rejected = true;
|
|
917
|
-
rejectReason = reason;
|
|
918
|
-
} else if (strategy === 'dequeue-one-at-a-time') {
|
|
919
|
-
oneAtATime = true;
|
|
920
|
-
if (totalToolProcessing > 0) {
|
|
921
|
-
reasons.push(appendWaitingForCurrentCommand(reason, locale));
|
|
922
|
-
}
|
|
923
|
-
} else {
|
|
924
|
-
// 'enqueue' - block unconditionally
|
|
925
|
-
reasons.push(reason);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return { ok: reasons.length === 0 && !rejected, reasons, oneAtATime, rejected, rejectReason };
|
|
677
|
+
return checkApiLimitsImpl(this, hasRunningToolProcess, toolProcessingCount, tool, options);
|
|
931
678
|
}
|
|
932
679
|
/**
|
|
933
680
|
* Record a throttle event for statistics
|