@link-assistant/hive-mind 1.2.4 → 1.2.6

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,5 +1,75 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.2.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 94dfb13: Fix gh-upload-log argument parsing bug causing "File does not exist" error
8
+ - Fixed bug where `gh-upload-log` received all arguments as a single concatenated string
9
+ - The issue was caused by using `${commandArgs.join(' ')}` in command-stream template literal, which treats the entire joined string as one argument
10
+ - Now using separate `${}` interpolations for each argument to ensure proper argument parsing
11
+ - Also fixed: description flag is now properly passed to gh-upload-log (was only displayed, never sent)
12
+ - Added comprehensive regression tests and case study documentation
13
+
14
+ ## 1.2.5
15
+
16
+ ### Patch Changes
17
+
18
+ - 65ee214: fix: Detect malformed flag patterns like "-- model" (Issue #1092)
19
+
20
+ Added `detectMalformedFlags()` function that catches malformed command-line options and provides helpful error messages:
21
+ - Detects "-- option" (space after --) and suggests "--option"
22
+ - Detects "-option" (single dash for long option) and suggests "--option"
23
+ - Detects "---option" (triple dash) and suggests "--option"
24
+ - Integrated into both Telegram bot and CLI argument parsing
25
+ - Added 23 comprehensive unit tests
26
+
27
+ - af950c8: fix(hive): require closing keywords for PR detection
28
+
29
+ The `/hive` command was incorrectly skipping issues by reporting they had
30
+ PRs when those PRs only mentioned the issues without actually solving them.
31
+
32
+ **Root cause**: The `batchCheckPullRequestsForIssues` function used GitHub's
33
+ `CROSS_REFERENCED_EVENT` timeline items, which are created whenever a PR
34
+ body/title/commit mentions an issue number - regardless of whether the PR
35
+ actually solves the issue.
36
+
37
+ **Example**: PR #369 in VisageDvachevsky/StoryGraph is an audit PR that
38
+ created 28 new issues (#370-#397) and listed them in a table. This caused
39
+ GitHub to create cross-reference events linking that PR to all 28 issues,
40
+ but PR #369 only actually fixes #368.
41
+
42
+ **Solution**:
43
+ - Add `prClosesIssue()` function to detect GitHub closing keywords
44
+ (fixes, closes, resolves - case-insensitive)
45
+ - Update GraphQL query to include PR body text
46
+ - Only count PRs that contain "fixes #N", "closes #N", or "resolves #N"
47
+ for the specific issue number
48
+ - Add verbose logging when PRs are skipped for only mentioning issues
49
+
50
+ This aligns with GitHub's own auto-close behavior where only specific
51
+ keywords trigger issue closure when a PR is merged.
52
+
53
+ Fixes #1094
54
+
55
+ - 0d997ac: fix(telegram-bot): stop solve queue on SIGINT/SIGTERM for clean exit
56
+
57
+ The telegram bot was hanging after pressing Ctrl+C because the SolveQueue
58
+ consumer loop kept running with active timers that prevented the Node.js
59
+ event loop from emptying.
60
+ - **Root cause identified**: The SIGINT/SIGTERM handlers only called
61
+ `bot.stop()` (Telegraf) but did not stop the SolveQueue, whose `sleep()`
62
+ timers kept the event loop alive.
63
+ - **Solution**: Added `solveQueue.stop()` call in both SIGINT and SIGTERM
64
+ handlers to stop the consumer loop before calling `bot.stop()`.
65
+ - **Added verbose logging**: When running with `--verbose`, the bot now
66
+ logs "Solve queue stopped" during shutdown.
67
+ - **Case study documentation**: Added detailed analysis in
68
+ `docs/case-studies/issue-1083/` with timeline, root cause investigation,
69
+ and evidence collection.
70
+
71
+ Fixes #1083
72
+
3
73
  ## 1.2.4
4
74
 
5
75
  ### Patch Changes
@@ -0,0 +1,72 @@
1
+ # Issue #1092 Analysis: Silent Ignoring of "-- model" (with space)
2
+
3
+ ## The Problem
4
+
5
+ When a user types `-- model` (with a space between the dashes) instead of `--model`,
6
+ the system silently ignores it and uses the default model instead of producing an error.
7
+
8
+ ## Why This Happens
9
+
10
+ ### Argument Parsing in Shell
11
+
12
+ When a shell receives the command:
13
+
14
+ ```bash
15
+ solve https://github.com/owner/repo/issues/1 -- model sonnet
16
+ ```
17
+
18
+ It parses this as:
19
+
20
+ - `https://github.com/owner/repo/issues/1` (positional arg)
21
+ - `-- model` (single string, NOT a flag)
22
+ - `sonnet` (positional arg)
23
+
24
+ The `"-- model"` string has a space in it, so it's not recognized as a flag by yargs.
25
+
26
+ ### Yargs Behavior with .strict()
27
+
28
+ Yargs `.strict()` mode only rejects:
29
+
30
+ 1. Unknown OPTIONS (flags that start with `-` or `--`)
31
+ 2. Unknown ARGUMENTS that look like flags
32
+
33
+ It does NOT reject:
34
+
35
+ - Positional arguments that look similar to flag names
36
+ - Strings like `"-- model"` (which are treated as regular positional arguments)
37
+
38
+ So when yargs sees `"-- model"` as a positional argument, it:
39
+
40
+ 1. Doesn't match it to `--model` option
41
+ 2. Doesn't throw an error because it's not in strict "option" format
42
+ 3. Silently ignores it
43
+ 4. Uses the default model value
44
+
45
+ ## The Flow
46
+
47
+ 1. User types: `solve <url> -- model sonnet`
48
+ 2. Shell parses as: `['<url>', '-- model', 'sonnet']`
49
+ 3. Yargs receives: `{ _: ['<url>', '-- model', 'sonnet'] }`
50
+ 4. Yargs strict mode checks: Is this an unknown OPTION? No.
51
+ 5. Yargs silently accepts the positional arguments
52
+ 6. Model defaults to 'sonnet'
53
+ 7. User sees no error, but their `--model` flag was ignored
54
+
55
+ ## Solution Approaches
56
+
57
+ 1. **Detect spaced flag patterns**: Check if any positional arguments match the pattern
58
+ `-- <option-name>` where option-name is a known flag
59
+
60
+ 2. **Validate positional arguments**: After parsing, check if the remaining positional
61
+ arguments look like mistyped flags (start with -- or -)
62
+
63
+ 3. **Improve error message**: When detecting such patterns, suggest the correct format
64
+
65
+ ## Key Code References
66
+
67
+ - `/src/solve.config.lib.mjs`: Contains yargs configuration and argument parsing
68
+ - `/src/option-suggestions.lib.mjs`: Already has logic for suggesting similar options
69
+ - `/src/model-validation.lib.mjs`: Validates model names
70
+
71
+ The current `.strict()` mode in solve.config.lib.mjs (line 316) only catches malformed
72
+ OPTIONS, not these kinds of subtle positional argument errors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -11,6 +11,53 @@ if (typeof globalThis.use === 'undefined') {
11
11
  import { log, cleanErrorMessage } from './lib.mjs';
12
12
  import { githubLimits, timeouts } from './config.lib.mjs';
13
13
 
14
+ /**
15
+ * Check if a PR body/title indicates it fixes/closes/resolves a specific issue number
16
+ * GitHub auto-closes issues when PR body contains keywords like "fixes #123", "closes #123", "resolves #123"
17
+ * See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
18
+ * @param {string} text - PR body or title text
19
+ * @param {number} issueNumber - Issue number to check for
20
+ * @returns {boolean} True if the text contains a closing keyword for this issue
21
+ */
22
+ export function prClosesIssue(text, issueNumber) {
23
+ if (!text || typeof text !== 'string') {
24
+ return false;
25
+ }
26
+
27
+ // GitHub closing keywords (case-insensitive)
28
+ // Supports: fix, fixes, fixed, close, closes, closed, resolve, resolves, resolved
29
+ // Also supports variations with repository prefix like "fixes owner/repo#123"
30
+ const closingKeywords = ['fix', 'fixes', 'fixed', 'close', 'closes', 'closed', 'resolve', 'resolves', 'resolved'];
31
+
32
+ // Build regex pattern that matches any of the keywords followed by #N or repo#N
33
+ // Examples matched:
34
+ // - "fixes #123"
35
+ // - "Closes #123"
36
+ // - "RESOLVED #123"
37
+ // - "fixes owner/repo#123"
38
+ // - "fix: #123" (common commit style)
39
+ const issueNum = issueNumber.toString();
40
+
41
+ for (const keyword of closingKeywords) {
42
+ // Pattern: keyword + optional colon/space + optional repo prefix + # + issue number
43
+ // Must be followed by word boundary (not part of larger number)
44
+ const patterns = [
45
+ // Standard format: "fixes #123"
46
+ new RegExp(`\\b${keyword}\\s*:?\\s*#${issueNum}\\b`, 'i'),
47
+ // With repo prefix: "fixes owner/repo#123"
48
+ new RegExp(`\\b${keyword}\\s*:?\\s*[\\w.-]+/[\\w.-]+#${issueNum}\\b`, 'i'),
49
+ ];
50
+
51
+ for (const pattern of patterns) {
52
+ if (pattern.test(text)) {
53
+ return true;
54
+ }
55
+ }
56
+ }
57
+
58
+ return false;
59
+ }
60
+
14
61
  /**
15
62
  * Batch fetch pull request information for multiple issues using GraphQL
16
63
  * @param {string} owner - Repository owner
@@ -34,6 +81,8 @@ export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers)
34
81
  const batch = issueNumbers.slice(i, i + BATCH_SIZE);
35
82
 
36
83
  // Build GraphQL query for this batch
84
+ // Issue #1094: Include PR body to check for "fixes/closes/resolves #N" keywords
85
+ // This prevents false positives from PRs that only mention issues without solving them
37
86
  const query = `
38
87
  query GetPullRequestsForIssues {
39
88
  repository(owner: "${owner}", name: "${repo}") {
@@ -51,6 +100,7 @@ export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers)
51
100
  ... on PullRequest {
52
101
  number
53
102
  title
103
+ body
54
104
  state
55
105
  isDraft
56
106
  url
@@ -92,14 +142,26 @@ export async function batchCheckPullRequestsForIssues(owner, repo, issueNumbers)
92
142
  const linkedPRs = [];
93
143
 
94
144
  // Extract linked PRs from timeline items
145
+ // Issue #1094: Only count PRs that explicitly fix/close/resolve this issue
146
+ // This prevents false positives from PRs that only mention issues without solving them
95
147
  for (const item of issueData.timelineItems?.nodes || []) {
96
148
  if (item?.source && item.source.state === 'OPEN' && !item.source.isDraft) {
97
- linkedPRs.push({
98
- number: item.source.number,
99
- title: item.source.title,
100
- state: item.source.state,
101
- url: item.source.url,
102
- });
149
+ // Check if PR actually closes this issue (has "fixes #N", "closes #N", or "resolves #N")
150
+ const prBody = item.source.body || '';
151
+ const prTitle = item.source.title || '';
152
+ const closesThisIssue = prClosesIssue(prBody, issueNum) || prClosesIssue(prTitle, issueNum);
153
+
154
+ if (closesThisIssue) {
155
+ linkedPRs.push({
156
+ number: item.source.number,
157
+ title: item.source.title,
158
+ state: item.source.state,
159
+ url: item.source.url,
160
+ });
161
+ } else {
162
+ // Log that we're skipping a PR that only mentions the issue
163
+ await log(` ℹ️ PR #${item.source.number} mentions issue #${issueNum} but doesn't close it (no fixes/closes/resolves keyword)`, { verbose: true });
164
+ }
103
165
  }
104
166
  }
105
167
 
@@ -271,6 +333,7 @@ export async function batchCheckArchivedRepositories(repositories) {
271
333
 
272
334
  // Export all functions as default object too
273
335
  export default {
336
+ prClosesIssue,
274
337
  batchCheckPullRequestsForIssues,
275
338
  batchCheckArchivedRepositories,
276
339
  };
@@ -33,23 +33,30 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
33
33
  const result = { success: false, url: null, rawUrl: null, type: null, chunks: 1 };
34
34
 
35
35
  try {
36
- // Build command with appropriate flags
36
+ // Build command flags
37
+ // IMPORTANT: When using command-stream's $ template tag, each ${} interpolation is treated
38
+ // as a single argument. DO NOT use commandArgs.join(' ') as it will make all flags part
39
+ // of the first positional argument, causing "File does not exist" errors.
40
+ // See case study: docs/case-studies/issue-1096/README.md
37
41
  const publicFlag = isPublic ? '--public' : '--private';
38
- const descFlag = description ? `--description "${description}"` : '';
39
- const verboseFlag = verbose ? '--verbose' : '';
40
-
41
- const command = `gh-upload-log "${logFile}" ${publicFlag} ${descFlag} ${verboseFlag}`.trim().replace(/\s+/g, ' ');
42
42
 
43
43
  if (verbose) {
44
- await log(` 📤 Running: ${command}`, { verbose: true });
44
+ const descDisplay = description ? ` --description "${description}"` : '';
45
+ await log(` 📤 Running: gh-upload-log "${logFile}" ${publicFlag}${descDisplay} --verbose`, { verbose: true });
45
46
  }
46
47
 
47
- // Build command arguments array, filtering out empty strings to prevent "Unknown argument: ''" error
48
- const commandArgs = [`"${logFile}"`, publicFlag];
49
- if (verbose) {
50
- commandArgs.push('--verbose');
48
+ // Execute command with separate interpolations for each argument
49
+ // Each ${} is properly passed as a separate argument to the shell
50
+ let uploadResult;
51
+ if (description && verbose) {
52
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --description ${description} --verbose`;
53
+ } else if (description) {
54
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --description ${description}`;
55
+ } else if (verbose) {
56
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag} --verbose`;
57
+ } else {
58
+ uploadResult = await $`gh-upload-log ${logFile} ${publicFlag}`;
51
59
  }
52
- const uploadResult = await $`gh-upload-log ${commandArgs.join(' ')}`;
53
60
  const output = (uploadResult.stdout?.toString() || '') + (uploadResult.stderr?.toString() || '');
54
61
 
55
62
  if (uploadResult.code !== 0) {
@@ -146,6 +146,126 @@ export function formatSuggestions(suggestions) {
146
146
  return `\n\nDid you mean one of these?\n${formattedOptions.map(opt => ` • ${opt}`).join('\n')}`;
147
147
  }
148
148
 
149
+ /**
150
+ * Known valid option names that we use for detecting "-- optionname" typos.
151
+ * These are common options that users might accidentally type with a space after --.
152
+ */
153
+ const KNOWN_OPTION_NAMES = [
154
+ 'model',
155
+ 'verbose',
156
+ 'help',
157
+ 'version',
158
+ 'resume',
159
+ 'fork',
160
+ 'dry-run',
161
+ 'tool',
162
+ 'think',
163
+ 'watch',
164
+ 'sentry',
165
+ 'attach-logs',
166
+ 'auto-continue',
167
+ 'auto-fork',
168
+ 'auto-cleanup',
169
+ 'base-branch',
170
+ 'log-dir',
171
+ 'skip-tool-check',
172
+ 'skip-tool-connection-check',
173
+ 'auto-resume-on-limit-reset',
174
+ 'auto-resume-on-errors',
175
+ 'auto-close-pull-request-on-fail',
176
+ 'auto-pull-request-creation',
177
+ 'auto-commit-uncommitted-changes',
178
+ 'auto-restart-on-uncommitted-changes',
179
+ 'continue-only-on-feedback',
180
+ 'claude-file',
181
+ 'gitkeep-file',
182
+ 'interactive-mode',
183
+ 'prompt-plan-sub-agent',
184
+ 'prompt-explore-sub-agent',
185
+ 'prompt-general-purpose-sub-agent',
186
+ 'prompt-issue-reporting',
187
+ 'prompt-architecture-care',
188
+ 'prompt-case-studies',
189
+ 'prompt-playwright-mcp',
190
+ 'prompt-check-sibling-pull-requests',
191
+ 'enable-workspaces',
192
+ 'execute-tool-with-bun',
193
+ 'tokens-budget-stats',
194
+ 'min-disk-space',
195
+ 'watch-interval',
196
+ 'only-prepare-command',
197
+ 'auto-merge-default-branch-to-pull-request-branch',
198
+ 'allow-fork-divergence-resolution-using-force-push-with-lease',
199
+ 'allow-to-push-to-contributors-pull-requests-as-maintainer',
200
+ 'prefix-fork-name-with-owner-name',
201
+ 'auto-restart-max-iterations',
202
+ 'auto-continue-only-on-new-comments',
203
+ ];
204
+
205
+ /**
206
+ * Detect malformed flag patterns in command line arguments.
207
+ * These are arguments that look like they were intended to be flags
208
+ * but have incorrect formatting (e.g., "-- model" instead of "--model").
209
+ *
210
+ * Issue #1092: When user types "-- model" (with space), yargs silently ignores it
211
+ * as a positional argument instead of producing an error.
212
+ *
213
+ * @param {string[]} args - Array of command line arguments
214
+ * @returns {{ malformed: string[], errors: string[] }} - Detected malformed arguments and error messages
215
+ */
216
+ export function detectMalformedFlags(args) {
217
+ const malformed = [];
218
+ const errors = [];
219
+
220
+ // Patterns that suggest user intended to type a flag but made a mistake
221
+ const malformedPatterns = [
222
+ // "-- option" - space between dashes and option name (Issue #1092)
223
+ { regex: /^-- +\w/, description: 'Space after "--"', suggestion: arg => `--${arg.replace(/^-- +/, '')}` },
224
+ // "- -option" - space between dashes
225
+ { regex: /^- +-/, description: 'Space between dashes', suggestion: arg => arg.replace(/^- +/, '') },
226
+ // "-option" for what looks like a long option (more than 1 char after single dash)
227
+ // Only flag this for known-looking patterns to avoid false positives
228
+ {
229
+ regex: /^-[a-z][a-z]+-?[a-z]*$/i,
230
+ description: 'Single dash for long option',
231
+ suggestion: arg => `-${arg}`,
232
+ },
233
+ // "---option" - triple dash or more
234
+ { regex: /^---+\w/, description: 'Too many dashes', suggestion: arg => arg.replace(/^-+/, '--') },
235
+ ];
236
+
237
+ for (const arg of args) {
238
+ for (const pattern of malformedPatterns) {
239
+ if (pattern.regex.test(arg)) {
240
+ malformed.push(arg);
241
+ const suggestion = pattern.suggestion(arg);
242
+ errors.push(`Malformed option "${arg}": ${pattern.description}. Did you mean "${suggestion}"?`);
243
+ break; // Don't double-report the same argument
244
+ }
245
+ }
246
+ }
247
+
248
+ // Issue #1092: Detect "-- optionname" pattern where the space caused
249
+ // the argument to be split into ['--', 'optionname'] by the tokenizer.
250
+ // Look for standalone '--' followed by a known option name.
251
+ for (let i = 0; i < args.length - 1; i++) {
252
+ if (args[i] === '--') {
253
+ const nextArg = args[i + 1];
254
+ // Check if next argument looks like an option name (not a URL or path)
255
+ // and is a known option name
256
+ if (nextArg && !nextArg.startsWith('-') && !nextArg.includes('/') && !nextArg.includes(':')) {
257
+ const lowerNextArg = nextArg.toLowerCase();
258
+ if (KNOWN_OPTION_NAMES.includes(lowerNextArg)) {
259
+ malformed.push(`-- ${nextArg}`);
260
+ errors.push(`Malformed option "-- ${nextArg}": Space after "--". Did you mean "--${nextArg}"?`);
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ return { malformed, errors };
267
+ }
268
+
149
269
  /**
150
270
  * Create an enhanced error message with suggestions for unknown arguments
151
271
  *
@@ -7,7 +7,10 @@
7
7
  // Note: Strict options validation is now handled by yargs built-in .strict() mode (see below)
8
8
  // This approach was adopted per issue #482 feedback to minimize custom code maintenance
9
9
 
10
- import { enhanceErrorMessage } from './option-suggestions.lib.mjs';
10
+ import { enhanceErrorMessage, detectMalformedFlags } from './option-suggestions.lib.mjs';
11
+
12
+ // Re-export for use by telegram-bot.mjs (avoids extra import lines there)
13
+ export { detectMalformedFlags };
11
14
 
12
15
  // Export an initialization function that accepts 'use'
13
16
  export const initializeConfig = async use => {
@@ -322,6 +325,16 @@ export const createYargsConfig = yargsInstance => {
322
325
  // Parse command line arguments - now needs yargs and hideBin passed in
323
326
  export const parseArguments = async (yargs, hideBin) => {
324
327
  const rawArgs = hideBin(process.argv);
328
+
329
+ // Issue #1092: Detect malformed flag patterns BEFORE yargs parsing
330
+ // This catches cases like "-- model" which yargs silently treats as positional arguments
331
+ const malformedResult = detectMalformedFlags(rawArgs);
332
+ if (malformedResult.malformed.length > 0) {
333
+ const error = new Error(malformedResult.errors.join('\n'));
334
+ error.name = 'MalformedArgumentError';
335
+ throw error;
336
+ }
337
+
325
338
  // Use .parse() instead of .argv to ensure .strict() mode works correctly
326
339
  // When you call yargs(args) and use .argv, strict mode doesn't trigger
327
340
  // See: https://github.com/yargs/yargs/issues - .strict() only works with .parse()
@@ -32,18 +32,12 @@ const yargs = yargsModule.default || yargsModule;
32
32
  const { hideBin } = await use('yargs@17.7.2/helpers');
33
33
 
34
34
  // Import solve and hive yargs configurations for validation
35
- const solveConfigLib = await import('./solve.config.lib.mjs');
36
- const { createYargsConfig: createSolveYargsConfig } = solveConfigLib;
37
-
38
- const hiveConfigLib = await import('./hive.config.lib.mjs');
39
- const { createYargsConfig: createHiveYargsConfig } = hiveConfigLib;
40
-
35
+ const { createYargsConfig: createSolveYargsConfig, detectMalformedFlags } = await import('./solve.config.lib.mjs');
36
+ const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config.lib.mjs');
41
37
  // Import GitHub URL parser for extracting URLs from messages
42
38
  const { parseGitHubUrl } = await import('./github.lib.mjs');
43
-
44
39
  // Import model validation for early validation with helpful error messages
45
40
  const { validateModelName } = await import('./model-validation.lib.mjs');
46
-
47
41
  // Import libraries for /limits, /version, and markdown escaping
48
42
  const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
49
43
  const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
@@ -1066,7 +1060,12 @@ bot.command(/^solve$/i, async ctx => {
1066
1060
  await ctx.reply(`❌ ${modelError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1067
1061
  return;
1068
1062
  }
1069
-
1063
+ // Issue #1092: Detect malformed flag patterns like "-- model" (space after --)
1064
+ const { malformed, errors: malformedErrors } = detectMalformedFlags(args);
1065
+ if (malformed.length > 0) {
1066
+ await ctx.reply(`❌ ${malformedErrors.join('\n')}\n\nPlease check your option syntax.`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1067
+ return;
1068
+ }
1070
1069
  // Validate merged arguments using solve's yargs config
1071
1070
  try {
1072
1071
  // Use .parse() instead of yargs(args).parseSync() to ensure .strict() mode works
@@ -1465,36 +1464,31 @@ bot.telegram
1465
1464
  process.exit(1);
1466
1465
  });
1467
1466
 
1467
+ // Helper to stop solve queue gracefully on shutdown
1468
+ // See: https://github.com/link-assistant/hive-mind/issues/1083
1469
+ const stopSolveQueue = () => {
1470
+ try {
1471
+ getSolveQueue({ verbose: VERBOSE }).stop();
1472
+ if (VERBOSE) console.log('[VERBOSE] Solve queue stopped');
1473
+ } catch (err) {
1474
+ if (VERBOSE) console.log('[VERBOSE] Could not stop solve queue:', err.message);
1475
+ }
1476
+ };
1477
+
1468
1478
  process.once('SIGINT', () => {
1469
1479
  isShuttingDown = true;
1470
1480
  console.log('\n🛑 Received SIGINT (Ctrl+C), stopping bot...');
1471
- if (VERBOSE) {
1472
- console.log('[VERBOSE] Signal: SIGINT');
1473
- console.log('[VERBOSE] Process ID:', process.pid);
1474
- console.log('[VERBOSE] Parent Process ID:', process.ppid);
1475
- }
1481
+ if (VERBOSE) console.log(`[VERBOSE] Signal: SIGINT, PID: ${process.pid}, PPID: ${process.ppid}`);
1482
+ stopSolveQueue();
1476
1483
  bot.stop('SIGINT');
1477
1484
  });
1478
1485
 
1479
1486
  process.once('SIGTERM', () => {
1480
1487
  isShuttingDown = true;
1481
1488
  console.log('\n🛑 Received SIGTERM, stopping bot...');
1482
- if (VERBOSE) {
1483
- console.log('[VERBOSE] Signal: SIGTERM');
1484
- console.log('[VERBOSE] Process ID:', process.pid);
1485
- console.log('[VERBOSE] Parent Process ID:', process.ppid);
1486
- console.log('[VERBOSE] Possible causes:');
1487
- console.log('[VERBOSE] - System shutdown/restart');
1488
- console.log('[VERBOSE] - Process manager (systemd, pm2, etc.) stopping the service');
1489
- console.log('[VERBOSE] - Manual kill command: kill <pid>');
1490
- console.log('[VERBOSE] - Container orchestration (Docker, Kubernetes) stopping container');
1491
- console.log('[VERBOSE] - Out of memory (OOM) killer');
1492
- }
1493
- console.log('ℹ️ SIGTERM is typically sent by:');
1494
- console.log(' - System shutdown/restart');
1495
- console.log(' - Process manager stopping the service');
1496
- console.log(' - Manual termination (kill command)');
1497
- console.log(' - Container/orchestration platform');
1498
- console.log('💡 Check system logs for more details: journalctl -u <service> or dmesg');
1489
+ if (VERBOSE) console.log(`[VERBOSE] Signal: SIGTERM, PID: ${process.pid}, PPID: ${process.ppid}`);
1490
+ console.log('ℹ️ SIGTERM is typically sent by: system shutdown, process manager, kill command, or container orchestration');
1491
+ console.log('💡 Check system logs for details: journalctl -u <service> or dmesg');
1492
+ stopSolveQueue();
1499
1493
  bot.stop('SIGTERM');
1500
1494
  });