@link-assistant/hive-mind 1.48.1 → 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 CHANGED
@@ -1,10 +1,26 @@
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
+
12
+ ## 1.48.2
13
+
14
+ ### Patch Changes
15
+
16
+ - 7c3a8c1: Fix agent queue not isolated from claude queue in bot entry point. The start decision and position display now use tool-specific queue counts instead of the total across all tools, so items in one tool's queue don't block or mislead the other.
17
+
3
18
  ## 1.48.1
4
19
 
5
20
  ### Patch Changes
6
21
 
7
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.
8
24
 
9
25
  ## 1.48.0
10
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.48.1",
3
+ "version": "1.48.3",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
+ }
@@ -1464,10 +1464,9 @@ export async function detectRepositoryVisibility(owner, repo) {
1464
1464
  return { isPublic: true, visibility: null };
1465
1465
  }
1466
1466
  }
1467
- // Re-export batch archived check from separate module
1468
- export const batchCheckArchivedRepositories = batchCheckArchived;
1469
- // Re-export log upload function from separate module
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 an isolated session is still running
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} sessionId - UUID of the session
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, verbose = false) {
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
- return result.exists && result.status === 'executing';
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 _querySessionStatus = null;
25
- async function getQuerySessionStatus() {
26
- if (!_querySessionStatus) {
27
- const mod = await import('./isolation-runner.lib.mjs');
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 _querySessionStatus;
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 using $ --status
53
- * @param {string} sessionId - UUID of the isolated session
54
- * @param {boolean} verbose - Whether to log verbose output
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, verbose = false) {
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 queryStatus = await getQuerySessionStatus();
60
- const result = await queryStatus(sessionId, verbose);
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 reliable tracking
173
- stillRunning = await checkIsolatedSessionRunning(sessionInfo.sessionId, verbose);
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 - this always runs regardless of --skip-tool-connection-check
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 (parseUrlComponents() removed due to hash fragment bug)
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;
@@ -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 placed after the dry-run check to significantly speed up
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 the default 90-second timeout for message handlers
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 to ignore messages sent before bot started
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
- // Use normalized URL from validation to ensure consistent duplicate detection
995
- // See: https://github.com/link-assistant/hive-mind/issues/1080
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' });
@@ -1025,12 +1028,13 @@ async function handleSolveCommand(ctx) {
1025
1028
  return;
1026
1029
  }
1027
1030
 
1028
- if (check.canStart && queueStats.queued === 0) {
1031
+ const toolQueuedCount = queueStats.queuedByTool[solveTool] || 0; // tool-specific queue count (#1551)
1032
+ if (check.canStart && toolQueuedCount === 0) {
1029
1033
  const startingMessage = await safeReply(ctx, `🚀 Starting solve command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1030
1034
  await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock, solvePerCommandIsolation);
1031
1035
  } else {
1032
1036
  const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: solvePerCommandIsolation });
1033
- let queueMessage = `📋 Solve command queued (position #${queueStats.queued + 1})\n\n${infoBlock}`;
1037
+ let queueMessage = `📋 Solve command queued (${solveTool} queue position #${toolQueuedCount + 1})\n\n${infoBlock}`; // tool-specific position (#1551)
1034
1038
  if (check.reason) queueMessage += `\n\n⏳ Waiting: ${escapeMarkdown(check.reason)}`;
1035
1039
  const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
1036
1040
  queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
@@ -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
- // Use rejectReason when threshold strategy is 'reject', otherwise use reason
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