@link-assistant/hive-mind 1.73.8 → 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
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
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
|
+
|
|
3
34
|
## 1.73.8
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -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);
|