@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.73.8",
3
+ "version": "1.73.9",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
- // Show the items this queue is actively processing for this tool, with a
1352
- // clickable link to each issue/PR (issue #1837). These come from the
1353
- // queue's own tracking, so they may differ from the pgrep-based count above.
1354
- const processingItems = Array.from(this.processing.values()).filter(item => item.tool === tool);
1355
- for (const item of processingItems.slice(0, QUEUE_CONFIG.MAX_DISPLAY_ITEMS_PER_QUEUE)) {
1356
- const waitTime = formatDuration(item.getWaitTime(), { locale });
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);