@link-assistant/hive-mind 1.69.10 → 1.69.12

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}}"
package/src/solve.mjs CHANGED
@@ -148,6 +148,14 @@ const cleanupWrapper = async () => {
148
148
  const interruptWrapper = createInterruptWrapper({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log });
149
149
  initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper, ({ code, reason }) => notifyIssueAboutPrePullRequestFailure({ code, reason, argv, globalState: global, $, log, getLogFile, shouldAttachLogs, attachLogToGitHub, sanitizeLogContent, rawCommand }));
150
150
  installGlobalExitHandlers();
151
+ const markFailureNotificationPosted = targetType => {
152
+ global.preExitFailureNotificationPosted = true;
153
+ if (targetType === 'pr') {
154
+ global.pullRequestFailureNotificationPosted = true;
155
+ } else if (targetType === 'issue') {
156
+ global.prePullRequestFailureNotificationPosted = true;
157
+ }
158
+ };
151
159
 
152
160
  // Now handle argument validation that was moved from early checks
153
161
  let issueUrl = argv['issue-url'] || argv._[0];
@@ -900,6 +908,7 @@ try {
900
908
  });
901
909
 
902
910
  if (logUploadSuccess) {
911
+ markFailureNotificationPosted('pr');
903
912
  await log(' ✅ Logs uploaded successfully');
904
913
  } else {
905
914
  // Issue #1212: Always show log upload failures (not just verbose)
@@ -924,6 +933,7 @@ try {
924
933
 
925
934
  const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: failureComment });
926
935
  if (posted.ok) {
936
+ markFailureNotificationPosted('pr');
927
937
  await log(` Posted failure comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
928
938
  }
929
939
  } catch (error) {
@@ -1078,6 +1088,7 @@ try {
1078
1088
  });
1079
1089
 
1080
1090
  if (logUploadSuccess) {
1091
+ markFailureNotificationPosted(logTargetType);
1081
1092
  await log(` 📎 Failure logs posted to ${logTargetLabel}`);
1082
1093
  } else {
1083
1094
  // Issue #1212: Always show log upload failures (not just verbose)
@@ -1,5 +1,7 @@
1
1
  import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
2
2
 
3
+ export const FORK_DIVERGENCE_RESOLUTION_OPTION = '--allow-fork-divergence-resolution-using-force-push-with-lease';
4
+
3
5
  const truncate = (value, maxLength = 2000) => {
4
6
  const text = value === null || value === undefined ? '' : String(value);
5
7
  if (text.length <= maxLength) return text;
@@ -8,10 +10,24 @@ const truncate = (value, maxLength = 2000) => {
8
10
 
9
11
  const fence = value => truncate(value || 'Unknown error').replaceAll('```', '` ` `');
10
12
 
13
+ const isForkDivergenceFailure = reason => {
14
+ const normalizedReason = String(reason || '').toLowerCase();
15
+ return normalizedReason.includes('fork divergence') || (normalizedReason.includes('fork') && normalizedReason.includes('non-fast-forward')) || normalizedReason.includes('force-with-lease');
16
+ };
17
+
11
18
  export function buildPrePullRequestFailureActionSection(reason = '') {
12
19
  const normalizedReason = String(reason || '').toLowerCase();
13
20
  const isForkOrRecoveryFailure = normalizedReason.includes('fork') || normalizedReason.includes('auto-recovery') || normalizedReason.includes('repository setup');
14
21
 
22
+ if (isForkDivergenceFailure(reason)) {
23
+ return `### What you can do
24
+ - If the fork's default branch can be overwritten safely, rerun with \`${FORK_DIVERGENCE_RESOLUTION_OPTION}\` to allow a guarded force-with-lease sync.
25
+ - If the fork has commits you need to preserve, resolve the divergence manually, then rerun the solver.
26
+ - If this requires elevated Hive Mind access, ask a Hive Mind administrator to handle the affected fork or repository.
27
+
28
+ Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this GitHub comment.`;
29
+ }
30
+
15
31
  if (isForkOrRecoveryFailure) {
16
32
  return `### What you can do
17
33
  - If the affected fork or repository belongs to you, remove, rename, archive, initialize, or otherwise repair it in GitHub, then rerun the solver.
@@ -36,6 +52,42 @@ export function shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState
36
52
  return getTrackedToolCommentIds().size === 0;
37
53
  }
38
54
 
55
+ export function resolvePreExitFailureNotificationTarget({ code, globalState }) {
56
+ if (code === 0) return null;
57
+ if (!globalState?.owner || !globalState?.repo) return null;
58
+ if (globalState.preExitFailureNotificationPosted || globalState.preExitFailureNotificationInProgress) return null;
59
+
60
+ const owner = globalState.owner;
61
+ const repo = globalState.repo;
62
+ const issueNumber = globalState.issueNumber || null;
63
+ const prNumber = globalState.createdPR?.number || globalState.prNumber || null;
64
+
65
+ if (prNumber) {
66
+ if (globalState.pullRequestFailureNotificationPosted || globalState.pullRequestFailureNotificationInProgress) return null;
67
+ return {
68
+ targetType: 'pr',
69
+ targetNumber: prNumber,
70
+ owner,
71
+ repo,
72
+ issueNumber,
73
+ prNumber,
74
+ };
75
+ }
76
+
77
+ if (!issueNumber) return null;
78
+ if (globalState.prePullRequestFailureNotificationPosted || globalState.prePullRequestFailureNotificationInProgress) return null;
79
+ if (getTrackedToolCommentIds().size !== 0) return null;
80
+
81
+ return {
82
+ targetType: 'issue',
83
+ targetNumber: issueNumber,
84
+ owner,
85
+ repo,
86
+ issueNumber,
87
+ prNumber: null,
88
+ };
89
+ }
90
+
39
91
  export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {}, logAttachmentAttempted = false }) {
40
92
  const tool = argv.tool || 'claude';
41
93
  const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
@@ -62,60 +114,124 @@ ${logLine}
62
114
  `;
63
115
  }
64
116
 
117
+ export function buildExistingPullRequestFailureComment({ reason, owner, repo, prNumber, issueNumber = null, argv = {}, logAttachmentAttempted = false }) {
118
+ const tool = argv.tool || 'claude';
119
+ const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
120
+ const issueLine = issueNumber ? `\n- **Linked issue**: #${issueNumber}` : '';
121
+ const logLine = logAttachmentAttempted ? 'Log attachment was attempted but failed. Check the solver terminal log for the complete failure output.' : 'Logs were not attached because `--attach-logs` was not enabled.';
122
+ const actionSection = buildPrePullRequestFailureActionSection(reason);
123
+
124
+ return `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
125
+
126
+ The automated solver stopped while continuing this existing pull request, so the failure details are posted here for review.
127
+
128
+ ### Failure
129
+ - **Repository**: \`${owner}/${repo}\`
130
+ - **Pull request**: #${prNumber}${issueLine}
131
+ - **Tool**: \`${tool}\`${modelLine}
132
+
133
+ **Reason**
134
+ \`\`\`text
135
+ ${fence(reason)}
136
+ \`\`\`
137
+
138
+ ${actionSection}
139
+
140
+ ${logLine}
141
+ `;
142
+ }
143
+
144
+ const markNotificationPosted = ({ globalState, targetType }) => {
145
+ globalState.preExitFailureNotificationPosted = true;
146
+ if (targetType === 'pr') {
147
+ globalState.pullRequestFailureNotificationPosted = true;
148
+ } else {
149
+ globalState.prePullRequestFailureNotificationPosted = true;
150
+ }
151
+ };
152
+
65
153
  export async function notifyIssueAboutPrePullRequestFailure(options) {
66
154
  const { code, reason, argv = {}, globalState = globalThis, $, log = async () => {}, getLogFile, shouldAttachLogs = false, attachLogToGitHub, sanitizeLogContent, rawCommand = null, postComment = postTrackedComment } = options;
67
155
 
68
- if (!shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState })) {
156
+ const target = resolvePreExitFailureNotificationTarget({ code, globalState });
157
+ if (!target) {
69
158
  return { notified: false, skipped: true };
70
159
  }
71
160
 
72
- const owner = globalState.owner;
73
- const repo = globalState.repo;
74
- const issueNumber = globalState.issueNumber;
75
- globalState.prePullRequestFailureNotificationInProgress = true;
161
+ const { owner, repo, issueNumber, prNumber, targetType, targetNumber } = target;
162
+ const targetLabel = targetType === 'pr' ? `pull request #${targetNumber}` : `issue #${targetNumber}`;
163
+ globalState.preExitFailureNotificationInProgress = true;
164
+ if (targetType === 'pr') {
165
+ globalState.pullRequestFailureNotificationInProgress = true;
166
+ } else {
167
+ globalState.prePullRequestFailureNotificationInProgress = true;
168
+ }
76
169
 
77
170
  try {
78
171
  if (shouldAttachLogs && getLogFile && attachLogToGitHub && sanitizeLogContent) {
79
- await log(`\n📄 Notifying issue #${issueNumber} about pre-PR failure with logs...`);
80
- const uploaded = await attachLogToGitHub({
81
- logFile: getLogFile(),
82
- targetType: 'issue',
83
- targetNumber: issueNumber,
84
- owner,
85
- repo,
86
- $,
87
- log,
88
- sanitizeLogContent,
89
- verbose: argv.verbose,
90
- errorMessage: `The solver stopped before creating a pull request.\n\nReason: ${reason || 'Unknown error'}`,
91
- requestedModel: argv.originalModel || argv.model,
92
- tool: argv.tool || 'claude',
93
- });
94
- if (uploaded) {
95
- globalState.prePullRequestFailureNotificationPosted = true;
96
- return { notified: true, method: 'log-upload' };
172
+ await log(`\n📄 Notifying ${targetLabel} about solver failure with logs...`);
173
+ const errorPrefix = targetType === 'pr' ? `The solver stopped while continuing pull request #${targetNumber}.` : 'The solver stopped before creating a pull request.';
174
+ try {
175
+ const uploaded = await attachLogToGitHub({
176
+ logFile: getLogFile(),
177
+ targetType,
178
+ targetNumber,
179
+ owner,
180
+ repo,
181
+ $,
182
+ log,
183
+ sanitizeLogContent,
184
+ verbose: argv.verbose,
185
+ errorMessage: `${errorPrefix}\n\nReason: ${reason || 'Unknown error'}`,
186
+ failureActionSection: buildPrePullRequestFailureActionSection(reason),
187
+ requestedModel: argv.originalModel || argv.model,
188
+ tool: argv.tool || 'claude',
189
+ });
190
+ if (uploaded) {
191
+ markNotificationPosted({ globalState, targetType });
192
+ return { notified: true, method: 'log-upload' };
193
+ }
194
+ } catch (error) {
195
+ const message = error && error.message ? error.message : String(error);
196
+ await log(` ⚠️ Could not upload solver failure log: ${message}`, { level: 'warning' });
97
197
  }
98
198
  }
99
199
 
100
- await log(`\n💬 Notifying issue #${issueNumber} about pre-PR failure...`);
101
- const body = buildPrePullRequestFailureComment({
102
- reason,
103
- owner,
104
- repo,
105
- issueNumber,
106
- argv,
107
- rawCommand,
108
- logAttachmentAttempted: shouldAttachLogs,
109
- });
110
- const posted = await postComment({ $, owner, repo, targetNumber: issueNumber, body });
200
+ await log(`\n💬 Notifying ${targetLabel} about solver failure...`);
201
+ const body =
202
+ targetType === 'pr'
203
+ ? buildExistingPullRequestFailureComment({
204
+ reason,
205
+ owner,
206
+ repo,
207
+ prNumber,
208
+ issueNumber,
209
+ argv,
210
+ rawCommand,
211
+ logAttachmentAttempted: shouldAttachLogs,
212
+ })
213
+ : buildPrePullRequestFailureComment({
214
+ reason,
215
+ owner,
216
+ repo,
217
+ issueNumber,
218
+ argv,
219
+ rawCommand,
220
+ logAttachmentAttempted: shouldAttachLogs,
221
+ });
222
+ const posted = await postComment({ $, owner, repo, targetNumber, body });
111
223
  if (posted.ok) {
112
- globalState.prePullRequestFailureNotificationPosted = true;
113
- await log(` ✅ Pre-PR failure comment posted to issue #${issueNumber}${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
224
+ markNotificationPosted({ globalState, targetType });
225
+ await log(` ✅ Solver failure comment posted to ${targetLabel}${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
114
226
  return { notified: true, method: 'comment', commentId: posted.commentId || null };
115
227
  }
116
- await log(` ⚠️ Could not post pre-PR failure comment: ${posted.stderr || 'unknown error'}`, { level: 'warning' });
228
+ await log(` ⚠️ Could not post solver failure comment: ${posted.stderr || 'unknown error'}`, { level: 'warning' });
117
229
  return { notified: false, error: posted.stderr || 'unknown error' };
118
230
  } finally {
231
+ globalState.preExitFailureNotificationInProgress = false;
232
+ if (targetType === 'pr') {
233
+ globalState.pullRequestFailureNotificationInProgress = false;
234
+ }
119
235
  globalState.prePullRequestFailureNotificationInProgress = false;
120
236
  }
121
237
  }
@@ -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) {