@link-assistant/hive-mind 1.73.7 → 1.73.9
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 +68 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +9 -0
- package/src/gemini.lib.mjs +4 -0
- package/src/lib.mjs +61 -0
- package/src/opencode.lib.mjs +6 -0
- package/src/qwen.lib.mjs +4 -0
- package/src/review.mjs +4 -2
- package/src/session-monitor.lib.mjs +72 -0
- package/src/solve.auto-merge.lib.mjs +7 -3
- package/src/solve.error-handlers.lib.mjs +28 -4
- package/src/solve.mjs +14 -13
- package/src/solve.restart-shared.lib.mjs +12 -3
- package/src/solve.watch.lib.mjs +5 -3
- package/src/telegram-solve-queue.helpers.lib.mjs +108 -0
- package/src/telegram-solve-queue.lib.mjs +14 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.73.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0a5b615: fix(telegram): list currently-executing tasks in `/solve_queue` (`/queue`), not just count them (#1837)
|
|
8
|
+
|
|
9
|
+
After the original #1837 work added clickable lists, the detailed status still
|
|
10
|
+
showed only a `processing: N` **count** for in-flight work — the executing task
|
|
11
|
+
itself was never rendered as a clickable link, which is exactly the case the
|
|
12
|
+
issue cares most about ("search tasks that are stuck or yet executing").
|
|
13
|
+
|
|
14
|
+
Root cause: the processing **count** comes from the external snapshot
|
|
15
|
+
(`max(pgrep, tracked-isolation-session count)`), but the processing **list**
|
|
16
|
+
iterated the queue's own in-memory `processing` Map. `executeItem()` deletes an
|
|
17
|
+
item from that Map the moment the work is dispatched to a detached
|
|
18
|
+
screen/isolation session, so while a task is actually executing the Map is empty
|
|
19
|
+
— count says `1`, list shows nothing.
|
|
20
|
+
|
|
21
|
+
The fix sources the executing items from the same place the count comes from. A
|
|
22
|
+
new `getRunningSessionItems()` in `session-monitor.lib.mjs` returns the
|
|
23
|
+
currently-running detached sessions (with their GitHub `url`, `tool`, `status`,
|
|
24
|
+
`startTime`), reusing the existing isolation `$ --status` / non-isolation
|
|
25
|
+
screen-liveness checks. New helpers `collectExecutingItems` and
|
|
26
|
+
`formatQueueProcessingItems` merge those sessions with the in-memory Map (deduped
|
|
27
|
+
by normalized GitHub URL, filtered by tool) and render them as the `▶️
|
|
28
|
+
[owner/repo#n](url) (status, duration)` lines, capped with `... and N more`.
|
|
29
|
+
`formatDetailedStatus()` now lists executing tasks from this merged source.
|
|
30
|
+
|
|
31
|
+
Adds `tests/test-issue-1837-executing-list.mjs` plus new `solve-queue.test.mjs`
|
|
32
|
+
cases, and documents the root cause and fix in `docs/case-studies/issue-1837`.
|
|
33
|
+
|
|
34
|
+
## 1.73.8
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- 324ed89: fix(solve): surface the core tool error instead of bare `CLAUDE execution failed` (#1845)
|
|
39
|
+
|
|
40
|
+
When an AI tool run failed, both the terminal and the posted GitHub
|
|
41
|
+
`🚨 Solution Draft Failed` comment showed only the generic
|
|
42
|
+
`CLAUDE execution failed`, even though the underlying tool had reported a
|
|
43
|
+
specific cause (for example `API Error: Output blocked by content filtering
|
|
44
|
+
policy`). The real message was captured inside the tool runner but dropped at
|
|
45
|
+
the failure-return boundary, so no downstream consumer could display it.
|
|
46
|
+
|
|
47
|
+
Every AI tool runner now surfaces a structured `errorInfo` (with a `.message`)
|
|
48
|
+
on its failure returns (`claude`, `gemini`, `opencode`, `qwen`; `codex` and
|
|
49
|
+
`agent` already did). Two shared helpers in `lib.mjs` — `extractToolErrorCore`
|
|
50
|
+
(the core error string) and `formatToolExecutionFailure` (the full
|
|
51
|
+
`CLAUDE execution failed with API Error: Output blocked by content filtering
|
|
52
|
+
policy` message) — share one precedence so every surface stays consistent.
|
|
53
|
+
All failure sites now use them: `solve.mjs` (terminal exit, GitHub failure
|
|
54
|
+
comment, critical-error auto-commit reason), `solve.auto-merge.lib.mjs` and
|
|
55
|
+
`solve.watch.lib.mjs` (GitHub message + new terminal `Error details:` lines),
|
|
56
|
+
and `review.mjs`. The helpers collapse whitespace, cap the core error length,
|
|
57
|
+
and never fall back to the agent's success summary.
|
|
58
|
+
|
|
59
|
+
`isApiError` in `solve.restart-shared.lib.mjs` now classifies through the same
|
|
60
|
+
extractor, so a Claude `API Error:` reported via `errorInfo` (never `result`)
|
|
61
|
+
is detected and watch mode's `MAX_API_ERROR_RETRIES` backoff guard keeps
|
|
62
|
+
working instead of retrying forever.
|
|
63
|
+
|
|
64
|
+
The auto-commit-on-critical-error path (#1834) is confirmed to run on the
|
|
65
|
+
failure exit and is now labeled with the real failure cause; the same guarded
|
|
66
|
+
auto-commit is also added to `handleFailure()` so the `uncaughtException`,
|
|
67
|
+
`unhandledRejection`, and top-level-catch exits preserve uncommitted work too.
|
|
68
|
+
Adds unit, cross-tool, auto-commit, and `isApiError` tests plus a deep case
|
|
69
|
+
study in `docs/case-studies/issue-1845`.
|
|
70
|
+
|
|
3
71
|
## 1.73.7
|
|
4
72
|
|
|
5
73
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -1191,6 +1191,8 @@ export const executeClaudeCommand = async params => {
|
|
|
1191
1191
|
is503Error,
|
|
1192
1192
|
anthropicTotalCostUSD,
|
|
1193
1193
|
resultSummary,
|
|
1194
|
+
// Issue #1845: surface the actual error so callers can show it to users
|
|
1195
|
+
errorInfo: { message: lastMessage || 'API explicitly marked error as not retryable', exitCode },
|
|
1194
1196
|
queuedFeedback, // Issue #817: Bidirectional mode feedback
|
|
1195
1197
|
};
|
|
1196
1198
|
}
|
|
@@ -1235,6 +1237,8 @@ export const executeClaudeCommand = async params => {
|
|
|
1235
1237
|
is503Error, // preserve for callers that check this
|
|
1236
1238
|
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1237
1239
|
resultSummary, // Issue #1263: Include result summary
|
|
1240
|
+
// Issue #1845: surface the actual error so callers can show it to users
|
|
1241
|
+
errorInfo: { message: lastMessage || `Transient API error persisted after ${maxRetries} retries`, exitCode },
|
|
1238
1242
|
queuedFeedback, // Issue #817: Bidirectional mode feedback
|
|
1239
1243
|
};
|
|
1240
1244
|
}
|
|
@@ -1294,6 +1298,9 @@ export const executeClaudeCommand = async params => {
|
|
|
1294
1298
|
errorDuringExecution,
|
|
1295
1299
|
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1296
1300
|
resultSummary, // Issue #1263: Include result summary
|
|
1301
|
+
// Issue #1845: surface the core error (e.g. "API Error: Output blocked by content
|
|
1302
|
+
// filtering policy") so users see what actually went wrong, not just a generic message.
|
|
1303
|
+
errorInfo: { message: lastMessage || `Claude command failed with exit code ${exitCode}`, exitCode },
|
|
1297
1304
|
queuedFeedback, // Issue #817: Bidirectional mode feedback
|
|
1298
1305
|
};
|
|
1299
1306
|
}
|
|
@@ -1374,6 +1381,8 @@ export const executeClaudeCommand = async params => {
|
|
|
1374
1381
|
toolUseCount,
|
|
1375
1382
|
anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
|
|
1376
1383
|
resultSummary, // Issue #1263: Include result summary
|
|
1384
|
+
// Issue #1845: surface the actual exception message so callers can show it to users
|
|
1385
|
+
errorInfo: { message: error.message || error.toString() },
|
|
1377
1386
|
queuedFeedback, // Issue #817: Bidirectional mode feedback
|
|
1378
1387
|
};
|
|
1379
1388
|
}
|
package/src/gemini.lib.mjs
CHANGED
|
@@ -575,6 +575,8 @@ export const executeGeminiCommand = async params => {
|
|
|
575
575
|
pricingInfo: { modelId: mappedModel, modelName: mappedModel, provider: 'Google', totalCostUSD: null },
|
|
576
576
|
publicPricingEstimate: null,
|
|
577
577
|
resultSummary: geminiJsonState.resultSummary || null,
|
|
578
|
+
// Issue #1845: surface the actual error so callers can show it to users
|
|
579
|
+
errorInfo: { message: errorText || `Gemini command failed with exit code ${exitCode}`, exitCode },
|
|
578
580
|
};
|
|
579
581
|
}
|
|
580
582
|
|
|
@@ -616,6 +618,8 @@ export const executeGeminiCommand = async params => {
|
|
|
616
618
|
pricingInfo: null,
|
|
617
619
|
publicPricingEstimate: null,
|
|
618
620
|
resultSummary: null,
|
|
621
|
+
// Issue #1845: surface the actual exception message so callers can show it to users
|
|
622
|
+
errorInfo: { message: error.message || error.toString() },
|
|
619
623
|
};
|
|
620
624
|
}
|
|
621
625
|
};
|
package/src/lib.mjs
CHANGED
|
@@ -664,6 +664,67 @@ export const cleanErrorMessage = error => {
|
|
|
664
664
|
return message;
|
|
665
665
|
};
|
|
666
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Extract the core/root error string from a tool runner result (Issue #1845).
|
|
669
|
+
*
|
|
670
|
+
* Applies a single precedence everywhere so every failure surface shows the
|
|
671
|
+
* same root cause: `errorInfo.message` → `errorInfo.errorMatch` → string
|
|
672
|
+
* `errorInfo` → `result`. Returns a collapsed single line, or null when no
|
|
673
|
+
* usable error string is available. Shared by `formatToolExecutionFailure`
|
|
674
|
+
* (GitHub comments / exit message) and the terminal "Error details:" lines in
|
|
675
|
+
* watch / auto-merge so they never diverge.
|
|
676
|
+
*
|
|
677
|
+
* @param {Object} options
|
|
678
|
+
* @param {Object} [options.toolResult] - Result object returned by the tool runner
|
|
679
|
+
* @returns {string|null} The core error string, or null when none is available
|
|
680
|
+
*/
|
|
681
|
+
export const extractToolErrorCore = ({ toolResult } = {}) => {
|
|
682
|
+
// Prefer the structured error message surfaced by the tool runner. We do NOT
|
|
683
|
+
// fall back to resultSummary here, because that holds the agent's normal
|
|
684
|
+
// work summary on success and would be misleading when used as an error.
|
|
685
|
+
const errorInfo = toolResult?.errorInfo;
|
|
686
|
+
const rawCore = errorInfo?.message || errorInfo?.errorMatch || (typeof errorInfo === 'string' ? errorInfo : null) || toolResult?.result || null;
|
|
687
|
+
|
|
688
|
+
if (!rawCore || typeof rawCore !== 'string') return null;
|
|
689
|
+
|
|
690
|
+
// Collapse to a single clean line and strip noise.
|
|
691
|
+
const core = rawCore.replace(/\s+/g, ' ').trim();
|
|
692
|
+
return core || null;
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Build a user-facing tool execution failure message that includes the core
|
|
697
|
+
* error reported by the underlying tool (Issue #1845).
|
|
698
|
+
*
|
|
699
|
+
* Previously users only saw the generic "<TOOL> execution failed" and had to
|
|
700
|
+
* dig through the full failure log to discover what actually went wrong (for
|
|
701
|
+
* example "API Error: Output blocked by content filtering policy"). When the
|
|
702
|
+
* tool runner surfaces a specific error this appends it so the failure is
|
|
703
|
+
* self-explanatory:
|
|
704
|
+
*
|
|
705
|
+
* "CLAUDE execution failed with API Error: Output blocked by content filtering policy"
|
|
706
|
+
*
|
|
707
|
+
* Falls back to the generic phrase when no specific error is available.
|
|
708
|
+
*
|
|
709
|
+
* @param {Object} options
|
|
710
|
+
* @param {string} [options.tool] - Tool name (e.g. 'claude'); defaults to 'claude'
|
|
711
|
+
* @param {Object} [options.toolResult] - Result object returned by the tool runner
|
|
712
|
+
* @param {number} [options.maxLength=300] - Max length of the appended core error
|
|
713
|
+
* @returns {string} The formatted failure message
|
|
714
|
+
*/
|
|
715
|
+
export const formatToolExecutionFailure = ({ tool, toolResult, maxLength = 300 } = {}) => {
|
|
716
|
+
const base = `${(tool || 'claude').toUpperCase()} execution failed`;
|
|
717
|
+
|
|
718
|
+
let core = extractToolErrorCore({ toolResult });
|
|
719
|
+
if (!core) return base;
|
|
720
|
+
|
|
721
|
+
// Avoid duplicating the base phrase if the core error already contains it.
|
|
722
|
+
if (core.toLowerCase().includes('execution failed')) return base;
|
|
723
|
+
|
|
724
|
+
if (core.length > maxLength) core = `${core.slice(0, maxLength - 1)}…`;
|
|
725
|
+
return `${base} with ${core}`;
|
|
726
|
+
};
|
|
727
|
+
|
|
667
728
|
/**
|
|
668
729
|
* Format aligned console output
|
|
669
730
|
* @param {string} icon - Icon to display
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -466,6 +466,8 @@ export const executeOpenCodeCommand = async params => {
|
|
|
466
466
|
permissionPromptDetected: true,
|
|
467
467
|
...pricingResult,
|
|
468
468
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
469
|
+
// Issue #1845: surface the actual error so callers can show it to users
|
|
470
|
+
errorInfo: { message: 'OpenCode requested an interactive permission prompt (non-interactive run cannot continue)' },
|
|
469
471
|
};
|
|
470
472
|
}
|
|
471
473
|
|
|
@@ -528,6 +530,8 @@ export const executeOpenCodeCommand = async params => {
|
|
|
528
530
|
limitResetTime,
|
|
529
531
|
...pricingResult,
|
|
530
532
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
533
|
+
// Issue #1845: surface the actual error so callers can show it to users
|
|
534
|
+
errorInfo: { message: lastMessage || allOutput || `OpenCode command failed with exit code ${exitCode}`, exitCode },
|
|
531
535
|
};
|
|
532
536
|
}
|
|
533
537
|
|
|
@@ -593,6 +597,8 @@ export const executeOpenCodeCommand = async params => {
|
|
|
593
597
|
pricingInfo: null,
|
|
594
598
|
publicPricingEstimate: null,
|
|
595
599
|
resultSummary: null, // Issue #1263: No result summary available on error
|
|
600
|
+
// Issue #1845: surface the actual exception message so callers can show it to users
|
|
601
|
+
errorInfo: { message: error.message || error.toString() },
|
|
596
602
|
};
|
|
597
603
|
}
|
|
598
604
|
};
|
package/src/qwen.lib.mjs
CHANGED
|
@@ -632,6 +632,8 @@ export const executeQwenCommand = async params => {
|
|
|
632
632
|
limitResetTime: null,
|
|
633
633
|
...usageResult,
|
|
634
634
|
resultSummary,
|
|
635
|
+
// Issue #1845: surface the actual error so callers can show it to users
|
|
636
|
+
errorInfo: { message: combinedErrorText || errorMessage || `Qwen Code command failed${exitCode !== 0 ? ` with exit code ${exitCode}` : ''}`, exitCode },
|
|
635
637
|
};
|
|
636
638
|
}
|
|
637
639
|
|
|
@@ -665,6 +667,8 @@ export const executeQwenCommand = async params => {
|
|
|
665
667
|
publicPricingEstimate: null,
|
|
666
668
|
tokenUsage: null,
|
|
667
669
|
resultSummary: null,
|
|
670
|
+
// Issue #1845: surface the actual exception message so callers can show it to users
|
|
671
|
+
errorInfo: { message: error.message || error.toString() },
|
|
668
672
|
};
|
|
669
673
|
}
|
|
670
674
|
};
|
package/src/review.mjs
CHANGED
|
@@ -43,7 +43,7 @@ const path = (await use('path')).default;
|
|
|
43
43
|
const fs = (await use('fs')).promises;
|
|
44
44
|
|
|
45
45
|
// Import shared functions from lib.mjs to follow DRY principle
|
|
46
|
-
import { log, setLogFile, getLogFile, formatAligned } from './lib.mjs';
|
|
46
|
+
import { log, setLogFile, getLogFile, formatAligned, extractToolErrorCore } from './lib.mjs';
|
|
47
47
|
import { parseCliArgumentsWithLino } from './cli-arguments.lib.mjs';
|
|
48
48
|
import { reportError } from './sentry.lib.mjs';
|
|
49
49
|
import * as memoryCheck from './memory-check.mjs';
|
|
@@ -398,7 +398,9 @@ Review this pull request thoroughly.`;
|
|
|
398
398
|
|
|
399
399
|
// Handle command failure
|
|
400
400
|
if (!commandSuccess) {
|
|
401
|
-
|
|
401
|
+
// Issue #1845: surface the core error (e.g. "API Error: ...") instead of just a generic message.
|
|
402
|
+
const reviewErrorCore = extractToolErrorCore({ toolResult: result });
|
|
403
|
+
await log(`\n❌ Command execution failed${reviewErrorCore ? ` with ${reviewErrorCore}` : '. Check the log file for details.'}`);
|
|
402
404
|
await log(`📁 Log file: ${getLogFile()}`);
|
|
403
405
|
process.exit(1);
|
|
404
406
|
}
|
|
@@ -578,6 +578,78 @@ export async function getRunningTrackedIsolationSessions(verbose = false, option
|
|
|
578
578
|
return { count: sessions.length, sessions, byTool };
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Return the currently-executing tracked sessions with the details needed to
|
|
583
|
+
* render them as a clickable list in `/solve_queue` (`/queue`): the issue/PR
|
|
584
|
+
* `url`, the `tool`, the start time, and (for isolation sessions) the backend
|
|
585
|
+
* status. Both isolation and non-isolation screen sessions are included so the
|
|
586
|
+
* list matches what is actually executing — the queue's own in-memory
|
|
587
|
+
* `processing` Map is empty once a task has been dispatched to a detached
|
|
588
|
+
* session, which is why executing tasks were previously not listed.
|
|
589
|
+
*
|
|
590
|
+
* Liveness is determined the same way as {@link monitorSessions}: isolation
|
|
591
|
+
* sessions via `$ --status`, non-isolation screen sessions via a timeout window
|
|
592
|
+
* plus a best-effort `screen -ls` check.
|
|
593
|
+
*
|
|
594
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
595
|
+
* @param {Object} [options] - Test/support options
|
|
596
|
+
* @param {Function} [options.statusProvider] - Optional `$ --status` provider
|
|
597
|
+
* @param {Function} [options.screenChecker] - Optional screen-existence checker
|
|
598
|
+
* @returns {Promise<Array<{sessionName: string, url: string|null, tool: string, status: string|null, startTime: (Date|string|number|null), isolationBackend: (string|null)}>>}
|
|
599
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1837
|
|
600
|
+
*/
|
|
601
|
+
export async function getRunningSessionItems(verbose = false, options = {}) {
|
|
602
|
+
const items = [];
|
|
603
|
+
const screenChecker = options.screenChecker || checkScreenSessionExists;
|
|
604
|
+
|
|
605
|
+
for (const [sessionName, sessionInfo] of activeSessions.entries()) {
|
|
606
|
+
let running = false;
|
|
607
|
+
let status = null;
|
|
608
|
+
|
|
609
|
+
if (sessionInfo.isolationBackend) {
|
|
610
|
+
const state = await getIsolationSessionState(sessionName, sessionInfo, {
|
|
611
|
+
verbose,
|
|
612
|
+
statusProvider: options.statusProvider,
|
|
613
|
+
});
|
|
614
|
+
running = state.running;
|
|
615
|
+
status = state.status || null;
|
|
616
|
+
if (!running) {
|
|
617
|
+
sessionInfo.lastKnownStatus = state.status || null;
|
|
618
|
+
sessionInfo.lastKnownExitCode = state.exitCode ?? null;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
|
|
623
|
+
const elapsed = Date.now() - startTime.getTime();
|
|
624
|
+
if (elapsed >= NON_ISOLATION_SESSION_TIMEOUT_MS) {
|
|
625
|
+
if (verbose) {
|
|
626
|
+
console.log(`[VERBOSE] Non-isolation session ${sessionName} expired after ${Math.round(elapsed / 1000)}s; excluded from running list`);
|
|
627
|
+
}
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
running = await screenChecker(sessionName);
|
|
631
|
+
if (!running) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
items.push({
|
|
637
|
+
sessionName,
|
|
638
|
+
url: sessionInfo.url || null,
|
|
639
|
+
tool: sessionInfo.tool || 'claude',
|
|
640
|
+
status,
|
|
641
|
+
startTime: sessionInfo.startTime || null,
|
|
642
|
+
isolationBackend: sessionInfo.isolationBackend || null,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (verbose) {
|
|
647
|
+
console.log(`[VERBOSE] getRunningSessionItems found ${items.length} running session(s)`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return items;
|
|
651
|
+
}
|
|
652
|
+
|
|
581
653
|
/**
|
|
582
654
|
* Get statistics about session tracking
|
|
583
655
|
* @param {boolean} verbose - Whether to log verbose output
|
|
@@ -22,7 +22,7 @@ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
|
|
|
22
22
|
const $ = wrapDollarWithGhRetry(__rawDollar$);
|
|
23
23
|
// Import shared library functions
|
|
24
24
|
const lib = await import('./lib.mjs');
|
|
25
|
-
const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
|
|
25
|
+
const { log, cleanErrorMessage, formatAligned, formatToolExecutionFailure, extractToolErrorCore, getLogFile } = lib;
|
|
26
26
|
|
|
27
27
|
// Note: We don't use detectAndCountFeedback from solve.feedback.lib.mjs
|
|
28
28
|
// because we have our own non-bot comment detection logic that's more
|
|
@@ -805,6 +805,8 @@ No further AI sessions will be started automatically for this run. Please review
|
|
|
805
805
|
// Resume failed for a non-limit reason — stop the loop
|
|
806
806
|
await log('');
|
|
807
807
|
await log(formatAligned('❌', `${argv.tool.toUpperCase()} RESUME FAILED`, ''));
|
|
808
|
+
// Issue #1845: surface the core error in the terminal, not just in the GitHub log.
|
|
809
|
+
await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult: resumeResult }) || 'Unknown error', 2));
|
|
808
810
|
await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed after limit reset', 2));
|
|
809
811
|
// Issue #1439: Attach failure log before stopping, so user can see what happened
|
|
810
812
|
const shouldAttachLogsOnResumeFail = argv.attachLogs || argv['attach-logs'];
|
|
@@ -822,7 +824,7 @@ No further AI sessions will be started automatically for this run. Please review
|
|
|
822
824
|
log,
|
|
823
825
|
sanitizeLogContent,
|
|
824
826
|
verbose: argv.verbose,
|
|
825
|
-
errorMessage: `${argv.tool
|
|
827
|
+
errorMessage: `${formatToolExecutionFailure({ tool: argv.tool, toolResult: resumeResult })} after limit reset`,
|
|
826
828
|
sessionId: latestSessionId,
|
|
827
829
|
tempDir,
|
|
828
830
|
requestedModel: argv.originalModel || argv.model,
|
|
@@ -855,6 +857,8 @@ No further AI sessions will be started automatically for this run. Please review
|
|
|
855
857
|
// Per reviewer feedback: non-limit failures should fail and stop attempts
|
|
856
858
|
await log('');
|
|
857
859
|
await log(formatAligned('❌', `${argv.tool.toUpperCase()} EXECUTION FAILED`, ''));
|
|
860
|
+
// Issue #1845: surface the core error in the terminal, not just in the GitHub log.
|
|
861
|
+
await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult }) || 'Unknown error', 2));
|
|
858
862
|
await log(formatAligned('', 'Action:', 'Stopping auto-restart — tool execution failed', 2));
|
|
859
863
|
// Issue #1439: Attach failure log before stopping, so user can see what happened
|
|
860
864
|
const shouldAttachLogsOnFail = argv.attachLogs || argv['attach-logs'];
|
|
@@ -872,7 +876,7 @@ No further AI sessions will be started automatically for this run. Please review
|
|
|
872
876
|
log,
|
|
873
877
|
sanitizeLogContent,
|
|
874
878
|
verbose: argv.verbose,
|
|
875
|
-
errorMessage:
|
|
879
|
+
errorMessage: formatToolExecutionFailure({ tool: argv.tool, toolResult }),
|
|
876
880
|
sessionId: latestSessionId,
|
|
877
881
|
tempDir,
|
|
878
882
|
requestedModel: argv.originalModel || argv.model,
|
|
@@ -18,9 +18,30 @@ export const isErrorIssueAutoCreationDisabled = argv => !!(argv?.disableReportIs
|
|
|
18
18
|
* Handles log attachment and PR closing on failure
|
|
19
19
|
*/
|
|
20
20
|
export const handleFailure = async options => {
|
|
21
|
-
const { error, errorType, shouldAttachLogs, argv, global, owner, repo, log, getLogFile, attachLogToGitHub, cleanErrorMessage, sanitizeLogContent, $ } = options;
|
|
21
|
+
const { error, errorType, shouldAttachLogs, argv, global, owner, repo, log, getLogFile, attachLogToGitHub, cleanErrorMessage, sanitizeLogContent, cleanupContext, $ } = options;
|
|
22
22
|
const disableIssueCreation = isErrorIssueAutoCreationDisabled(argv);
|
|
23
23
|
|
|
24
|
+
// Issue #1845 / #1834: "On all failures we automatically commit uncommitted changes by default."
|
|
25
|
+
// Exceptions, unhandled rejections and main-execution errors exit here WITHOUT passing through the
|
|
26
|
+
// tool-failure auto-commit chokepoint in solve.mjs, so preserve (commit + push) any work the agent
|
|
27
|
+
// left on disk first. Gated by config (default on; HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false).
|
|
28
|
+
// Best-effort: never let a commit failure mask the original error.
|
|
29
|
+
try {
|
|
30
|
+
const { criticalErrorRecovery } = await import('./config.lib.mjs');
|
|
31
|
+
if (criticalErrorRecovery.autoCommitUncommittedChanges && cleanupContext?.tempDir) {
|
|
32
|
+
const { commitUncommittedChangesOnCriticalError } = await import('./critical-error-commit.lib.mjs');
|
|
33
|
+
await commitUncommittedChangesOnCriticalError({
|
|
34
|
+
tempDir: cleanupContext.tempDir,
|
|
35
|
+
branchName: cleanupContext.branchName,
|
|
36
|
+
$,
|
|
37
|
+
log,
|
|
38
|
+
reason: `${errorType || 'execution'} error`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} catch (preserveError) {
|
|
42
|
+
await log(` ⚠️ Could not auto-commit changes before failure exit: ${preserveError.message}`, { verbose: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
// Offer to create GitHub issue for the error
|
|
25
46
|
try {
|
|
26
47
|
await handleErrorWithIssueCreation({
|
|
@@ -117,7 +138,7 @@ export const handleFailure = async options => {
|
|
|
117
138
|
* Creates an uncaught exception handler
|
|
118
139
|
*/
|
|
119
140
|
export const createUncaughtExceptionHandler = options => {
|
|
120
|
-
const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, $ } = options;
|
|
141
|
+
const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, cleanupContext, $ } = options;
|
|
121
142
|
|
|
122
143
|
return async error => {
|
|
123
144
|
await log(`\n❌ Uncaught Exception: ${cleanErrorMessage(error)}`, { level: 'error' });
|
|
@@ -136,6 +157,7 @@ export const createUncaughtExceptionHandler = options => {
|
|
|
136
157
|
attachLogToGitHub,
|
|
137
158
|
cleanErrorMessage,
|
|
138
159
|
sanitizeLogContent,
|
|
160
|
+
cleanupContext,
|
|
139
161
|
$,
|
|
140
162
|
});
|
|
141
163
|
|
|
@@ -147,7 +169,7 @@ export const createUncaughtExceptionHandler = options => {
|
|
|
147
169
|
* Creates an unhandled rejection handler
|
|
148
170
|
*/
|
|
149
171
|
export const createUnhandledRejectionHandler = options => {
|
|
150
|
-
const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, $ } = options;
|
|
172
|
+
const { log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, cleanupContext, $ } = options;
|
|
151
173
|
|
|
152
174
|
return async reason => {
|
|
153
175
|
await log(`\n❌ Unhandled Rejection: ${cleanErrorMessage(reason)}`, { level: 'error' });
|
|
@@ -166,6 +188,7 @@ export const createUnhandledRejectionHandler = options => {
|
|
|
166
188
|
attachLogToGitHub,
|
|
167
189
|
cleanErrorMessage,
|
|
168
190
|
sanitizeLogContent,
|
|
191
|
+
cleanupContext,
|
|
169
192
|
$,
|
|
170
193
|
});
|
|
171
194
|
|
|
@@ -219,7 +242,7 @@ export const handleNoPrAvailableError = async ({ isContinueMode, tempDir, issueN
|
|
|
219
242
|
* Handles execution errors in the main catch block
|
|
220
243
|
*/
|
|
221
244
|
export const handleMainExecutionError = async options => {
|
|
222
|
-
const { error, log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, $ } = options;
|
|
245
|
+
const { error, log, cleanErrorMessage, absoluteLogPath, shouldAttachLogs, argv, global, owner, repo, getLogFile, attachLogToGitHub, sanitizeLogContent, cleanupContext, $ } = options;
|
|
223
246
|
|
|
224
247
|
// Special handling for authentication errors
|
|
225
248
|
if (error.isAuthError) {
|
|
@@ -256,6 +279,7 @@ export const handleMainExecutionError = async options => {
|
|
|
256
279
|
attachLogToGitHub,
|
|
257
280
|
cleanErrorMessage,
|
|
258
281
|
sanitizeLogContent,
|
|
282
|
+
cleanupContext,
|
|
259
283
|
$,
|
|
260
284
|
});
|
|
261
285
|
|
package/src/solve.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const fs = (await use('fs')).promises;
|
|
|
21
21
|
const crypto = (await use('crypto')).default;
|
|
22
22
|
const memoryCheck = await import('./memory-check.mjs');
|
|
23
23
|
const lib = await import('./lib.mjs');
|
|
24
|
-
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
|
|
24
|
+
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, formatToolExecutionFailure, getVersionInfo, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
|
|
25
25
|
const githubLib = await import('./github.lib.mjs');
|
|
26
26
|
const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
|
|
27
27
|
const validation = await import('./solve.validation.lib.mjs');
|
|
@@ -181,9 +181,8 @@ const { isIssueUrl, isPrUrl, normalizedUrl, owner, repo, number: urlNumber } = u
|
|
|
181
181
|
issueUrl = normalizedUrl || issueUrl;
|
|
182
182
|
global.owner = owner;
|
|
183
183
|
global.repo = repo;
|
|
184
|
-
// Issue #1752:
|
|
185
|
-
// before
|
|
186
|
-
// the URL is validated so the pre-exit notifier can still comment on it.
|
|
184
|
+
// Issue #1752: record the source issue as soon as the URL is validated so the pre-exit
|
|
185
|
+
// notifier can still comment on it if a check fails before normal issue-mode setup below.
|
|
187
186
|
if (isIssueUrl) {
|
|
188
187
|
global.issueNumber = urlNumber;
|
|
189
188
|
}
|
|
@@ -193,8 +192,7 @@ if (argv.autoLanguage) {
|
|
|
193
192
|
const { applyAutoLanguageToArgv } = await import('./auto-language.lib.mjs');
|
|
194
193
|
await applyAutoLanguageToArgv({ argv, githubLib, owner, repo, number: urlNumber, isIssueUrl, isPrUrl, log });
|
|
195
194
|
}
|
|
196
|
-
// Initialize i18n
|
|
197
|
-
// (or detected system locale). --auto-language may set only the work track.
|
|
195
|
+
// Initialize i18n from --language / --ui-language / --work-language (or system locale).
|
|
198
196
|
const { initI18n } = await import('./i18n.lib.mjs');
|
|
199
197
|
await initI18n({
|
|
200
198
|
language: argv.language,
|
|
@@ -209,6 +207,7 @@ const errorHandlerOptions = {
|
|
|
209
207
|
shouldAttachLogs,
|
|
210
208
|
argv,
|
|
211
209
|
global,
|
|
210
|
+
cleanupContext, // #1845: mutated in place; lets exception handlers auto-commit uncommitted work
|
|
212
211
|
owner: null, // Will be set later when parsed
|
|
213
212
|
repo: null, // Will be set later when parsed
|
|
214
213
|
getLogFile,
|
|
@@ -1072,6 +1071,8 @@ try {
|
|
|
1072
1071
|
// 2. Autonomous claude - one-shot claude --resume w/ --dangerously-skip-permissions -p (claude only)
|
|
1073
1072
|
// 3. Solve resume - re-enters solve.mjs with --resume, preserving tool/model/dir
|
|
1074
1073
|
const toolForFailure = argv.tool || 'claude';
|
|
1074
|
+
// Issue #1845: surface the core error instead of just "<TOOL> execution failed" (terminal + comment).
|
|
1075
|
+
const toolFailureMessage = formatToolExecutionFailure({ tool: toolForFailure, toolResult });
|
|
1075
1076
|
if (sessionId) {
|
|
1076
1077
|
await log('');
|
|
1077
1078
|
await log('💡 To continue this session:');
|
|
@@ -1116,7 +1117,7 @@ try {
|
|
|
1116
1117
|
// Include sessionId so the PR comment can present it
|
|
1117
1118
|
sessionId,
|
|
1118
1119
|
// If not a usage limit case, fall back to generic failure format
|
|
1119
|
-
errorMessage: limitReached ? undefined :
|
|
1120
|
+
errorMessage: limitReached ? undefined : toolFailureMessage,
|
|
1120
1121
|
requestedModel: argv.originalModel || argv.model,
|
|
1121
1122
|
tool: argv.tool || 'claude',
|
|
1122
1123
|
// Issue #1454: Pass resultModelUsage for accurate multi-model display
|
|
@@ -1136,21 +1137,20 @@ try {
|
|
|
1136
1137
|
}
|
|
1137
1138
|
}
|
|
1138
1139
|
|
|
1139
|
-
// Issue #1834 (PR #1835 feedback): "on all critical errors we auto commit uncommitted changes
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
// first. On by default; disable via HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false. Never throws.
|
|
1140
|
+
// Issue #1834 (PR #1835 feedback): "on all critical errors we auto commit uncommitted changes by
|
|
1141
|
+
// default." A failed session exits here before the normal auto-commit chokepoint below, so commit
|
|
1142
|
+
// + push any work first. On by default; disable via HIVE_MIND_AUTO_COMMIT_ON_CRITICAL_ERROR=false.
|
|
1143
1143
|
try {
|
|
1144
1144
|
const { criticalErrorRecovery } = await import('./config.lib.mjs');
|
|
1145
1145
|
if (criticalErrorRecovery.autoCommitUncommittedChanges) {
|
|
1146
1146
|
const { commitUncommittedChangesOnCriticalError } = await import('./critical-error-commit.lib.mjs');
|
|
1147
|
-
await commitUncommittedChangesOnCriticalError({ tempDir, branchName, $, log, reason:
|
|
1147
|
+
await commitUncommittedChangesOnCriticalError({ tempDir, branchName, $, log, reason: toolFailureMessage });
|
|
1148
1148
|
}
|
|
1149
1149
|
} catch (preserveError) {
|
|
1150
1150
|
await log(` ⚠️ Could not auto-commit before failure exit: ${preserveError.message}`, { verbose: true });
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
|
-
await safeExit(1,
|
|
1153
|
+
await safeExit(1, toolFailureMessage);
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
1156
|
// Clean up .playwright-mcp/ to prevent browser artifacts from triggering auto-restart (Issue #1124)
|
|
@@ -1463,6 +1463,7 @@ try {
|
|
|
1463
1463
|
}
|
|
1464
1464
|
await handleMainExecutionError({
|
|
1465
1465
|
error,
|
|
1466
|
+
cleanupContext, // #1845: enable auto-commit of uncommitted work before the failure exit
|
|
1466
1467
|
log,
|
|
1467
1468
|
cleanErrorMessage,
|
|
1468
1469
|
absoluteLogPath,
|
|
@@ -29,7 +29,7 @@ const fs = (await use('fs')).promises;
|
|
|
29
29
|
|
|
30
30
|
// Import shared library functions
|
|
31
31
|
const lib = await import('./lib.mjs');
|
|
32
|
-
const { log, formatAligned } = lib;
|
|
32
|
+
const { log, formatAligned, extractToolErrorCore } = lib;
|
|
33
33
|
|
|
34
34
|
// Import Sentry integration
|
|
35
35
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
@@ -507,11 +507,20 @@ export const buildUncommittedChangesFeedback = (changes, restartCount = 0, maxIt
|
|
|
507
507
|
* @returns {boolean}
|
|
508
508
|
*/
|
|
509
509
|
export const isApiError = toolResult => {
|
|
510
|
-
if (!toolResult
|
|
510
|
+
if (!toolResult) return false;
|
|
511
|
+
|
|
512
|
+
// Issue #1845: runners report failures via `errorInfo` (e.g. claude sets
|
|
513
|
+
// `errorInfo.message` but NOT `result`). Use the shared core-error extractor so an
|
|
514
|
+
// "API Error:" is classified correctly regardless of which field the runner populated —
|
|
515
|
+
// otherwise the MAX_API_ERROR_RETRIES guard never trips for claude and watch mode can
|
|
516
|
+
// retry a hard API error indefinitely. `extractToolErrorCore` still falls back to
|
|
517
|
+
// `result`, preserving the original behavior for runners that set it.
|
|
518
|
+
const errorText = extractToolErrorCore({ toolResult });
|
|
519
|
+
if (!errorText) return false;
|
|
511
520
|
|
|
512
521
|
const errorPatterns = ['API Error:', 'not_found_error', 'authentication_error', 'invalid_request_error'];
|
|
513
522
|
|
|
514
|
-
return errorPatterns.some(pattern =>
|
|
523
|
+
return errorPatterns.some(pattern => errorText.includes(pattern));
|
|
515
524
|
};
|
|
516
525
|
|
|
517
526
|
/**
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -20,7 +20,7 @@ const { wrapDollarWithGhRetry } = await import('./github-rate-limit.lib.mjs');
|
|
|
20
20
|
const $ = wrapDollarWithGhRetry(__rawDollar$);
|
|
21
21
|
// Import shared library functions
|
|
22
22
|
const lib = await import('./lib.mjs');
|
|
23
|
-
const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
|
|
23
|
+
const { log, cleanErrorMessage, formatAligned, formatToolExecutionFailure, extractToolErrorCore, getLogFile } = lib;
|
|
24
24
|
|
|
25
25
|
// Import feedback detection functions
|
|
26
26
|
const feedbackLib = await import('./solve.feedback.lib.mjs');
|
|
@@ -373,7 +373,9 @@ export const watchForFeedback = async params => {
|
|
|
373
373
|
if (consecutiveApiErrors >= MAX_API_ERROR_RETRIES) {
|
|
374
374
|
await log('');
|
|
375
375
|
await log(formatAligned('❌', 'MAXIMUM API ERROR RETRIES REACHED', ''));
|
|
376
|
-
|
|
376
|
+
// Issue #1845: surface the core error (e.g. "API Error: Output blocked by content
|
|
377
|
+
// filtering policy"); toolResult.result is often unset on failure, so prefer errorInfo.
|
|
378
|
+
await log(formatAligned('', 'Error details:', extractToolErrorCore({ toolResult }) || 'Unknown API error', 2));
|
|
377
379
|
await log(formatAligned('', 'Consecutive failures:', `${consecutiveApiErrors}`, 2));
|
|
378
380
|
await log(formatAligned('', 'Action:', 'Exiting watch mode to prevent infinite loop', 2));
|
|
379
381
|
await log('');
|
|
@@ -421,7 +423,7 @@ export const watchForFeedback = async params => {
|
|
|
421
423
|
sessionId: toolResult.sessionId || latestSessionId,
|
|
422
424
|
tempDir,
|
|
423
425
|
// Include error information in the log upload
|
|
424
|
-
errorMessage:
|
|
426
|
+
errorMessage: formatToolExecutionFailure({ tool: argv.tool, toolResult }),
|
|
425
427
|
// Include pricing data if available from failed attempt
|
|
426
428
|
publicPricingEstimate: toolResult.publicPricingEstimate,
|
|
427
429
|
pricingInfo: toolResult.pricingInfo,
|
|
@@ -64,6 +64,114 @@ export function formatQueueHistorySection({ items, emoji, label, max, locale, wi
|
|
|
64
64
|
return `${section}\n`;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Normalize an issue/PR URL for de-duplication: drop a trailing slash, drop any
|
|
69
|
+
* `#fragment`, and lowercase. Two URLs that point at the same issue/PR collapse
|
|
70
|
+
* to the same key so an item that is both in the queue's in-memory `processing`
|
|
71
|
+
* Map and in the tracked-session list is listed only once (issue #1837).
|
|
72
|
+
*
|
|
73
|
+
* @param {string} url
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function normalizeQueueUrl(url) {
|
|
77
|
+
return typeof url === 'string' ? url.replace(/\/+$/, '').replace(/#.*$/, '').toLowerCase() : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the list of tasks a tool is actively *executing* for the detailed queue
|
|
82
|
+
* status, by merging the queue's in-memory `processing` items with the
|
|
83
|
+
* externally-tracked running sessions (detached screen/isolation work),
|
|
84
|
+
* de-duplicated by issue/PR URL.
|
|
85
|
+
*
|
|
86
|
+
* This is the fix for the follow-up on issue #1837: once a task is dispatched to
|
|
87
|
+
* a detached session the queue's own `processing` Map is emptied, so the running
|
|
88
|
+
* task — although still counted via `pgrep`/`$ --status` — was never listed.
|
|
89
|
+
* Pulling the tracked running sessions in here makes executing tasks show up as
|
|
90
|
+
* clickable links again.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} opts
|
|
93
|
+
* @param {Iterable} [opts.processingItems] - `this.processing.values()` (each with `tool`, `url`, `status`, `getWaitTime()`).
|
|
94
|
+
* @param {Array} [opts.sessionItems] - Tracked running sessions (`{url, tool, startTime, status}`).
|
|
95
|
+
* @param {string} opts.tool - Tool key to filter by.
|
|
96
|
+
* @param {number} [opts.now] - Current epoch ms (injectable for tests).
|
|
97
|
+
* @returns {Array<{url: string, queueStatus: (string|null), waitMs: number}>}
|
|
98
|
+
*/
|
|
99
|
+
export function collectExecutingItems({ processingItems = [], sessionItems = [], tool, now = Date.now() }) {
|
|
100
|
+
const byKey = new Map();
|
|
101
|
+
|
|
102
|
+
for (const item of processingItems) {
|
|
103
|
+
if (item.tool !== tool) continue;
|
|
104
|
+
const key = normalizeQueueUrl(item.url) || item.id;
|
|
105
|
+
byKey.set(key, {
|
|
106
|
+
url: item.url,
|
|
107
|
+
queueStatus: item.status || null,
|
|
108
|
+
waitMs: typeof item.getWaitTime === 'function' ? item.getWaitTime() : 0,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const session of sessionItems) {
|
|
113
|
+
if ((session.tool || 'claude') !== tool) continue;
|
|
114
|
+
if (!session.url) continue; // can't render a clickable link without a URL
|
|
115
|
+
const key = normalizeQueueUrl(session.url);
|
|
116
|
+
if (key && byKey.has(key)) continue; // already represented by an in-memory item
|
|
117
|
+
const startMs = session.startTime ? new Date(session.startTime).getTime() : null;
|
|
118
|
+
byKey.set(key || session.sessionName, {
|
|
119
|
+
url: session.url,
|
|
120
|
+
// Tracked sessions report a backend status (e.g. 'executing'); fall back to
|
|
121
|
+
// the generic "processing" label rendered by formatQueueProcessingItems.
|
|
122
|
+
queueStatus: null,
|
|
123
|
+
waitMs: startMs && !Number.isNaN(startMs) ? Math.max(0, now - startMs) : 0,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return [...byKey.values()];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Render the per-tool "executing" lines (`▶️ link (status, elapsed)`) for the
|
|
132
|
+
* detailed queue status, capped at `max` items with a localized "... and N more"
|
|
133
|
+
* line (issue #1837).
|
|
134
|
+
*
|
|
135
|
+
* @param {object} opts
|
|
136
|
+
* @param {Array} opts.items - Output of {@link collectExecutingItems}.
|
|
137
|
+
* @param {number} opts.max - Maximum items to list before collapsing.
|
|
138
|
+
* @param {string|null} opts.locale - Locale for labels/durations.
|
|
139
|
+
* @returns {string} The formatted lines (empty string when no items).
|
|
140
|
+
*/
|
|
141
|
+
export function formatQueueProcessingItems({ items, max, locale }) {
|
|
142
|
+
if (!items || items.length === 0) return '';
|
|
143
|
+
let out = '';
|
|
144
|
+
for (const item of items.slice(0, max)) {
|
|
145
|
+
const label = item.queueStatus ? lt(`queue_status_${item.queueStatus}`, {}, { locale }) : lt('queue_processing', {}, { locale });
|
|
146
|
+
out += ` ▶️ ${formatQueueItemLink(item.url)} (${label}, ${formatDuration(item.waitMs, { locale })})\n`;
|
|
147
|
+
}
|
|
148
|
+
if (items.length > max) {
|
|
149
|
+
out += ` ... ${lt('queue_and_more', { count: items.length - max }, { locale })}\n`;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Lazy wrapper around session-monitor's `getRunningSessionItems` so the queue
|
|
156
|
+
* can list executing detached sessions without a static import (mirrors how the
|
|
157
|
+
* queue lazily loads isolation-session counts). Returns an empty list on error
|
|
158
|
+
* so the detailed status still renders (issue #1837).
|
|
159
|
+
*
|
|
160
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
161
|
+
* @returns {Promise<Array>}
|
|
162
|
+
*/
|
|
163
|
+
export async function getRunningSessionItems(verbose = false) {
|
|
164
|
+
try {
|
|
165
|
+
const { getRunningSessionItems: impl } = await import('./session-monitor.lib.mjs');
|
|
166
|
+
return await impl(verbose);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (verbose) {
|
|
169
|
+
console.error('[VERBOSE] /solve_queue error getting running session items:', error.message);
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
67
175
|
/**
|
|
68
176
|
* Count running processes by name.
|
|
69
177
|
* @param {string} processName - Process name to search for (e.g., 'claude', 'agent', 'codex', 'gemini')
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { getCachedClaudeLimits, getCachedCodexLimits, getCachedGitHubLimits, getCachedMemoryInfo, getCachedCpuInfo, getCachedDiskInfo, getLimitCache } from './limits.lib.mjs';
|
|
19
19
|
export { formatDuration, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
|
|
20
|
-
import { formatDuration, formatQueueHistorySection, formatQueueItemLink, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses } from './telegram-solve-queue.helpers.lib.mjs';
|
|
20
|
+
import { collectExecutingItems, formatDuration, formatQueueHistorySection, formatQueueItemLink, formatQueueProcessingItems, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningCodexProcesses, getRunningGeminiProcesses, getRunningProcesses, getRunningQwenProcesses, getRunningSessionItems } from './telegram-solve-queue.helpers.lib.mjs';
|
|
21
21
|
export { QUEUE_CONFIG, THRESHOLD_STRATEGIES } from './queue-config.lib.mjs';
|
|
22
22
|
import { QUEUE_CONFIG } from './queue-config.lib.mjs';
|
|
23
23
|
import { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
|
|
@@ -164,6 +164,9 @@ export class SolveQueue {
|
|
|
164
164
|
this.messageUpdateCallback = options.messageUpdateCallback || null;
|
|
165
165
|
this.getRunningProcessesFn = options.getRunningProcesses || getRunningProcesses;
|
|
166
166
|
this.getRunningIsolatedSessionsFn = options.getRunningIsolatedSessions || getRunningIsolatedSessions;
|
|
167
|
+
// Source of currently-executing detached sessions (with issue/PR URLs) used
|
|
168
|
+
// to list executing tasks in the detailed status (issue #1837).
|
|
169
|
+
this.getRunningSessionItemsFn = options.getRunningSessionItems || getRunningSessionItems;
|
|
167
170
|
this.autoStart = options.autoStart !== false;
|
|
168
171
|
|
|
169
172
|
// Separate queues per tool type - claude tasks never block other tool tasks
|
|
@@ -1336,6 +1339,10 @@ export class SolveQueue {
|
|
|
1336
1339
|
const locale = getLocale(options);
|
|
1337
1340
|
const stats = this.getStats();
|
|
1338
1341
|
const externalProcessing = await this.getExternalProcessingSnapshot(Object.keys(this.queues));
|
|
1342
|
+
// Currently-executing detached sessions (with issue/PR URLs). These are the
|
|
1343
|
+
// real running tasks; the queue's own `processing` Map is emptied once a task
|
|
1344
|
+
// is dispatched, so without this the executing items are never listed (#1837).
|
|
1345
|
+
const runningSessionItems = await this.getRunningSessionItemsFn(this.verbose);
|
|
1339
1346
|
|
|
1340
1347
|
// Get actual processing counts for each tool queue.
|
|
1341
1348
|
// This combines pgrep with tracked isolation status so users see detached
|
|
@@ -1348,14 +1355,12 @@ export class SolveQueue {
|
|
|
1348
1355
|
const processing = externalProcessing.byTool[tool] || 0;
|
|
1349
1356
|
message += `*${tool}* (${lt('queue_pending', {}, { locale })}: ${pending}, ${lt('queue_processing', {}, { locale })}: ${processing})\n`;
|
|
1350
1357
|
|
|
1351
|
-
//
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
message += ` ▶️ ${formatQueueItemLink(item.url)} (${queueStatusLabel(item.status, locale)}, ${waitTime})\n`;
|
|
1358
|
-
}
|
|
1358
|
+
// List the tasks this tool is actively executing as clickable links. We
|
|
1359
|
+
// merge the queue's in-memory processing Map with the externally-tracked
|
|
1360
|
+
// running sessions (detached screen/isolation work), deduped by URL, so
|
|
1361
|
+
// executing tasks are listed even after dispatch (issue #1837).
|
|
1362
|
+
const executing = collectExecutingItems({ processingItems: this.processing.values(), sessionItems: runningSessionItems, tool });
|
|
1363
|
+
message += formatQueueProcessingItems({ items: executing, max: QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE, locale });
|
|
1359
1364
|
|
|
1360
1365
|
// Show first queued items for this tool with clickable links
|
|
1361
1366
|
const displayItems = toolQueue.slice(0, QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE);
|