@link-assistant/hive-mind 1.73.6 → 1.73.8

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,60 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.73.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 324ed89: fix(solve): surface the core tool error instead of bare `CLAUDE execution failed` (#1845)
8
+
9
+ When an AI tool run failed, both the terminal and the posted GitHub
10
+ `🚨 Solution Draft Failed` comment showed only the generic
11
+ `CLAUDE execution failed`, even though the underlying tool had reported a
12
+ specific cause (for example `API Error: Output blocked by content filtering
13
+ policy`). The real message was captured inside the tool runner but dropped at
14
+ the failure-return boundary, so no downstream consumer could display it.
15
+
16
+ Every AI tool runner now surfaces a structured `errorInfo` (with a `.message`)
17
+ on its failure returns (`claude`, `gemini`, `opencode`, `qwen`; `codex` and
18
+ `agent` already did). Two shared helpers in `lib.mjs` — `extractToolErrorCore`
19
+ (the core error string) and `formatToolExecutionFailure` (the full
20
+ `CLAUDE execution failed with API Error: Output blocked by content filtering
21
+ policy` message) — share one precedence so every surface stays consistent.
22
+ All failure sites now use them: `solve.mjs` (terminal exit, GitHub failure
23
+ comment, critical-error auto-commit reason), `solve.auto-merge.lib.mjs` and
24
+ `solve.watch.lib.mjs` (GitHub message + new terminal `Error details:` lines),
25
+ and `review.mjs`. The helpers collapse whitespace, cap the core error length,
26
+ and never fall back to the agent's success summary.
27
+
28
+ `isApiError` in `solve.restart-shared.lib.mjs` now classifies through the same
29
+ extractor, so a Claude `API Error:` reported via `errorInfo` (never `result`)
30
+ is detected and watch mode's `MAX_API_ERROR_RETRIES` backoff guard keeps
31
+ working instead of retrying forever.
32
+
33
+ The auto-commit-on-critical-error path (#1834) is confirmed to run on the
34
+ failure exit and is now labeled with the real failure cause; the same guarded
35
+ auto-commit is also added to `handleFailure()` so the `uncaughtException`,
36
+ `unhandledRejection`, and top-level-catch exits preserve uncommitted work too.
37
+ Adds unit, cross-tool, auto-commit, and `isApiError` tests plus a deep case
38
+ study in `docs/case-studies/issue-1845`.
39
+
40
+ ## 1.73.7
41
+
42
+ ### Patch Changes
43
+
44
+ - 6188172: feat(telegram): list executed issues/PRs as clickable links in /solve_queue, add /queue alias (#1837)
45
+
46
+ The `/solve_queue` detailed status previously showed only per-tool counts and a
47
+ final `Completed: N, Failed: M` line, so a stuck or running task could not be
48
+ opened from the message. It now lists each processing (`▶️`), pending (`•`),
49
+ recently completed (`✅`), and failed (`❌`, with the error reason) item as a
50
+ clickable `[owner/repo#number](url)` link, capped per section
51
+ (`HIVE_MIND_MAX_DISPLAY_ITEMS_PER_QUEUE`, default 5) with a localized
52
+ `... and N more` line to stay under Telegram's 4096-character limit.
53
+
54
+ Also adds `/queue` as a shorter alias for `/solve_queue` (both the entity-based
55
+ command regex and the text-based fallback handler), and documents the work in
56
+ `docs/case-studies/issue-1837`.
57
+
3
58
  ## 1.73.6
4
59
 
5
60
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.73.6",
3
+ "version": "1.73.8",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -1191,6 +1191,8 @@ export const executeClaudeCommand = async params => {
1191
1191
  is503Error,
1192
1192
  anthropicTotalCostUSD,
1193
1193
  resultSummary,
1194
+ // Issue #1845: surface the actual error so callers can show it to users
1195
+ errorInfo: { message: lastMessage || 'API explicitly marked error as not retryable', exitCode },
1194
1196
  queuedFeedback, // Issue #817: Bidirectional mode feedback
1195
1197
  };
1196
1198
  }
@@ -1235,6 +1237,8 @@ export const executeClaudeCommand = async params => {
1235
1237
  is503Error, // preserve for callers that check this
1236
1238
  anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1237
1239
  resultSummary, // Issue #1263: Include result summary
1240
+ // Issue #1845: surface the actual error so callers can show it to users
1241
+ errorInfo: { message: lastMessage || `Transient API error persisted after ${maxRetries} retries`, exitCode },
1238
1242
  queuedFeedback, // Issue #817: Bidirectional mode feedback
1239
1243
  };
1240
1244
  }
@@ -1294,6 +1298,9 @@ export const executeClaudeCommand = async params => {
1294
1298
  errorDuringExecution,
1295
1299
  anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1296
1300
  resultSummary, // Issue #1263: Include result summary
1301
+ // Issue #1845: surface the core error (e.g. "API Error: Output blocked by content
1302
+ // filtering policy") so users see what actually went wrong, not just a generic message.
1303
+ errorInfo: { message: lastMessage || `Claude command failed with exit code ${exitCode}`, exitCode },
1297
1304
  queuedFeedback, // Issue #817: Bidirectional mode feedback
1298
1305
  };
1299
1306
  }
@@ -1374,6 +1381,8 @@ export const executeClaudeCommand = async params => {
1374
1381
  toolUseCount,
1375
1382
  anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1376
1383
  resultSummary, // Issue #1263: Include result summary
1384
+ // Issue #1845: surface the actual exception message so callers can show it to users
1385
+ errorInfo: { message: error.message || error.toString() },
1377
1386
  queuedFeedback, // Issue #817: Bidirectional mode feedback
1378
1387
  };
1379
1388
  }
@@ -575,6 +575,8 @@ export const executeGeminiCommand = async params => {
575
575
  pricingInfo: { modelId: mappedModel, modelName: mappedModel, provider: 'Google', totalCostUSD: null },
576
576
  publicPricingEstimate: null,
577
577
  resultSummary: geminiJsonState.resultSummary || null,
578
+ // Issue #1845: surface the actual error so callers can show it to users
579
+ errorInfo: { message: errorText || `Gemini command failed with exit code ${exitCode}`, exitCode },
578
580
  };
579
581
  }
580
582
 
@@ -616,6 +618,8 @@ export const executeGeminiCommand = async params => {
616
618
  pricingInfo: null,
617
619
  publicPricingEstimate: null,
618
620
  resultSummary: null,
621
+ // Issue #1845: surface the actual exception message so callers can show it to users
622
+ errorInfo: { message: error.message || error.toString() },
619
623
  };
620
624
  }
621
625
  };
package/src/lib.mjs CHANGED
@@ -664,6 +664,67 @@ export const cleanErrorMessage = error => {
664
664
  return message;
665
665
  };
666
666
 
667
+ /**
668
+ * Extract the core/root error string from a tool runner result (Issue #1845).
669
+ *
670
+ * Applies a single precedence everywhere so every failure surface shows the
671
+ * same root cause: `errorInfo.message` → `errorInfo.errorMatch` → string
672
+ * `errorInfo` → `result`. Returns a collapsed single line, or null when no
673
+ * usable error string is available. Shared by `formatToolExecutionFailure`
674
+ * (GitHub comments / exit message) and the terminal "Error details:" lines in
675
+ * watch / auto-merge so they never diverge.
676
+ *
677
+ * @param {Object} options
678
+ * @param {Object} [options.toolResult] - Result object returned by the tool runner
679
+ * @returns {string|null} The core error string, or null when none is available
680
+ */
681
+ export const extractToolErrorCore = ({ toolResult } = {}) => {
682
+ // Prefer the structured error message surfaced by the tool runner. We do NOT
683
+ // fall back to resultSummary here, because that holds the agent's normal
684
+ // work summary on success and would be misleading when used as an error.
685
+ const errorInfo = toolResult?.errorInfo;
686
+ const rawCore = errorInfo?.message || errorInfo?.errorMatch || (typeof errorInfo === 'string' ? errorInfo : null) || toolResult?.result || null;
687
+
688
+ if (!rawCore || typeof rawCore !== 'string') return null;
689
+
690
+ // Collapse to a single clean line and strip noise.
691
+ const core = rawCore.replace(/\s+/g, ' ').trim();
692
+ return core || null;
693
+ };
694
+
695
+ /**
696
+ * Build a user-facing tool execution failure message that includes the core
697
+ * error reported by the underlying tool (Issue #1845).
698
+ *
699
+ * Previously users only saw the generic "<TOOL> execution failed" and had to
700
+ * dig through the full failure log to discover what actually went wrong (for
701
+ * example "API Error: Output blocked by content filtering policy"). When the
702
+ * tool runner surfaces a specific error this appends it so the failure is
703
+ * self-explanatory:
704
+ *
705
+ * "CLAUDE execution failed with API Error: Output blocked by content filtering policy"
706
+ *
707
+ * Falls back to the generic phrase when no specific error is available.
708
+ *
709
+ * @param {Object} options
710
+ * @param {string} [options.tool] - Tool name (e.g. 'claude'); defaults to 'claude'
711
+ * @param {Object} [options.toolResult] - Result object returned by the tool runner
712
+ * @param {number} [options.maxLength=300] - Max length of the appended core error
713
+ * @returns {string} The formatted failure message
714
+ */
715
+ export const formatToolExecutionFailure = ({ tool, toolResult, maxLength = 300 } = {}) => {
716
+ const base = `${(tool || 'claude').toUpperCase()} execution failed`;
717
+
718
+ let core = extractToolErrorCore({ toolResult });
719
+ if (!core) return base;
720
+
721
+ // Avoid duplicating the base phrase if the core error already contains it.
722
+ if (core.toLowerCase().includes('execution failed')) return base;
723
+
724
+ if (core.length > maxLength) core = `${core.slice(0, maxLength - 1)}…`;
725
+ return `${base} with ${core}`;
726
+ };
727
+
667
728
  /**
668
729
  * Format aligned console output
669
730
  * @param {string} icon - Icon to display
@@ -515,7 +515,7 @@ en
515
515
  detail "Tool aliases imply `--tool <tool>`: `/codex <github-url>` equals `/solve <github-url> --tool codex`"
516
516
  reply "Or reply to a message with a GitHub link: `/solve`"
517
517
  disabled "*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ Disabled"
518
- queue "`/solve_queue` - Show solve queue status"
518
+ queue "`/solve_queue` (alias: `/queue`) - Show solve queue status"
519
519
  locked
520
520
  options "🔒 Locked options: `{{options}}`"
521
521
  task
@@ -515,7 +515,7 @@ hi
515
515
  detail "Tool aliases `--tool <tool>` लगाते हैं: `/codex <github-url>` का अर्थ `/solve <github-url> --tool codex` है"
516
516
  reply "या GitHub लिंक वाले संदेश का उत्तर दें: `/solve`"
517
517
  disabled "*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ अक्षम"
518
- queue "`/solve_queue` - solve queue status दिखाएँ"
518
+ queue "`/solve_queue` (उपनाम: `/queue`) - solve queue status दिखाएँ"
519
519
  locked
520
520
  options "🔒 लॉक किए गए विकल्प: `{{options}}`"
521
521
  task
@@ -515,7 +515,7 @@ ru
515
515
  detail "Алиасы инструментов добавляют `--tool <tool>`: `/codex <github-url>` равно `/solve <github-url> --tool codex`"
516
516
  reply "Или ответьте на сообщение со ссылкой GitHub: `/solve`"
517
517
  disabled "*/solve* (алиасы: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ Отключено"
518
- queue "`/solve_queue` - Показать состояние очереди solve"
518
+ queue "`/solve_queue` (псевдоним: `/queue`) - Показать состояние очереди solve"
519
519
  locked
520
520
  options "🔒 Заблокированные опции: `{{options}}`"
521
521
  task
@@ -515,7 +515,7 @@ zh
515
515
  detail "工具别名会添加 `--tool <tool>`:`/codex <github-url>` 等同于 `/solve <github-url> --tool codex`"
516
516
  reply "也可以回复包含 GitHub 链接的消息:`/solve`"
517
517
  disabled "*/solve*(别名:*/do*、*/continue*、*/claude*、*/codex*、*/opencode*、*/agent*、*/gemini*、*/qwen*)- ❌ 已禁用"
518
- queue "`/solve_queue` - 显示 solve 队列状态"
518
+ queue "`/solve_queue`(别名:`/queue`)- 显示 solve 队列状态"
519
519
  locked
520
520
  options "🔒 锁定选项:`{{options}}`"
521
521
  task
@@ -466,6 +466,8 @@ export const executeOpenCodeCommand = async params => {
466
466
  permissionPromptDetected: true,
467
467
  ...pricingResult,
468
468
  resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
469
+ // Issue #1845: surface the actual error so callers can show it to users
470
+ errorInfo: { message: 'OpenCode requested an interactive permission prompt (non-interactive run cannot continue)' },
469
471
  };
470
472
  }
471
473
 
@@ -528,6 +530,8 @@ export const executeOpenCodeCommand = async params => {
528
530
  limitResetTime,
529
531
  ...pricingResult,
530
532
  resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
533
+ // Issue #1845: surface the actual error so callers can show it to users
534
+ errorInfo: { message: lastMessage || allOutput || `OpenCode command failed with exit code ${exitCode}`, exitCode },
531
535
  };
532
536
  }
533
537
 
@@ -593,6 +597,8 @@ export const executeOpenCodeCommand = async params => {
593
597
  pricingInfo: null,
594
598
  publicPricingEstimate: null,
595
599
  resultSummary: null, // Issue #1263: No result summary available on error
600
+ // Issue #1845: surface the actual exception message so callers can show it to users
601
+ errorInfo: { message: error.message || error.toString() },
596
602
  };
597
603
  }
598
604
  };
@@ -280,6 +280,13 @@ export const QUEUE_CONFIG = {
280
280
 
281
281
  // Process detection
282
282
  CLAUDE_PROCESS_NAMES: ['claude'], // Process names to detect
283
+
284
+ // Display
285
+ // Maximum number of items shown per section (pending/processing/completed/failed)
286
+ // in the /solve_queue (/queue) detailed status before collapsing into a
287
+ // "... and N more" line. Keeps the Telegram message under the 4096-char cap.
288
+ // See: https://github.com/link-assistant/hive-mind/issues/1837
289
+ MAX_DISPLAY_ITEMS_PER_QUEUE: parseIntWithDefault('HIVE_MIND_MAX_DISPLAY_ITEMS_PER_QUEUE', 5),
283
290
  };
284
291
 
285
292
  /**
package/src/qwen.lib.mjs CHANGED
@@ -632,6 +632,8 @@ export const executeQwenCommand = async params => {
632
632
  limitResetTime: null,
633
633
  ...usageResult,
634
634
  resultSummary,
635
+ // Issue #1845: surface the actual error so callers can show it to users
636
+ errorInfo: { message: combinedErrorText || errorMessage || `Qwen Code command failed${exitCode !== 0 ? ` with exit code ${exitCode}` : ''}`, exitCode },
635
637
  };
636
638
  }
637
639
 
@@ -665,6 +667,8 @@ export const executeQwenCommand = async params => {
665
667
  publicPricingEstimate: null,
666
668
  tokenUsage: null,
667
669
  resultSummary: null,
670
+ // Issue #1845: surface the actual exception message so callers can show it to users
671
+ errorInfo: { message: error.message || error.toString() },
668
672
  };
669
673
  }
670
674
  };
package/src/review.mjs CHANGED
@@ -43,7 +43,7 @@ const path = (await use('path')).default;
43
43
  const fs = (await use('fs')).promises;
44
44
 
45
45
  // Import shared functions from lib.mjs to follow DRY principle
46
- import { log, setLogFile, getLogFile, formatAligned } from './lib.mjs';
46
+ import { log, setLogFile, getLogFile, formatAligned, extractToolErrorCore } from './lib.mjs';
47
47
  import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
48
48
  import { reportError } from './sentry.lib.mjs';
49
49
  import * as memoryCheck from './memory-check.mjs';
@@ -398,7 +398,9 @@ Review this pull request thoroughly.`;
398
398
 
399
399
  // Handle command failure
400
400
  if (!commandSuccess) {
401
- await log('\n❌ Command execution failed. Check the log file for details.');
401
+ // Issue #1845: surface the core error (e.g. "API Error: ...") instead of just a generic message.
402
+ const reviewErrorCore = extractToolErrorCore({ toolResult: result });
403
+ await log(`\n❌ Command execution failed${reviewErrorCore ? ` with ${reviewErrorCore}` : '. Check the log file for details.'}`);
402
404
  await log(`📁 Log file: ${getLogFile()}`);
403
405
  process.exit(1);
404
406
  }
@@ -22,7 +22,7 @@ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
22
22
  const $ = wrapDollarWithGhRetry(__rawDollar$);
23
23
  // Import shared library functions
24
24
  const lib = await import('./lib.mjs');
25
- const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
25
+ const { log, cleanErrorMessage, formatAligned, formatToolExecutionFailure, extractToolErrorCore, getLogFile } = lib;
26
26
 
27
27
  // Note: We don't use detectAndCountFeedback from solve.feedback.lib.mjs
28
28
  // because we have our own non-bot comment detection logic that's more
@@ -805,6 +805,8 @@ No further AI sessions will be started automatically for this run. Please review
805
805
  // Resume failed for a non-limit reason — stop the loop
806
806
  await log('');
807
807
  await log(formatAligned('❌', `${argv.tool.toUpperCase()} RESUME FAILED`, ''));
808
+ // Issue #1845: surface the core error in the terminal, not just in the GitHub log.
809
+ await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult: resumeResult }) || 'Unknown error', 2));
808
810
  await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed after limit reset', 2));
809
811
  // Issue #1439: Attach failure log before stopping, so user can see what happened
810
812
  const shouldAttachLogsOnResumeFail = argv.attachLogs || argv['attach-logs'];
@@ -822,7 +824,7 @@ No further AI sessions will be started automatically for this run. Please review
822
824
  log,
823
825
  sanitizeLogContent,
824
826
  verbose: argv.verbose,
825
- errorMessage: `${argv.tool.toUpperCase()} execution failed after limit reset`,
827
+ errorMessage: `${formatToolExecutionFailure({ tool: argv.tool, toolResult: resumeResult })} after limit reset`,
826
828
  sessionId: latestSessionId,
827
829
  tempDir,
828
830
  requestedModel: argv.originalModel || argv.model,
@@ -855,6 +857,8 @@ No further AI sessions will be started automatically for this run. Please review
855
857
  // Per reviewer feedback: non-limit failures should fail and stop attempts
856
858
  await log('');
857
859
  await log(formatAligned('❌', `${argv.tool.toUpperCase()} EXECUTION FAILED`, ''));
860
+ // Issue #1845: surface the core error in the terminal, not just in the GitHub log.
861
+ await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult }) || 'Unknown error', 2));
858
862
  await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed', 2));
859
863
  // Issue #1439: Attach failure log before stopping, so user can see what happened
860
864
  const shouldAttachLogsOnFail = argv.attachLogs || argv['attach-logs'];
@@ -872,7 +876,7 @@ No further AI sessions will be started automatically for this run. Please review
872
876
  log,
873
877
  sanitizeLogContent,
874
878
  verbose: argv.verbose,
875
- errorMessage: `${argv.tool.toUpperCase()} execution failed`,
879
+ errorMessage: formatToolExecutionFailure({ tool: argv.tool, toolResult }),
876
880
  sessionId: latestSessionId,
877
881
  tempDir,
878
882
  requestedModel: argv.originalModel || argv.model,
@@ -18,9 +18,30 @@ export const isErrorIssueAutoCreationDisabled = argv => !!(argv?.disableReportIs
18
18
  * Handles log attachment and PR closing on failure
19
19
  */
20
20
  export const handleFailure = async options => {
21
- const { error, errorType, shouldAttachLogs, argv, global, owner, repo, log, getLogFile, attachLogToGitHub, cleanErrorMessage, sanitizeLogContent, $ } = options;
21
+ const { error, errorType, shouldAttachLogs, argv, global, owner, repo, log, getLogFile, attachLogToGitHub, cleanErrorMessage, sanitizeLogContent, cleanupContext, $ } = options;
22
22
  const disableIssueCreation = isErrorIssueAutoCreationDisabled(argv);
23
23
 
24
+ // Issue #1845 / #1834: "On all failures we automatically commit uncommitted changes by default."
25
+ // Exceptions, unhandled rejections and main-execution errors exit here WITHOUT passing through the
26
+ // tool-failure auto-commit chokepoint in solve.mjs, so preserve (commit + push) any work the agent
27
+ // left on disk first. Gated by config (default on; HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false).
28
+ // Best-effort: never let a commit failure mask the original error.
29
+ try {
30
+ const { criticalErrorRecovery } = await import('./config.lib.mjs');
31
+ if (criticalErrorRecovery.autoCommitUncommittedChanges && cleanupContext?.tempDir) {
32
+ const { commitUncommittedChangesOnCriticalError } = await import('./critical-error-commit.lib.mjs');
33
+ await commitUncommittedChangesOnCriticalError({
34
+ tempDir: cleanupContext.tempDir,
35
+ branchName: cleanupContext.branchName,
36
+ $,
37
+ log,
38
+ reason: `${errorType || 'execution'} error`,
39
+ });
40
+ }
41
+ } catch (preserveError) {
42
+ await log(` ⚠️ Could not auto-commit changes before failure exit: ${preserveError.message}`, { verbose: true });
43
+ }
44
+
24
45
  // Offer to create GitHub issue for the error
25
46
  try {
26
47
  await handleErrorWithIssueCreation({
@@ -117,7 +138,7 @@ export const handleFailure = async options => {
117
138
  * Creates an uncaught exception handler
118
139
  */
119
140
  export const createUncaughtExceptionHandler = options => {
120
- const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, $ } = options;
141
+ const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, cleanupContext, $ } = options;
121
142
 
122
143
  return async error => {
123
144
  await log(`\n❌ Uncaught Exception: ${cleanErrorMessage(error)}`, { level: 'error' });
@@ -136,6 +157,7 @@ export const createUncaughtExceptionHandler = options => {
136
157
  attachLogToGitHub,
137
158
  cleanErrorMessage,
138
159
  sanitizeLogContent,
160
+ cleanupContext,
139
161
  $,
140
162
  });
141
163
 
@@ -147,7 +169,7 @@ export const createUncaughtExceptionHandler = options => {
147
169
  * Creates an unhandled rejection handler
148
170
  */
149
171
  export const createUnhandledRejectionHandler = options => {
150
- const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, $ } = options;
172
+ const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, cleanupContext, $ } = options;
151
173
 
152
174
  return async reason => {
153
175
  await log(`\n❌ Unhandled Rejection: ${cleanErrorMessage(reason)}`, { level: 'error' });
@@ -166,6 +188,7 @@ export const createUnhandledRejectionHandler = options => {
166
188
  attachLogToGitHub,
167
189
  cleanErrorMessage,
168
190
  sanitizeLogContent,
191
+ cleanupContext,
169
192
  $,
170
193
  });
171
194
 
@@ -219,7 +242,7 @@ export const handleNoPrAvailableError = async ({ isContinueMode, tempDir, issueN
219
242
  * Handles execution errors in the main catch block
220
243
  */
221
244
  export const handleMainExecutionError = async options => {
222
- const { error, log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, $ } = options;
245
+ const { error, log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, cleanupContext, $ } = options;
223
246
 
224
247
  // Special handling for authentication errors
225
248
  if (error.isAuthError) {
@@ -256,6 +279,7 @@ export const handleMainExecutionError = async options => {
256
279
  attachLogToGitHub,
257
280
  cleanErrorMessage,
258
281
  sanitizeLogContent,
282
+ cleanupContext,
259
283
  $,
260
284
  });
261
285
 
package/src/solve.mjs CHANGED
@@ -21,7 +21,7 @@ const fs = (await use('fs')).promises;
21
21
  const crypto = (await use('crypto')).default;
22
22
  const memoryCheck = await import('./memory-check.mjs');
23
23
  const lib = await import('./lib.mjs');
24
- const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
24
+ const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, formatToolExecutionFailure, getVersionInfo, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
25
25
  const githubLib = await import('./github.lib.mjs');
26
26
  const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
27
27
  const validation = await import('./solve.validation.lib.mjs');
@@ -181,9 +181,8 @@ const { isIssueUrl, isPrUrl, normalizedUrl, owner, repo, number: urlNumber } = u
181
181
  issueUrl = normalizedUrl || issueUrl;
182
182
  global.owner = owner;
183
183
  global.repo = repo;
184
- // Issue #1752: failures before PR creation can happen during checks that run
185
- // before the normal issue-mode setup below. Record the source issue as soon as
186
- // the URL is validated so the pre-exit notifier can still comment on it.
184
+ // Issue #1752: record the source issue as soon as the URL is validated so the pre-exit
185
+ // notifier can still comment on it if a check fails before normal issue-mode setup below.
187
186
  if (isIssueUrl) {
188
187
  global.issueNumber = urlNumber;
189
188
  }
@@ -193,8 +192,7 @@ if (argv.autoLanguage) {
193
192
  const { applyAutoLanguageToArgv } = await import('./auto-language.lib.mjs');
194
193
  await applyAutoLanguageToArgv({ argv, githubLib, owner, repo, number: urlNumber, isIssueUrl, isPrUrl, log });
195
194
  }
196
- // Initialize i18n based on --language / --ui-language / --work-language
197
- // (or detected system locale). --auto-language may set only the work track.
195
+ // Initialize i18n from --language / --ui-language / --work-language (or system locale).
198
196
  const { initI18n } = await import('./i18n.lib.mjs');
199
197
  await initI18n({
200
198
  language: argv.language,
@@ -209,6 +207,7 @@ const errorHandlerOptions = {
209
207
  shouldAttachLogs,
210
208
  argv,
211
209
  global,
210
+ cleanupContext, // #1845: mutated in place; lets exception handlers auto-commit uncommitted work
212
211
  owner: null, // Will be set later when parsed
213
212
  repo: null, // Will be set later when parsed
214
213
  getLogFile,
@@ -1072,6 +1071,8 @@ try {
1072
1071
  // 2. Autonomous claude - one-shot claude --resume w/ --dangerously-skip-permissions -p (claude only)
1073
1072
  // 3. Solve resume - re-enters solve.mjs with --resume, preserving tool/model/dir
1074
1073
  const toolForFailure = argv.tool || 'claude';
1074
+ // Issue #1845: surface the core error instead of just "<TOOL> execution failed" (terminal + comment).
1075
+ const toolFailureMessage = formatToolExecutionFailure({ tool: toolForFailure, toolResult });
1075
1076
  if (sessionId) {
1076
1077
  await log('');
1077
1078
  await log('💡 To continue this session:');
@@ -1116,7 +1117,7 @@ try {
1116
1117
  // Include sessionId so the PR comment can present it
1117
1118
  sessionId,
1118
1119
  // If not a usage limit case, fall back to generic failure format
1119
- errorMessage: limitReached ? undefined : `${argv.tool.toUpperCase()} execution failed`,
1120
+ errorMessage: limitReached ? undefined : toolFailureMessage,
1120
1121
  requestedModel: argv.originalModel || argv.model,
1121
1122
  tool: argv.tool || 'claude',
1122
1123
  // Issue #1454: Pass resultModelUsage for accurate multi-model display
@@ -1136,21 +1137,20 @@ try {
1136
1137
  }
1137
1138
  }
1138
1139
 
1139
- // Issue #1834 (PR #1835 feedback): "on all critical errors we auto commit uncommitted changes
1140
- // by default." A failed session is a critical error and exits here before the normal
1141
- // auto-commit chokepoint below, so preserve (commit + push) any work the agent left on disk
1142
- // first. On by default; disable via HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false. Never throws.
1140
+ // Issue #1834 (PR #1835 feedback): "on all critical errors we auto commit uncommitted changes by
1141
+ // default." A failed session exits here before the normal auto-commit chokepoint below, so commit
1142
+ // + push any work first. On by default; disable via HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false.
1143
1143
  try {
1144
1144
  const { criticalErrorRecovery } = await import('./config.lib.mjs');
1145
1145
  if (criticalErrorRecovery.autoCommitUncommittedChanges) {
1146
1146
  const { commitUncommittedChangesOnCriticalError } = await import('./critical-error-commit.lib.mjs');
1147
- await commitUncommittedChangesOnCriticalError({ tempDir, branchName, $, log, reason: `${argv.tool || 'claude'} execution failed` });
1147
+ await commitUncommittedChangesOnCriticalError({ tempDir, branchName, $, log, reason: toolFailureMessage });
1148
1148
  }
1149
1149
  } catch (preserveError) {
1150
1150
  await log(` ⚠️ Could not auto-commit before failure exit: ${preserveError.message}`, { verbose: true });
1151
1151
  }
1152
1152
 
1153
- await safeExit(1, `${argv.tool.toUpperCase()} execution failed`);
1153
+ await safeExit(1, toolFailureMessage);
1154
1154
  }
1155
1155
 
1156
1156
  // Clean up .playwright-mcp/ to prevent browser artifacts from triggering auto-restart (Issue #1124)
@@ -1463,6 +1463,7 @@ try {
1463
1463
  }
1464
1464
  await handleMainExecutionError({
1465
1465
  error,
1466
+ cleanupContext, // #1845: enable auto-commit of uncommitted work before the failure exit
1466
1467
  log,
1467
1468
  cleanErrorMessage,
1468
1469
  absoluteLogPath,
@@ -29,7 +29,7 @@ const fs = (await use('fs')).promises;
29
29
 
30
30
  // Import shared library functions
31
31
  const lib = await import('./lib.mjs');
32
- const { log, formatAligned } = lib;
32
+ const { log, formatAligned, extractToolErrorCore } = lib;
33
33
 
34
34
  // Import Sentry integration
35
35
  const sentryLib = await import('./sentry.lib.mjs');
@@ -507,11 +507,20 @@ export const buildUncommittedChangesFeedback = (changes, restartCount = 0, maxIt
507
507
  * @returns {boolean}
508
508
  */
509
509
  export const isApiError = toolResult => {
510
- if (!toolResult || !toolResult.result) return false;
510
+ if (!toolResult) return false;
511
+
512
+ // Issue #1845: runners report failures via `errorInfo` (e.g. claude sets
513
+ // `errorInfo.message` but NOT `result`). Use the shared core-error extractor so an
514
+ // "API Error:" is classified correctly regardless of which field the runner populated —
515
+ // otherwise the MAX_API_ERROR_RETRIES guard never trips for claude and watch mode can
516
+ // retry a hard API error indefinitely. `extractToolErrorCore` still falls back to
517
+ // `result`, preserving the original behavior for runners that set it.
518
+ const errorText = extractToolErrorCore({ toolResult });
519
+ if (!errorText) return false;
511
520
 
512
521
  const errorPatterns = ['API Error:', 'not_found_error', 'authentication_error', 'invalid_request_error'];
513
522
 
514
- return errorPatterns.some(pattern => toolResult.result.includes(pattern));
523
+ return errorPatterns.some(pattern => errorText.includes(pattern));
515
524
  };
516
525
 
517
526
  /**
@@ -20,7 +20,7 @@ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
20
20
  const $ = wrapDollarWithGhRetry(__rawDollar$);
21
21
  // Import shared library functions
22
22
  const lib = await import('./lib.mjs');
23
- const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
23
+ const { log, cleanErrorMessage, formatAligned, formatToolExecutionFailure, extractToolErrorCore, getLogFile } = lib;
24
24
 
25
25
  // Import feedback detection functions
26
26
  const feedbackLib = await import('./solve.feedback.lib.mjs');
@@ -373,7 +373,9 @@ export const watchForFeedback = async params => {
373
373
  if (consecutiveApiErrors >= MAX_API_ERROR_RETRIES) {
374
374
  await log('');
375
375
  await log(formatAligned('❌', 'MAXIMUM API ERROR RETRIES REACHED', ''));
376
- await log(formatAligned('', 'Error details:', toolResult.result || 'Unknown API error', 2));
376
+ // Issue #1845: surface the core error (e.g. "API Error: Output blocked by content
377
+ // filtering policy"); toolResult.result is often unset on failure, so prefer errorInfo.
378
+ await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult }) || 'Unknown API error', 2));
377
379
  await log(formatAligned('', 'Consecutive failures:', `${consecutiveApiErrors}`, 2));
378
380
  await log(formatAligned('', 'Action:', 'Exiting watch mode to prevent infinite loop', 2));
379
381
  await log('');
@@ -421,7 +423,7 @@ export const watchForFeedback = async params => {
421
423
  sessionId: toolResult.sessionId || latestSessionId,
422
424
  tempDir,
423
425
  // Include error information in the log upload
424
- errorMessage: toolResult.errorInfo?.message || toolResult.result || `${argv.tool.toUpperCase()} execution failed`,
426
+ errorMessage: formatToolExecutionFailure({ tool: argv.tool, toolResult }),
425
427
  // Include pricing data if available from failed attempt
426
428
  publicPricingEstimate: toolResult.publicPricingEstimate,
427
429
  pricingInfo: toolResult.pricingInfo,
@@ -1169,7 +1169,8 @@ bot.on('message', async (ctx, next) => {
1169
1169
  // /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback — Telegraf's bot.command() is sufficient.
1170
1170
  const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
1171
1171
  const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
1172
- const handlers = { ...solveHandlers, ...taskHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand };
1172
+ // /queue is the short alias for /solve_queue (issue #1837)
1173
+ const handlers = { ...solveHandlers, ...taskHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
1173
1174
 
1174
1175
  const handler = handlers[extracted.command];
1175
1176
  if (!handler) return next();
@@ -97,11 +97,12 @@ export function registerSolveQueueCommand(bot, options) {
97
97
  });
98
98
  }
99
99
 
100
- // Match /solve_queue, /solve-queue, or /solvequeue (case-insensitive)
100
+ // Match /solve_queue, /solve-queue, /solvequeue, or the short /queue alias (case-insensitive)
101
101
  // Note: Telegram Bot API only supports underscores in command names, not hyphens.
102
- // The entity-based matching handles /solve_queue and /solvequeue.
102
+ // The entity-based matching handles /solve_queue, /solvequeue, and /queue.
103
103
  // /solve-queue is handled by the text-based fallback in telegram-bot.mjs (issue #1232).
104
- bot.command(/^solve[_-]?queue$/i, handleSolveQueueCommand);
104
+ // The /queue alias was added in issue #1837 to make checking the queue faster to type.
105
+ bot.command(/^(?:solve[_-]?queue|queue)$/i, handleSolveQueueCommand);
105
106
 
106
107
  return { handleSolveQueueCommand };
107
108
  }
@@ -6,6 +6,64 @@ import { lt } from './limits-i18n.lib.mjs';
6
6
 
7
7
  const execAsync = promisify(exec);
8
8
 
9
+ /**
10
+ * Build a clickable, human-readable link to a queued issue/PR for the
11
+ * /solve_queue (/queue) detailed status (issue #1837).
12
+ *
13
+ * For GitHub issue/PR URLs we render a compact `[owner/repo#number](url)`
14
+ * Markdown link so the list is scannable and clickable. When the label would
15
+ * contain Markdown-special characters (e.g. `_` or `*` in an owner/repo name)
16
+ * that could break Telegram's legacy Markdown parser, we fall back to the bare
17
+ * URL — which Telegram still auto-links and renders as clickable.
18
+ *
19
+ * Non-GitHub or unparseable URLs also fall back to the bare URL.
20
+ *
21
+ * @param {string} url - The issue/PR URL.
22
+ * @returns {string} A Markdown link or bare URL safe for `parse_mode: 'Markdown'`.
23
+ */
24
+ export function formatQueueItemLink(url) {
25
+ if (!url || typeof url !== 'string') return String(url ?? '');
26
+ const match = url.match(/github\.com\/([^/\s]+)\/([^/\s]+)\/(?:issues|pull)\/(\d+)/i);
27
+ if (!match) return url;
28
+ const [, owner, repo, number] = match;
29
+ const label = `${owner}/${repo}#${number}`;
30
+ // Only build a Markdown link when the label has no Markdown-special chars
31
+ // that would break the legacy parser inside link text. Otherwise the bare
32
+ // URL is still clickable in Telegram.
33
+ if (/^[A-Za-z0-9/#.-]+$/.test(label)) {
34
+ return `[${label}](${url})`;
35
+ }
36
+ return url;
37
+ }
38
+
39
+ /**
40
+ * Render a history section (Completed / Failed) for the detailed queue status
41
+ * as a clickable list, most-recent-first, capped at `max` items with a
42
+ * "... and N more" line (issue #1837).
43
+ *
44
+ * @param {object} opts
45
+ * @param {Array} opts.items - History items (each with `url`, optional `error`).
46
+ * @param {string} opts.emoji - Leading emoji for each row (e.g. '✅' or '❌').
47
+ * @param {string} opts.label - Localized section heading.
48
+ * @param {number} opts.max - Maximum items to list before collapsing.
49
+ * @param {string|null} opts.locale - Locale for the "and N more" label.
50
+ * @param {boolean} [opts.withError] - Append `— error` when the item failed.
51
+ * @returns {string} The formatted section (empty string when no items).
52
+ */
53
+ export function formatQueueHistorySection({ items, emoji, label, max, locale, withError = false }) {
54
+ if (!items || items.length === 0) return '';
55
+ let section = `*${label}* (${items.length}):\n`;
56
+ for (const item of [...items].reverse().slice(0, max)) {
57
+ section += ` ${emoji} ${formatQueueItemLink(item.url)}`;
58
+ if (withError && item.error) section += ` — ${item.error}`;
59
+ section += '\n';
60
+ }
61
+ if (items.length > max) {
62
+ section += ` ... ${lt('queue_and_more', { count: items.length - max }, { locale })}\n`;
63
+ }
64
+ return `${section}\n`;
65
+ }
66
+
9
67
  /**
10
68
  * Count running processes by name.
11
69
  * @param {string} processName - Process name to search for (e.g., 'claude', 'agent', 'codex', 'gemini')
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { getCachedClaudeLimits, getCachedCodexLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
19
19
  export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
20
- import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
20
+ import { formatDuration, formatQueueHistorySection, formatQueueItemLink, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
21
21
  export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
22
22
  import { QUEUE_CONFIG } from './queue-config.lib.mjs';
23
23
  import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
@@ -1320,28 +1320,17 @@ export class SolveQueue {
1320
1320
  }
1321
1321
 
1322
1322
  /**
1323
- * Format detailed queue status for Telegram message
1324
- * Groups output by tool queue, shows first 5 items per queue, and uses human-readable time.
1323
+ * Format detailed queue status for Telegram message.
1324
+ * Groups output by tool queue (clickable links per item), then lists the
1325
+ * Completed and Failed history as clickable links, capped per section.
1325
1326
  *
1326
1327
  * Processing count = max(actual AI CLI processes via pgrep, tracked
1327
1328
  * `$ --status` executing screen-isolated sessions), not queue state.
1328
1329
  *
1329
- * Output format:
1330
- * ```
1331
- * 📋 Solve Queue Status
1332
- *
1333
- * claude (pending: 6, processing: 0)
1334
- * • url1 (waiting, 5h 43m 23s)
1335
- * └ RAM usage is 70% (threshold: 65%)
1336
- * • url2 (queued, 2m 15s)
1337
- *
1338
- * agent (pending: 2, processing: 0)
1339
- * • url3 (waiting, 1h 2m 5s)
1340
- * ```
1341
- *
1342
1330
  * @returns {Promise<string>}
1343
1331
  * @see https://github.com/link-assistant/hive-mind/issues/1159
1344
1332
  * @see https://github.com/link-assistant/hive-mind/issues/1267
1333
+ * @see https://github.com/link-assistant/hive-mind/issues/1837
1345
1334
  */
1346
1335
  async formatDetailedStatus(options = {}) {
1347
1336
  const locale = getLocale(options);
@@ -1359,22 +1348,37 @@ export class SolveQueue {
1359
1348
  const processing = externalProcessing.byTool[tool] || 0;
1360
1349
  message += `*${tool}* (${lt('queue_pending', {}, { locale })}: ${pending}, ${lt('queue_processing', {}, { locale })}: ${processing})\n`;
1361
1350
 
1362
- // Show first 5 queued items for this tool
1363
- const displayItems = toolQueue.slice(0, 5);
1351
+ // Show the items this queue is actively processing for this tool, with a
1352
+ // clickable link to each issue/PR (issue #1837). These come from the
1353
+ // queue's own tracking, so they may differ from the pgrep-based count above.
1354
+ const processingItems = Array.from(this.processing.values()).filter(item => item.tool === tool);
1355
+ for (const item of processingItems.slice(0, QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE)) {
1356
+ const waitTime = formatDuration(item.getWaitTime(), { locale });
1357
+ message += ` ▶️ ${formatQueueItemLink(item.url)} (${queueStatusLabel(item.status, locale)}, ${waitTime})\n`;
1358
+ }
1359
+
1360
+ // Show first queued items for this tool with clickable links
1361
+ const displayItems = toolQueue.slice(0, QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE);
1364
1362
  for (const item of displayItems) {
1365
1363
  const waitTime = formatDuration(item.getWaitTime(), { locale });
1366
- message += ` • ${item.url} (${queueStatusLabel(item.status, locale)}, ${waitTime})\n`;
1364
+ message += ` • ${formatQueueItemLink(item.url)} (${queueStatusLabel(item.status, locale)}, ${waitTime})\n`;
1367
1365
  if (item.waitingReason) {
1368
1366
  message += ` └ ${item.waitingReason}\n`;
1369
1367
  }
1370
1368
  }
1371
- if (toolQueue.length > 5) {
1372
- message += ` ... ${lt('queue_and_more', { count: toolQueue.length - 5 }, { locale })}\n`;
1369
+ if (toolQueue.length > QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE) {
1370
+ message += ` ... ${lt('queue_and_more', { count: toolQueue.length - QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE }, { locale })}\n`;
1373
1371
  }
1374
1372
 
1375
1373
  message += '\n';
1376
1374
  }
1377
1375
 
1376
+ // Completed / Failed lists - clickable links to the executed issues/PRs,
1377
+ // most-recent-first so the newest results are easy to find (issue #1837).
1378
+ const max = QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE;
1379
+ message += formatQueueHistorySection({ items: this.completed, emoji: '✅', label: lt('queue_completed', {}, { locale }), max, locale });
1380
+ message += formatQueueHistorySection({ items: this.failed, emoji: '❌', label: lt('queue_failed', {}, { locale }), max, locale, withError: true });
1381
+
1378
1382
  // Summary stats
1379
1383
  message += `${lt('queue_completed', {}, { locale })}: ${stats.completed}, ${lt('queue_failed', {}, { locale })}: ${stats.failed}\n`;
1380
1384