@link-assistant/hive-mind 1.38.0 → 1.38.1

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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.38.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 1525ecb: fix: prevent 'Failed to send formatted message' Telegram error by adding safeReply helper and escaping unescaped Markdown in bot messages
8
+
3
9
  ## 1.38.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.38.0",
3
+ "version": "1.38.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -558,25 +558,26 @@ function validateGitHubUrl(args, options = {}) {
558
558
  return { valid: true, parsed, normalizedUrl: url };
559
559
  }
560
560
 
561
- /**
562
- * Escape special characters for Telegram's legacy Markdown parser.
563
- * In Telegram's Markdown, these characters need escaping: _ * [ ] ( ) ~ ` > # + - = | { } . !
564
- * However, for plain text (not inside markup), we primarily need to escape _ and *
565
- * to prevent them from being interpreted as formatting.
566
- *
567
- * @param {string} text - Text to escape
568
- * @returns {string} Escaped text safe for Markdown parse_mode
569
- */
570
- /**
571
- * Execute a start-screen command and update the initial message with the result.
572
- * Used by both /solve and /hive commands to reduce code duplication.
573
- *
574
- * @param {Object} ctx - Telegram context
575
- * @param {Object} startingMessage - The initial message to update
576
- * @param {string} commandName - Command name (e.g., 'solve' or 'hive')
577
- * @param {string[]} args - Command arguments
578
- * @param {string} infoBlock - Info block with request details
579
- */
561
+ // Issue #1460/#1497: safeReply - try Markdown first, fall back to plain text on parsing errors
562
+ async function safeReply(ctx, text, options = {}) {
563
+ try {
564
+ return await ctx.reply(text, { parse_mode: 'Markdown', ...options });
565
+ } catch (error) {
566
+ const isParsingError = error.message && (error.message.includes("can't parse entities") || error.message.includes("Can't parse entities") || error.message.includes("can't find end of") || (error.message.includes('Bad Request') && error.message.includes('400')));
567
+ if (!isParsingError) throw error;
568
+ console.error(`[telegram-bot] safeReply: Markdown parsing failed: ${error.message}`);
569
+ console.error(`[telegram-bot] safeReply: Failing message (${Buffer.byteLength(text, 'utf-8')} bytes): ${text}`);
570
+ const plainText = text
571
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
572
+ .replace(/\\_/g, '_')
573
+ .replace(/\\\*/g, '*')
574
+ .replace(/\*([^*]+)\*/g, '$1')
575
+ .replace(/`([^`]+)`/g, '$1');
576
+ return await ctx.reply(plainText, { ...options, parse_mode: undefined });
577
+ }
578
+ }
579
+
580
+ // Execute a start-screen command and update the initial message with the result
580
581
  async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock) {
581
582
  const result = await executeStartScreen(commandName, args);
582
583
  const { chat, message_id } = startingMessage;
@@ -914,8 +915,7 @@ async function handleSolveCommand(ctx) {
914
915
  if (VERBOSE) {
915
916
  console.log('[VERBOSE] Multiple GitHub URLs found in replied message');
916
917
  }
917
- await ctx.reply(`❌ ${extraction.error}`, {
918
- parse_mode: 'Markdown',
918
+ await safeReply(ctx, `❌ ${escapeMarkdown(extraction.error)}`, {
919
919
  reply_to_message_id: ctx.message.message_id,
920
920
  });
921
921
  return;
@@ -931,7 +931,7 @@ async function handleSolveCommand(ctx) {
931
931
  if (VERBOSE) {
932
932
  console.log('[VERBOSE] No GitHub URL found in replied message');
933
933
  }
934
- await ctx.reply('❌ No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`\n\nOr with options: `/solve --model opus`', { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
934
+ await safeReply(ctx, '❌ No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`\n\nOr with options: `/solve --model opus`', { reply_to_message_id: ctx.message.message_id });
935
935
  return;
936
936
  }
937
937
  }
@@ -943,7 +943,7 @@ async function handleSolveCommand(ctx) {
943
943
  errorMsg += `\n\n💡 Did you mean: \`${validation.suggestion}\``;
944
944
  }
945
945
  errorMsg += '\n\nExample: `/solve https://github.com/owner/repo/issues/123`\n\nOr reply to a message containing a GitHub link with `/solve`';
946
- await ctx.reply(errorMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
946
+ await safeReply(ctx, errorMsg, { reply_to_message_id: ctx.message.message_id });
947
947
  return;
948
948
  }
949
949
 
@@ -963,19 +963,19 @@ async function handleSolveCommand(ctx) {
963
963
  // Validate model name with helpful error message (before yargs validation)
964
964
  const modelError = validateModelInArgs(args, solveTool);
965
965
  if (modelError) {
966
- await ctx.reply(`❌ ${modelError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
966
+ await safeReply(ctx, `❌ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
967
967
  return;
968
968
  }
969
969
  // Issue #1482: Validate --base-branch early to reject URLs and invalid branch names
970
970
  const branchError = validateBranchInArgs(args);
971
971
  if (branchError) {
972
- await ctx.reply(`❌ ${branchError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
972
+ await safeReply(ctx, `❌ ${escapeMarkdown(branchError)}`, { reply_to_message_id: ctx.message.message_id });
973
973
  return;
974
974
  }
975
975
  // Issue #1092: Detect malformed flag patterns like "-- model" (space after --)
976
976
  const { malformed, errors: malformedErrors } = detectMalformedFlags(args);
977
977
  if (malformed.length > 0) {
978
- await ctx.reply(`❌ ${malformedErrors.join('\n')}\n\nPlease check your option syntax.`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
978
+ await safeReply(ctx, `❌ ${escapeMarkdown(malformedErrors.join('\n'))}\n\nPlease check your option syntax.`, { reply_to_message_id: ctx.message.message_id });
979
979
  return;
980
980
  }
981
981
  // Validate merged arguments using solve's yargs config
@@ -994,8 +994,7 @@ async function handleSolveCommand(ctx) {
994
994
 
995
995
  testYargs.parse(args);
996
996
  } catch (error) {
997
- await ctx.reply(`❌ Invalid options: ${error.message || String(error)}\n\nUse /help to see available options`, {
998
- parse_mode: 'Markdown',
997
+ await safeReply(ctx, `❌ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
999
998
  reply_to_message_id: ctx.message.message_id,
1000
999
  });
1001
1000
  return;
@@ -1019,7 +1018,7 @@ async function handleSolveCommand(ctx) {
1019
1018
  const existingItem = solveQueue.findByUrl(normalizedUrl);
1020
1019
  if (existingItem) {
1021
1020
  const statusText = existingItem.status === 'starting' || existingItem.status === 'started' ? 'being processed' : 'already in the queue';
1022
- await ctx.reply(`❌ This URL is ${statusText}.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nStatus: ${existingItem.status}\n\n💡 Use /solve_queue to check the queue status.`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1021
+ await safeReply(ctx, `❌ This URL is ${statusText}.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nStatus: ${existingItem.status}\n\n💡 Use /solve\\_queue to check the queue status.`, { reply_to_message_id: ctx.message.message_id });
1023
1022
  return;
1024
1023
  }
1025
1024
 
@@ -1031,18 +1030,18 @@ async function handleSolveCommand(ctx) {
1031
1030
  // their command cannot be processed (e.g., disk full, server maintenance pending).
1032
1031
  // See: https://github.com/link-assistant/hive-mind/issues/1267
1033
1032
  if (check.rejected) {
1034
- await ctx.reply(`❌ Solve command rejected.\n\n${infoBlock}\n\n🚫 Reason: ${check.rejectReason}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1033
+ await safeReply(ctx, `❌ Solve command rejected.\n\n${infoBlock}\n\n🚫 Reason: ${escapeMarkdown(check.rejectReason || 'Unknown')}`, { reply_to_message_id: ctx.message.message_id });
1035
1034
  return;
1036
1035
  }
1037
1036
 
1038
1037
  if (check.canStart && queueStats.queued === 0) {
1039
- const startingMessage = await ctx.reply(`🚀 Starting solve command...\n\n${infoBlock}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1038
+ const startingMessage = await safeReply(ctx, `🚀 Starting solve command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1040
1039
  await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock);
1041
1040
  } else {
1042
1041
  const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool });
1043
1042
  let queueMessage = `📋 Solve command queued (position #${queueStats.queued + 1})\n\n${infoBlock}`;
1044
- if (check.reason) queueMessage += `\n\n⏳ Waiting: ${check.reason}`;
1045
- const queuedMessage = await ctx.reply(queueMessage, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1043
+ if (check.reason) queueMessage += `\n\n⏳ Waiting: ${escapeMarkdown(check.reason)}`;
1044
+ const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
1046
1045
  queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
1047
1046
  if (!solveQueue.executeCallback) solveQueue.executeCallback = createQueueExecuteCallback(executeStartScreen);
1048
1047
  }
@@ -1122,7 +1121,7 @@ async function handleHiveCommand(ctx) {
1122
1121
  let errorMsg = `❌ ${validation.error}`;
1123
1122
  if (validation.suggestion) errorMsg += `\n\n💡 Did you mean: \`${escapeMarkdown(validation.suggestion)}\``;
1124
1123
  errorMsg += '\n\nExample: `/hive https://github.com/owner/repo`';
1125
- await ctx.reply(errorMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1124
+ await safeReply(ctx, errorMsg, { reply_to_message_id: ctx.message.message_id });
1126
1125
  return;
1127
1126
  }
1128
1127
  // Normalize issues_list/pulls_list to base repo URL, or use cleaned URL
@@ -1149,13 +1148,13 @@ async function handleHiveCommand(ctx) {
1149
1148
  // Validate model name with helpful error message (before yargs validation)
1150
1149
  const hiveModelError = validateModelInArgs(args, hiveTool);
1151
1150
  if (hiveModelError) {
1152
- await ctx.reply(`❌ ${hiveModelError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1151
+ await safeReply(ctx, `❌ ${escapeMarkdown(hiveModelError)}`, { reply_to_message_id: ctx.message.message_id });
1153
1152
  return;
1154
1153
  }
1155
1154
  // Issue #1482: Validate branch flags early to reject URLs and invalid branch names
1156
1155
  const hiveBranchError = validateBranchInArgs(args);
1157
1156
  if (hiveBranchError) {
1158
- await ctx.reply(`❌ ${hiveBranchError}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1157
+ await safeReply(ctx, `❌ ${escapeMarkdown(hiveBranchError)}`, { reply_to_message_id: ctx.message.message_id });
1159
1158
  return;
1160
1159
  }
1161
1160
 
@@ -1175,8 +1174,7 @@ async function handleHiveCommand(ctx) {
1175
1174
 
1176
1175
  testYargs.parse(args);
1177
1176
  } catch (error) {
1178
- await ctx.reply(`❌ Invalid options: ${error.message || String(error)}\n\nUse /help to see available options`, {
1179
- parse_mode: 'Markdown',
1177
+ await safeReply(ctx, `❌ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
1180
1178
  reply_to_message_id: ctx.message.message_id,
1181
1179
  });
1182
1180
  return;
@@ -1193,7 +1191,7 @@ async function handleHiveCommand(ctx) {
1193
1191
  infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}🔒 Locked options: ${escapeMarkdown(hiveOverrides.join(' '))}`;
1194
1192
  }
1195
1193
 
1196
- const startingMessage = await ctx.reply(`🚀 Starting hive command...\n\n${infoBlock}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
1194
+ const startingMessage = await safeReply(ctx, `🚀 Starting hive command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1197
1195
  await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock);
1198
1196
  }
1199
1197