@link-assistant/hive-mind 1.35.3 → 1.35.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.35.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 37481da: fix: improve PR creation failure error messaging and log upload fallback (Issue #1462)
8
+ - Consolidate triple error output into a single clear error message when PR creation fails
9
+ - Upload failure logs to the issue as fallback when PR is not available (--attach-logs)
10
+ - Capture and log `gh pr create` stdout/stderr in verbose mode for root cause diagnosis
11
+ - Add fallback GitHub user detection via `gh auth status` when `gh api user` fails
12
+ - Rename `github-issue-creator.lib.mjs` to `github-error-reporter.lib.mjs` for clarity
13
+
14
+ ## 1.35.4
15
+
16
+ ### Patch Changes
17
+
18
+ - 0df2139: Harden Telegram message formatting: escape special characters in user mentions, options text, and server overrides. Add safeReply with plain text fallback and diagnostic logging when Telegram rejects Markdown. Improve error messages with user identity context for root cause analysis.
19
+
3
20
  ## 1.35.3
4
21
 
5
22
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.35.3",
3
+ "version": "1.35.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -40,9 +40,12 @@ export function buildUserMention({ user, id: idParam, username: usernameParam, f
40
40
  const link = username ? `https://t.me/${username}` : `tg://user?id=${id}`;
41
41
 
42
42
  switch (parseMode) {
43
- case 'Markdown':
43
+ case 'Markdown': {
44
44
  // Legacy Markdown: [text](url)
45
- return `[${displayName}](${link})`;
45
+ // Escape _ and * in display name to prevent "can't find end of entity" errors (issue #1460)
46
+ const escapedMarkdownName = displayName.replace(/_/g, '\\_').replace(/\*/g, '\\*');
47
+ return `[${escapedMarkdownName}](${link})`;
48
+ }
46
49
  case 'MarkdownV2': {
47
50
  // MarkdownV2 requires escaping special characters
48
51
  const escapedName = displayName.replace(/([_*[\]()~`>#+\-=|{}.!])/g, '\\$1');
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Automatic GitHub issue creation for error reporting
4
+ * GitHub error reporter - handles error reporting via GitHub issues and comments
5
5
  */
6
6
 
7
7
  import { createInterface } from 'readline';
@@ -52,7 +52,8 @@ const getCurrentGitHubUser = async () => {
52
52
  try {
53
53
  const result = await $`gh api user --jq .login`;
54
54
  if (result.exitCode === 0) {
55
- return result.stdout.toString().trim();
55
+ const user = result.stdout.toString().trim();
56
+ if (user) return user;
56
57
  }
57
58
  } catch (error) {
58
59
  reportError(error, {
@@ -60,6 +61,18 @@ const getCurrentGitHubUser = async () => {
60
61
  operation: 'gh_api_user',
61
62
  });
62
63
  }
64
+ // Issue #1462: Fallback to gh auth status when gh api user fails
65
+ // This handles OAuth tokens (gho_****) that may lack the 'user' API scope
66
+ try {
67
+ const authResult = await $`gh auth status --hostname github.com 2>&1`;
68
+ const output = (authResult.stdout?.toString() || '') + (authResult.stderr?.toString() || '');
69
+ const userMatch = output.match(/Logged in to github\.com account (\S+)/i) || output.match(/Logged in to github\.com as (\S+)/i);
70
+ if (userMatch) {
71
+ return userMatch[1];
72
+ }
73
+ } catch {
74
+ // Silently ignore - will return null below
75
+ }
63
76
  return null;
64
77
  };
65
78
 
@@ -1113,12 +1113,14 @@ ${prBody}`,
1113
1113
  }
1114
1114
 
1115
1115
  let output;
1116
+ let prCreateStderr = '';
1116
1117
  let assigneeFailed = false;
1117
1118
 
1118
1119
  // Try to create PR with assignee first (if specified)
1119
1120
  try {
1120
1121
  const result = await execAsync(command, { encoding: 'utf8', cwd: tempDir, env: process.env });
1121
1122
  output = result.stdout;
1123
+ prCreateStderr = result.stderr || '';
1122
1124
  } catch (firstError) {
1123
1125
  // Check if the error is specifically about assignee validation
1124
1126
  const errorMsg = firstError.message || '';
@@ -1146,6 +1148,7 @@ ${prBody}`,
1146
1148
  // Retry without assignee - if this fails, let the error propagate to outer catch
1147
1149
  const retryResult = await execAsync(command, { encoding: 'utf8', cwd: tempDir, env: process.env });
1148
1150
  output = retryResult.stdout;
1151
+ prCreateStderr = retryResult.stderr || '';
1149
1152
  } else {
1150
1153
  // Not an assignee error, re-throw the original error
1151
1154
  throw firstError;
@@ -1168,6 +1171,14 @@ ${prBody}`,
1168
1171
  });
1169
1172
  });
1170
1173
 
1174
+ // Log gh pr create output for debugging (Issue #1462)
1175
+ if (argv.verbose) {
1176
+ await log(` gh pr create stdout: ${(output || '').trim() || '(empty)'}`, { verbose: true });
1177
+ if (prCreateStderr) {
1178
+ await log(` gh pr create stderr: ${prCreateStderr.trim()}`, { verbose: true });
1179
+ }
1180
+ }
1181
+
1171
1182
  // Extract PR URL from output - gh pr create outputs the URL to stdout
1172
1183
  prUrl = output.trim();
1173
1184
 
@@ -1214,22 +1225,11 @@ ${prBody}`,
1214
1225
  }
1215
1226
  } else {
1216
1227
  // PR does not exist - gh pr create must have failed silently
1217
- await log('');
1218
- await log(formatAligned('❌', 'FATAL ERROR:', 'PR creation failed'), { level: 'error' });
1219
- await log('');
1220
- await log(' 🔍 What happened:');
1221
- await log(' The gh pr create command returned a URL, but the PR does not exist on GitHub.');
1222
- await log('');
1223
- await log(' 🔧 How to fix:');
1224
- await log(' 1. Check if PR exists manually:');
1225
- await log(` gh pr list --repo ${owner}/${repo} --head ${branchName}`);
1226
- await log(' 2. Try creating PR manually:');
1227
- await log(` cd ${tempDir}`);
1228
- await log(` gh pr create --draft --title "Fix issue #${issueNumber}" --body "Fixes #${issueNumber}"`);
1229
- await log(' 3. Check GitHub authentication:');
1230
- await log(' gh auth status');
1231
- await log('');
1232
- throw new Error('PR creation failed - PR does not exist on GitHub');
1228
+ // Issue #1462: Include gh pr create stderr for root cause diagnosis
1229
+ const verifyStderr = verifyResult.stderr ? verifyResult.stderr.toString().trim() : '';
1230
+ const stderrInfo = prCreateStderr ? ` (gh pr create stderr: ${prCreateStderr.trim()})` : '';
1231
+ const verifyInfo = verifyStderr ? ` (gh pr view stderr: ${verifyStderr})` : '';
1232
+ throw new Error(`PR verification failed - gh pr create returned URL "${prUrl}" but PR #${localPrNumber} does not exist on GitHub${stderrInfo}${verifyInfo}`);
1233
1233
  }
1234
1234
  // Store PR info globally for error handlers
1235
1235
  global.createdPR = { number: localPrNumber, url: prUrl };
@@ -1374,73 +1374,22 @@ ${prBody}`,
1374
1374
  branchName,
1375
1375
  operation: 'create_pull_request',
1376
1376
  });
1377
+ // Issue #1462: Don't log verbose error block here - let the outer catch
1378
+ // handler produce a single consolidated error message to avoid triple error output.
1379
+ // Extract clean error message and re-throw with context.
1377
1380
  const errorMsg = prCreateError.message || '';
1378
-
1379
- // Clean up the error message - extract the meaningful part
1380
1381
  let cleanError = errorMsg;
1381
1382
  if (errorMsg.includes('pull request create failed:')) {
1382
1383
  cleanError = errorMsg.split('pull request create failed:')[1].trim();
1383
1384
  } else if (errorMsg.includes('Command failed:')) {
1384
- // Extract just the error part, not the full command
1385
1385
  const lines = errorMsg.split('\n');
1386
1386
  cleanError = lines[lines.length - 1] || errorMsg;
1387
1387
  }
1388
1388
 
1389
- // Check for specific error types
1390
- // Note: Assignee errors are now handled by automatic retry in the try block above
1391
- // This catch block only handles other types of PR creation failures
1392
1389
  if (errorMsg.includes('No commits between') || errorMsg.includes("Head sha can't be blank")) {
1393
- // Empty PR error
1394
- await log('');
1395
- await log(formatAligned('❌', 'PR CREATION FAILED', ''), { level: 'error' });
1396
- await log('');
1397
- await log(' 🔍 What happened:');
1398
- await log(' Cannot create PR - no commits between branches.');
1399
- await log('');
1400
- await log(' 📦 Error details:');
1401
- for (const line of cleanError.split('\n')) {
1402
- if (line.trim()) await log(` ${line.trim()}`);
1403
- }
1404
- await log('');
1405
- await log(' 💡 Possible causes:');
1406
- await log(" • The branch wasn't pushed properly");
1407
- await log(" • The commit wasn't created");
1408
- await log(' • GitHub sync issue');
1409
- await log('');
1410
- await log(' 🔧 How to fix:');
1411
- await log(' 1. Verify commit exists:');
1412
- await log(` cd ${tempDir} && git log --format="%h %s" -5`);
1413
- await log(' 2. Push again with tracking:');
1414
- await log(` cd ${tempDir} && git push -u origin ${branchName}`);
1415
- await log(' 3. Create PR manually:');
1416
- await log(` cd ${tempDir} && gh pr create --draft`);
1417
- await log('');
1418
- await log(` 📂 Working directory: ${tempDir}`);
1419
- await log(` 🌿 Current branch: ${branchName}`);
1420
- await log('');
1421
- throw new Error('PR creation failed - no commits between branches');
1390
+ throw new Error(`PR creation failed - no commits between branches: ${cleanError}`);
1422
1391
  } else {
1423
- // Generic PR creation error
1424
- await log('');
1425
- await log(formatAligned('❌', 'PR CREATION FAILED', ''), { level: 'error' });
1426
- await log('');
1427
- await log(' 🔍 What happened:');
1428
- await log(' Failed to create pull request.');
1429
- await log('');
1430
- await log(' 📦 Error details:');
1431
- for (const line of cleanError.split('\n')) {
1432
- if (line.trim()) await log(` ${line.trim()}`);
1433
- }
1434
- await log('');
1435
- await log(' 🔧 How to fix:');
1436
- await log(' 1. Try creating PR manually:');
1437
- await log(` cd ${tempDir} && gh pr create --draft`);
1438
- await log(' 2. Check branch status:');
1439
- await log(` cd ${tempDir} && git status`);
1440
- await log(' 3. Verify GitHub authentication:');
1441
- await log(' gh auth status');
1442
- await log('');
1443
- throw new Error('PR creation failed');
1392
+ throw new Error(`PR creation failed: ${cleanError}`);
1444
1393
  }
1445
1394
  }
1446
1395
  }
@@ -1452,23 +1401,22 @@ ${prBody}`,
1452
1401
  operation: 'handle_auto_pr',
1453
1402
  });
1454
1403
 
1455
- // CRITICAL: PR creation failure should stop the entire process
1456
- // We cannot continue without a PR when auto-PR creation is enabled
1404
+ // Issue #1462: Single consolidated error message for PR creation failure.
1405
+ // Previously this was the third of three error blocks, causing confusing output.
1406
+ // Now this is the ONLY error block shown for PR creation failures.
1457
1407
  await log('');
1458
1408
  await log(formatAligned('❌', 'FATAL ERROR:', 'PR creation failed'), { level: 'error' });
1459
1409
  await log('');
1460
- await log(' 🔍 What this means:');
1461
- await log(' The solve command cannot continue without a pull request.');
1462
- await log(' Auto-PR creation is enabled but failed to create the PR.');
1463
- await log('');
1464
- await log(' 📦 Error details:');
1410
+ await log(' 🔍 What happened:');
1465
1411
  await log(` ${prError.message}`);
1466
1412
  await log('');
1413
+ await log(' 💡 The solve command cannot continue without a pull request.');
1414
+ await log('');
1467
1415
  await log(' 🔧 How to fix:');
1468
1416
  await log('');
1469
1417
  await log(' Option 1: Retry without auto-PR creation');
1470
1418
  await log(` ./solve.mjs "${issueUrl}" --no-auto-pull-request-creation`);
1471
- await log(' (Claude will create the PR during the session)');
1419
+ await log(' (The AI agent will create the PR during the session)');
1472
1420
  await log('');
1473
1421
  await log(' Option 2: Create PR manually first');
1474
1422
  await log(` cd ${tempDir}`);
@@ -1482,8 +1430,9 @@ ${prBody}`,
1482
1430
  await log(' gh pr create --draft # Try manually to see detailed error');
1483
1431
  await log('');
1484
1432
 
1485
- // Re-throw the error to stop execution
1486
- throw new Error(`PR creation failed: ${prError.message}`);
1433
+ // Re-throw the error to stop execution - use prError.message directly
1434
+ // to avoid "PR creation failed: PR creation failed" redundancy (Issue #1462)
1435
+ throw prError;
1487
1436
  }
1488
1437
 
1489
1438
  return { prUrl, prNumber: localPrNumber, claudeCommitHash };
@@ -8,8 +8,8 @@ import { safeExit } from './exit-handler.lib.mjs';
8
8
  // Import Sentry integration
9
9
  import { reportError } from './sentry.lib.mjs';
10
10
 
11
- // Import GitHub issue creator
12
- import { handleErrorWithIssueCreation } from './github-issue-creator.lib.mjs';
11
+ // Import GitHub error reporter
12
+ import { handleErrorWithIssueCreation } from './github-error-reporter.lib.mjs';
13
13
 
14
14
  /**
15
15
  * Handles log attachment and PR closing on failure
@@ -40,35 +40,45 @@ export const handleFailure = async options => {
40
40
  }
41
41
 
42
42
  // If --attach-logs is enabled, try to attach failure logs
43
- if (shouldAttachLogs && getLogFile() && global.createdPR && global.createdPR.number) {
44
- await log('\n📄 Attempting to attach failure logs...');
45
- try {
46
- const logUploadSuccess = await attachLogToGitHub({
47
- logFile: getLogFile(),
48
- targetType: 'pr',
49
- targetNumber: global.createdPR.number,
50
- owner: global.owner || owner,
51
- repo: global.repo || repo,
52
- $,
53
- log,
54
- sanitizeLogContent,
55
- verbose: argv.verbose,
56
- errorMessage: cleanErrorMessage(error),
57
- // Issue #1225: Pass model and tool info for PR comments
58
- requestedModel: argv.model,
59
- tool: argv.tool || 'claude',
60
- });
61
- if (logUploadSuccess) {
62
- await log('📎 Failure log attached to Pull Request');
43
+ if (shouldAttachLogs && getLogFile()) {
44
+ // Issue #1462: Upload logs to PR if available, otherwise fall back to the issue
45
+ const hasPR = global.createdPR && global.createdPR.number;
46
+ const hasIssue = global.issueNumber;
47
+ const targetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
48
+ const targetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
49
+ const targetLabel = hasPR ? 'Pull Request' : 'Issue';
50
+
51
+ if (targetType && targetNumber) {
52
+ await log(`\n📄 Attempting to attach failure logs to ${targetLabel}...`);
53
+ try {
54
+ const logUploadSuccess = await attachLogToGitHub({
55
+ logFile: getLogFile(),
56
+ targetType,
57
+ targetNumber,
58
+ owner: global.owner || owner,
59
+ repo: global.repo || repo,
60
+ $,
61
+ log,
62
+ sanitizeLogContent,
63
+ verbose: argv.verbose,
64
+ errorMessage: cleanErrorMessage(error),
65
+ // Issue #1225: Pass model and tool info for PR comments
66
+ requestedModel: argv.model,
67
+ tool: argv.tool || 'claude',
68
+ });
69
+ if (logUploadSuccess) {
70
+ await log(`📎 Failure log attached to ${targetLabel}`);
71
+ }
72
+ } catch (attachError) {
73
+ reportError(attachError, {
74
+ context: 'attach_failure_log',
75
+ targetType,
76
+ targetNumber,
77
+ errorType,
78
+ operation: `attach_log_to_${targetType}`,
79
+ });
80
+ await log(`⚠️ Could not attach failure log to ${targetLabel}: ${attachError.message}`, { level: 'warning' });
63
81
  }
64
- } catch (attachError) {
65
- reportError(attachError, {
66
- context: 'attach_failure_log',
67
- prNumber: global.createdPR?.number,
68
- errorType,
69
- operation: 'attach_log_to_pr',
70
- });
71
- await log(`⚠️ Could not attach failure log: ${attachError.message}`, { level: 'warning' });
72
82
  }
73
83
  }
74
84
 
package/src/solve.mjs CHANGED
@@ -496,6 +496,9 @@ if (isPrUrl) {
496
496
  issueNumber = urlNumber;
497
497
  await log(`📝 Issue mode: Working with issue #${issueNumber}`);
498
498
  }
499
+ // Issue #1462: Store issueNumber in global so error handlers can upload logs to the issue
500
+ // as a fallback when PR creation fails and global.createdPR is not available
501
+ global.issueNumber = issueNumber;
499
502
  const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
500
503
  const { tempDir, workspaceTmpDir, needsClone } = await setupTempDirectory(argv, workspaceInfo);
501
504
  cleanupContext.tempDir = tempDir;
@@ -1076,19 +1079,25 @@ try {
1076
1079
  await log('');
1077
1080
  }
1078
1081
 
1079
- // If --attach-logs is enabled and we have a PR, attach failure logs before exiting
1082
+ // If --attach-logs is enabled, attach failure logs before exiting
1080
1083
  // Note: sessionId is not required - logs should be uploaded even if agent failed before establishing a session
1081
- // This aligns with the pattern in handleFailure() in solve.error-handlers.lib.mjs
1082
- if (shouldAttachLogs && global.createdPR && global.createdPR.number) {
1083
- await log('\n📄 Attaching failure logs to Pull Request...');
1084
+ // Issue #1462: Fall back to uploading logs to the issue if PR is not available
1085
+ const hasPR = global.createdPR && global.createdPR.number;
1086
+ const hasIssue = global.issueNumber;
1087
+ const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
1088
+ const logTargetNumber = hasPR ? global.createdPR.number : hasIssue ? global.issueNumber : null;
1089
+ const logTargetLabel = hasPR ? 'Pull Request' : 'Issue';
1090
+
1091
+ if (shouldAttachLogs && logTargetType && logTargetNumber) {
1092
+ await log(`\n📄 Attaching failure logs to ${logTargetLabel}...`);
1084
1093
  try {
1085
1094
  // Build Claude CLI resume command
1086
1095
  const tool = argv.tool || 'claude';
1087
1096
  const resumeCommand = sessionId && tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : null;
1088
1097
  const logUploadSuccess = await attachLogToGitHub({
1089
1098
  logFile: getLogFile(),
1090
- targetType: 'pr',
1091
- targetNumber: global.createdPR.number,
1099
+ targetType: logTargetType,
1100
+ targetNumber: logTargetNumber,
1092
1101
  owner,
1093
1102
  repo,
1094
1103
  $,
@@ -1110,7 +1119,7 @@ try {
1110
1119
  });
1111
1120
 
1112
1121
  if (logUploadSuccess) {
1113
- await log(' ✅ Failure logs uploaded successfully');
1122
+ await log(` ✅ Failure logs uploaded to ${logTargetLabel} successfully`);
1114
1123
  } else {
1115
1124
  await log(' ⚠️ Failed to upload logs', { verbose: true });
1116
1125
  }
@@ -992,9 +992,11 @@ async function handleSolveCommand(ctx) {
992
992
 
993
993
  const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
994
994
  // Issue #1228: Show only user-provided options (exclude locked overrides to avoid duplication)
995
- const userOptionsText = userArgs.slice(1).join(' ') || 'none';
996
- let infoBlock = `Requested by: ${requester}\nURL: ${escapeMarkdown(normalizedUrl)}\n\n🛠 Options: ${userOptionsText}`;
997
- if (solveOverrides.length > 0) infoBlock += `\n🔒 Locked options: ${solveOverrides.join(' ')}`;
995
+ // Issue #1460: Escape options text to prevent Markdown parsing errors
996
+ const userOptionsRaw = userArgs.slice(1).join(' ');
997
+ let infoBlock = `Requested by: ${requester}\nURL: ${escapeMarkdown(normalizedUrl)}`;
998
+ if (userOptionsRaw) infoBlock += `\n\n🛠 Options: ${escapeMarkdown(userOptionsRaw)}`;
999
+ if (solveOverrides.length > 0) infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}🔒 Locked options: ${escapeMarkdown(solveOverrides.join(' '))}`;
998
1000
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
999
1001
 
1000
1002
  // Check for duplicate URL in queue
@@ -1162,10 +1164,12 @@ async function handleHiveCommand(ctx) {
1162
1164
  const requester = buildUserMention({ user: ctx.from, parseMode: 'Markdown' });
1163
1165
  const escapedUrl = escapeMarkdown(args[0]);
1164
1166
  // Issue #1228: Show only user-provided options (exclude locked overrides to avoid duplication)
1165
- const userOptionsText = normalizedArgs.slice(1).join(' ') || 'none';
1166
- let infoBlock = `Requested by: ${requester}\nURL: ${escapedUrl}\n\n🛠 Options: ${userOptionsText}`;
1167
+ // Issue #1460: Escape options text to prevent Markdown parsing errors
1168
+ const userOptionsRaw = normalizedArgs.slice(1).join(' ');
1169
+ let infoBlock = `Requested by: ${requester}\nURL: ${escapedUrl}`;
1170
+ if (userOptionsRaw) infoBlock += `\n\n🛠 Options: ${escapeMarkdown(userOptionsRaw)}`;
1167
1171
  if (hiveOverrides.length > 0) {
1168
- infoBlock += `\n🔒 Locked options: ${hiveOverrides.join(' ')}`;
1172
+ infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}🔒 Locked options: ${escapeMarkdown(hiveOverrides.join(' '))}`;
1169
1173
  }
1170
1174
 
1171
1175
  const startingMessage = await ctx.reply(`🚀 Starting hive command...\n\n${infoBlock}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
@@ -1305,18 +1309,22 @@ bot.catch((error, ctx) => {
1305
1309
  let errorMessage;
1306
1310
 
1307
1311
  if (isTelegramParsingError) {
1308
- // Special handling for Telegram API parsing errors caused by unescaped special characters
1309
- errorMessage = `❌ A message formatting error occurred.\n\n💡 This usually means there was a problem with special characters in the response.\nPlease try your command again with a different URL or contact support.`;
1310
- // Show the user's input with special characters visible (if available)
1312
+ // Issue #1460: Log detailed context for root cause analysis (always logged, not just in verbose mode)
1313
+ const userInfo = ctx.from ? { id: ctx.from.id, username: ctx.from.username, first_name: ctx.from.first_name, last_name: ctx.from.last_name } : 'unknown';
1314
+ console.error(`[telegram-bot] Parsing error: ${error.message}`);
1315
+ console.error(`[telegram-bot] Parsing error context - user: ${JSON.stringify(userInfo)}, command: ${ctx.message?.text?.split(' ')[0] || 'unknown'}`);
1316
+ console.error(`[telegram-bot] User input text: ${ctx.message?.text || 'none'}`);
1311
1317
  if (ctx.message?.text) {
1318
+ const visibleInput = makeSpecialCharsVisible(ctx.message.text, { maxLength: 500 });
1319
+ console.error(`[telegram-bot] User input (special chars visible): ${visibleInput}`);
1312
1320
  const cleanedInput = cleanNonPrintableChars(ctx.message.text);
1313
- const visibleInput = makeSpecialCharsVisible(cleanedInput, { maxLength: 150 });
1314
- if (visibleInput !== cleanedInput) errorMessage += `\n\n📝 Your input (with special chars visible):\n\`${escapeMarkdown(visibleInput)}\``;
1315
- }
1316
- if (VERBOSE) {
1317
- const escapedError = escapeMarkdown(error.message || 'Unknown error');
1318
- errorMessage += `\n\n🔍 Debug info: ${escapedError}\nUpdate ID: ${ctx.update.update_id}`;
1321
+ if (cleanedInput !== ctx.message.text) {
1322
+ console.error(`[telegram-bot] ${ctx.message.text.length - cleanedInput.length} hidden character(s) detected in input`);
1323
+ }
1319
1324
  }
1325
+
1326
+ // Issue #1460: Show user a simple, non-confusing message — all details are in the logs
1327
+ errorMessage = `❌ Failed to send formatted message. Please try your command again.\n\nIf the issue persists, contact support with Update ID: ${ctx.update.update_id}`;
1320
1328
  } else {
1321
1329
  // Build informative error message for other errors
1322
1330
  errorMessage = '❌ An error occurred while processing your request.\n\n';
@@ -1334,14 +1342,21 @@ bot.catch((error, ctx) => {
1334
1342
  if (VERBOSE) errorMessage += `\n\n🔍 Debug info: Update ID: ${ctx.update.update_id}`;
1335
1343
  }
1336
1344
 
1337
- ctx.reply(errorMessage, { parse_mode: 'Markdown' }).catch(replyError => {
1338
- console.error('Failed to send error message to user:', replyError);
1339
- // Try sending a simple text message without Markdown if Markdown parsing failed
1340
- const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
1341
- ctx.reply(plainMessage).catch(fallbackError => {
1342
- console.error('Failed to send fallback error message:', fallbackError);
1345
+ // Issue #1460: For parsing errors, always send as plain text (we already know Markdown is the problem)
1346
+ // For other errors, try Markdown first, then fall back to plain text
1347
+ if (isTelegramParsingError) {
1348
+ ctx.reply(errorMessage).catch(fallbackError => {
1349
+ console.error('Failed to send plain text error message:', fallbackError);
1343
1350
  });
1344
- });
1351
+ } else {
1352
+ ctx.reply(errorMessage, { parse_mode: 'Markdown' }).catch(replyError => {
1353
+ console.error('Failed to send error message to user:', replyError);
1354
+ const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
1355
+ ctx.reply(plainMessage).catch(fallbackError => {
1356
+ console.error('Failed to send fallback error message:', fallbackError);
1357
+ });
1358
+ });
1359
+ }
1345
1360
  }
1346
1361
  });
1347
1362