@link-assistant/hive-mind 2.0.27 → 2.0.29
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 +12 -0
- package/package.json +1 -1
- package/src/bot-lifecycle.lib.mjs +17 -1
- package/src/locales/en.lino +5 -5
- package/src/locales/hi.lino +5 -5
- package/src/locales/ru.lino +5 -5
- package/src/locales/zh.lino +5 -5
- package/src/queue-config.lib.mjs +1 -1
- package/src/session-monitor.lib.mjs +12 -5
- package/src/solve.mjs +14 -3
- package/src/solve.resource-diagnostics.lib.mjs +322 -0
- package/src/solve.restart-shared.lib.mjs +14 -0
- package/src/telegram-bot.mjs +1 -2
- package/src/telegram-solve-queue-command.lib.mjs +12 -17
- package/src/telegram-solve-queue.helpers.lib.mjs +5 -5
- package/src/telegram-solve-queue.lib.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.0.29
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0cafc64: Add solve resource diagnostics and Docker disk-usage fallback markers so Telegram completion messages can show full container filesystem usage even when the task container cannot be inspected after exit.
|
|
8
|
+
|
|
9
|
+
## 2.0.28
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 5b4f3df: Recommend and accept `/queue` instead of the legacy solve-prefixed queue commands, and recommend `/stop` for cancelling running sessions.
|
|
14
|
+
|
|
3
15
|
## 2.0.27
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @see https://github.com/link-assistant/hive-mind/issues/1927
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { RESOURCE_PHASE_BOT_HEARTBEAT, captureResourceSnapshot, summarizeResourceSnapshot } from './solve.resource-diagnostics.lib.mjs';
|
|
16
|
+
|
|
15
17
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 60 * 1000;
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -26,14 +28,28 @@ const DEFAULT_HEARTBEAT_INTERVAL_MS = 60 * 1000;
|
|
|
26
28
|
*
|
|
27
29
|
* @returns {{ start: () => void, stop: () => void, beat: () => void, get timer(): any }}
|
|
28
30
|
*/
|
|
29
|
-
export function createHeartbeat({ logger, getActiveSessionCount, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, processImpl = process, setIntervalImpl = setInterval, clearIntervalImpl = clearInterval } = {}) {
|
|
31
|
+
export function createHeartbeat({ logger, getActiveSessionCount, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, processImpl = process, setIntervalImpl = setInterval, clearIntervalImpl = clearInterval, captureResources = captureResourceSnapshot, resourceDiskPath = '/' } = {}) {
|
|
30
32
|
let timer = null;
|
|
31
33
|
|
|
32
34
|
const beat = () => {
|
|
33
35
|
try {
|
|
36
|
+
let resources = null;
|
|
37
|
+
try {
|
|
38
|
+
if (typeof captureResources === 'function') {
|
|
39
|
+
resources = summarizeResourceSnapshot(
|
|
40
|
+
captureResources({
|
|
41
|
+
phase: RESOURCE_PHASE_BOT_HEARTBEAT,
|
|
42
|
+
diskPath: resourceDiskPath,
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
resources = null;
|
|
48
|
+
}
|
|
34
49
|
logger.heartbeat({
|
|
35
50
|
activeSessions: typeof getActiveSessionCount === 'function' ? getActiveSessionCount(false) : undefined,
|
|
36
51
|
uptimeSec: Math.floor(processImpl.uptime()),
|
|
52
|
+
resources,
|
|
37
53
|
});
|
|
38
54
|
} catch {
|
|
39
55
|
/* heartbeat must never crash the bot */
|
package/src/locales/en.lino
CHANGED
|
@@ -355,7 +355,7 @@ en
|
|
|
355
355
|
queue
|
|
356
356
|
only
|
|
357
357
|
in
|
|
358
|
-
groups "❌ The /
|
|
358
|
+
groups "❌ The /queue command only works in group chats. Please add this bot to a group and make it an admin."
|
|
359
359
|
disabled "❌ The solve command is disabled on this bot instance."
|
|
360
360
|
rejected """
|
|
361
361
|
❌ Solve command rejected.
|
|
@@ -413,7 +413,7 @@ en
|
|
|
413
413
|
URL: {{url}}
|
|
414
414
|
Status: {{status}}
|
|
415
415
|
|
|
416
|
-
💡 Use /
|
|
416
|
+
💡 Use /queue to check the queue status.
|
|
417
417
|
"""
|
|
418
418
|
session
|
|
419
419
|
running """
|
|
@@ -422,7 +422,7 @@ en
|
|
|
422
422
|
URL: {{url}}
|
|
423
423
|
Session: `{{session}}`
|
|
424
424
|
|
|
425
|
-
💡 Wait for the current session to complete, or use /
|
|
425
|
+
💡 Wait for the current session to complete, or use /stop to cancel it.
|
|
426
426
|
"""
|
|
427
427
|
issues
|
|
428
428
|
list
|
|
@@ -515,7 +515,7 @@ en
|
|
|
515
515
|
detail "Tool aliases imply `--tool <tool>`: `/codex <github-url>` equals `/solve <github-url> --tool codex`"
|
|
516
516
|
reply "Or reply to a message with a GitHub link: `/solve`"
|
|
517
517
|
disabled "*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ Disabled"
|
|
518
|
-
queue "`/
|
|
518
|
+
queue "`/queue` - Show solve queue status"
|
|
519
519
|
locked
|
|
520
520
|
options "🔒 Locked options: `{{options}}`"
|
|
521
521
|
task
|
|
@@ -553,7 +553,7 @@ en
|
|
|
553
553
|
isolation
|
|
554
554
|
mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
|
|
555
555
|
group
|
|
556
|
-
note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /
|
|
556
|
+
note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
|
|
557
557
|
common
|
|
558
558
|
options "🔧 *Common Options:*"
|
|
559
559
|
model
|
package/src/locales/hi.lino
CHANGED
|
@@ -355,7 +355,7 @@ hi
|
|
|
355
355
|
queue
|
|
356
356
|
only
|
|
357
357
|
in
|
|
358
|
-
groups "❌ /
|
|
358
|
+
groups "❌ /queue कमांड केवल समूह चैट में काम करती है। कृपया बॉट को समूह में जोड़ें और व्यवस्थापक बनाएँ।"
|
|
359
359
|
disabled "❌ इस बॉट इंस्टेंस पर solve कमांड अक्षम है।"
|
|
360
360
|
rejected """
|
|
361
361
|
❌ Solve कमांड अस्वीकृत।
|
|
@@ -413,7 +413,7 @@ hi
|
|
|
413
413
|
URL: {{url}}
|
|
414
414
|
स्थिति: {{status}}
|
|
415
415
|
|
|
416
|
-
💡 कतार स्थिति देखने के लिए /
|
|
416
|
+
💡 कतार स्थिति देखने के लिए /queue का उपयोग करें।
|
|
417
417
|
"""
|
|
418
418
|
session
|
|
419
419
|
running """
|
|
@@ -422,7 +422,7 @@ hi
|
|
|
422
422
|
URL: {{url}}
|
|
423
423
|
सत्र: `{{session}}`
|
|
424
424
|
|
|
425
|
-
💡 वर्तमान सत्र के पूरा होने की प्रतीक्षा करें, या रद्द करने के लिए /
|
|
425
|
+
💡 वर्तमान सत्र के पूरा होने की प्रतीक्षा करें, या रद्द करने के लिए /stop का उपयोग करें।
|
|
426
426
|
"""
|
|
427
427
|
issues
|
|
428
428
|
list
|
|
@@ -515,7 +515,7 @@ hi
|
|
|
515
515
|
detail "Tool aliases `--tool <tool>` लगाते हैं: `/codex <github-url>` का अर्थ `/solve <github-url> --tool codex` है"
|
|
516
516
|
reply "या GitHub लिंक वाले संदेश का उत्तर दें: `/solve`"
|
|
517
517
|
disabled "*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ अक्षम"
|
|
518
|
-
queue "`/
|
|
518
|
+
queue "`/queue` - solve queue status दिखाएँ"
|
|
519
519
|
locked
|
|
520
520
|
options "🔒 लॉक किए गए विकल्प: `{{options}}`"
|
|
521
521
|
task
|
|
@@ -553,7 +553,7 @@ hi
|
|
|
553
553
|
isolation
|
|
554
554
|
mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
|
|
555
555
|
group
|
|
556
|
-
note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /
|
|
556
|
+
note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
|
|
557
557
|
common
|
|
558
558
|
options "🔧 *Common Options:*"
|
|
559
559
|
model
|
package/src/locales/ru.lino
CHANGED
|
@@ -355,7 +355,7 @@ ru
|
|
|
355
355
|
queue
|
|
356
356
|
only
|
|
357
357
|
in
|
|
358
|
-
groups "❌ Команда /
|
|
358
|
+
groups "❌ Команда /queue работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
|
|
359
359
|
disabled "❌ Команда solve отключена в этом экземпляре бота."
|
|
360
360
|
rejected """
|
|
361
361
|
❌ Команда solve отклонена.
|
|
@@ -413,7 +413,7 @@ ru
|
|
|
413
413
|
URL: {{url}}
|
|
414
414
|
Статус: {{status}}
|
|
415
415
|
|
|
416
|
-
💡 Используйте /
|
|
416
|
+
💡 Используйте /queue для проверки очереди.
|
|
417
417
|
"""
|
|
418
418
|
session
|
|
419
419
|
running """
|
|
@@ -422,7 +422,7 @@ ru
|
|
|
422
422
|
URL: {{url}}
|
|
423
423
|
Сеанс: `{{session}}`
|
|
424
424
|
|
|
425
|
-
💡 Дождитесь завершения текущего сеанса или используйте /
|
|
425
|
+
💡 Дождитесь завершения текущего сеанса или используйте /stop для отмены.
|
|
426
426
|
"""
|
|
427
427
|
issues
|
|
428
428
|
list
|
|
@@ -515,7 +515,7 @@ ru
|
|
|
515
515
|
detail "Алиасы инструментов добавляют `--tool <tool>`: `/codex <github-url>` равно `/solve <github-url> --tool codex`"
|
|
516
516
|
reply "Или ответьте на сообщение со ссылкой GitHub: `/solve`"
|
|
517
517
|
disabled "*/solve* (алиасы: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ Отключено"
|
|
518
|
-
queue "`/
|
|
518
|
+
queue "`/queue` - Показать состояние очереди solve"
|
|
519
519
|
locked
|
|
520
520
|
options "🔒 Заблокированные опции: `{{options}}`"
|
|
521
521
|
task
|
|
@@ -553,7 +553,7 @@ ru
|
|
|
553
553
|
isolation
|
|
554
554
|
mode "🔒 *Режим изоляции:* `{{isolationBackend}}` (экспериментально)"
|
|
555
555
|
group
|
|
556
|
-
note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /
|
|
556
|
+
note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
|
|
557
557
|
common
|
|
558
558
|
options "🔧 *Общие опции:*"
|
|
559
559
|
model
|
package/src/locales/zh.lino
CHANGED
|
@@ -355,7 +355,7 @@ zh
|
|
|
355
355
|
queue
|
|
356
356
|
only
|
|
357
357
|
in
|
|
358
|
-
groups "❌ /
|
|
358
|
+
groups "❌ /queue 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
|
|
359
359
|
disabled "❌ 此机器人实例已禁用 solve 命令。"
|
|
360
360
|
rejected """
|
|
361
361
|
❌ Solve 命令被拒绝。
|
|
@@ -413,7 +413,7 @@ zh
|
|
|
413
413
|
URL:{{url}}
|
|
414
414
|
状态:{{status}}
|
|
415
415
|
|
|
416
|
-
💡 使用 /
|
|
416
|
+
💡 使用 /queue 查看队列状态。
|
|
417
417
|
"""
|
|
418
418
|
session
|
|
419
419
|
running """
|
|
@@ -422,7 +422,7 @@ zh
|
|
|
422
422
|
URL:{{url}}
|
|
423
423
|
会话:`{{session}}`
|
|
424
424
|
|
|
425
|
-
💡 等待当前会话完成,或使用 /
|
|
425
|
+
💡 等待当前会话完成,或使用 /stop 取消。
|
|
426
426
|
"""
|
|
427
427
|
issues
|
|
428
428
|
list
|
|
@@ -515,7 +515,7 @@ zh
|
|
|
515
515
|
detail "工具别名会添加 `--tool <tool>`:`/codex <github-url>` 等同于 `/solve <github-url> --tool codex`"
|
|
516
516
|
reply "也可以回复包含 GitHub 链接的消息:`/solve`"
|
|
517
517
|
disabled "*/solve*(别名:*/do*、*/continue*、*/claude*、*/codex*、*/opencode*、*/agent*、*/gemini*、*/qwen*)- ❌ 已禁用"
|
|
518
|
-
queue "`/
|
|
518
|
+
queue "`/queue` - 显示 solve 队列状态"
|
|
519
519
|
locked
|
|
520
520
|
options "🔒 锁定选项:`{{options}}`"
|
|
521
521
|
task
|
|
@@ -553,7 +553,7 @@ zh
|
|
|
553
553
|
isolation
|
|
554
554
|
mode "🔒 *隔离模式:* `{{isolationBackend}}`(实验性)"
|
|
555
555
|
group
|
|
556
|
-
note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/hive、/
|
|
556
|
+
note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/hive、/queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
|
|
557
557
|
common
|
|
558
558
|
options "🔧 *常用选项:*"
|
|
559
559
|
model
|
package/src/queue-config.lib.mjs
CHANGED
|
@@ -282,7 +282,7 @@ export const QUEUE_CONFIG = {
|
|
|
282
282
|
|
|
283
283
|
// Display
|
|
284
284
|
// Maximum number of items shown per section (pending/processing/completed/failed)
|
|
285
|
-
// in the /
|
|
285
|
+
// in the /queue detailed status before collapsing into a
|
|
286
286
|
// "... and N more" line. Keeps the Telegram message under the 4096-char cap.
|
|
287
287
|
// See: https://github.com/link-assistant/hive-mind/issues/1837
|
|
288
288
|
MAX_DISPLAY_ITEMS_PER_QUEUE: parseIntWithDefault('HIVE_MIND_MAX_DISPLAY_ITEMS_PER_QUEUE', 5),
|
|
@@ -343,10 +343,11 @@ async function resolvePullRequestUrlFromSessionLog(logPath, ctx, { verbose = fal
|
|
|
343
343
|
* build the Telegram extraSection. Returns an empty string if there is no
|
|
344
344
|
* repository or docker filesystem data to show.
|
|
345
345
|
*/
|
|
346
|
-
async function buildDiskDiagnosticsExtraSection(logPath, { verbose = false, readFile = fs.readFile, isolationBackend = null, containerFilesystemStartBytes = null, containerFilesystemAfterBytes = null } = {}) {
|
|
346
|
+
export async function buildDiskDiagnosticsExtraSection(logPath, { verbose = false, readFile = fs.readFile, isolationBackend = null, containerFilesystemStartBytes = null, containerFilesystemAfterBytes = null } = {}) {
|
|
347
347
|
if (!logPath && !Number.isFinite(containerFilesystemStartBytes) && !Number.isFinite(containerFilesystemAfterBytes)) return '';
|
|
348
348
|
try {
|
|
349
349
|
const diskLib = await import('./solve.disk-diagnostics.lib.mjs');
|
|
350
|
+
const resourceLib = await import('./solve.resource-diagnostics.lib.mjs');
|
|
350
351
|
let logText = '';
|
|
351
352
|
if (logPath) {
|
|
352
353
|
try {
|
|
@@ -358,11 +359,17 @@ async function buildDiskDiagnosticsExtraSection(logPath, { verbose = false, read
|
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
const parsed = diskLib.parseDiskMarkers(logText);
|
|
361
|
-
|
|
362
|
+
const parsedResources = resourceLib.parseResourceMarkers(logText);
|
|
363
|
+
const bestResourceMarker = resourceLib.selectBestDiskResourceMarker(parsedResources);
|
|
364
|
+
const solveStartResourceMarker = parsedResources.byPhase?.[resourceLib.RESOURCE_PHASE_SOLVE_START] || null;
|
|
365
|
+
const useResourceFallback = String(isolationBackend || '').toLowerCase() === 'docker' && !Number.isFinite(containerFilesystemAfterBytes) && Number.isFinite(bestResourceMarker?.disk?.usedBytes);
|
|
366
|
+
const effectiveContainerFilesystemAfterBytes = useResourceFallback ? bestResourceMarker.disk.usedBytes : containerFilesystemAfterBytes;
|
|
367
|
+
const effectiveContainerFilesystemStartBytes = useResourceFallback && Number.isFinite(solveStartResourceMarker?.disk?.usedBytes) ? solveStartResourceMarker.disk.usedBytes : containerFilesystemStartBytes;
|
|
368
|
+
if (!parsed.afterClone && !parsed.afterAgent && !Number.isFinite(effectiveContainerFilesystemStartBytes) && !Number.isFinite(effectiveContainerFilesystemAfterBytes)) return '';
|
|
362
369
|
return diskLib.formatDiskDiagnosticsBlock(parsed, {
|
|
363
370
|
isolationBackend,
|
|
364
|
-
containerFilesystemStartBytes,
|
|
365
|
-
containerFilesystemAfterBytes,
|
|
371
|
+
containerFilesystemStartBytes: effectiveContainerFilesystemStartBytes,
|
|
372
|
+
containerFilesystemAfterBytes: effectiveContainerFilesystemAfterBytes,
|
|
366
373
|
});
|
|
367
374
|
} catch (error) {
|
|
368
375
|
if (verbose) {
|
|
@@ -1274,7 +1281,7 @@ export async function getRunningTrackedIsolationSessions(verbose = false, option
|
|
|
1274
1281
|
|
|
1275
1282
|
/**
|
|
1276
1283
|
* Return the currently-executing tracked sessions with the details needed to
|
|
1277
|
-
* render them as a clickable list in `/
|
|
1284
|
+
* render them as a clickable list in `/queue`: the issue/PR
|
|
1278
1285
|
* `url`, the `tool`, the start time, and (for isolation sessions) the backend
|
|
1279
1286
|
* status. Both isolation and non-isolation screen sessions are included so the
|
|
1280
1287
|
* list matches what is actually executing — the queue's own in-memory
|
package/src/solve.mjs
CHANGED
|
@@ -49,7 +49,8 @@ const { runKeepWorkingUntilDone } = await import('./solve.keep-working.lib.mjs')
|
|
|
49
49
|
const { runEscalation } = await import('./solve.escalate.lib.mjs');
|
|
50
50
|
const { finalizeSolveProcess } = await import('./solve.finalize.lib.mjs');
|
|
51
51
|
const exitHandler = await import('./exit-handler.lib.mjs');
|
|
52
|
-
const { initializeExitHandler, installGlobalExitHandlers, safeExit, logActiveHandles } = exitHandler;
|
|
52
|
+
const { initializeExitHandler, installGlobalExitHandlers, safeExit: baseSafeExit, logActiveHandles } = exitHandler;
|
|
53
|
+
const { RESOURCE_PHASE_AFTER_AGENT, RESOURCE_PHASE_AFTER_CLONE, RESOURCE_PHASE_SOLVE_EXIT, RESOURCE_PHASE_SOLVE_START, recordResourceSnapshot } = await import('./solve.resource-diagnostics.lib.mjs');
|
|
53
54
|
const { createInterruptWrapper } = await import('./solve.interrupt.lib.mjs');
|
|
54
55
|
// Issue #1823: working-session guard for --do-not-shutdown-in-the-middle-of-working-session.
|
|
55
56
|
const { configureWorkingSession, beginWorkingSession, endWorkingSession } = await import('./working-session.lib.mjs');
|
|
@@ -69,9 +70,7 @@ const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions
|
|
|
69
70
|
const { validateAndExitOnInvalidClaudeSubAgentModel, validateAndExitOnInvalidModel } = await import('./models/index.mjs');
|
|
70
71
|
const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs');
|
|
71
72
|
const { handleAutoForkOption, handleMaintainerForkAccess } = await import('./solve.fork-detection.lib.mjs');
|
|
72
|
-
// Initialize log file early (before argument parsing) to capture all output
|
|
73
73
|
const logFile = await initializeLogFile(null);
|
|
74
|
-
// Log version and raw command IMMEDIATELY after log file initialization
|
|
75
74
|
const versionInfo = await getVersionInfo();
|
|
76
75
|
await log('');
|
|
77
76
|
await log(`🚀 solve v${versionInfo}`);
|
|
@@ -80,6 +79,15 @@ await log('🔧 Raw command executed:');
|
|
|
80
79
|
await log(` ${rawCommand}`);
|
|
81
80
|
await log('');
|
|
82
81
|
|
|
82
|
+
let finalResourceSnapshotRecorded = false;
|
|
83
|
+
const safeExit = async (code = 0, reason = 'Process completed', options = {}) => {
|
|
84
|
+
if (!finalResourceSnapshotRecorded) {
|
|
85
|
+
finalResourceSnapshotRecorded = true;
|
|
86
|
+
await recordResourceSnapshot({ phase: RESOURCE_PHASE_SOLVE_EXIT, log, diskPath: '/', label: `solve exit ${code}` });
|
|
87
|
+
}
|
|
88
|
+
return await baseSafeExit(code, reason, options);
|
|
89
|
+
};
|
|
90
|
+
|
|
83
91
|
let argv;
|
|
84
92
|
try {
|
|
85
93
|
argv = await parseArguments(yargs, hideBin);
|
|
@@ -98,6 +106,7 @@ configureGitHubRateLimitLogging({
|
|
|
98
106
|
enabled: argv.githubRateLimitsLogging === true,
|
|
99
107
|
log,
|
|
100
108
|
});
|
|
109
|
+
await recordResourceSnapshot({ phase: RESOURCE_PHASE_SOLVE_START, log, diskPath: '/', label: 'solve start' });
|
|
101
110
|
|
|
102
111
|
// Early logs go to cwd; custom log dir takes effect after argv is parsed
|
|
103
112
|
// Conditionally import tool-specific functions after argv is parsed
|
|
@@ -507,6 +516,7 @@ try {
|
|
|
507
516
|
});
|
|
508
517
|
|
|
509
518
|
cleanupContext.diskDiagnostics = { beforeBytes: await recordAfterCloneSize({ tempDir, log }) };
|
|
519
|
+
await recordResourceSnapshot({ phase: RESOURCE_PHASE_AFTER_CLONE, log, diskPath: '/', label: 'after repository clone' });
|
|
510
520
|
|
|
511
521
|
// Verify default branch and status using the new module
|
|
512
522
|
// Pass argv, owner, repo, issueUrl for empty repository auto-initialization (--auto-init-repository)
|
|
@@ -830,6 +840,7 @@ try {
|
|
|
830
840
|
} catch (diskError) {
|
|
831
841
|
await log(`⚠️ Disk-size measurement failed: ${cleanErrorMessage(diskError)}`, { level: 'warning', verbose: true });
|
|
832
842
|
}
|
|
843
|
+
await recordResourceSnapshot({ phase: RESOURCE_PHASE_AFTER_AGENT, log, diskPath: '/', label: 'after AI execution' });
|
|
833
844
|
|
|
834
845
|
// Issue #1823: Mark the end of the AI working session. If a graceful-shutdown interrupt arrived
|
|
835
846
|
// during the session (deferred by the working-session guard), honor it now: auto-commit any
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
export const RESOURCE_MARKER_PREFIX = '📈 [RESOURCES]';
|
|
5
|
+
|
|
6
|
+
export const RESOURCE_PHASE_SOLVE_START = 'solve_start';
|
|
7
|
+
export const RESOURCE_PHASE_AFTER_CLONE = 'after_clone';
|
|
8
|
+
export const RESOURCE_PHASE_AFTER_AGENT = 'after_agent';
|
|
9
|
+
export const RESOURCE_PHASE_SOLVE_EXIT = 'solve_exit';
|
|
10
|
+
export const RESOURCE_PHASE_RESTART_BEFORE = 'restart_before';
|
|
11
|
+
export const RESOURCE_PHASE_RESTART_AFTER = 'restart_after';
|
|
12
|
+
export const RESOURCE_PHASE_BOT_HEARTBEAT = 'bot_heartbeat';
|
|
13
|
+
|
|
14
|
+
const RESOURCE_PHASES_BY_PREFERENCE = [RESOURCE_PHASE_SOLVE_EXIT, RESOURCE_PHASE_AFTER_AGENT, RESOURCE_PHASE_RESTART_AFTER, RESOURCE_PHASE_AFTER_CLONE, RESOURCE_PHASE_SOLVE_START, RESOURCE_PHASE_RESTART_BEFORE];
|
|
15
|
+
|
|
16
|
+
function finiteNumber(value) {
|
|
17
|
+
return Number.isFinite(value) ? value : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clampPercent(value) {
|
|
21
|
+
if (!Number.isFinite(value)) return null;
|
|
22
|
+
return Math.max(0, Math.min(100, value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readLinuxMemAvailableBytes(readFileSync = fs.readFileSync, platform = process.platform) {
|
|
26
|
+
if (platform !== 'linux') return null;
|
|
27
|
+
try {
|
|
28
|
+
const text = readFileSync('/proc/meminfo', 'utf8');
|
|
29
|
+
const match = text.match(/^MemAvailable:\s+(\d+)\s+kB$/m);
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
return Number.parseInt(match[1], 10) * 1024;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function captureResourceSnapshot(options = {}) {
|
|
38
|
+
const { phase = 'snapshot', diskPath = '/', now = () => new Date(), osImpl = os, fsImpl = fs, processImpl = process } = options;
|
|
39
|
+
|
|
40
|
+
const timestamp = (() => {
|
|
41
|
+
try {
|
|
42
|
+
return now().toISOString();
|
|
43
|
+
} catch {
|
|
44
|
+
return new Date().toISOString();
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
const load = (() => {
|
|
49
|
+
try {
|
|
50
|
+
const values = osImpl.loadavg();
|
|
51
|
+
return {
|
|
52
|
+
load1: finiteNumber(values[0]),
|
|
53
|
+
load5: finiteNumber(values[1]),
|
|
54
|
+
load15: finiteNumber(values[2]),
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return { load1: null, load5: null, load15: null };
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
const cpuCount = (() => {
|
|
62
|
+
try {
|
|
63
|
+
const cpus = osImpl.cpus();
|
|
64
|
+
return Array.isArray(cpus) ? cpus.length : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
const totalMemoryBytes = (() => {
|
|
71
|
+
try {
|
|
72
|
+
return finiteNumber(osImpl.totalmem());
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
const freeMemoryBytes = (() => {
|
|
79
|
+
try {
|
|
80
|
+
return finiteNumber(osImpl.freemem());
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
|
|
86
|
+
const availableMemoryBytes = readLinuxMemAvailableBytes(fsImpl.readFileSync?.bind(fsImpl), processImpl.platform || process.platform) ?? freeMemoryBytes;
|
|
87
|
+
const usedMemoryBytes = totalMemoryBytes !== null && availableMemoryBytes !== null ? Math.max(0, totalMemoryBytes - availableMemoryBytes) : null;
|
|
88
|
+
|
|
89
|
+
const processMemory = (() => {
|
|
90
|
+
try {
|
|
91
|
+
const usage = processImpl.memoryUsage();
|
|
92
|
+
return {
|
|
93
|
+
rssBytes: finiteNumber(usage.rss),
|
|
94
|
+
heapUsedBytes: finiteNumber(usage.heapUsed),
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
return { rssBytes: null, heapUsedBytes: null };
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
const disk = (() => {
|
|
102
|
+
const path = String(diskPath || '/');
|
|
103
|
+
try {
|
|
104
|
+
if (typeof fsImpl.statfsSync !== 'function') {
|
|
105
|
+
return { path, totalBytes: null, freeBytes: null, availableBytes: null, usedBytes: null, usedPercent: null, error: 'statfs unavailable' };
|
|
106
|
+
}
|
|
107
|
+
const stat = fsImpl.statfsSync(path);
|
|
108
|
+
const blockSize = Number(stat.bsize || stat.frsize || 0);
|
|
109
|
+
const blocks = Number(stat.blocks);
|
|
110
|
+
const bfree = Number(stat.bfree);
|
|
111
|
+
const bavail = Number(stat.bavail);
|
|
112
|
+
const totalBytes = Number.isFinite(blockSize) && Number.isFinite(blocks) ? blockSize * blocks : null;
|
|
113
|
+
const freeBytes = Number.isFinite(blockSize) && Number.isFinite(bfree) ? blockSize * bfree : null;
|
|
114
|
+
const availableBytes = Number.isFinite(blockSize) && Number.isFinite(bavail) ? blockSize * bavail : freeBytes;
|
|
115
|
+
const usedBytes = totalBytes !== null && freeBytes !== null ? Math.max(0, totalBytes - freeBytes) : null;
|
|
116
|
+
const usedPercent = totalBytes && usedBytes !== null ? clampPercent((usedBytes / totalBytes) * 100) : null;
|
|
117
|
+
return { path, totalBytes, freeBytes, availableBytes, usedBytes, usedPercent, error: null };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
path,
|
|
121
|
+
totalBytes: null,
|
|
122
|
+
freeBytes: null,
|
|
123
|
+
availableBytes: null,
|
|
124
|
+
usedBytes: null,
|
|
125
|
+
usedPercent: null,
|
|
126
|
+
error: error?.message || String(error),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
phase: String(phase || 'snapshot'),
|
|
133
|
+
timestamp,
|
|
134
|
+
cpu: { ...load, cpuCount },
|
|
135
|
+
memory: {
|
|
136
|
+
totalBytes: totalMemoryBytes,
|
|
137
|
+
freeBytes: freeMemoryBytes,
|
|
138
|
+
availableBytes: availableMemoryBytes,
|
|
139
|
+
usedBytes: usedMemoryBytes,
|
|
140
|
+
processRssBytes: processMemory.rssBytes,
|
|
141
|
+
processHeapUsedBytes: processMemory.heapUsedBytes,
|
|
142
|
+
},
|
|
143
|
+
disk,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function formatBytes(bytes) {
|
|
148
|
+
if (!Number.isFinite(bytes)) return '? B';
|
|
149
|
+
const abs = Math.abs(bytes);
|
|
150
|
+
if (abs >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
151
|
+
if (abs >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
152
|
+
if (abs >= 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
153
|
+
return `${Math.round(bytes)} B`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatNumber(value, decimals = 2) {
|
|
157
|
+
return Number.isFinite(value) ? value.toFixed(decimals) : '?';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function encodeValue(value) {
|
|
161
|
+
if (value === null || value === undefined) return 'null';
|
|
162
|
+
return encodeURIComponent(String(value));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function numberField(name, value) {
|
|
166
|
+
return Number.isFinite(value) ? `${name}=${value}` : `${name}=null`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildResourceMarker(snapshot) {
|
|
170
|
+
const s = snapshot || {};
|
|
171
|
+
const cpu = s.cpu || {};
|
|
172
|
+
const memory = s.memory || {};
|
|
173
|
+
const disk = s.disk || {};
|
|
174
|
+
return [
|
|
175
|
+
RESOURCE_MARKER_PREFIX,
|
|
176
|
+
`phase=${encodeValue(s.phase || 'snapshot')}`,
|
|
177
|
+
`ts=${encodeValue(s.timestamp || new Date().toISOString())}`,
|
|
178
|
+
numberField('load1', cpu.load1),
|
|
179
|
+
numberField('load5', cpu.load5),
|
|
180
|
+
numberField('load15', cpu.load15),
|
|
181
|
+
numberField('cpuCount', cpu.cpuCount),
|
|
182
|
+
numberField('memTotalBytes', memory.totalBytes),
|
|
183
|
+
numberField('memAvailableBytes', memory.availableBytes),
|
|
184
|
+
numberField('memUsedBytes', memory.usedBytes),
|
|
185
|
+
numberField('processRssBytes', memory.processRssBytes),
|
|
186
|
+
`diskPath=${encodeValue(disk.path || '/')}`,
|
|
187
|
+
numberField('diskTotalBytes', disk.totalBytes),
|
|
188
|
+
numberField('diskAvailableBytes', disk.availableBytes),
|
|
189
|
+
numberField('diskUsedBytes', disk.usedBytes),
|
|
190
|
+
numberField('diskUsedPercent', disk.usedPercent),
|
|
191
|
+
disk.error ? `error=${encodeValue(disk.error)}` : null,
|
|
192
|
+
`mem=${encodeValue(`${formatBytes(memory.availableBytes)} available / ${formatBytes(memory.totalBytes)} total`)}`,
|
|
193
|
+
`disk=${encodeValue(`${formatBytes(disk.availableBytes)} available / ${formatBytes(disk.totalBytes)} total`)}`,
|
|
194
|
+
]
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join(' ');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseNumber(value) {
|
|
200
|
+
if (value === 'null' || value === undefined) return null;
|
|
201
|
+
const n = Number(value);
|
|
202
|
+
return Number.isFinite(n) ? n : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseMarkerLine(line) {
|
|
206
|
+
const idx = line.indexOf(RESOURCE_MARKER_PREFIX);
|
|
207
|
+
if (idx < 0) return null;
|
|
208
|
+
const payload = line.slice(idx + RESOURCE_MARKER_PREFIX.length).trim();
|
|
209
|
+
const parts = payload.split(/\s+/).filter(Boolean);
|
|
210
|
+
const fields = {};
|
|
211
|
+
for (const part of parts) {
|
|
212
|
+
const eq = part.indexOf('=');
|
|
213
|
+
if (eq <= 0) continue;
|
|
214
|
+
fields[part.slice(0, eq)] = part.slice(eq + 1);
|
|
215
|
+
}
|
|
216
|
+
const phase = decodeURIComponent(fields.phase || 'snapshot');
|
|
217
|
+
return {
|
|
218
|
+
phase,
|
|
219
|
+
timestamp: decodeURIComponent(fields.ts || ''),
|
|
220
|
+
cpu: {
|
|
221
|
+
load1: parseNumber(fields.load1),
|
|
222
|
+
load5: parseNumber(fields.load5),
|
|
223
|
+
load15: parseNumber(fields.load15),
|
|
224
|
+
cpuCount: parseNumber(fields.cpuCount),
|
|
225
|
+
},
|
|
226
|
+
memory: {
|
|
227
|
+
totalBytes: parseNumber(fields.memTotalBytes),
|
|
228
|
+
availableBytes: parseNumber(fields.memAvailableBytes),
|
|
229
|
+
usedBytes: parseNumber(fields.memUsedBytes),
|
|
230
|
+
processRssBytes: parseNumber(fields.processRssBytes),
|
|
231
|
+
},
|
|
232
|
+
disk: {
|
|
233
|
+
path: decodeURIComponent(fields.diskPath || '/'),
|
|
234
|
+
totalBytes: parseNumber(fields.diskTotalBytes),
|
|
235
|
+
availableBytes: parseNumber(fields.diskAvailableBytes),
|
|
236
|
+
usedBytes: parseNumber(fields.diskUsedBytes),
|
|
237
|
+
usedPercent: parseNumber(fields.diskUsedPercent),
|
|
238
|
+
error: fields.error ? decodeURIComponent(fields.error) : null,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function parseResourceMarkers(logText) {
|
|
244
|
+
if (typeof logText !== 'string' || !logText) return { markers: [], byPhase: {} };
|
|
245
|
+
const markers = [];
|
|
246
|
+
const byPhase = {};
|
|
247
|
+
for (const line of logText.split(/\r?\n/)) {
|
|
248
|
+
const marker = parseMarkerLine(line);
|
|
249
|
+
if (!marker) continue;
|
|
250
|
+
markers.push(marker);
|
|
251
|
+
byPhase[marker.phase] = marker;
|
|
252
|
+
}
|
|
253
|
+
return { markers, byPhase };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function selectBestDiskResourceMarker(parsed) {
|
|
257
|
+
const byPhase = parsed?.byPhase || {};
|
|
258
|
+
for (const phase of RESOURCE_PHASES_BY_PREFERENCE) {
|
|
259
|
+
const marker = byPhase[phase];
|
|
260
|
+
if (Number.isFinite(marker?.disk?.usedBytes)) return marker;
|
|
261
|
+
}
|
|
262
|
+
const markers = Array.isArray(parsed?.markers) ? parsed.markers : [];
|
|
263
|
+
for (let i = markers.length - 1; i >= 0; i--) {
|
|
264
|
+
if (Number.isFinite(markers[i]?.disk?.usedBytes)) return markers[i];
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function formatResourceSnapshotForLog(snapshot, label = null) {
|
|
270
|
+
const s = snapshot || {};
|
|
271
|
+
const phaseLabel = label || String(s.phase || 'snapshot').replace(/_/g, ' ');
|
|
272
|
+
const cpu = s.cpu || {};
|
|
273
|
+
const memory = s.memory || {};
|
|
274
|
+
const disk = s.disk || {};
|
|
275
|
+
const lines = [`📈 Resource usage (${phaseLabel}):`, ` CPU load: ${formatNumber(cpu.load1)} ${formatNumber(cpu.load5)} ${formatNumber(cpu.load15)}${Number.isFinite(cpu.cpuCount) ? ` (${cpu.cpuCount} CPUs)` : ''}`, ` Memory: ${formatBytes(memory.availableBytes)} available / ${formatBytes(memory.totalBytes)} total (${formatBytes(memory.usedBytes)} used)`, ` Process RSS: ${formatBytes(memory.processRssBytes)}${Number.isFinite(memory.processHeapUsedBytes) ? `, heap ${formatBytes(memory.processHeapUsedBytes)}` : ''}`, ` Disk (${disk.path || '/'}): ${formatBytes(disk.availableBytes)} available / ${formatBytes(disk.totalBytes)} total${Number.isFinite(disk.usedPercent) ? ` (${disk.usedPercent.toFixed(1)}% used)` : ''}`];
|
|
276
|
+
if (disk.error) lines.push(` Disk probe error: ${disk.error}`);
|
|
277
|
+
lines.push(buildResourceMarker(snapshot));
|
|
278
|
+
return lines.join('\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function recordResourceSnapshot({ phase, log, diskPath = '/', label = null, capture = captureResourceSnapshot } = {}) {
|
|
282
|
+
if (typeof log !== 'function') return null;
|
|
283
|
+
try {
|
|
284
|
+
const snapshot = capture({ phase, diskPath });
|
|
285
|
+
await log(formatResourceSnapshotForLog(snapshot, label));
|
|
286
|
+
return snapshot;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
await log(`⚠️ Resource usage measurement failed (${phase || 'snapshot'}): ${error?.message || error}`, { level: 'warning', verbose: true });
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function summarizeResourceSnapshot(snapshot) {
|
|
294
|
+
if (!snapshot) return null;
|
|
295
|
+
const cpu = snapshot.cpu || {};
|
|
296
|
+
const memory = snapshot.memory || {};
|
|
297
|
+
const disk = snapshot.disk || {};
|
|
298
|
+
return {
|
|
299
|
+
phase: snapshot.phase || null,
|
|
300
|
+
timestamp: snapshot.timestamp || null,
|
|
301
|
+
cpu: {
|
|
302
|
+
load1: cpu.load1,
|
|
303
|
+
load5: cpu.load5,
|
|
304
|
+
load15: cpu.load15,
|
|
305
|
+
cpuCount: cpu.cpuCount,
|
|
306
|
+
},
|
|
307
|
+
memory: {
|
|
308
|
+
totalBytes: memory.totalBytes,
|
|
309
|
+
availableBytes: memory.availableBytes,
|
|
310
|
+
usedBytes: memory.usedBytes,
|
|
311
|
+
processRssBytes: memory.processRssBytes,
|
|
312
|
+
},
|
|
313
|
+
disk: {
|
|
314
|
+
path: disk.path,
|
|
315
|
+
totalBytes: disk.totalBytes,
|
|
316
|
+
availableBytes: disk.availableBytes,
|
|
317
|
+
usedBytes: disk.usedBytes,
|
|
318
|
+
usedPercent: disk.usedPercent,
|
|
319
|
+
error: disk.error || null,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -32,6 +32,7 @@ const fs = (await use('fs')).promises;
|
|
|
32
32
|
const lib = await import('./lib.mjs');
|
|
33
33
|
const { log, formatAligned, extractToolErrorCore } = lib;
|
|
34
34
|
const { ensurePullRequestBaseBranch } = await import('./solve.pr-base-guard.lib.mjs');
|
|
35
|
+
const { RESOURCE_PHASE_RESTART_AFTER, RESOURCE_PHASE_RESTART_BEFORE, recordResourceSnapshot } = await import('./solve.resource-diagnostics.lib.mjs');
|
|
35
36
|
|
|
36
37
|
// Import Sentry integration
|
|
37
38
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
@@ -177,6 +178,13 @@ export const getUncommittedChangesDetails = async tempDir => {
|
|
|
177
178
|
export const executeToolIteration = async params => {
|
|
178
179
|
const { issueUrl, owner, repo, issueNumber, prNumber, branchName, tempDir, workspaceTmpDir, mergeStateStatus, feedbackLines, argv } = params;
|
|
179
180
|
|
|
181
|
+
await recordResourceSnapshot({
|
|
182
|
+
phase: RESOURCE_PHASE_RESTART_BEFORE,
|
|
183
|
+
log,
|
|
184
|
+
diskPath: '/',
|
|
185
|
+
label: 'before AI restart iteration',
|
|
186
|
+
});
|
|
187
|
+
|
|
180
188
|
// Import necessary modules for tool execution
|
|
181
189
|
const memoryCheck = await import('./memory-check.mjs');
|
|
182
190
|
const { getResourceSnapshot } = memoryCheck;
|
|
@@ -462,6 +470,12 @@ export const executeToolIteration = async params => {
|
|
|
462
470
|
}
|
|
463
471
|
|
|
464
472
|
await ensurePullRequestBaseBranch({ owner, repo, prNumber, argv, log, formatAligned, $ });
|
|
473
|
+
await recordResourceSnapshot({
|
|
474
|
+
phase: RESOURCE_PHASE_RESTART_AFTER,
|
|
475
|
+
log,
|
|
476
|
+
diskPath: '/',
|
|
477
|
+
label: 'after AI restart iteration',
|
|
478
|
+
});
|
|
465
479
|
return toolResult;
|
|
466
480
|
};
|
|
467
481
|
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -1234,8 +1234,7 @@ bot.on('message', async (ctx, next) => {
|
|
|
1234
1234
|
// /subscribe + /unsubscribe (#1688) are intentionally not in the text fallback — Telegraf's bot.command() is sufficient.
|
|
1235
1235
|
const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
|
|
1236
1236
|
const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
|
|
1237
|
-
|
|
1238
|
-
const handlers = { ...solveHandlers, ...taskHandlers, auth: handleAuthCommand, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
|
|
1237
|
+
const handlers = { ...solveHandlers, ...taskHandlers, auth: handleAuthCommand, hive: handleHiveCommand, queue: handleSolveQueueCommand };
|
|
1239
1238
|
|
|
1240
1239
|
const handler = handlers[extracted.command];
|
|
1241
1240
|
if (!handler) return next();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram /
|
|
2
|
+
* Telegram /queue command implementation
|
|
3
3
|
*
|
|
4
|
-
* This module provides the /
|
|
4
|
+
* This module provides the /queue command functionality for the Telegram bot,
|
|
5
5
|
* allowing users to view the current solve queue status.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { t } from './i18n.lib.mjs';
|
|
17
17
|
|
|
18
|
-
const GROUP_ONLY_MESSAGE = '❌ The /
|
|
18
|
+
const GROUP_ONLY_MESSAGE = '❌ The /queue command only works in group chats. Please add this bot to a group and make it an admin.';
|
|
19
19
|
|
|
20
20
|
function commandText(key, params = {}, locale = null, fallback = key) {
|
|
21
21
|
const translated = t(key, params, locale ? { locale } : {});
|
|
@@ -23,7 +23,7 @@ function commandText(key, params = {}, locale = null, fallback = key) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Registers the /
|
|
26
|
+
* Registers the /queue command handler with the bot
|
|
27
27
|
* @param {Object} bot - The Telegraf bot instance
|
|
28
28
|
* @param {Object} options - Options object
|
|
29
29
|
* @param {boolean} options.VERBOSE - Whether to enable verbose logging
|
|
@@ -41,11 +41,11 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
41
41
|
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage, addBreadcrumb, getSolveQueue, safeReply, resolveLocale } = options;
|
|
42
42
|
|
|
43
43
|
async function handleSolveQueueCommand(ctx) {
|
|
44
|
-
VERBOSE && console.log('[VERBOSE] /
|
|
44
|
+
VERBOSE && console.log('[VERBOSE] /queue command received');
|
|
45
45
|
|
|
46
46
|
await addBreadcrumb({
|
|
47
47
|
category: 'telegram.command',
|
|
48
|
-
message: '/
|
|
48
|
+
message: '/queue command received',
|
|
49
49
|
level: 'info',
|
|
50
50
|
data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username },
|
|
51
51
|
});
|
|
@@ -54,18 +54,18 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
54
54
|
|
|
55
55
|
// Ignore messages sent before bot started
|
|
56
56
|
if (isOldMessage(ctx)) {
|
|
57
|
-
VERBOSE && console.log('[VERBOSE] /
|
|
57
|
+
VERBOSE && console.log('[VERBOSE] /queue ignored: old message');
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Ignore forwarded or reply messages
|
|
62
62
|
if (isForwardedOrReply(ctx)) {
|
|
63
|
-
VERBOSE && console.log('[VERBOSE] /
|
|
63
|
+
VERBOSE && console.log('[VERBOSE] /queue ignored: forwarded or reply');
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (!isGroupChat(ctx)) {
|
|
68
|
-
VERBOSE && console.log('[VERBOSE] /
|
|
68
|
+
VERBOSE && console.log('[VERBOSE] /queue ignored: not a group chat');
|
|
69
69
|
await replyWithFallback(commandText('telegram.solve_queue_only_in_groups', {}, locale, GROUP_ONLY_MESSAGE), {
|
|
70
70
|
reply_to_message_id: ctx.message.message_id,
|
|
71
71
|
fallbackLocale: locale,
|
|
@@ -75,13 +75,13 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
75
75
|
|
|
76
76
|
const authorize = isTopicAuthorized || (ctx => isChatAuthorized(ctx.chat.id));
|
|
77
77
|
if (!authorize(ctx)) {
|
|
78
|
-
VERBOSE && console.log('[VERBOSE] /
|
|
78
|
+
VERBOSE && console.log('[VERBOSE] /queue ignored: not authorized');
|
|
79
79
|
const errMsg = buildAuthErrorMessage ? buildAuthErrorMessage(ctx) : `❌ This chat (ID: ${ctx.chat.id}) is not authorized.`;
|
|
80
80
|
await replyWithFallback(errMsg, { reply_to_message_id: ctx.message.message_id, fallbackLocale: locale });
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
VERBOSE && console.log('[VERBOSE] /
|
|
84
|
+
VERBOSE && console.log('[VERBOSE] /queue passed all checks, generating status...');
|
|
85
85
|
|
|
86
86
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
87
87
|
|
|
@@ -97,12 +97,7 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
// Note: Telegram Bot API only supports underscores in command names, not hyphens.
|
|
102
|
-
// The entity-based matching handles /solve_queue, /solvequeue, and /queue.
|
|
103
|
-
// /solve-queue is handled by the text-based fallback in telegram-bot.mjs (issue #1232).
|
|
104
|
-
// The /queue alias was added in issue #1837 to make checking the queue faster to type.
|
|
105
|
-
bot.command(/^(?:solve[_-]?queue|queue)$/i, handleSolveQueueCommand);
|
|
100
|
+
bot.command(/^queue$/i, handleSolveQueueCommand);
|
|
106
101
|
|
|
107
102
|
return { handleSolveQueueCommand };
|
|
108
103
|
}
|
|
@@ -8,7 +8,7 @@ const execAsync = promisify(exec);
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Build a clickable, human-readable link to a queued issue/PR for the
|
|
11
|
-
* /
|
|
11
|
+
* /queue detailed status (issue #1837).
|
|
12
12
|
*
|
|
13
13
|
* For GitHub issue/PR URLs we render a compact `[owner/repo#number](url)`
|
|
14
14
|
* Markdown link so the list is scannable and clickable. When the label would
|
|
@@ -269,7 +269,7 @@ export async function getRunningSessionItems(verbose = false) {
|
|
|
269
269
|
return await impl(verbose);
|
|
270
270
|
} catch (error) {
|
|
271
271
|
if (verbose) {
|
|
272
|
-
console.error('[VERBOSE] /
|
|
272
|
+
console.error('[VERBOSE] /queue error getting running session items:', error.message);
|
|
273
273
|
}
|
|
274
274
|
return [];
|
|
275
275
|
}
|
|
@@ -300,9 +300,9 @@ export async function getRunningProcesses(processName, verbose = false) {
|
|
|
300
300
|
.filter(p => p.pid);
|
|
301
301
|
|
|
302
302
|
if (verbose) {
|
|
303
|
-
console.log(`[VERBOSE] /
|
|
303
|
+
console.log(`[VERBOSE] /queue found ${processes.length} running ${processName} processes`);
|
|
304
304
|
if (processes.length > 0) {
|
|
305
|
-
console.log(`[VERBOSE] /
|
|
305
|
+
console.log(`[VERBOSE] /queue processes: ${JSON.stringify(processes)}`);
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
@@ -312,7 +312,7 @@ export async function getRunningProcesses(processName, verbose = false) {
|
|
|
312
312
|
};
|
|
313
313
|
} catch (error) {
|
|
314
314
|
if (verbose) {
|
|
315
|
-
console.error(`[VERBOSE] /
|
|
315
|
+
console.error(`[VERBOSE] /queue error counting ${processName} processes:`, error.message);
|
|
316
316
|
}
|
|
317
317
|
return { count: 0, processes: [] };
|
|
318
318
|
}
|
|
@@ -250,7 +250,7 @@ export class SolveQueue {
|
|
|
250
250
|
*/
|
|
251
251
|
log(message) {
|
|
252
252
|
if (this.verbose) {
|
|
253
|
-
console.log(`[VERBOSE] /
|
|
253
|
+
console.log(`[VERBOSE] /queue: ${message}`);
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -1469,7 +1469,7 @@ export async function getRunningIsolatedSessions(verbose = false) {
|
|
|
1469
1469
|
return await getRunningTrackedIsolationSessions(verbose);
|
|
1470
1470
|
} catch (error) {
|
|
1471
1471
|
if (verbose) {
|
|
1472
|
-
console.error(`[VERBOSE] /
|
|
1472
|
+
console.error(`[VERBOSE] /queue error getting isolated sessions:`, error.message);
|
|
1473
1473
|
}
|
|
1474
1474
|
return { count: 0, sessions: [], byTool: {} };
|
|
1475
1475
|
}
|