@link-assistant/hive-mind 1.21.2 → 1.21.4
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 +27 -0
- package/package.json +2 -2
- package/src/agent.lib.mjs +45 -0
- package/src/github-merge.lib.mjs +27 -3
- package/src/limits.lib.mjs +18 -16
- package/src/telegram-bot.mjs +17 -10
- package/src/telegram-merge-command.lib.mjs +59 -8
- package/src/telegram-merge-queue.lib.mjs +18 -0
- package/src/telegram-solve-queue-command.lib.mjs +5 -7
- package/src/telegram-solve-queue.lib.mjs +147 -55
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.21.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ea19c72: Fix queue issues: rejection, display, and formatting
|
|
8
|
+
- Fix disk rejection not blocking queue placement when threshold exceeded
|
|
9
|
+
- Restore "used" label on progress bars when below threshold
|
|
10
|
+
- Show per-queue breakdown in /limits command
|
|
11
|
+
- Group queue items by tool and use human-readable time in /solve_queue
|
|
12
|
+
|
|
13
|
+
- aa42f3a: fix: improve merge queue error handling and debugging (Issue #1269)
|
|
14
|
+
- Always log errors (not just in verbose mode) for critical merge queue failures
|
|
15
|
+
- Always notify users via Telegram when merge queue fails unexpectedly
|
|
16
|
+
- Add timeout wrapper (60s) for onStatusUpdate callback to prevent infinite blocking
|
|
17
|
+
- Add error handling for CI check failures in waitForCI loop
|
|
18
|
+
- Add comprehensive case study documentation in docs/case-studies/issue-1269/
|
|
19
|
+
|
|
20
|
+
## 1.21.3
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 4426112: Fix error detection for `--tool agent` when JSON errors are pretty-printed (Issue #1258)
|
|
25
|
+
- Add fallback pattern matching for error events when NDJSON parsing fails
|
|
26
|
+
- Detect `"type": "error"` and `"type": "step_error"` patterns in raw output
|
|
27
|
+
- Detect critical error patterns like `AI_RetryError` and `UnhandledRejection`
|
|
28
|
+
- Extract error messages from output for better error reporting
|
|
29
|
+
|
|
3
30
|
## 1.21.2
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.4",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs",
|
|
16
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs",
|
|
17
17
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
18
18
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
19
19
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
package/src/agent.lib.mjs
CHANGED
|
@@ -703,6 +703,51 @@ export const executeAgentCommand = async params => {
|
|
|
703
703
|
outputError.match = streamingErrorMessage;
|
|
704
704
|
}
|
|
705
705
|
|
|
706
|
+
// Issue #1258: Fallback pattern match for error detection
|
|
707
|
+
// When JSON parsing fails (e.g., multi-line pretty-printed JSON in logs),
|
|
708
|
+
// we need to detect error patterns in the raw output string
|
|
709
|
+
if (!outputError.detected && !streamingErrorDetected) {
|
|
710
|
+
// Check for error type patterns in raw output (handles pretty-printed JSON)
|
|
711
|
+
const errorTypePatterns = [
|
|
712
|
+
{ pattern: '"type": "error"', type: 'AgentError' },
|
|
713
|
+
{ pattern: '"type":"error"', type: 'AgentError' },
|
|
714
|
+
{ pattern: '"type": "step_error"', type: 'AgentStepError' },
|
|
715
|
+
{ pattern: '"type":"step_error"', type: 'AgentStepError' },
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
for (const { pattern, type } of errorTypePatterns) {
|
|
719
|
+
if (fullOutput.includes(pattern)) {
|
|
720
|
+
outputError.detected = true;
|
|
721
|
+
outputError.type = type;
|
|
722
|
+
// Try to extract the error message from the output
|
|
723
|
+
const messageMatch = fullOutput.match(/"message":\s*"([^"]+)"/);
|
|
724
|
+
outputError.match = messageMatch ? messageMatch[1] : `Error event detected in output (fallback pattern match for ${pattern})`;
|
|
725
|
+
await log(`⚠️ Error event detected via fallback pattern match: ${outputError.match}`, { level: 'warning' });
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Also check for known critical error patterns that indicate failure
|
|
731
|
+
if (!outputError.detected) {
|
|
732
|
+
const criticalErrorPatterns = [
|
|
733
|
+
{ pattern: 'AI_RetryError:', extract: /AI_RetryError:\s*(.+?)(?:\n|$)/ },
|
|
734
|
+
{ pattern: 'UnhandledRejection', extract: /"errorType":\s*"UnhandledRejection"/ },
|
|
735
|
+
{ pattern: 'Failed after 3 attempts', extract: /Failed after \d+ attempts[^"]*/ },
|
|
736
|
+
];
|
|
737
|
+
|
|
738
|
+
for (const { pattern, extract } of criticalErrorPatterns) {
|
|
739
|
+
if (fullOutput.includes(pattern)) {
|
|
740
|
+
outputError.detected = true;
|
|
741
|
+
outputError.type = 'CriticalError';
|
|
742
|
+
const match = fullOutput.match(extract);
|
|
743
|
+
outputError.match = match ? match[0] : `Critical error pattern detected: ${pattern}`;
|
|
744
|
+
await log(`⚠️ Critical error pattern detected via fallback: ${outputError.match}`, { level: 'warning' });
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
706
751
|
if (exitCode !== 0 || outputError.detected) {
|
|
707
752
|
// Build JSON error structure for consistent error reporting
|
|
708
753
|
const errorInfo = {
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -485,15 +485,39 @@ export async function mergePullRequest(owner, repo, prNumber, options = {}, verb
|
|
|
485
485
|
* @returns {Promise<{success: boolean, status: string, error: string|null}>}
|
|
486
486
|
*/
|
|
487
487
|
export async function waitForCI(owner, repo, prNumber, options = {}, verbose = false) {
|
|
488
|
-
const {
|
|
488
|
+
const {
|
|
489
|
+
timeout = 30 * 60 * 1000,
|
|
490
|
+
pollInterval = 30 * 1000,
|
|
491
|
+
onStatusUpdate = null,
|
|
492
|
+
// Issue #1269: Add timeout for callback to prevent infinite blocking
|
|
493
|
+
callbackTimeout = 60 * 1000, // 1 minute max for callback
|
|
494
|
+
} = options;
|
|
489
495
|
|
|
490
496
|
const startTime = Date.now();
|
|
491
497
|
|
|
492
498
|
while (Date.now() - startTime < timeout) {
|
|
493
|
-
|
|
499
|
+
let ciStatus;
|
|
500
|
+
try {
|
|
501
|
+
ciStatus = await checkPRCIStatus(owner, repo, prNumber, verbose);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
// Issue #1269: Log and continue on CI check errors instead of crashing
|
|
504
|
+
console.error(`[ERROR] /merge: Error checking CI status for PR #${prNumber}: ${error.message}`);
|
|
505
|
+
verbose && console.error(`[VERBOSE] /merge: CI check error details:`, error);
|
|
506
|
+
// Wait and retry
|
|
507
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
494
510
|
|
|
495
511
|
if (onStatusUpdate) {
|
|
496
|
-
|
|
512
|
+
// Issue #1269: Wrap callback with timeout to prevent infinite blocking
|
|
513
|
+
try {
|
|
514
|
+
await Promise.race([onStatusUpdate(ciStatus), new Promise((_, reject) => setTimeout(() => reject(new Error(`Callback timeout after ${callbackTimeout}ms`)), callbackTimeout))]);
|
|
515
|
+
} catch (callbackError) {
|
|
516
|
+
// Issue #1269: Log callback errors but continue processing
|
|
517
|
+
console.error(`[ERROR] /merge: Status update callback failed for PR #${prNumber}: ${callbackError.message}`);
|
|
518
|
+
verbose && console.error(`[VERBOSE] /merge: Callback error details:`, callbackError);
|
|
519
|
+
// Continue processing even if callback fails - don't let UI issues block merging
|
|
520
|
+
}
|
|
497
521
|
}
|
|
498
522
|
|
|
499
523
|
if (ciStatus.status === 'success') {
|
package/src/limits.lib.mjs
CHANGED
|
@@ -714,11 +714,13 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
714
714
|
if (cpuLoad) {
|
|
715
715
|
message += 'CPU\n';
|
|
716
716
|
const usedBar = getProgressBar(cpuLoad.usagePercentage, DISPLAY_THRESHOLDS.CPU);
|
|
717
|
-
|
|
718
|
-
|
|
717
|
+
// Show 'used' label when below threshold, warning emoji when at/above threshold
|
|
718
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
719
|
+
const suffix = cpuLoad.usagePercentage >= DISPLAY_THRESHOLDS.CPU ? ' ⚠️' : ' used';
|
|
720
|
+
message += `${usedBar} ${cpuLoad.usagePercentage}%${suffix}\n`;
|
|
719
721
|
// Show cores used based on 5m load average (e.g., "0.04/6 CPU cores used" or "3/6 CPU cores used")
|
|
720
722
|
// Use parseFloat to strip unnecessary trailing zeros (3.00 -> 3, 0.10 -> 0.1, 0.04 -> 0.04)
|
|
721
|
-
message += `${parseFloat(cpuLoad.loadAvg5.toFixed(2))}/${cpuLoad.cpuCount} CPU cores
|
|
723
|
+
message += `${parseFloat(cpuLoad.loadAvg5.toFixed(2))}/${cpuLoad.cpuCount} CPU cores\n\n`;
|
|
722
724
|
}
|
|
723
725
|
|
|
724
726
|
// Memory section (if provided)
|
|
@@ -726,8 +728,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
726
728
|
if (memory) {
|
|
727
729
|
message += 'RAM\n';
|
|
728
730
|
const usedBar = getProgressBar(memory.usedPercentage, DISPLAY_THRESHOLDS.RAM);
|
|
729
|
-
const
|
|
730
|
-
message += `${usedBar} ${memory.usedPercentage}%${
|
|
731
|
+
const suffix = memory.usedPercentage >= DISPLAY_THRESHOLDS.RAM ? ' ⚠️' : ' used';
|
|
732
|
+
message += `${usedBar} ${memory.usedPercentage}%${suffix}\n`;
|
|
731
733
|
message += `${formatBytesRange(memory.usedBytes, memory.totalBytes)}\n\n`;
|
|
732
734
|
}
|
|
733
735
|
|
|
@@ -737,8 +739,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
737
739
|
message += 'Disk space\n';
|
|
738
740
|
// Show used percentage with progress bar and threshold marker
|
|
739
741
|
const usedBar = getProgressBar(diskSpace.usedPercentage, DISPLAY_THRESHOLDS.DISK);
|
|
740
|
-
const
|
|
741
|
-
message += `${usedBar} ${diskSpace.usedPercentage}%${
|
|
742
|
+
const suffix = diskSpace.usedPercentage >= DISPLAY_THRESHOLDS.DISK ? ' ⚠️' : ' used';
|
|
743
|
+
message += `${usedBar} ${diskSpace.usedPercentage}%${suffix}\n`;
|
|
742
744
|
message += `${formatBytesRange(diskSpace.usedBytes, diskSpace.totalBytes)}\n\n`;
|
|
743
745
|
}
|
|
744
746
|
|
|
@@ -748,9 +750,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
748
750
|
message += 'GitHub API\n';
|
|
749
751
|
// Show used percentage with progress bar and threshold marker
|
|
750
752
|
const usedBar = getProgressBar(githubRateLimit.usedPercentage, DISPLAY_THRESHOLDS.GITHUB_API);
|
|
751
|
-
const
|
|
752
|
-
message += `${usedBar} ${githubRateLimit.usedPercentage}%${
|
|
753
|
-
message += `${githubRateLimit.used}/${githubRateLimit.limit} requests
|
|
753
|
+
const suffix = githubRateLimit.usedPercentage >= DISPLAY_THRESHOLDS.GITHUB_API ? ' ⚠️' : ' used';
|
|
754
|
+
message += `${usedBar} ${githubRateLimit.usedPercentage}%${suffix}\n`;
|
|
755
|
+
message += `${githubRateLimit.used}/${githubRateLimit.limit} requests\n`;
|
|
754
756
|
if (githubRateLimit.relativeReset) {
|
|
755
757
|
message += `Resets in ${githubRateLimit.relativeReset} (${githubRateLimit.resetTime})\n`;
|
|
756
758
|
} else if (githubRateLimit.resetTime) {
|
|
@@ -775,8 +777,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
775
777
|
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
776
778
|
const pct = Math.floor(usage.currentSession.percentage);
|
|
777
779
|
const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION);
|
|
778
|
-
const
|
|
779
|
-
message += `${bar} ${pct}%${
|
|
780
|
+
const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION ? ' ⚠️' : ' used';
|
|
781
|
+
message += `${bar} ${pct}%${suffix}\n`;
|
|
780
782
|
|
|
781
783
|
if (usage.currentSession.resetTime) {
|
|
782
784
|
const relativeTime = formatRelativeTime(usage.currentSession.resetsAt);
|
|
@@ -807,8 +809,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
807
809
|
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
808
810
|
const pct = Math.floor(usage.allModels.percentage);
|
|
809
811
|
const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
|
|
810
|
-
const
|
|
811
|
-
message += `${bar} ${pct}%${
|
|
812
|
+
const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ' used';
|
|
813
|
+
message += `${bar} ${pct}%${suffix}\n`;
|
|
812
814
|
|
|
813
815
|
if (usage.allModels.resetTime) {
|
|
814
816
|
const relativeTime = formatRelativeTime(usage.allModels.resetsAt);
|
|
@@ -839,8 +841,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
839
841
|
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
840
842
|
const pct = Math.floor(usage.sonnetOnly.percentage);
|
|
841
843
|
const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
|
|
842
|
-
const
|
|
843
|
-
message += `${bar} ${pct}%${
|
|
844
|
+
const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ' used';
|
|
845
|
+
message += `${bar} ${pct}%${suffix}\n`;
|
|
844
846
|
|
|
845
847
|
if (usage.sonnetOnly.resetTime) {
|
|
846
848
|
const relativeTime = formatRelativeTime(usage.sonnetOnly.resetsAt);
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -42,7 +42,7 @@ const { validateModelName } = await import('./model-validation.lib.mjs');
|
|
|
42
42
|
const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
|
|
43
43
|
const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
|
|
44
44
|
const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
|
|
45
|
-
const { getSolveQueue,
|
|
45
|
+
const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
|
|
46
46
|
// Import extracted message filter functions for testability (issue #1207)
|
|
47
47
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText } = await import('./telegram-message-filters.lib.mjs');
|
|
48
48
|
|
|
@@ -784,16 +784,14 @@ bot.command('limits', async ctx => {
|
|
|
784
784
|
// Format the message with usage limits and queue status
|
|
785
785
|
let message = '📊 *Usage Limits*\n\n' + formatUsageMessage(limits.claude.usage, 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);
|
|
786
786
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1133
|
|
792
|
-
const totalProcessing = queueStats.processing + claudeProcs.count;
|
|
787
|
+
// Insert per-queue status into the code block
|
|
788
|
+
// Shows each queue (claude, agent) with pending/processing counts
|
|
789
|
+
// Processing counts are actual running system processes (via pgrep)
|
|
790
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
793
791
|
const codeBlockEnd = message.lastIndexOf('```');
|
|
794
792
|
if (codeBlockEnd !== -1) {
|
|
795
|
-
const queueStatus =
|
|
796
|
-
message = message.slice(0, codeBlockEnd) + `\
|
|
793
|
+
const queueStatus = await solveQueue.formatStatus();
|
|
794
|
+
message = message.slice(0, codeBlockEnd) + `\n${queueStatus}` + message.slice(codeBlockEnd);
|
|
797
795
|
}
|
|
798
796
|
await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
|
|
799
797
|
});
|
|
@@ -850,7 +848,6 @@ const { handleSolveQueueCommand } = registerSolveQueueCommand(bot, {
|
|
|
850
848
|
isChatAuthorized,
|
|
851
849
|
addBreadcrumb,
|
|
852
850
|
getSolveQueue,
|
|
853
|
-
getRunningClaudeProcesses,
|
|
854
851
|
});
|
|
855
852
|
|
|
856
853
|
// Named handler for /solve command - extracted for reuse by text-based fallback (issue #1207)
|
|
@@ -1043,6 +1040,16 @@ async function handleSolveCommand(ctx) {
|
|
|
1043
1040
|
|
|
1044
1041
|
const check = await solveQueue.canStartCommand({ tool: solveTool }); // Skip Claude limits for agent (#1159)
|
|
1045
1042
|
const queueStats = solveQueue.getStats();
|
|
1043
|
+
|
|
1044
|
+
// Handle rejection: when a threshold strategy is 'reject', the command should fail immediately
|
|
1045
|
+
// without being placed in the queue. This ensures users get clear feedback about why
|
|
1046
|
+
// their command cannot be processed (e.g., disk full, server maintenance pending).
|
|
1047
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
1048
|
+
if (check.rejected) {
|
|
1049
|
+
await ctx.reply(`❌ Solve command rejected.\n\n${infoBlock}\n\n🚫 Reason: ${check.rejectReason}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1046
1053
|
if (check.canStart && queueStats.queued === 0) {
|
|
1047
1054
|
const startingMessage = await ctx.reply(`🚀 Starting solve command...\n\n${infoBlock}`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
|
|
1048
1055
|
await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock);
|
|
@@ -246,21 +246,31 @@ export function registerMergeCommand(bot, options) {
|
|
|
246
246
|
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, message, {
|
|
247
247
|
parse_mode: 'MarkdownV2',
|
|
248
248
|
});
|
|
249
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Merge queue completed successfully for ${repoKey}`);
|
|
249
250
|
} catch (err) {
|
|
250
|
-
|
|
251
|
+
// Issue #1269: Always log completion failures (critical for debugging)
|
|
252
|
+
console.error(`[ERROR] /merge: Error sending final message for ${repoKey}: ${err.message}`);
|
|
251
253
|
}
|
|
252
254
|
activeMergeOperations.delete(repoKey);
|
|
253
255
|
},
|
|
254
256
|
onError: async error => {
|
|
255
|
-
|
|
257
|
+
// Issue #1269: Always log errors (not just in verbose mode)
|
|
258
|
+
console.error(`[ERROR] /merge: Queue error for ${repoKey}:`, error.message);
|
|
259
|
+
VERBOSE && console.error(`[VERBOSE] /merge: Full error:`, error);
|
|
256
260
|
try {
|
|
257
261
|
const userMessage = formatUserError(error, VERBOSE);
|
|
258
262
|
const finalReport = processor.formatFinalMessage();
|
|
259
|
-
|
|
263
|
+
// Issue #1269: Show error in the reply message with immediate feedback
|
|
264
|
+
// Keep a button so users know the error was displayed and can dismiss
|
|
265
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `❌ *Merge queue failed*\n\n⚠️ *Error:* ${escapeMarkdownV2(userMessage)}\n\n${finalReport}`, {
|
|
260
266
|
parse_mode: 'MarkdownV2',
|
|
267
|
+
reply_markup: {
|
|
268
|
+
inline_keyboard: [[{ text: '❌ Failed - Click to dismiss', callback_data: `merge_dismiss_${repoKey}` }]],
|
|
269
|
+
},
|
|
261
270
|
});
|
|
262
271
|
} catch (err) {
|
|
263
|
-
|
|
272
|
+
// Issue #1269: Always log notification failures
|
|
273
|
+
console.error(`[ERROR] /merge: Error sending error message for ${repoKey}: ${err.message}`);
|
|
264
274
|
}
|
|
265
275
|
activeMergeOperations.delete(repoKey);
|
|
266
276
|
},
|
|
@@ -301,10 +311,36 @@ export function registerMergeCommand(bot, options) {
|
|
|
301
311
|
});
|
|
302
312
|
|
|
303
313
|
// Run the merge queue (this runs asynchronously)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
314
|
+
// Issue #1269: Improved error handling - always log errors and notify users
|
|
315
|
+
processor
|
|
316
|
+
.run()
|
|
317
|
+
.then(() => {
|
|
318
|
+
VERBOSE && console.log(`[VERBOSE] /merge: Merge queue completed for ${repoKey}`);
|
|
319
|
+
})
|
|
320
|
+
.catch(async error => {
|
|
321
|
+
// Always log errors (not just in verbose mode) - critical for debugging stuck queues
|
|
322
|
+
console.error(`[ERROR] /merge: Unhandled error in run() for ${repoKey}:`, error.message);
|
|
323
|
+
if (error.stack) {
|
|
324
|
+
console.error(`[ERROR] /merge: Stack trace: ${error.stack.split('\n').slice(0, 5).join('\n')}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Always notify user about failure (Issue #1269)
|
|
328
|
+
// Show error in the reply message with immediate feedback
|
|
329
|
+
try {
|
|
330
|
+
const userMessage = formatUserError(error, VERBOSE);
|
|
331
|
+
await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, `❌ *Merge queue failed unexpectedly*\n\n⚠️ *Error:* ${escapeMarkdownV2(userMessage)}\n\n_The queue processing has stopped\\. Please try again or check server logs\\._`, {
|
|
332
|
+
parse_mode: 'MarkdownV2',
|
|
333
|
+
reply_markup: {
|
|
334
|
+
inline_keyboard: [[{ text: '❌ Failed - Click to dismiss', callback_data: `merge_dismiss_${repoKey}` }]],
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
} catch (notifyError) {
|
|
338
|
+
// Log notification failure but don't throw
|
|
339
|
+
console.error(`[ERROR] /merge: Failed to notify user about error: ${notifyError.message}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
activeMergeOperations.delete(repoKey);
|
|
343
|
+
});
|
|
308
344
|
} catch (error) {
|
|
309
345
|
VERBOSE && console.error('[VERBOSE] /merge error:', error);
|
|
310
346
|
|
|
@@ -334,6 +370,21 @@ export function registerMergeCommand(bot, options) {
|
|
|
334
370
|
|
|
335
371
|
VERBOSE && console.log(`[VERBOSE] /merge: Cancelled operation for ${repoKey}`);
|
|
336
372
|
});
|
|
373
|
+
|
|
374
|
+
// Handle dismiss button callback (Issue #1269: for error acknowledgement)
|
|
375
|
+
bot.action(/^merge_dismiss_(.+)$/, async ctx => {
|
|
376
|
+
const repoKey = ctx.match[1];
|
|
377
|
+
VERBOSE && console.log(`[VERBOSE] /merge dismiss callback received for ${repoKey}`);
|
|
378
|
+
|
|
379
|
+
// Remove the inline keyboard button after user acknowledges the error
|
|
380
|
+
try {
|
|
381
|
+
await ctx.editMessageReplyMarkup({ inline_keyboard: [] });
|
|
382
|
+
await ctx.answerCbQuery('Error acknowledged.');
|
|
383
|
+
} catch {
|
|
384
|
+
// Ignore errors (message might have been edited already)
|
|
385
|
+
await ctx.answerCbQuery('Error acknowledged.');
|
|
386
|
+
}
|
|
387
|
+
});
|
|
337
388
|
}
|
|
338
389
|
|
|
339
390
|
/**
|
|
@@ -360,6 +360,11 @@ export class MergeQueueProcessor {
|
|
|
360
360
|
item.error = error.message;
|
|
361
361
|
item.completedAt = new Date();
|
|
362
362
|
this.stats.failed++;
|
|
363
|
+
// Issue #1269: Always log errors (not just in verbose mode) for debugging
|
|
364
|
+
console.error(`[ERROR] /merge-queue: Error processing PR #${item.pr.number}: ${error.message}`);
|
|
365
|
+
if (error.stack) {
|
|
366
|
+
console.error(`[ERROR] /merge-queue: Stack trace: ${error.stack.split('\n').slice(0, 5).join('\n')}`);
|
|
367
|
+
}
|
|
363
368
|
this.log(`Error processing PR #${item.pr.number}: ${error.message}`);
|
|
364
369
|
}
|
|
365
370
|
}
|
|
@@ -450,6 +455,19 @@ export class MergeQueueProcessor {
|
|
|
450
455
|
message += `${statusEmoji} ${update.current}\n\n`;
|
|
451
456
|
}
|
|
452
457
|
|
|
458
|
+
// Show errors/failures inline so user gets immediate feedback (Issue #1269)
|
|
459
|
+
const failedItems = update.items.filter(item => item.status === MergeItemStatus.FAILED && item.error);
|
|
460
|
+
if (failedItems.length > 0) {
|
|
461
|
+
message += `⚠️ *Errors:*\n`;
|
|
462
|
+
for (const item of failedItems.slice(0, 3)) {
|
|
463
|
+
message += ` \\#${item.prNumber}: ${this.escapeMarkdown(item.error.substring(0, 60))}${item.error.length > 60 ? '...' : ''}\n`;
|
|
464
|
+
}
|
|
465
|
+
if (failedItems.length > 3) {
|
|
466
|
+
message += ` _...and ${failedItems.length - 3} more errors_\n`;
|
|
467
|
+
}
|
|
468
|
+
message += '\n';
|
|
469
|
+
}
|
|
470
|
+
|
|
453
471
|
// PRs list with emojis
|
|
454
472
|
message += `*Queue:*\n`;
|
|
455
473
|
for (const item of update.items.slice(0, 10)) {
|
|
@@ -24,11 +24,10 @@
|
|
|
24
24
|
* @param {Function} options.isChatAuthorized - Function to check if chat is authorized
|
|
25
25
|
* @param {Function} options.addBreadcrumb - Function to add breadcrumbs for monitoring
|
|
26
26
|
* @param {Function} options.getSolveQueue - Function to get the solve queue instance
|
|
27
|
-
* @param {Function} options.getRunningClaudeProcesses - Function to get running claude processes
|
|
28
27
|
* @returns {{ handleSolveQueueCommand: Function }} The command handler for use in text fallback
|
|
29
28
|
*/
|
|
30
29
|
export function registerSolveQueueCommand(bot, options) {
|
|
31
|
-
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb, getSolveQueue
|
|
30
|
+
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb, getSolveQueue } = options;
|
|
32
31
|
|
|
33
32
|
async function handleSolveQueueCommand(ctx) {
|
|
34
33
|
VERBOSE && console.log('[VERBOSE] /solve_queue command received');
|
|
@@ -72,13 +71,12 @@ export function registerSolveQueueCommand(bot, options) {
|
|
|
72
71
|
VERBOSE && console.log('[VERBOSE] /solve_queue passed all checks, generating status...');
|
|
73
72
|
|
|
74
73
|
const solveQueue = getSolveQueue({ verbose: VERBOSE });
|
|
75
|
-
const claudeProcs = await getRunningClaudeProcesses(VERBOSE);
|
|
76
74
|
|
|
77
75
|
// Use the queue's built-in detailed status formatter
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
message
|
|
76
|
+
// Shows per-queue breakdown with first 5 items per queue and human-readable times
|
|
77
|
+
// Processing counts are actual running system processes (via pgrep)
|
|
78
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
79
|
+
const message = await solveQueue.formatDetailedStatus();
|
|
82
80
|
|
|
83
81
|
await ctx.reply(message, {
|
|
84
82
|
parse_mode: 'Markdown',
|
|
@@ -43,13 +43,14 @@ export const QueueItemStatus = {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Count running
|
|
46
|
+
* Count running processes by name
|
|
47
|
+
* @param {string} processName - Process name to search for (e.g., 'claude', 'agent')
|
|
47
48
|
* @param {boolean} verbose - Whether to log verbose output
|
|
48
49
|
* @returns {Promise<{count: number, processes: string[]}>}
|
|
49
50
|
*/
|
|
50
|
-
export async function
|
|
51
|
+
export async function getRunningProcesses(processName, verbose = false) {
|
|
51
52
|
try {
|
|
52
|
-
const { stdout } = await execAsync(
|
|
53
|
+
const { stdout } = await execAsync(`pgrep -l -x ${processName} 2>/dev/null || true`);
|
|
53
54
|
const lines = stdout
|
|
54
55
|
.trim()
|
|
55
56
|
.split('\n')
|
|
@@ -60,13 +61,13 @@ export async function getRunningClaudeProcesses(verbose = false) {
|
|
|
60
61
|
const parts = line.trim().split(/\s+/);
|
|
61
62
|
return {
|
|
62
63
|
pid: parts[0],
|
|
63
|
-
name: parts.slice(1).join(' ') ||
|
|
64
|
+
name: parts.slice(1).join(' ') || processName,
|
|
64
65
|
};
|
|
65
66
|
})
|
|
66
67
|
.filter(p => p.pid);
|
|
67
68
|
|
|
68
69
|
if (verbose) {
|
|
69
|
-
console.log(`[VERBOSE] /solve_queue found ${processes.length} running
|
|
70
|
+
console.log(`[VERBOSE] /solve_queue found ${processes.length} running ${processName} processes`);
|
|
70
71
|
if (processes.length > 0) {
|
|
71
72
|
console.log(`[VERBOSE] /solve_queue processes: ${JSON.stringify(processes)}`);
|
|
72
73
|
}
|
|
@@ -78,12 +79,30 @@ export async function getRunningClaudeProcesses(verbose = false) {
|
|
|
78
79
|
};
|
|
79
80
|
} catch (error) {
|
|
80
81
|
if (verbose) {
|
|
81
|
-
console.error(
|
|
82
|
+
console.error(`[VERBOSE] /solve_queue error counting ${processName} processes:`, error.message);
|
|
82
83
|
}
|
|
83
84
|
return { count: 0, processes: [] };
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Count running claude processes
|
|
90
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
91
|
+
* @returns {Promise<{count: number, processes: string[]}>}
|
|
92
|
+
*/
|
|
93
|
+
export async function getRunningClaudeProcesses(verbose = false) {
|
|
94
|
+
return getRunningProcesses('claude', verbose);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Count running agent processes
|
|
99
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
100
|
+
* @returns {Promise<{count: number, processes: string[]}>}
|
|
101
|
+
*/
|
|
102
|
+
export async function getRunningAgentProcesses(verbose = false) {
|
|
103
|
+
return getRunningProcesses('agent', verbose);
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
/**
|
|
88
107
|
* Format a threshold as percentage for display
|
|
89
108
|
* @param {number} ratio - Ratio (0.0 - 1.0)
|
|
@@ -93,6 +112,33 @@ function formatThresholdPercent(ratio) {
|
|
|
93
112
|
return `${Math.round(ratio * 100)}%`;
|
|
94
113
|
}
|
|
95
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Format milliseconds into human-readable duration
|
|
117
|
+
* Shows days, hours, minutes, and seconds as appropriate.
|
|
118
|
+
* Examples: "5h 43m 23s", "2m 15s", "45s", "1d 3h 12m 5s"
|
|
119
|
+
*
|
|
120
|
+
* @param {number} ms - Duration in milliseconds
|
|
121
|
+
* @returns {string} Human-readable duration
|
|
122
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1267
|
|
123
|
+
*/
|
|
124
|
+
export function formatDuration(ms) {
|
|
125
|
+
if (ms < 0) ms = 0;
|
|
126
|
+
|
|
127
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
128
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
129
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
130
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
131
|
+
const seconds = totalSeconds % 60;
|
|
132
|
+
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (days > 0) parts.push(`${days}d`);
|
|
135
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
136
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
137
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
138
|
+
|
|
139
|
+
return parts.join(' ');
|
|
140
|
+
}
|
|
141
|
+
|
|
96
142
|
/**
|
|
97
143
|
* Generate human-readable waiting reason based on threshold violation
|
|
98
144
|
* @param {string} metric - The metric name (ram, cpu, disk, etc.)
|
|
@@ -1035,7 +1081,11 @@ export class SolveQueue {
|
|
|
1035
1081
|
const itemCheck = await this.canStartCommand({ tool: item.tool });
|
|
1036
1082
|
const previousStatus = item.status;
|
|
1037
1083
|
const previousReason = item.waitingReason;
|
|
1038
|
-
|
|
1084
|
+
// Use rejectReason when threshold strategy is 'reject', otherwise use reason
|
|
1085
|
+
// This ensures disk-full and other rejection reasons are shown properly
|
|
1086
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
1087
|
+
const waitReason = itemCheck.rejectReason || itemCheck.reason || 'Waiting in queue';
|
|
1088
|
+
item.setWaiting(waitReason);
|
|
1039
1089
|
|
|
1040
1090
|
// Update message if status/reason changed or it's time for periodic update
|
|
1041
1091
|
const shouldUpdate = previousStatus !== item.status || previousReason !== item.waitingReason || this.shouldUpdateMessage(item);
|
|
@@ -1158,74 +1208,113 @@ export class SolveQueue {
|
|
|
1158
1208
|
}
|
|
1159
1209
|
|
|
1160
1210
|
/**
|
|
1161
|
-
* Format queue status for display
|
|
1162
|
-
* Shows per-tool queue counts.
|
|
1163
|
-
*
|
|
1211
|
+
* Format queue status for display in /limits command
|
|
1212
|
+
* Shows per-tool queue breakdown with processing counts.
|
|
1213
|
+
*
|
|
1214
|
+
* Processing count = actual running system processes (via pgrep), not items in queue processing state.
|
|
1215
|
+
* This is because items transition quickly through the processing state, but the actual
|
|
1216
|
+
* work happens in the spawned system process (claude, agent, etc.).
|
|
1217
|
+
*
|
|
1218
|
+
* Output format:
|
|
1219
|
+
* ```
|
|
1220
|
+
* Queues
|
|
1221
|
+
* claude (pending: 6, processing: 0)
|
|
1222
|
+
* agent (pending: 2, processing: 0)
|
|
1223
|
+
* ```
|
|
1224
|
+
*
|
|
1225
|
+
* @returns {Promise<string>}
|
|
1164
1226
|
* @see https://github.com/link-assistant/hive-mind/issues/1159
|
|
1227
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1267
|
|
1165
1228
|
*/
|
|
1166
|
-
formatStatus() {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1229
|
+
async formatStatus() {
|
|
1230
|
+
// Get actual process counts for each tool queue
|
|
1231
|
+
// The "processing" count is the number of running system processes, not queue internal state
|
|
1232
|
+
// This ensures users see accurate counts of what's actually running
|
|
1233
|
+
const claudeProcs = await getRunningClaudeProcesses(this.verbose);
|
|
1234
|
+
const agentProcs = await getRunningAgentProcesses(this.verbose);
|
|
1235
|
+
|
|
1236
|
+
const processCounts = {
|
|
1237
|
+
claude: claudeProcs.count,
|
|
1238
|
+
agent: agentProcs.count,
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
// Always show per-tool breakdown for all known queues
|
|
1242
|
+
let message = 'Queues\n';
|
|
1243
|
+
for (const [tool, toolQueue] of Object.entries(this.queues)) {
|
|
1244
|
+
const pending = toolQueue.length;
|
|
1245
|
+
const processing = processCounts[tool] || 0;
|
|
1246
|
+
message += `${tool} (pending: ${pending}, processing: ${processing})\n`;
|
|
1176
1247
|
}
|
|
1177
|
-
|
|
1248
|
+
|
|
1249
|
+
return message;
|
|
1178
1250
|
}
|
|
1179
1251
|
|
|
1180
1252
|
/**
|
|
1181
1253
|
* Format detailed queue status for Telegram message
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1254
|
+
* Groups output by tool queue, shows first 5 items per queue, and uses human-readable time.
|
|
1255
|
+
*
|
|
1256
|
+
* Processing count = actual running system processes (via pgrep), not items in queue processing state.
|
|
1257
|
+
* This is because items transition quickly through the processing state, but the actual
|
|
1258
|
+
* work happens in the spawned system process (claude, agent, etc.).
|
|
1259
|
+
*
|
|
1260
|
+
* Output format:
|
|
1261
|
+
* ```
|
|
1262
|
+
* 📋 Solve Queue Status
|
|
1263
|
+
*
|
|
1264
|
+
* claude (pending: 6, processing: 0)
|
|
1265
|
+
* • url1 (waiting, 5h 43m 23s)
|
|
1266
|
+
* └ RAM usage is 70% (threshold: 65%)
|
|
1267
|
+
* • url2 (queued, 2m 15s)
|
|
1268
|
+
*
|
|
1269
|
+
* agent (pending: 2, processing: 0)
|
|
1270
|
+
* • url3 (waiting, 1h 2m 5s)
|
|
1271
|
+
* ```
|
|
1272
|
+
*
|
|
1273
|
+
* @returns {Promise<string>}
|
|
1184
1274
|
* @see https://github.com/link-assistant/hive-mind/issues/1159
|
|
1275
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1267
|
|
1185
1276
|
*/
|
|
1186
|
-
formatDetailedStatus() {
|
|
1277
|
+
async formatDetailedStatus() {
|
|
1187
1278
|
const stats = this.getStats();
|
|
1188
|
-
const summary = this.getQueueSummary();
|
|
1189
1279
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
const
|
|
1195
|
-
.filter(entry => entry[1] > 0)
|
|
1196
|
-
.map(([tool, count]) => `${tool}: ${count}`)
|
|
1197
|
-
.join(', ');
|
|
1198
|
-
if (toolBreakdown) {
|
|
1199
|
-
message += ` (${toolBreakdown})`;
|
|
1200
|
-
}
|
|
1201
|
-
message += '\n';
|
|
1280
|
+
// Get actual process counts for each tool queue
|
|
1281
|
+
// The "processing" count is the number of running system processes, not queue internal state
|
|
1282
|
+
// This ensures users see accurate counts of what's actually running
|
|
1283
|
+
const claudeProcs = await getRunningClaudeProcesses(this.verbose);
|
|
1284
|
+
const agentProcs = await getRunningAgentProcesses(this.verbose);
|
|
1202
1285
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1286
|
+
const processCounts = {
|
|
1287
|
+
claude: claudeProcs.count,
|
|
1288
|
+
agent: agentProcs.count,
|
|
1289
|
+
};
|
|
1206
1290
|
|
|
1207
|
-
|
|
1208
|
-
message += '*Currently Processing:*\n';
|
|
1209
|
-
for (const item of summary.processing) {
|
|
1210
|
-
message += `• ${item.url} [${item.tool}]\n`;
|
|
1211
|
-
}
|
|
1212
|
-
message += '\n';
|
|
1213
|
-
}
|
|
1291
|
+
let message = '📋 *Solve Queue Status*\n\n';
|
|
1214
1292
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1293
|
+
// Show per-tool queue breakdown with items grouped by queue
|
|
1294
|
+
for (const [tool, toolQueue] of Object.entries(this.queues)) {
|
|
1295
|
+
const pending = toolQueue.length;
|
|
1296
|
+
const processing = processCounts[tool] || 0;
|
|
1297
|
+
message += `*${tool}* (pending: ${pending}, processing: ${processing})\n`;
|
|
1298
|
+
|
|
1299
|
+
// Show first 5 queued items for this tool
|
|
1300
|
+
const displayItems = toolQueue.slice(0, 5);
|
|
1301
|
+
for (const item of displayItems) {
|
|
1302
|
+
const waitTime = formatDuration(item.getWaitTime());
|
|
1303
|
+
message += ` • ${item.url} (${item.status}, ${waitTime})\n`;
|
|
1220
1304
|
if (item.waitingReason) {
|
|
1221
|
-
message += `
|
|
1305
|
+
message += ` └ ${item.waitingReason}\n`;
|
|
1222
1306
|
}
|
|
1223
1307
|
}
|
|
1224
|
-
if (
|
|
1225
|
-
message += `
|
|
1308
|
+
if (toolQueue.length > 5) {
|
|
1309
|
+
message += ` ... and ${toolQueue.length - 5} more\n`;
|
|
1226
1310
|
}
|
|
1311
|
+
|
|
1312
|
+
message += '\n';
|
|
1227
1313
|
}
|
|
1228
1314
|
|
|
1315
|
+
// Summary stats
|
|
1316
|
+
message += `Completed: ${stats.completed}, Failed: ${stats.failed}\n`;
|
|
1317
|
+
|
|
1229
1318
|
return message;
|
|
1230
1319
|
}
|
|
1231
1320
|
}
|
|
@@ -1275,8 +1364,11 @@ export default {
|
|
|
1275
1364
|
SolveQueueItem,
|
|
1276
1365
|
getSolveQueue,
|
|
1277
1366
|
resetSolveQueue,
|
|
1367
|
+
getRunningProcesses,
|
|
1278
1368
|
getRunningClaudeProcesses,
|
|
1369
|
+
getRunningAgentProcesses,
|
|
1279
1370
|
createQueueExecuteCallback,
|
|
1371
|
+
formatDuration,
|
|
1280
1372
|
QUEUE_CONFIG,
|
|
1281
1373
|
QueueItemStatus,
|
|
1282
1374
|
};
|