@link-assistant/hive-mind 1.48.2 → 1.48.3
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 +10 -0
- package/package.json +1 -1
- package/src/github-entity-validation.lib.mjs +129 -0
- package/src/github.lib.mjs +3 -4
- package/src/isolation-runner.lib.mjs +54 -4
- package/src/session-monitor.lib.mjs +30 -15
- package/src/solve.mjs +15 -16
- package/src/telegram-bot.mjs +19 -16
- package/src/telegram-solve-queue.lib.mjs +54 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.48.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2ac7f3c: Fix CI/CD lint failure caused by code duplication exceeding jscpd threshold (11.03% > 11%). Refactored test files to use shared `test-helpers.mjs` instead of duplicating assert/summary boilerplate, reducing duplication to 10.93%.
|
|
8
|
+
- 0b06bda: Fix `--isolation screen` session monitoring bug where sessions were prematurely detected as completed (Issue #1545). Add `screen -ls` fallback for screen-backend sessions to work around start-command UUID mismatch issues (link-foundation/start#101).
|
|
9
|
+
- 94eeaac: Immediately reject queued tasks when disk space (or any reject-strategy threshold) is exceeded, instead of leaving them in a waiting state indefinitely
|
|
10
|
+
- f955f0b: Add GitHub entity existence validation to /solve command to fail immediately on non-existent issues, PRs, repos, or users
|
|
11
|
+
|
|
3
12
|
## 1.48.2
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -11,6 +20,7 @@
|
|
|
11
20
|
### Patch Changes
|
|
12
21
|
|
|
13
22
|
- 6d385ab: Simplified cost display when public and Anthropic costs match, removed USD suffix from Anthropic cost line
|
|
23
|
+
- Validate GitHub entity existence (user/org, repository, issue/PR) before executing /solve command. The telegram bot and solve CLI now fail immediately with helpful error messages when targeting non-existent entities, preventing wasted resources and providing faster feedback.
|
|
14
24
|
|
|
15
25
|
## 1.48.0
|
|
16
26
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub entity existence validation for /solve command.
|
|
3
|
+
* Extracted from github.lib.mjs to keep files under 1500 line limit.
|
|
4
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1552
|
|
5
|
+
*/
|
|
6
|
+
if (typeof globalThis.use === 'undefined') globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
7
|
+
const { $ } = await use('command-stream');
|
|
8
|
+
import { ghCmdRetry } from './lib.mjs';
|
|
9
|
+
import { ghPrView, ghIssueView } from './github.lib.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate existence of GitHub entities (user/org, repository, issue/PR) before executing a command.
|
|
13
|
+
* Checks each level in order: user/org → repository → issue/PR, failing fast at the first missing entity.
|
|
14
|
+
*
|
|
15
|
+
* When autoAcceptInvite is enabled, invitations should be accepted BEFORE calling this function,
|
|
16
|
+
* so that newly-accepted repos/orgs are visible to the API checks.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options - Validation options
|
|
19
|
+
* @param {string} options.owner - Repository owner (user or organization login)
|
|
20
|
+
* @param {string} options.repo - Repository name
|
|
21
|
+
* @param {number|string} [options.number] - Issue or PR number (if applicable)
|
|
22
|
+
* @param {string} [options.type] - URL type: 'issue' or 'pull'
|
|
23
|
+
* @param {boolean} [options.verbose=false] - Whether verbose logging is enabled
|
|
24
|
+
* @returns {Promise<{valid: boolean, error?: string, level?: string, details?: string}>}
|
|
25
|
+
* - valid: true if all entities exist and are accessible
|
|
26
|
+
* - error: user-facing error message (when valid=false)
|
|
27
|
+
* - level: which entity level failed ('user', 'repo', 'issue', 'pull')
|
|
28
|
+
* - details: additional context for verbose logging
|
|
29
|
+
*/
|
|
30
|
+
export async function validateGitHubEntityExistence({ owner, repo, number, type, verbose = false }) {
|
|
31
|
+
// Step 1: Check user/organization existence
|
|
32
|
+
try {
|
|
33
|
+
const userResult = await ghCmdRetry(() => $`gh api users/${owner} --jq .login`, { label: `check user ${owner}` });
|
|
34
|
+
if (userResult.code !== 0) {
|
|
35
|
+
const errorOutput = (userResult.stderr ? userResult.stderr.toString() : '') + (userResult.stdout ? userResult.stdout.toString() : '');
|
|
36
|
+
if (errorOutput.includes('404') || errorOutput.includes('Not Found')) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
error: `GitHub user or organization '${owner}' does not exist.\n\n💡 Please check:\n• The username/organization name is spelled correctly\n• The account has not been deleted or renamed`,
|
|
40
|
+
level: 'user',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Non-404 errors (network, auth) - don't block, let downstream handle
|
|
44
|
+
verbose && console.log(`[VERBOSE] Entity check: Could not verify user '${owner}': ${errorOutput.trim()}`);
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
verbose && console.log(`[VERBOSE] Entity check: User check error for '${owner}': ${e.message}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Step 2: Check repository existence
|
|
51
|
+
try {
|
|
52
|
+
const repoResult = await ghCmdRetry(() => $`gh api repos/${owner}/${repo} --jq .full_name`, { label: `check repo ${owner}/${repo}` });
|
|
53
|
+
if (repoResult.code !== 0) {
|
|
54
|
+
const errorOutput = (repoResult.stderr ? repoResult.stderr.toString() : '') + (repoResult.stdout ? repoResult.stdout.toString() : '');
|
|
55
|
+
if (errorOutput.includes('404') || errorOutput.includes('Not Found')) {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
error: `Repository '${owner}/${repo}' not found or not accessible.\n\n💡 Please check:\n• The repository name is spelled correctly\n• If it's a private repository, ensure the bot has been granted access (GitHub returns 404 for private repos without permissions)\n• The repository has not been deleted or transferred\n• If you were recently invited, try using --auto-accept-invite to accept pending invitations`,
|
|
59
|
+
level: 'repo',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
verbose && console.log(`[VERBOSE] Entity check: Could not verify repo '${owner}/${repo}': ${errorOutput.trim()}`);
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
verbose && console.log(`[VERBOSE] Entity check: Repo check error for '${owner}/${repo}': ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 3: Check issue or PR existence (if number is provided)
|
|
69
|
+
if (number) {
|
|
70
|
+
if (type === 'pull') {
|
|
71
|
+
try {
|
|
72
|
+
const prResult = await ghPrView({ prNumber: number, owner, repo, jsonFields: 'number,state' });
|
|
73
|
+
if (prResult.code !== 0 || !prResult.data) {
|
|
74
|
+
const errorOutput = prResult.output || '';
|
|
75
|
+
if (errorOutput.includes('Could not resolve') || errorOutput.includes('not found') || errorOutput.includes('404')) {
|
|
76
|
+
// Check if an issue with this number exists (common confusion)
|
|
77
|
+
let suggestion = '';
|
|
78
|
+
try {
|
|
79
|
+
const issueCheck = await ghIssueView({ issueNumber: number, owner, repo, jsonFields: 'number,title' });
|
|
80
|
+
if (issueCheck.code === 0 && issueCheck.data) {
|
|
81
|
+
suggestion = `\n\n💡 However, Issue #${number} exists: "${issueCheck.data.title}"\n Did you mean: https://github.com/${owner}/${repo}/issues/${number}`;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
valid: false,
|
|
88
|
+
error: `Pull request #${number} does not exist in ${owner}/${repo}.${suggestion}\n\n💡 Please check:\n• The PR number is correct\n• The PR has not been deleted`,
|
|
89
|
+
level: 'pull',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
verbose && console.log(`[VERBOSE] Entity check: Could not verify PR #${number}: ${errorOutput.trim()}`);
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
verbose && console.log(`[VERBOSE] Entity check: PR check error for #${number}: ${e.message}`);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// type === 'issue' or default
|
|
99
|
+
try {
|
|
100
|
+
const issueResult = await ghIssueView({ issueNumber: number, owner, repo, jsonFields: 'number,title' });
|
|
101
|
+
if (issueResult.code !== 0 || !issueResult.data) {
|
|
102
|
+
const errorOutput = issueResult.output || '';
|
|
103
|
+
if (errorOutput.includes('Could not resolve') || errorOutput.includes('not found') || errorOutput.includes('404')) {
|
|
104
|
+
// Check if a PR with this number exists (common confusion)
|
|
105
|
+
let suggestion = '';
|
|
106
|
+
try {
|
|
107
|
+
const prCheck = await ghPrView({ prNumber: number, owner, repo, jsonFields: 'number,title' });
|
|
108
|
+
if (prCheck.code === 0 && prCheck.data) {
|
|
109
|
+
suggestion = `\n\n💡 However, Pull Request #${number} exists: "${prCheck.data.title}"\n Did you mean: https://github.com/${owner}/${repo}/pull/${number}`;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
/* ignore */
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
valid: false,
|
|
116
|
+
error: `Issue #${number} does not exist in ${owner}/${repo}.${suggestion}\n\n💡 Please check:\n• The issue number is correct\n• The issue has not been deleted or transferred`,
|
|
117
|
+
level: 'issue',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
verbose && console.log(`[VERBOSE] Entity check: Could not verify issue #${number}: ${errorOutput.trim()}`);
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
verbose && console.log(`[VERBOSE] Entity check: Issue check error for #${number}: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { valid: true };
|
|
129
|
+
}
|
package/src/github.lib.mjs
CHANGED
|
@@ -1464,10 +1464,9 @@ export async function detectRepositoryVisibility(owner, repo) {
|
|
|
1464
1464
|
return { isPublic: true, visibility: null };
|
|
1465
1465
|
}
|
|
1466
1466
|
}
|
|
1467
|
-
|
|
1468
|
-
export const batchCheckArchivedRepositories = batchCheckArchived;
|
|
1469
|
-
// Re-export log upload function
|
|
1470
|
-
export { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
|
|
1467
|
+
export { validateGitHubEntityExistence } from './github-entity-validation.lib.mjs'; // Issue #1552
|
|
1468
|
+
export const batchCheckArchivedRepositories = batchCheckArchived; // Re-export batch archived check
|
|
1469
|
+
export { uploadLogWithGhUploadLog } from './log-upload.lib.mjs'; // Re-export log upload function
|
|
1471
1470
|
// Export all functions as default object too
|
|
1472
1471
|
export default {
|
|
1473
1472
|
maskGitHubToken,
|
|
@@ -181,15 +181,65 @@ export async function querySessionStatus(sessionId, verbose = false) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
|
-
* Check if
|
|
184
|
+
* Check if a screen session exists via `screen -ls`.
|
|
185
|
+
* Used as a fallback when `$ --status` fails to find or correctly track
|
|
186
|
+
* screen-based isolation sessions.
|
|
185
187
|
*
|
|
186
|
-
* @param {string}
|
|
188
|
+
* @param {string} sessionName - Name of the screen session to check
|
|
187
189
|
* @param {boolean} [verbose] - Enable verbose logging
|
|
190
|
+
* @returns {Promise<boolean>} True if screen session exists
|
|
191
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1545
|
|
192
|
+
*/
|
|
193
|
+
export async function checkScreenSessionRunning(sessionName, verbose = false) {
|
|
194
|
+
try {
|
|
195
|
+
const result = await $({ mirror: false })`screen -ls`;
|
|
196
|
+
const output = result.stdout?.toString() || '';
|
|
197
|
+
const exists = output.includes(sessionName);
|
|
198
|
+
if (verbose) {
|
|
199
|
+
console.log(`[VERBOSE] isolation-runner: screen -ls check for '${sessionName}': ${exists ? 'running' : 'not found'}`);
|
|
200
|
+
}
|
|
201
|
+
return exists;
|
|
202
|
+
} catch {
|
|
203
|
+
// screen -ls returns exit code 1 when no sessions exist
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if an isolated session is still running.
|
|
210
|
+
* Uses `$ --status` first, with a `screen -ls` fallback for screen-backend
|
|
211
|
+
* sessions to work around start-command UUID mismatch issues.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} sessionId - UUID of the session (used for both $ --status and screen session name)
|
|
214
|
+
* @param {Object} [options] - Options
|
|
215
|
+
* @param {string} [options.backend] - Isolation backend ('screen', 'tmux', 'docker')
|
|
216
|
+
* @param {boolean} [options.verbose] - Enable verbose logging
|
|
188
217
|
* @returns {Promise<boolean>} True if session is still executing
|
|
189
218
|
*/
|
|
190
|
-
export async function isSessionRunning(sessionId,
|
|
219
|
+
export async function isSessionRunning(sessionId, options = {}) {
|
|
220
|
+
// Support legacy call signature: isSessionRunning(sessionId, verbose)
|
|
221
|
+
const opts = typeof options === 'boolean' ? { verbose: options } : options;
|
|
222
|
+
const { backend, verbose = false } = opts;
|
|
223
|
+
|
|
191
224
|
const result = await querySessionStatus(sessionId, verbose);
|
|
192
|
-
|
|
225
|
+
if (result.exists && result.status === 'executing') {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Fallback: for screen backend, check screen -ls directly.
|
|
230
|
+
// This works around start-command bugs where:
|
|
231
|
+
// 1. $ --status can't find session by --session name (only by internal UUID)
|
|
232
|
+
// 2. $ --status reports "executed" immediately for --detached screen sessions
|
|
233
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1545
|
|
234
|
+
if (backend === 'screen') {
|
|
235
|
+
const screenRunning = await checkScreenSessionRunning(sessionId, verbose);
|
|
236
|
+
if (screenRunning && verbose) {
|
|
237
|
+
console.log(`[VERBOSE] isolation-runner: $ --status says not running, but screen -ls confirms session '${sessionId}' is still active`);
|
|
238
|
+
}
|
|
239
|
+
return screenRunning;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return false;
|
|
193
243
|
}
|
|
194
244
|
|
|
195
245
|
/**
|
|
@@ -21,13 +21,17 @@ import { exec as execCallback } from 'child_process';
|
|
|
21
21
|
const exec = promisify(execCallback);
|
|
22
22
|
|
|
23
23
|
// Lazy import for isolation runner (only when needed)
|
|
24
|
-
let
|
|
25
|
-
async function
|
|
26
|
-
if (!
|
|
27
|
-
|
|
28
|
-
_querySessionStatus = mod.querySessionStatus;
|
|
24
|
+
let _isolationRunner = null;
|
|
25
|
+
async function getIsolationRunner() {
|
|
26
|
+
if (!_isolationRunner) {
|
|
27
|
+
_isolationRunner = await import('./isolation-runner.lib.mjs');
|
|
29
28
|
}
|
|
30
|
-
return
|
|
29
|
+
return _isolationRunner;
|
|
30
|
+
}
|
|
31
|
+
// Legacy accessor for querySessionStatus
|
|
32
|
+
async function getQuerySessionStatus() {
|
|
33
|
+
const mod = await getIsolationRunner();
|
|
34
|
+
return mod.querySessionStatus;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
// In-memory session store
|
|
@@ -49,16 +53,23 @@ export async function checkScreenSessionExists(sessionName) {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
|
-
* Check if an isolated session is still running
|
|
53
|
-
*
|
|
54
|
-
*
|
|
56
|
+
* Check if an isolated session is still running.
|
|
57
|
+
* Uses isolation-runner's isSessionRunning which includes screen -ls fallback
|
|
58
|
+
* for screen-backend sessions to work around start-command UUID mismatch.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} sessionId - UUID of the isolated session (screen session name)
|
|
61
|
+
* @param {Object} [options] - Options
|
|
62
|
+
* @param {string} [options.backend] - Isolation backend ('screen', 'tmux', 'docker')
|
|
63
|
+
* @param {boolean} [options.verbose] - Whether to log verbose output
|
|
55
64
|
* @returns {Promise<boolean>} True if session is still running
|
|
65
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1545
|
|
56
66
|
*/
|
|
57
|
-
async function checkIsolatedSessionRunning(sessionId,
|
|
67
|
+
async function checkIsolatedSessionRunning(sessionId, options = {}) {
|
|
68
|
+
const opts = typeof options === 'boolean' ? { verbose: options } : options;
|
|
69
|
+
const { backend, verbose = false } = opts;
|
|
58
70
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
return result.exists && result.status === 'executing';
|
|
71
|
+
const runner = await getIsolationRunner();
|
|
72
|
+
return await runner.isSessionRunning(sessionId, { backend, verbose });
|
|
62
73
|
} catch (error) {
|
|
63
74
|
if (verbose) {
|
|
64
75
|
console.error(`[VERBOSE] Error checking isolated session ${sessionId}: ${error.message}`);
|
|
@@ -169,8 +180,12 @@ export async function monitorSessions(bot, verbose = false) {
|
|
|
169
180
|
let exitCode = null;
|
|
170
181
|
|
|
171
182
|
if (sessionInfo.isolationBackend && sessionInfo.sessionId) {
|
|
172
|
-
// Isolation mode: use $ --status for
|
|
173
|
-
|
|
183
|
+
// Isolation mode: use $ --status with screen -ls fallback for screen backend
|
|
184
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1545
|
|
185
|
+
stillRunning = await checkIsolatedSessionRunning(sessionInfo.sessionId, {
|
|
186
|
+
backend: sessionInfo.isolationBackend,
|
|
187
|
+
verbose,
|
|
188
|
+
});
|
|
174
189
|
if (!stillRunning) {
|
|
175
190
|
exitCode = await getIsolatedSessionExitCode(sessionInfo.sessionId, verbose);
|
|
176
191
|
}
|
package/src/solve.mjs
CHANGED
|
@@ -89,8 +89,7 @@ const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs'
|
|
|
89
89
|
|
|
90
90
|
// Initialize log file early (before argument parsing) to capture all output
|
|
91
91
|
const logFile = await initializeLogFile(null);
|
|
92
|
-
|
|
93
|
-
// Log version and raw command IMMEDIATELY after log file initialization (ensures they appear even if parsing fails)
|
|
92
|
+
// Log version and raw command IMMEDIATELY after log file initialization
|
|
94
93
|
const versionInfo = await getVersionInfo();
|
|
95
94
|
await log('');
|
|
96
95
|
await log(`🚀 solve v${versionInfo}`);
|
|
@@ -115,7 +114,6 @@ setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log fi
|
|
|
115
114
|
setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
|
|
116
115
|
|
|
117
116
|
// Early logs go to cwd; custom log dir takes effect after argv is parsed
|
|
118
|
-
|
|
119
117
|
// Conditionally import tool-specific functions after argv is parsed
|
|
120
118
|
let checkForUncommittedChanges;
|
|
121
119
|
if (argv.tool === 'opencode') {
|
|
@@ -206,8 +204,7 @@ if (!(await validateContinueOnlyOnFeedback(argv, isPrUrl, isIssueUrl))) {
|
|
|
206
204
|
await safeExit(1, 'Feedback validation failed');
|
|
207
205
|
}
|
|
208
206
|
|
|
209
|
-
// Validate model name EARLY -
|
|
210
|
-
// Model validation is a simple string check and should always be performed
|
|
207
|
+
// Validate model name EARLY - always runs regardless of --skip-tool-connection-check
|
|
211
208
|
const tool = argv.tool || 'claude';
|
|
212
209
|
await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
|
|
213
210
|
|
|
@@ -233,12 +230,14 @@ if (argv.verbose) {
|
|
|
233
230
|
await log(` Is PR URL: ${!!isPrUrl}`, { verbose: true });
|
|
234
231
|
}
|
|
235
232
|
const claudePath = argv.executeToolWithBun ? 'bunx claude' : process.env.CLAUDE_PATH || 'claude';
|
|
236
|
-
// Note: owner, repo, and urlNumber are extracted from validateGitHubUrl() above
|
|
237
|
-
|
|
233
|
+
// Note: owner, repo, and urlNumber are extracted from validateGitHubUrl() above
|
|
234
|
+
// Accept pending invitation BEFORE any access checks (auto-fork, permissions, entity validation)
|
|
235
|
+
if (argv.autoAcceptInvite) {
|
|
236
|
+
await autoAcceptInviteForRepo(owner, repo, log, argv.verbose);
|
|
237
|
+
}
|
|
238
238
|
// Handle --auto-fork option: automatically fork public repositories without write access
|
|
239
239
|
if (argv.autoFork && !argv.fork) {
|
|
240
240
|
const { detectRepositoryVisibility } = githubLib;
|
|
241
|
-
|
|
242
241
|
// Check if we have write access first (issue #1536: retry on transient network errors)
|
|
243
242
|
await log('🔍 Checking repository access for auto-fork...');
|
|
244
243
|
const permResult = await lib.ghCmdRetry(() => $`gh api repos/${owner}/${repo} --jq .permissions`, { label: 'auto-fork perms' });
|
|
@@ -304,14 +303,7 @@ if (argv.autoFork && !argv.fork) {
|
|
|
304
303
|
argv.fork = true;
|
|
305
304
|
}
|
|
306
305
|
}
|
|
307
|
-
|
|
308
|
-
// Accept pending GitHub invitation for the specific repo/org before checking write access
|
|
309
|
-
if (argv.autoAcceptInvite) {
|
|
310
|
-
await autoAcceptInviteForRepo(owner, repo, log, argv.verbose);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Early check: Verify repository write permissions BEFORE doing any work
|
|
314
|
-
// This prevents wasting AI tokens when user doesn't have access and --fork is not used
|
|
306
|
+
// Permission check BEFORE entity validation (#1552): avoids false 404 on private repos without access
|
|
315
307
|
const { checkRepositoryWritePermission } = githubLib;
|
|
316
308
|
const hasWriteAccess = await checkRepositoryWritePermission(owner, repo, {
|
|
317
309
|
useFork: argv.fork,
|
|
@@ -324,6 +316,13 @@ if (!hasWriteAccess) {
|
|
|
324
316
|
await safeExit(1, 'Permission check failed');
|
|
325
317
|
}
|
|
326
318
|
|
|
319
|
+
// Issue #1552: Validate entity existence AFTER permissions (cascade: user/org → repo → issue/PR)
|
|
320
|
+
const entityCheck = await (await import('./github-entity-validation.lib.mjs')).validateGitHubEntityExistence({ owner, repo, number: urlNumber, type: isIssueUrl ? 'issue' : isPrUrl ? 'pull' : undefined, verbose: argv.verbose });
|
|
321
|
+
if (!entityCheck.valid) {
|
|
322
|
+
await log(`\n❌ ${entityCheck.error}\n`, { level: 'error' });
|
|
323
|
+
await safeExit(1, `GitHub entity not found (${entityCheck.level})`);
|
|
324
|
+
}
|
|
325
|
+
|
|
327
326
|
// Detect repository visibility and set auto-cleanup default if not explicitly set
|
|
328
327
|
if (argv.autoCleanup === undefined) {
|
|
329
328
|
const { detectRepositoryVisibility } = githubLib;
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -37,7 +37,7 @@ const _helpersBot = helpersModuleBot.default || helpersModuleBot;
|
|
|
37
37
|
const hideBin = _helpersBot.hideBin || (argv => argv.slice(2));
|
|
38
38
|
const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
|
|
39
39
|
const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
|
|
40
|
-
const { parseGitHubUrl } = await import('./github.lib.mjs');
|
|
40
|
+
const { parseGitHubUrl, validateGitHubEntityExistence } = await import('./github.lib.mjs');
|
|
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');
|
|
@@ -282,10 +282,7 @@ if (config.dryRun) {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
// === HEAVY DEPENDENCIES LOADED BELOW (skipped in dry-run mode) ===
|
|
285
|
-
// These imports are
|
|
286
|
-
// configuration validation. The telegraf module in particular can take 3-8 seconds
|
|
287
|
-
// to load on cold start due to network fetch from unpkg.com CDN.
|
|
288
|
-
// See issue #801 for details.
|
|
285
|
+
// These imports are after dry-run check to speed up config validation. Telegraf can take 3-8s to load on cold start (issue #801).
|
|
289
286
|
|
|
290
287
|
// Initialize Sentry for error tracking
|
|
291
288
|
await initializeSentry({
|
|
@@ -297,17 +294,12 @@ const telegrafModule = await use('telegraf');
|
|
|
297
294
|
const { Telegraf } = telegrafModule;
|
|
298
295
|
|
|
299
296
|
const bot = new Telegraf(BOT_TOKEN, {
|
|
300
|
-
// Remove
|
|
301
|
-
// This is important because command handlers (like /solve) spawn long-running processes
|
|
302
|
-
handlerTimeout: Infinity,
|
|
297
|
+
handlerTimeout: Infinity, // Remove default 90s timeout; command handlers like /solve spawn long-running processes
|
|
303
298
|
});
|
|
304
299
|
|
|
305
|
-
// Track bot startup time
|
|
306
|
-
// Using Unix timestamp (seconds since epoch) to match Telegram's message.date format
|
|
300
|
+
// Track bot startup time (Unix seconds to match Telegram's message.date format)
|
|
307
301
|
const BOT_START_TIME = Math.floor(Date.now() / 1000);
|
|
308
|
-
|
|
309
|
-
// Wrapper functions that bind extracted filter functions to bot-specific state
|
|
310
|
-
// The actual logic is in telegram-message-filters.lib.mjs for testability (issue #1207)
|
|
302
|
+
// Wrapper functions binding filter logic to bot state (actual logic in telegram-message-filters.lib.mjs, issue #1207)
|
|
311
303
|
function isChatAuthorized(chatId) {
|
|
312
304
|
return _isChatAuthorized(chatId, allowedChats);
|
|
313
305
|
}
|
|
@@ -990,9 +982,20 @@ async function handleSolveCommand(ctx) {
|
|
|
990
982
|
});
|
|
991
983
|
return;
|
|
992
984
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
985
|
+
// Issue #1552: Validate GitHub entity existence before queueing/executing
|
|
986
|
+
if (args.some(a => a === '--auto-accept-invite') && validation.parsed.owner && validation.parsed.repo) {
|
|
987
|
+
try {
|
|
988
|
+
await (await import('./solve.accept-invite.lib.mjs')).autoAcceptInviteForRepo(validation.parsed.owner, validation.parsed.repo, async () => {}, false);
|
|
989
|
+
} catch (e) {
|
|
990
|
+
VERBOSE && console.log(`[VERBOSE] Auto-accept invite pre-check failed: ${e.message}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const entityCheck = await validateGitHubEntityExistence({ owner: validation.parsed.owner, repo: validation.parsed.repo, number: validation.parsed.number, type: validation.parsed.type, verbose: VERBOSE });
|
|
994
|
+
if (!entityCheck.valid) {
|
|
995
|
+
await safeReply(ctx, `❌ ${escapeMarkdown(entityCheck.error)}`, { reply_to_message_id: ctx.message.message_id });
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
// Use normalized URL from validation to ensure consistent duplicate detection (issue #1080)
|
|
996
999
|
const normalizedUrl = validation.parsed.normalized;
|
|
997
1000
|
|
|
998
1001
|
const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
|
|
@@ -480,8 +480,13 @@ export class SolveQueue {
|
|
|
480
480
|
* Find startable items from each tool queue
|
|
481
481
|
* Returns the first item from each tool queue that can start.
|
|
482
482
|
* With separate queues, each tool is checked independently so they don't block each other.
|
|
483
|
+
*
|
|
484
|
+
* Also immediately rejects all queued items when a 'reject' strategy threshold
|
|
485
|
+
* is exceeded, instead of leaving them waiting indefinitely.
|
|
486
|
+
*
|
|
483
487
|
* @returns {Promise<Array<{item: SolveQueueItem, tool: string, index: number, check: Object}>>}
|
|
484
488
|
* @see https://github.com/link-assistant/hive-mind/issues/1159
|
|
489
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1555
|
|
485
490
|
*/
|
|
486
491
|
async findStartableItems() {
|
|
487
492
|
const startableItems = [];
|
|
@@ -490,10 +495,18 @@ export class SolveQueue {
|
|
|
490
495
|
if (toolQueue.length === 0) continue;
|
|
491
496
|
|
|
492
497
|
// Check if first item in this tool's queue can start
|
|
493
|
-
const item = toolQueue[0];
|
|
494
498
|
const check = await this.canStartCommand({ tool });
|
|
495
499
|
|
|
500
|
+
// When a 'reject' strategy threshold is exceeded, immediately reject
|
|
501
|
+
// all items in this tool's queue instead of leaving them waiting.
|
|
502
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1555
|
|
503
|
+
if (check.rejected) {
|
|
504
|
+
await this.rejectAllItemsInQueue(tool, toolQueue, check.rejectReason);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
496
508
|
if (check.canStart) {
|
|
509
|
+
const item = toolQueue[0];
|
|
497
510
|
// Also check one-at-a-time mode for this specific tool
|
|
498
511
|
// For tool-specific one-at-a-time, only count that tool's processing items
|
|
499
512
|
const toolProcessingCount = this.getProcessingCountByTool(tool);
|
|
@@ -509,6 +522,30 @@ export class SolveQueue {
|
|
|
509
522
|
return startableItems;
|
|
510
523
|
}
|
|
511
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Reject all items in a tool queue and notify users.
|
|
527
|
+
* Called when a 'reject' strategy threshold is exceeded for queued items.
|
|
528
|
+
*
|
|
529
|
+
* @param {string} tool - Tool type (e.g., 'claude', 'agent')
|
|
530
|
+
* @param {SolveQueueItem[]} toolQueue - The tool's queue array
|
|
531
|
+
* @param {string} rejectReason - Reason for rejection
|
|
532
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1555
|
|
533
|
+
*/
|
|
534
|
+
async rejectAllItemsInQueue(tool, toolQueue, rejectReason) {
|
|
535
|
+
const reason = rejectReason || 'Resource limit exceeded';
|
|
536
|
+
while (toolQueue.length > 0) {
|
|
537
|
+
const item = toolQueue.shift();
|
|
538
|
+
item.setFailed(reason);
|
|
539
|
+
this.failed.push(item);
|
|
540
|
+
this.stats.totalFailed++;
|
|
541
|
+
|
|
542
|
+
this.log(`Rejected queued item: ${item.toString()} from ${tool} queue - ${reason}`);
|
|
543
|
+
|
|
544
|
+
await this.updateItemMessage(item, `❌ Solve command rejected.\n\n${item.infoBlock}\n\n🚫 Reason: ${reason}`);
|
|
545
|
+
}
|
|
546
|
+
while (this.failed.length > 100) this.failed.shift();
|
|
547
|
+
}
|
|
548
|
+
|
|
512
549
|
/**
|
|
513
550
|
* Find first queue item that can start based on its tool's limits (legacy compatibility)
|
|
514
551
|
* With separate queues, returns the first startable item from any tool queue.
|
|
@@ -1069,22 +1106,31 @@ export class SolveQueue {
|
|
|
1069
1106
|
}
|
|
1070
1107
|
|
|
1071
1108
|
/**
|
|
1072
|
-
* Update all waiting items with their tool-specific waiting reasons
|
|
1109
|
+
* Update all waiting items with their tool-specific waiting reasons.
|
|
1110
|
+
* Items blocked by a 'reject' strategy threshold are immediately rejected
|
|
1111
|
+
* and removed from the queue, since they cannot proceed and keeping them
|
|
1112
|
+
* queued would only confuse users (the queue is lost on restart anyway).
|
|
1113
|
+
*
|
|
1073
1114
|
* @see https://github.com/link-assistant/hive-mind/issues/1078
|
|
1115
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1555
|
|
1074
1116
|
*/
|
|
1075
1117
|
async updateAllWaitingItems() {
|
|
1076
1118
|
for (const [tool, toolQueue] of Object.entries(this.queues)) {
|
|
1119
|
+
// First check if the tool's threshold triggers a 'reject' strategy.
|
|
1120
|
+
// If so, reject all items at once rather than iterating one by one.
|
|
1121
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1555
|
|
1122
|
+
const toolCheck = await this.canStartCommand({ tool });
|
|
1123
|
+
if (toolCheck.rejected) {
|
|
1124
|
+
await this.rejectAllItemsInQueue(tool, toolQueue, toolCheck.rejectReason);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1077
1128
|
for (let i = 0; i < toolQueue.length; i++) {
|
|
1078
1129
|
const item = toolQueue[i];
|
|
1079
1130
|
if (item.status === QueueItemStatus.QUEUED || item.status === QueueItemStatus.WAITING) {
|
|
1080
|
-
// Get the specific reason for this item's tool
|
|
1081
|
-
const itemCheck = await this.canStartCommand({ tool: item.tool });
|
|
1082
1131
|
const previousStatus = item.status;
|
|
1083
1132
|
const previousReason = item.waitingReason;
|
|
1084
|
-
|
|
1085
|
-
// This ensures disk-full and other rejection reasons are shown properly
|
|
1086
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
1087
|
-
const waitReason = itemCheck.rejectReason || itemCheck.reason || 'Waiting in queue';
|
|
1133
|
+
const waitReason = toolCheck.reason || 'Waiting in queue';
|
|
1088
1134
|
item.setWaiting(waitReason);
|
|
1089
1135
|
|
|
1090
1136
|
// Update message if status/reason changed or it's time for periodic update
|