@link-assistant/hive-mind 1.69.11 → 1.69.13

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.
@@ -80,6 +80,10 @@ ru
80
80
  limits.current_week_sonnet_only "Текущая неделя (только Sonnet)"
81
81
  limits.disabled_by_admin "`--show-limits` отключён администратором бота."
82
82
  limits.disk_space "Дисковое пространство"
83
+ limits.duration_day_short "д"
84
+ limits.duration_hour_short "ч"
85
+ limits.duration_minute_short "мин"
86
+ limits.duration_second_short "с"
83
87
  limits.end "Конец"
84
88
  limits.five_hour_session "5-часовой сеанс"
85
89
  limits.five_min_load_avg "средняя нагрузка за 5 мин"
@@ -92,9 +96,41 @@ ru
92
96
  limits.passed "прошло"
93
97
  limits.plan "План"
94
98
  limits.ram "RAM"
99
+ limits.reason_claude_5_hour_session "Лимит 5-часового сеанса Claude {{currentPercent}}% (порог: {{thresholdPercent}})"
100
+ limits.reason_claude_running "Процесс Claude уже выполняется"
101
+ limits.reason_claude_weekly "Недельный лимит Claude {{currentPercent}}% (порог: {{thresholdPercent}})"
102
+ limits.reason_codex_5_hour_session "Лимит 5-часового сеанса Codex {{currentPercent}}% (порог: {{thresholdPercent}})"
103
+ limits.reason_codex_running "Процесс Codex уже выполняется"
104
+ limits.reason_codex_weekly "Недельный лимит Codex {{currentPercent}}% (порог: {{thresholdPercent}})"
105
+ limits.reason_cpu_usage "Использование CPU {{currentPercent}}% (порог: {{thresholdPercent}})"
106
+ limits.reason_disk_usage "Использование диска {{currentPercent}}% (порог: {{thresholdPercent}})"
107
+ limits.reason_gemini_running "Процесс Gemini CLI уже выполняется"
108
+ limits.reason_github_api "Использование GitHub API {{currentPercent}}% (порог: {{thresholdPercent}})"
109
+ limits.reason_min_interval "Минимальный интервал между командами ещё не прошёл"
110
+ limits.reason_qwen_running "Процесс Qwen Code уже выполняется"
111
+ limits.reason_ram_usage "Использование RAM {{currentPercent}}% (порог: {{thresholdPercent}})"
112
+ limits.reason_threshold_exceeded "Превышен порог {{metric}}"
113
+ limits.remaining "осталось {{duration}}"
95
114
  limits.requests "запросов"
96
115
  limits.resets_at "Сброс {{time}}"
97
116
  limits.resets_in "Сброс через {{duration}}"
117
+ limits.resource_limit_exceeded "Превышен лимит ресурсов"
118
+ limits.solve_queue_status "Статус очереди solve"
119
+ limits.queue_completed "Завершено"
120
+ limits.queue_failed "С ошибкой"
121
+ limits.queue_and_more "и ещё {{count}}"
122
+ limits.queue_pending "ожидает"
123
+ limits.queue_processing "выполняется"
124
+ limits.queue_processes "процессов: {{count}}"
125
+ limits.queue_status_cancelled "отменено"
126
+ limits.queue_status_failed "ошибка"
127
+ limits.queue_status_queued "в очереди"
128
+ limits.queue_status_started "запущено"
129
+ limits.queue_status_starting "запускается"
130
+ limits.queue_status_waiting "ожидание"
131
+ limits.queue_waiting_current_command "ожидание текущей команды"
132
+ limits.queue_waiting_in_queue "Ожидание в очереди"
133
+ limits.queues "Очереди"
98
134
  limits.seven_day_all_models "7 дней, все модели"
99
135
  limits.seven_day_sonnet_only "7 дней, только Sonnet"
100
136
  limits.session "сеанс"
@@ -107,10 +143,12 @@ ru
107
143
  telegram.fetching_limits "🔄 Получение лимитов использования..."
108
144
  telegram.gathering_version "🔄 Сбор информации о версии..."
109
145
  telegram.usage_limits_title "📊 *Лимиты использования*"
110
- telegram.version_information_title "🤖 *Информация о версии*"
146
+ telegram.version_information_title "🤖 *Информация о версиях*"
147
+ telegram.formatting_fallback_warning "⚠️ Обнаружена ошибка форматирования. Показываю обычный текст."
111
148
  telegram.limits_only_in_groups "❌ Команда /limits работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
112
149
  telegram.version_only_in_groups "❌ Команда /version работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
113
150
  telegram.solve_only_in_groups "❌ Команда {{commandDisplay}} работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
151
+ telegram.solve_queue_only_in_groups "❌ Команда /solve_queue работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
114
152
  telegram.hive_only_in_groups "❌ Команда /hive работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
115
153
  telegram.solve_disabled "❌ Команда solve отключена в этом экземпляре бота."
116
154
  telegram.hive_disabled "❌ Команда /hive отключена в этом экземпляре бота."
@@ -215,6 +253,20 @@ ru
215
253
  language.ru "Русский"
216
254
  language.zh "Китайский"
217
255
  language.hi "Хинди"
256
+ version.ai_agents "AI-агенты"
257
+ version.browsers "Браузеры"
258
+ version.browser_automation "Автоматизация браузера"
259
+ version.connected "подключено"
260
+ version.development_tools "Инструменты разработки"
261
+ version.environment "Среда"
262
+ version.architecture "Архитектура"
263
+ version.kernel "Ядро"
264
+ version.not_connected "не подключено"
265
+ version.os "ОС"
266
+ version.platform "Платформа"
267
+ version.process_running_restart_needed "Запущенный процесс: `{{processVersion}}` (требуется перезапуск)"
268
+ version.system "Система"
269
+ version.version "Версия"
218
270
  prompt.user.issue_to_solve "Задача для решения: {{issueUrl}}"
219
271
  prompt.user.issue_linked_to_pr "Задача для решения: задача, связанная с PR #{{prNumber}}"
220
272
  prompt.user.prepared_branch "Подготовленная ветка: {{branchName}}"
@@ -80,6 +80,10 @@ zh
80
80
  limits.current_week_sonnet_only "本周(仅 Sonnet)"
81
81
  limits.disabled_by_admin "`--show-limits` 已被机器人管理员禁用。"
82
82
  limits.disk_space "磁盘空间"
83
+ limits.duration_day_short "天"
84
+ limits.duration_hour_short "小时"
85
+ limits.duration_minute_short "分钟"
86
+ limits.duration_second_short "秒"
83
87
  limits.end "结束"
84
88
  limits.five_hour_session "5 小时会话"
85
89
  limits.five_min_load_avg "5 分钟平均负载"
@@ -92,9 +96,41 @@ zh
92
96
  limits.passed "已过"
93
97
  limits.plan "方案"
94
98
  limits.ram "内存"
99
+ limits.reason_claude_5_hour_session "Claude 5 小时会话限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
100
+ limits.reason_claude_running "Claude 进程已在运行"
101
+ limits.reason_claude_weekly "Claude 每周限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
102
+ limits.reason_codex_5_hour_session "Codex 5 小时会话限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
103
+ limits.reason_codex_running "Codex 进程已在运行"
104
+ limits.reason_codex_weekly "Codex 每周限额为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
105
+ limits.reason_cpu_usage "CPU 使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
106
+ limits.reason_disk_usage "磁盘使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
107
+ limits.reason_gemini_running "Gemini CLI 进程已在运行"
108
+ limits.reason_github_api "GitHub API 使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
109
+ limits.reason_min_interval "尚未达到命令之间的最小间隔"
110
+ limits.reason_qwen_running "Qwen Code 进程已在运行"
111
+ limits.reason_ram_usage "内存使用率为 {{currentPercent}}%(阈值:{{thresholdPercent}})"
112
+ limits.reason_threshold_exceeded "{{metric}} 超过阈值"
113
+ limits.remaining "剩余 {{duration}}"
95
114
  limits.requests "请求"
96
115
  limits.resets_at "{{time}} 重置"
97
116
  limits.resets_in "{{duration}} 后重置"
117
+ limits.resource_limit_exceeded "资源限额已超出"
118
+ limits.solve_queue_status "Solve 队列状态"
119
+ limits.queue_completed "已完成"
120
+ limits.queue_failed "失败"
121
+ limits.queue_and_more "还有 {{count}} 个"
122
+ limits.queue_pending "等待"
123
+ limits.queue_processing "处理中"
124
+ limits.queue_processes "{{count}} 个进程"
125
+ limits.queue_status_cancelled "已取消"
126
+ limits.queue_status_failed "失败"
127
+ limits.queue_status_queued "已排队"
128
+ limits.queue_status_started "已启动"
129
+ limits.queue_status_starting "正在启动"
130
+ limits.queue_status_waiting "等待中"
131
+ limits.queue_waiting_current_command "等待当前命令"
132
+ limits.queue_waiting_in_queue "在队列中等待"
133
+ limits.queues "队列"
98
134
  limits.seven_day_all_models "7 天所有模型"
99
135
  limits.seven_day_sonnet_only "7 天仅 Sonnet"
100
136
  limits.session "会话"
@@ -108,9 +144,11 @@ zh
108
144
  telegram.gathering_version "🔄 正在收集版本信息……"
109
145
  telegram.usage_limits_title "📊 *使用限额*"
110
146
  telegram.version_information_title "🤖 *版本信息*"
147
+ telegram.formatting_fallback_warning "⚠️ 检测到格式错误。正在显示纯文本备用内容。"
111
148
  telegram.limits_only_in_groups "❌ /limits 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
112
149
  telegram.version_only_in_groups "❌ /version 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
113
150
  telegram.solve_only_in_groups "❌ {{commandDisplay}} 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
151
+ telegram.solve_queue_only_in_groups "❌ /solve_queue 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
114
152
  telegram.hive_only_in_groups "❌ /hive 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
115
153
  telegram.solve_disabled "❌ 此机器人实例已禁用 solve 命令。"
116
154
  telegram.hive_disabled "❌ 此机器人实例已禁用 /hive 命令。"
@@ -215,6 +253,20 @@ zh
215
253
  language.ru "俄语"
216
254
  language.zh "中文"
217
255
  language.hi "印地语"
256
+ version.ai_agents "AI 代理"
257
+ version.browsers "浏览器"
258
+ version.browser_automation "浏览器自动化"
259
+ version.connected "已连接"
260
+ version.development_tools "开发工具"
261
+ version.environment "环境"
262
+ version.architecture "架构"
263
+ version.kernel "内核"
264
+ version.not_connected "未连接"
265
+ version.os "操作系统"
266
+ version.platform "平台"
267
+ version.process_running_restart_needed "正在运行的进程:`{{processVersion}}`(需要重启)"
268
+ version.system "系统"
269
+ version.version "版本"
218
270
  prompt.user.issue_to_solve "要解决的问题:{{issueUrl}}"
219
271
  prompt.user.issue_linked_to_pr "要解决的问题:与 PR #{{prNumber}} 关联的问题"
220
272
  prompt.user.prepared_branch "为你准备的分支:{{branchName}}"
@@ -361,6 +361,20 @@ const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
361
361
  }
362
362
  };
363
363
 
364
+ const wasFileTouchedAfterCommit = async (tempDir, commitHash, fileName) => {
365
+ const changedCommitsResult = await $({ cwd: tempDir, silent: true })`git log --format=%H ${commitHash}..HEAD -- ${fileName}`;
366
+ if (changedCommitsResult.code === 0) {
367
+ return Boolean(changedCommitsResult.stdout?.trim());
368
+ }
369
+
370
+ if (changedCommitsResult.code !== 0) {
371
+ await log(` Could not inspect ${fileName} changes after initial commit`, { verbose: true });
372
+ await log(` git log output: ${changedCommitsResult.stderr || changedCommitsResult.stdout || 'no output'}`, { verbose: true });
373
+ }
374
+
375
+ return true;
376
+ };
377
+
364
378
  // Revert the CLAUDE.md or .gitkeep commit to restore original state
365
379
  export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash = null) => {
366
380
  try {
@@ -406,6 +420,16 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
406
420
 
407
421
  const commitToRevert = claudeCommitHash;
408
422
 
423
+ // Issue #1791: .gitkeep is a normal repository file in some projects, and
424
+ // user work may intentionally edit or delete it. Once later PR commits touch
425
+ // .gitkeep, final cleanup must not restore the pre-session version.
426
+ if (fileName === '.gitkeep' && (await wasFileTouchedAfterCommit(tempDir, commitToRevert, fileName))) {
427
+ await log(` ${fileName} changed after the initial auto-commit; leaving PR changes untouched`, {
428
+ verbose: true,
429
+ });
430
+ return;
431
+ }
432
+
409
433
  // APPROACH 3: Check for modifications before reverting (proactive detection)
410
434
  // This is the main strategy - detect if the file was modified after initial commit
411
435
  await log(` Checking if ${fileName} was modified since initial commit...`, { verbose: true });
@@ -40,7 +40,7 @@ const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFro
40
40
  const { executeStartScreen: executeStartScreenCommand, buildExecuteAndUpdateMessage } = await import('./telegram-command-execution.lib.mjs');
41
41
  const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
42
42
  const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
43
- const { safeReply } = await import('./telegram-safe-reply.lib.mjs');
43
+ const { installTelegramFormattingFallback, safeEditMessageText, safeReply } = await import('./telegram-safe-reply.lib.mjs');
44
44
  const { registerTerminalWatchCommand, startAutoTerminalWatchForSession } = await import('./telegram-terminal-watch-command.lib.mjs');
45
45
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
46
46
  const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
@@ -315,6 +315,7 @@ const { Telegraf } = telegrafModule;
315
315
  const bot = new Telegraf(BOT_TOKEN, {
316
316
  handlerTimeout: Infinity, // Remove default 90s timeout; command handlers like /solve spawn long-running processes
317
317
  });
318
+ installTelegramFormattingFallback(bot.telegram, { verbose: VERBOSE });
318
319
 
319
320
  // Track bot startup time (Unix seconds to match Telegram's message.date format)
320
321
  const BOT_START_TIME = Math.floor(Date.now() / 1000);
@@ -514,7 +515,7 @@ bot.command('help', async ctx => {
514
515
  allowTopicHint: topicId ? `TELEGRAM_ALLOWED_TOPICS="(${chatId} ${topicId})"` : '',
515
516
  });
516
517
 
517
- await ctx.reply(message, { parse_mode: 'Markdown' });
518
+ await safeReply(ctx, message, { fallbackLocale: helpLocale });
518
519
  });
519
520
 
520
521
  bot.command('limits', async ctx => {
@@ -564,10 +565,10 @@ bot.command('limits', async ctx => {
564
565
  const claudeError = limits.claude.success ? null : limits.claude.error;
565
566
  const codexError = limits.codex.success ? null : limits.codex.error;
566
567
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
567
- const queueStatus = await solveQueue.formatStatus();
568
+ const queueStatus = await solveQueue.formatStatus({ locale: userLocale });
568
569
  const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError, { locale: userLocale });
569
570
  const message = t('telegram.usage_limits_title', {}, { locale: userLocale }) + '\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus], { locale: userLocale });
570
- await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
571
+ await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown', fallbackLocale: userLocale, verbose: VERBOSE });
571
572
  });
572
573
  bot.command('version', async ctx => {
573
574
  VERBOSE && console.log('[VERBOSE] /version command received');
@@ -585,8 +586,8 @@ bot.command('version', async ctx => {
585
586
  reply_to_message_id: ctx.message.message_id,
586
587
  });
587
588
  const result = await getVersionInfo(VERBOSE);
588
- if (!result.success) return await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ ${escapeMarkdownV2(result.error, { preserveCodeBlocks: true })}`, { parse_mode: 'MarkdownV2' });
589
- await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, t('telegram.version_information_title', {}, { locale: versionLocale }) + '\n\n' + formatVersionMessage(result.versions), { parse_mode: 'Markdown' });
589
+ if (!result.success) return await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, `❌ ${escapeMarkdownV2(result.error, { preserveCodeBlocks: true })}`, { parse_mode: 'MarkdownV2', fallbackLocale: versionLocale, verbose: VERBOSE });
590
+ await safeEditMessageText(ctx.telegram, fetchingMessage.chat.id, fetchingMessage.message_id, undefined, t('telegram.version_information_title', {}, { locale: versionLocale }) + '\n\n' + formatVersionMessage(result.versions, { locale: versionLocale }), { parse_mode: 'Markdown', fallbackLocale: versionLocale, verbose: VERBOSE });
590
591
  });
591
592
 
592
593
  const { registerLanguageCommand } = await import('./telegram-language-command.lib.mjs');
@@ -598,7 +599,7 @@ registerAcceptInvitesCommand(bot, sharedCommandOpts);
598
599
  const { registerMergeCommand } = await import('./telegram-merge-command.lib.mjs');
599
600
  registerMergeCommand(bot, sharedCommandOpts);
600
601
  const { registerSolveQueueCommand } = await import('./telegram-solve-queue-command.lib.mjs');
601
- const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, { ...sharedCommandOpts, getSolveQueue });
602
+ const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, { ...sharedCommandOpts, getSolveQueue, safeReply, resolveLocale: resolveLocaleFromTelegramCtx });
602
603
  const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.mjs'); // #1688
603
604
  registerSubscribeCommands(bot, sharedCommandOpts);
604
605
  const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
@@ -848,7 +849,7 @@ async function handleSolveCommand(ctx) {
848
849
  await safeReply(ctx, t('telegram.url_session_running', { url: escapeMarkdown(normalizedUrl), session: activeSession.sessionName }, { locale: solveLocale }), { reply_to_message_id: ctx.message.message_id });
849
850
  return;
850
851
  }
851
- const check = await solveQueue.canStartCommand({ tool: solveTool }); // Skip Claude limits for agent (#1159)
852
+ const check = await solveQueue.canStartCommand({ tool: solveTool, locale: solveLocale }); // Skip Claude limits for agent (#1159)
852
853
  const queueStats = solveQueue.getStats();
853
854
  // Handle rejection: threshold strategy is 'reject' — fail immediately (issue #1267)
854
855
  if (check.rejected) {
@@ -1,19 +1,122 @@
1
- // Issue #1460/#1497: safeReply - try Markdown first, fall back to plain text on parsing errors.
1
+ import { normalizeLocale, t } from './i18n.lib.mjs';
2
+
3
+ const FORMATTING_FALLBACK_INSTALLED = Symbol.for('hiveMind.telegramFormattingFallbackInstalled');
4
+ const DEFAULT_FORMATTING_FALLBACK_WARNING = '⚠️ Formatting error detected. Showing plain text fallback.';
5
+ const FORMATTING_FALLBACK_WARNINGS = {
6
+ en: DEFAULT_FORMATTING_FALLBACK_WARNING,
7
+ ru: '⚠️ Обнаружена ошибка форматирования. Показываю обычный текст.',
8
+ zh: '⚠️ 检测到格式错误。正在显示纯文本备用内容。',
9
+ hi: '⚠️ फ़ॉर्मैटिंग त्रुटि मिली। सादा पाठ fallback दिखाया जा रहा है।',
10
+ };
11
+
12
+ function splitOptions(options = {}) {
13
+ const { fallbackLocale, locale, verbose, ...telegramOptions } = options || {};
14
+ return {
15
+ telegramOptions,
16
+ fallbackLocale: fallbackLocale || locale || null,
17
+ verbose: Boolean(verbose),
18
+ };
19
+ }
20
+
21
+ export function isTelegramFormattingError(error) {
22
+ const message = error?.description || error?.message || String(error || '');
23
+ return /can't parse entities/i.test(message) || /can't find end of/i.test(message) || /entity.*parse/i.test(message) || (/bad request/i.test(message) && /400|parse|entity/i.test(message));
24
+ }
25
+
26
+ export function stripTelegramMarkdown(text) {
27
+ return String(text ?? '')
28
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
29
+ .replace(/```([\s\S]*?)```/g, '$1')
30
+ .replace(/\\_/g, '_')
31
+ .replace(/\\\*/g, '*')
32
+ .replace(/\\`/g, '`')
33
+ .replace(/\\\[/g, '[')
34
+ .replace(/\*([^*\n]+)\*/g, '$1')
35
+ .replace(/_([^_\n]+)_/g, '$1')
36
+ .replace(/`([^`]+)`/g, '$1');
37
+ }
38
+
39
+ function getFormattingFallbackWarning(locale) {
40
+ const key = 'telegram.formatting_fallback_warning';
41
+ const normalizedLocale = normalizeLocale(locale);
42
+ const warning = t(key, {}, locale ? { locale } : {});
43
+ if (warning !== key && (normalizedLocale === 'en' || warning !== DEFAULT_FORMATTING_FALLBACK_WARNING)) return warning;
44
+ return FORMATTING_FALLBACK_WARNINGS[normalizedLocale] || DEFAULT_FORMATTING_FALLBACK_WARNING;
45
+ }
46
+
47
+ export function buildTelegramFormattingFallbackText(text, options = {}) {
48
+ const locale = options?.fallbackLocale || options?.locale || null;
49
+ return `${getFormattingFallbackWarning(locale)}\n\n${stripTelegramMarkdown(text)}`;
50
+ }
51
+
52
+ function logFormattingFailure(scope, error, text, verbose = false) {
53
+ const message = error?.description || error?.message || String(error || '');
54
+ console.error(`[telegram-bot] ${scope}: formatted Telegram message failed: ${message}`);
55
+ if (verbose) {
56
+ console.error(`[telegram-bot] ${scope}: Failing message (${Buffer.byteLength(String(text ?? ''), 'utf-8')} bytes): ${text}`);
57
+ }
58
+ }
59
+
60
+ // Issue #1460/#1497/#1788: try Markdown first, fall back to localized plain text on parsing errors.
2
61
  export async function safeReply(ctx, text, options = {}) {
62
+ const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
63
+ const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
3
64
  try {
4
- return await ctx.reply(text, { parse_mode: 'Markdown', ...options });
65
+ return await ctx.reply(text, firstOptions);
5
66
  } catch (error) {
6
- const message = error?.message || '';
7
- const isParsingError = message.includes("can't parse entities") || message.includes("Can't parse entities") || message.includes("can't find end of") || (message.includes('Bad Request') && message.includes('400'));
8
- if (!isParsingError) throw error;
9
- console.error(`[telegram-bot] safeReply: Markdown parsing failed: ${message}`);
10
- console.error(`[telegram-bot] safeReply: Failing message (${Buffer.byteLength(text, 'utf-8')} bytes): ${text}`);
11
- const plainText = text
12
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
13
- .replace(/\\_/g, '_')
14
- .replace(/\\\*/g, '*')
15
- .replace(/\*([^*]+)\*/g, '$1')
16
- .replace(/`([^`]+)`/g, '$1');
17
- return await ctx.reply(plainText, { ...options, parse_mode: undefined });
67
+ if (!isTelegramFormattingError(error)) throw error;
68
+ logFormattingFailure('safeReply', error, text, verbose);
69
+ const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
70
+ return await ctx.reply(fallbackText, { ...telegramOptions, parse_mode: undefined });
18
71
  }
19
72
  }
73
+
74
+ export async function safeEditMessageText(telegram, chatId, messageId, inlineMessageId, text, options = {}) {
75
+ const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
76
+ const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
77
+ try {
78
+ return await telegram.editMessageText(chatId, messageId, inlineMessageId, text, firstOptions);
79
+ } catch (error) {
80
+ if (!isTelegramFormattingError(error)) throw error;
81
+ logFormattingFailure('safeEditMessageText', error, text, verbose);
82
+ const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
83
+ return await telegram.editMessageText(chatId, messageId, inlineMessageId, fallbackText, { ...telegramOptions, parse_mode: undefined });
84
+ }
85
+ }
86
+
87
+ function wrapTelegramMethod(telegram, methodName, textIndex, optionsIndex, defaults = {}) {
88
+ const original = telegram?.[methodName];
89
+ if (typeof original !== 'function') return;
90
+
91
+ telegram[methodName] = async function wrappedTelegramMessageMethod(...args) {
92
+ const text = args[textIndex];
93
+ const originalOptions = args[optionsIndex] || {};
94
+ const { telegramOptions, fallbackLocale, verbose } = splitOptions(originalOptions);
95
+ args[optionsIndex] = telegramOptions;
96
+
97
+ try {
98
+ return await original.apply(this, args);
99
+ } catch (error) {
100
+ if (!isTelegramFormattingError(error) || typeof text !== 'string') throw error;
101
+ logFormattingFailure(methodName, error, text, verbose || defaults.verbose);
102
+ const retryArgs = [...args];
103
+ retryArgs[textIndex] = buildTelegramFormattingFallbackText(text, { fallbackLocale: fallbackLocale || defaults.fallbackLocale });
104
+ retryArgs[optionsIndex] = { ...telegramOptions, parse_mode: undefined };
105
+ return await original.apply(this, retryArgs);
106
+ }
107
+ };
108
+ }
109
+
110
+ export function installTelegramFormattingFallback(telegram, options = {}) {
111
+ if (!telegram || telegram[FORMATTING_FALLBACK_INSTALLED]) return telegram;
112
+
113
+ const defaults = {
114
+ fallbackLocale: options.fallbackLocale || options.locale || null,
115
+ verbose: Boolean(options.verbose),
116
+ };
117
+
118
+ wrapTelegramMethod(telegram, 'sendMessage', 1, 2, defaults);
119
+ wrapTelegramMethod(telegram, 'editMessageText', 3, 4, defaults);
120
+ telegram[FORMATTING_FALLBACK_INSTALLED] = true;
121
+ return telegram;
122
+ }
@@ -13,6 +13,15 @@
13
13
  * @see https://github.com/link-assistant/hive-mind/issues/1232
14
14
  */
15
15
 
16
+ import { t } from './i18n.lib.mjs';
17
+
18
+ const GROUP_ONLY_MESSAGE = '❌ The /solve_queue command only works in group chats. Please add this bot to a group and make it an admin.';
19
+
20
+ function commandText(key, params = {}, locale = null, fallback = key) {
21
+ const translated = t(key, params, locale ? { locale } : {});
22
+ return translated === key ? fallback : translated;
23
+ }
24
+
16
25
  /**
17
26
  * Registers the /solve_queue command handler with the bot
18
27
  * @param {Object} bot - The Telegraf bot instance
@@ -29,7 +38,7 @@
29
38
  * @returns {{ handleSolveQueueCommand: Function }} The command handler for use in text fallback
30
39
  */
31
40
  export function registerSolveQueueCommand(bot, options) {
32
- const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, getSolveQueue } = options;
41
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, getSolveQueue, safeReply, resolveLocale } = options;
33
42
 
34
43
  async function handleSolveQueueCommand(ctx) {
35
44
  VERBOSE && console.log('[VERBOSE] /solve_queue command received');
@@ -40,6 +49,8 @@ export function registerSolveQueueCommand(bot, options) {
40
49
  level: 'info',
41
50
  data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
42
51
  });
52
+ const locale = resolveLocale ? resolveLocale(ctx) : null;
53
+ const replyWithFallback = (text, replyOptions = {}) => (safeReply ? safeReply(ctx, text, replyOptions) : ctx.reply(text, { parse_mode: 'Markdown', ...replyOptions }));
43
54
 
44
55
  // Ignore messages sent before bot started
45
56
  if (isOldMessage(ctx)) {
@@ -55,8 +66,9 @@ export function registerSolveQueueCommand(bot, options) {
55
66
 
56
67
  if (!isGroupChat(ctx)) {
57
68
  VERBOSE && console.log('[VERBOSE] /solve_queue ignored: not a group chat');
58
- await ctx.reply('❌ The /solve_queue command only works in group chats. Please add this bot to a group and make it an admin.', {
69
+ await replyWithFallback(commandText('telegram.solve_queue_only_in_groups', {}, locale, GROUP_ONLY_MESSAGE), {
59
70
  reply_to_message_id: ctx.message.message_id,
71
+ fallbackLocale: locale,
60
72
  });
61
73
  return;
62
74
  }
@@ -65,7 +77,7 @@ export function registerSolveQueueCommand(bot, options) {
65
77
  if (!authorize(ctx)) {
66
78
  VERBOSE && console.log('[VERBOSE] /solve_queue ignored: not authorized');
67
79
  const errMsg = buildAuthErrorMessage ? buildAuthErrorMessage(ctx) : `❌ This chat (ID: ${ctx.chat.id}) is not authorized.`;
68
- await ctx.reply(errMsg, { reply_to_message_id: ctx.message.message_id });
80
+ await replyWithFallback(errMsg, { reply_to_message_id: ctx.message.message_id, fallbackLocale: locale });
69
81
  return;
70
82
  }
71
83
 
@@ -77,11 +89,11 @@ export function registerSolveQueueCommand(bot, options) {
77
89
  // Shows per-queue breakdown with first 5 items per queue and human-readable times
78
90
  // Processing counts are actual running system processes (via pgrep)
79
91
  // See: https://github.com/link-assistant/hive-mind/issues/1267
80
- const message = await solveQueue.formatDetailedStatus();
92
+ const message = await solveQueue.formatDetailedStatus({ locale });
81
93
 
82
- await ctx.reply(message, {
83
- parse_mode: 'Markdown',
94
+ await replyWithFallback(message, {
84
95
  reply_to_message_id: ctx.message.message_id,
96
+ fallbackLocale: locale,
85
97
  });
86
98
  }
87
99
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { exec } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
+ import { lt } from './limits-i18n.lib.mjs';
5
6
 
6
7
  const execAsync = promisify(exec);
7
8
 
@@ -111,8 +112,9 @@ export function formatThresholdPercent(ratio) {
111
112
  * @returns {string} Human-readable duration
112
113
  * @see https://github.com/link-assistant/hive-mind/issues/1267
113
114
  */
114
- export function formatDuration(ms) {
115
+ export function formatDuration(ms, options = {}) {
115
116
  if (ms < 0) ms = 0;
117
+ const locale = typeof options === 'string' ? options : options?.locale || null;
116
118
 
117
119
  const totalSeconds = Math.floor(ms / 1000);
118
120
  const days = Math.floor(totalSeconds / 86400);
@@ -120,11 +122,26 @@ export function formatDuration(ms) {
120
122
  const minutes = Math.floor((totalSeconds % 3600) / 60);
121
123
  const seconds = totalSeconds % 60;
122
124
 
125
+ const labels =
126
+ locale && locale !== 'en'
127
+ ? {
128
+ day: lt('duration_day_short', {}, { locale }),
129
+ hour: lt('duration_hour_short', {}, { locale }),
130
+ minute: lt('duration_minute_short', {}, { locale }),
131
+ second: lt('duration_second_short', {}, { locale }),
132
+ }
133
+ : {
134
+ day: 'd',
135
+ hour: 'h',
136
+ minute: 'm',
137
+ second: 's',
138
+ };
139
+
123
140
  const parts = [];
124
- if (days > 0) parts.push(`${days}d`);
125
- if (hours > 0) parts.push(`${hours}h`);
126
- if (minutes > 0) parts.push(`${minutes}m`);
127
- if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
141
+ if (days > 0) parts.push(`${days}${locale && locale !== 'en' ? ' ' : ''}${labels.day}`);
142
+ if (hours > 0) parts.push(`${hours}${locale && locale !== 'en' ? ' ' : ''}${labels.hour}`);
143
+ if (minutes > 0) parts.push(`${minutes}${locale && locale !== 'en' ? ' ' : ''}${labels.minute}`);
144
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}${locale && locale !== 'en' ? ' ' : ''}${labels.second}`);
128
145
 
129
146
  return parts.join(' ');
130
147
  }
@@ -136,9 +153,44 @@ export function formatDuration(ms) {
136
153
  * @param {number} threshold - Threshold ratio (0.0 - 1.0)
137
154
  * @returns {string} Human-readable reason
138
155
  */
139
- export function formatWaitingReason(metric, currentValue, threshold) {
156
+ export function formatWaitingReason(metric, currentValue, threshold, options = {}) {
157
+ const locale = typeof options === 'string' ? options : options?.locale || null;
140
158
  const thresholdPercent = formatThresholdPercent(threshold);
141
159
  const currentPercent = Math.round(currentValue);
160
+ const params = { currentPercent, thresholdPercent, metric };
161
+
162
+ if (locale && locale !== 'en') {
163
+ switch (metric) {
164
+ case 'ram':
165
+ return lt('reason_ram_usage', params, { locale });
166
+ case 'cpu':
167
+ return lt('reason_cpu_usage', params, { locale });
168
+ case 'disk':
169
+ return lt('reason_disk_usage', params, { locale });
170
+ case 'claude_5_hour_session':
171
+ return lt('reason_claude_5_hour_session', params, { locale });
172
+ case 'claude_weekly':
173
+ return lt('reason_claude_weekly', params, { locale });
174
+ case 'codex_5_hour_session':
175
+ return lt('reason_codex_5_hour_session', params, { locale });
176
+ case 'codex_weekly':
177
+ return lt('reason_codex_weekly', params, { locale });
178
+ case 'github':
179
+ return lt('reason_github_api', params, { locale });
180
+ case 'min_interval':
181
+ return lt('reason_min_interval', params, { locale });
182
+ case 'claude_running':
183
+ return lt('reason_claude_running', params, { locale });
184
+ case 'codex_running':
185
+ return lt('reason_codex_running', params, { locale });
186
+ case 'qwen_running':
187
+ return lt('reason_qwen_running', params, { locale });
188
+ case 'gemini_running':
189
+ return lt('reason_gemini_running', params, { locale });
190
+ default:
191
+ return lt('reason_threshold_exceeded', params, { locale });
192
+ }
193
+ }
142
194
 
143
195
  switch (metric) {
144
196
  case 'ram':