@link-assistant/hive-mind 1.56.7 → 1.56.8
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 +7 -0
- package/package.json +2 -2
- package/src/isolation-runner.lib.mjs +86 -27
- package/src/session-monitor.lib.mjs +161 -77
- package/src/telegram-bot.mjs +9 -9
- package/src/telegram-isolation.lib.mjs +2 -2
- package/src/telegram-solve-queue.lib.mjs +80 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.56.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 05a3e42: Fix CI/CD change detection for pull request synchronize events so metadata-only updates skip expensive test jobs while still reporting completed checks.
|
|
8
|
+
- c12f99d: Fix screen-isolated solve monitoring so completed `$ --status` sessions no longer block duplicate commands, queued status displays executing isolation sessions, and Telegram start messages stay in an executing state until completion.
|
|
9
|
+
|
|
3
10
|
## 1.56.7
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.8",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
18
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
19
19
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
20
20
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
21
21
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
|
@@ -22,6 +22,8 @@ const { $ } = await use('command-stream');
|
|
|
22
22
|
|
|
23
23
|
// Valid isolation backends
|
|
24
24
|
const VALID_ISOLATION_BACKENDS = ['screen', 'tmux', 'docker'];
|
|
25
|
+
const RUNNING_SESSION_STATUSES = new Set(['executing', 'running']);
|
|
26
|
+
const TERMINAL_SESSION_STATUSES = new Set(['executed', 'completed', 'failed', 'cancelled', 'canceled', 'error']);
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Generate a UUID v4 for unique session identification
|
|
@@ -31,6 +33,76 @@ export function generateSessionId() {
|
|
|
31
33
|
return crypto.randomUUID();
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Parse output from `$ --status <session>`.
|
|
38
|
+
*
|
|
39
|
+
* start-command versions used in the wild may return JSON when
|
|
40
|
+
* `--output-format json` is supported, or human-readable key/value text.
|
|
41
|
+
* Keep the parser tolerant so completion monitoring survives either format.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} output - Raw stdout from `$ --status`
|
|
44
|
+
* @returns {{exists: boolean, uuid: string|null, status: string|null, exitCode: number|null, startTime: string|null, endTime: string|null, currentTime: string|null, raw: string}}
|
|
45
|
+
*/
|
|
46
|
+
export function parseSessionStatusOutput(output) {
|
|
47
|
+
const raw = (output || '').trim();
|
|
48
|
+
if (!raw) {
|
|
49
|
+
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, raw: '' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
const data = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
55
|
+
return {
|
|
56
|
+
exists: true,
|
|
57
|
+
uuid: data?.uuid || null,
|
|
58
|
+
status: typeof data?.status === 'string' ? data.status.toLowerCase() : null,
|
|
59
|
+
exitCode: data?.exitCode !== undefined && data?.exitCode !== null ? Number(data.exitCode) : null,
|
|
60
|
+
startTime: data?.startTime || null,
|
|
61
|
+
endTime: data?.endTime || null,
|
|
62
|
+
currentTime: data?.currentTime || null,
|
|
63
|
+
raw,
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
// Fall through to text parsing.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const firstLine =
|
|
70
|
+
raw
|
|
71
|
+
.split('\n')
|
|
72
|
+
.find(line => line.trim() && !line.includes(' '))
|
|
73
|
+
?.trim() || null;
|
|
74
|
+
const readField = name => {
|
|
75
|
+
const match = raw.match(new RegExp(`^\\s*${name}\\s+"?([^"\\n]+)"?\\s*$`, 'mi'));
|
|
76
|
+
return match ? match[1].trim() : null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const status = readField('status')?.toLowerCase() || null;
|
|
80
|
+
const exitCodeText = readField('exitCode');
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
exists: Boolean(status || firstLine),
|
|
84
|
+
uuid: readField('uuid') || firstLine,
|
|
85
|
+
status,
|
|
86
|
+
exitCode: exitCodeText !== null ? Number(exitCodeText) : null,
|
|
87
|
+
startTime: readField('startTime'),
|
|
88
|
+
endTime: readField('endTime'),
|
|
89
|
+
currentTime: readField('currentTime'),
|
|
90
|
+
raw,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isExecutingSessionStatus(status) {
|
|
95
|
+
return RUNNING_SESSION_STATUSES.has(String(status || '').toLowerCase());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isTerminalSessionStatus(status) {
|
|
99
|
+
return TERMINAL_SESSION_STATUSES.has(String(status || '').toLowerCase());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function shouldFallbackToScreenStatus(statusResult) {
|
|
103
|
+
return !statusResult?.exists || !statusResult?.status;
|
|
104
|
+
}
|
|
105
|
+
|
|
34
106
|
/**
|
|
35
107
|
* Find the `$` CLI binary path
|
|
36
108
|
* @returns {Promise<string|null>} Path to `$` binary or null
|
|
@@ -133,7 +205,7 @@ export async function executeWithIsolation(command, args, options = {}) {
|
|
|
133
205
|
*
|
|
134
206
|
* @param {string} sessionId - UUID of the session to check
|
|
135
207
|
* @param {boolean} [verbose] - Enable verbose logging
|
|
136
|
-
* @returns {Promise<{exists: boolean, status: string|null, exitCode: number|null, raw: string}>}
|
|
208
|
+
* @returns {Promise<{exists: boolean, uuid: string|null, status: string|null, exitCode: number|null, startTime: string|null, endTime: string|null, currentTime: string|null, raw: string}>}
|
|
137
209
|
*/
|
|
138
210
|
export async function querySessionStatus(sessionId, verbose = false) {
|
|
139
211
|
const binPath = await findStartCommandBinary();
|
|
@@ -141,7 +213,7 @@ export async function querySessionStatus(sessionId, verbose = false) {
|
|
|
141
213
|
if (verbose) {
|
|
142
214
|
console.log('[VERBOSE] isolation-runner: Cannot query status - $ binary not found');
|
|
143
215
|
}
|
|
144
|
-
return { exists: false, status: null, exitCode: null, raw: '' };
|
|
216
|
+
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, raw: '' };
|
|
145
217
|
}
|
|
146
218
|
|
|
147
219
|
try {
|
|
@@ -153,30 +225,12 @@ export async function querySessionStatus(sessionId, verbose = false) {
|
|
|
153
225
|
console.log(`[VERBOSE] isolation-runner: Status query result: ${stdout.substring(0, 300)}`);
|
|
154
226
|
}
|
|
155
227
|
|
|
156
|
-
|
|
157
|
-
const data = JSON.parse(stdout);
|
|
158
|
-
return {
|
|
159
|
-
exists: true,
|
|
160
|
-
status: data.status || null,
|
|
161
|
-
exitCode: data.exitCode !== undefined ? data.exitCode : null,
|
|
162
|
-
raw: stdout,
|
|
163
|
-
};
|
|
164
|
-
} catch {
|
|
165
|
-
// If JSON parsing fails, try text-based detection
|
|
166
|
-
const isExecuting = stdout.includes('executing');
|
|
167
|
-
const isExecuted = stdout.includes('executed');
|
|
168
|
-
return {
|
|
169
|
-
exists: isExecuting || isExecuted,
|
|
170
|
-
status: isExecuting ? 'executing' : isExecuted ? 'executed' : null,
|
|
171
|
-
exitCode: null,
|
|
172
|
-
raw: stdout,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
228
|
+
return parseSessionStatusOutput(stdout);
|
|
175
229
|
} catch (error) {
|
|
176
230
|
if (verbose) {
|
|
177
231
|
console.log(`[VERBOSE] isolation-runner: Status query error: ${error.message}`);
|
|
178
232
|
}
|
|
179
|
-
return { exists: false, status: null, exitCode: null, raw: '' };
|
|
233
|
+
return { exists: false, uuid: null, status: null, exitCode: null, startTime: null, endTime: null, currentTime: null, raw: '' };
|
|
180
234
|
}
|
|
181
235
|
}
|
|
182
236
|
|
|
@@ -222,16 +276,21 @@ export async function isSessionRunning(sessionId, options = {}) {
|
|
|
222
276
|
const { backend, verbose = false } = opts;
|
|
223
277
|
|
|
224
278
|
const result = await querySessionStatus(sessionId, verbose);
|
|
225
|
-
if (result.exists && result.status
|
|
226
|
-
|
|
279
|
+
if (result.exists && result.status) {
|
|
280
|
+
if (isExecutingSessionStatus(result.status)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (isTerminalSessionStatus(result.status)) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
227
286
|
}
|
|
228
287
|
|
|
229
288
|
// Fallback: for screen backend, check screen -ls directly.
|
|
230
|
-
// This works around
|
|
289
|
+
// Only use this when $ --status has no usable record. This works around
|
|
290
|
+
// older start-command bugs where:
|
|
231
291
|
// 1. $ --status can't find session by --session name (only by internal UUID)
|
|
232
|
-
// 2. $ --status reports "executed" immediately for --detached screen sessions
|
|
233
292
|
// See: https://github.com/link-assistant/hive-mind/issues/1545
|
|
234
|
-
if (backend === 'screen') {
|
|
293
|
+
if (backend === 'screen' && shouldFallbackToScreenStatus(result)) {
|
|
235
294
|
const screenRunning = await checkScreenSessionRunning(sessionId, verbose);
|
|
236
295
|
if (screenRunning && verbose) {
|
|
237
296
|
console.log(`[VERBOSE] isolation-runner: $ --status says not running, but screen -ls confirms session '${sessionId}' is still active`);
|
|
@@ -28,12 +28,6 @@ async function getIsolationRunner() {
|
|
|
28
28
|
}
|
|
29
29
|
return _isolationRunner;
|
|
30
30
|
}
|
|
31
|
-
// Legacy accessor for querySessionStatus
|
|
32
|
-
async function getQuerySessionStatus() {
|
|
33
|
-
const mod = await getIsolationRunner();
|
|
34
|
-
return mod.querySessionStatus;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
31
|
// In-memory session store
|
|
38
32
|
const activeSessions = new Map();
|
|
39
33
|
|
|
@@ -65,51 +59,6 @@ export async function checkScreenSessionExists(sessionName) {
|
|
|
65
59
|
}
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
/**
|
|
69
|
-
* Check if an isolated session is still running.
|
|
70
|
-
* Uses isolation-runner's isSessionRunning which includes screen -ls fallback
|
|
71
|
-
* for screen-backend sessions to work around start-command UUID mismatch.
|
|
72
|
-
*
|
|
73
|
-
* @param {string} sessionId - UUID of the isolated session (screen session name)
|
|
74
|
-
* @param {Object} [options] - Options
|
|
75
|
-
* @param {string} [options.backend] - Isolation backend ('screen', 'tmux', 'docker')
|
|
76
|
-
* @param {boolean} [options.verbose] - Whether to log verbose output
|
|
77
|
-
* @returns {Promise<boolean>} True if session is still running
|
|
78
|
-
* @see https://github.com/link-assistant/hive-mind/issues/1545
|
|
79
|
-
*/
|
|
80
|
-
async function checkIsolatedSessionRunning(sessionId, options = {}) {
|
|
81
|
-
const opts = typeof options === 'boolean' ? { verbose: options } : options;
|
|
82
|
-
const { backend, verbose = false } = opts;
|
|
83
|
-
try {
|
|
84
|
-
const runner = await getIsolationRunner();
|
|
85
|
-
return await runner.isSessionRunning(sessionId, { backend, verbose });
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (verbose) {
|
|
88
|
-
console.error(`[VERBOSE] Error checking isolated session ${sessionId}: ${error.message}`);
|
|
89
|
-
}
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get the exit code of a completed isolated session
|
|
96
|
-
* @param {string} sessionId - UUID of the isolated session
|
|
97
|
-
* @param {boolean} verbose - Whether to log verbose output
|
|
98
|
-
* @returns {Promise<number|null>} Exit code or null if unknown
|
|
99
|
-
*/
|
|
100
|
-
async function getIsolatedSessionExitCode(sessionId, verbose = false) {
|
|
101
|
-
try {
|
|
102
|
-
const queryStatus = await getQuerySessionStatus();
|
|
103
|
-
const result = await queryStatus(sessionId, verbose);
|
|
104
|
-
if (result.exists && result.status === 'executed') {
|
|
105
|
-
return result.exitCode;
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
} catch {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
62
|
/**
|
|
114
63
|
* Track a new session for completion monitoring
|
|
115
64
|
*
|
|
@@ -172,6 +121,67 @@ function completeSession(sessionName, exitCode = 0, verbose = false) {
|
|
|
172
121
|
}
|
|
173
122
|
}
|
|
174
123
|
|
|
124
|
+
function normalizeSessionUrl(url) {
|
|
125
|
+
return url.replace(/\/+$/, '').replace(/#.*$/, '').toLowerCase();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isNonIsolationSessionActive(sessionName, sessionInfo, verbose = false) {
|
|
129
|
+
const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
|
|
130
|
+
const elapsed = Date.now() - startTime.getTime();
|
|
131
|
+
if (elapsed >= NON_ISOLATION_SESSION_TIMEOUT_MS) {
|
|
132
|
+
if (verbose) {
|
|
133
|
+
console.log(`[VERBOSE] Non-isolation session ${sessionName} expired after ${Math.round(elapsed / 1000)}s (timeout: ${NON_ISOLATION_SESSION_TIMEOUT_MS / 1000}s), removing from tracking`);
|
|
134
|
+
}
|
|
135
|
+
activeSessions.delete(sessionName);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (verbose) {
|
|
139
|
+
const remainingSec = Math.round((NON_ISOLATION_SESSION_TIMEOUT_MS - elapsed) / 1000);
|
|
140
|
+
console.log(`[VERBOSE] Non-isolation session ${sessionName} still within timeout (${remainingSec}s remaining)`);
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function getIsolationSessionState(sessionName, sessionInfo, options = {}) {
|
|
146
|
+
const { verbose = false, statusProvider = null } = options;
|
|
147
|
+
const sessionId = sessionInfo.sessionId || sessionName;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const runner = await getIsolationRunner();
|
|
151
|
+
const statusResult = statusProvider ? await statusProvider(sessionId, sessionInfo) : await runner.querySessionStatus(sessionId, verbose);
|
|
152
|
+
|
|
153
|
+
if (statusResult?.exists && statusResult.status) {
|
|
154
|
+
if (runner.isExecutingSessionStatus(statusResult.status)) {
|
|
155
|
+
return { running: true, exitCode: null, status: statusResult.status, statusResult };
|
|
156
|
+
}
|
|
157
|
+
if (runner.isTerminalSessionStatus(statusResult.status)) {
|
|
158
|
+
return {
|
|
159
|
+
running: false,
|
|
160
|
+
exitCode: statusResult.exitCode !== undefined ? statusResult.exitCode : null,
|
|
161
|
+
status: statusResult.status,
|
|
162
|
+
statusResult,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const running = await runner.isSessionRunning(sessionId, {
|
|
168
|
+
backend: sessionInfo.isolationBackend,
|
|
169
|
+
verbose,
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
running,
|
|
173
|
+
exitCode: running ? null : (statusResult?.exitCode ?? null),
|
|
174
|
+
status: statusResult?.status || null,
|
|
175
|
+
statusResult,
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (verbose) {
|
|
179
|
+
console.error(`[VERBOSE] Error refreshing isolated session ${sessionId}: ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
return { running: false, exitCode: null, status: null, statusResult: null };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
175
185
|
/**
|
|
176
186
|
* Monitor active sessions and send notifications when they complete
|
|
177
187
|
* @param {Object} bot - Telegraf bot instance for sending messages
|
|
@@ -193,15 +203,12 @@ export async function monitorSessions(bot, verbose = false) {
|
|
|
193
203
|
let exitCode = null;
|
|
194
204
|
|
|
195
205
|
if (sessionInfo.isolationBackend && sessionInfo.sessionId) {
|
|
196
|
-
// Isolation mode: use $ --status with screen -ls
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (!stillRunning) {
|
|
203
|
-
exitCode = await getIsolatedSessionExitCode(sessionInfo.sessionId, verbose);
|
|
204
|
-
}
|
|
206
|
+
// Isolation mode: use $ --status, with screen -ls only as a fallback
|
|
207
|
+
// when the status record is unavailable. Terminal $ statuses are
|
|
208
|
+
// authoritative so completed screen sessions do not stay blocked.
|
|
209
|
+
const state = await getIsolationSessionState(sessionName, sessionInfo, { verbose });
|
|
210
|
+
stillRunning = state.running;
|
|
211
|
+
exitCode = state.exitCode;
|
|
205
212
|
} else {
|
|
206
213
|
// Issue #1586: Non-isolation screen sessions cannot reliably detect
|
|
207
214
|
// completion because start-screen keeps the screen alive via `exec bash`.
|
|
@@ -294,27 +301,14 @@ export function hasActiveSessionForUrl(url, verbose = false) {
|
|
|
294
301
|
if (!url) return { isActive: false, sessionName: null };
|
|
295
302
|
|
|
296
303
|
// Normalize the URL for comparison (remove trailing slashes, fragments, etc.)
|
|
297
|
-
const
|
|
298
|
-
const normalizedUrl = normalizeUrl(url);
|
|
304
|
+
const normalizedUrl = normalizeSessionUrl(url);
|
|
299
305
|
|
|
300
306
|
for (const [sessionName, sessionInfo] of activeSessions.entries()) {
|
|
301
307
|
// Issue #1586: Auto-expire non-isolation sessions after timeout
|
|
302
|
-
if (!sessionInfo.isolationBackend) {
|
|
303
|
-
|
|
304
|
-
const elapsed = Date.now() - startTime.getTime();
|
|
305
|
-
if (elapsed >= NON_ISOLATION_SESSION_TIMEOUT_MS) {
|
|
306
|
-
if (verbose) {
|
|
307
|
-
console.log(`[VERBOSE] Non-isolation session ${sessionName} expired after ${Math.round(elapsed / 1000)}s (timeout: ${NON_ISOLATION_SESSION_TIMEOUT_MS / 1000}s), removing from tracking`);
|
|
308
|
-
}
|
|
309
|
-
activeSessions.delete(sessionName);
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (verbose) {
|
|
313
|
-
const remainingSec = Math.round((NON_ISOLATION_SESSION_TIMEOUT_MS - elapsed) / 1000);
|
|
314
|
-
console.log(`[VERBOSE] Non-isolation session ${sessionName} still within timeout (${remainingSec}s remaining)`);
|
|
315
|
-
}
|
|
308
|
+
if (!sessionInfo.isolationBackend && !isNonIsolationSessionActive(sessionName, sessionInfo, verbose)) {
|
|
309
|
+
continue;
|
|
316
310
|
}
|
|
317
|
-
if (sessionInfo.url &&
|
|
311
|
+
if (sessionInfo.url && normalizeSessionUrl(sessionInfo.url) === normalizedUrl) {
|
|
318
312
|
if (verbose) {
|
|
319
313
|
const mode = sessionInfo.isolationBackend ? `isolation:${sessionInfo.isolationBackend}` : 'non-isolation (timeout-based)';
|
|
320
314
|
console.log(`[VERBOSE] Found active session for URL ${url}: ${sessionName} (${mode})`);
|
|
@@ -329,6 +323,96 @@ export function hasActiveSessionForUrl(url, verbose = false) {
|
|
|
329
323
|
return { isActive: false, sessionName: null };
|
|
330
324
|
}
|
|
331
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Async active-session check for command handlers.
|
|
328
|
+
*
|
|
329
|
+
* Isolation-backed sessions are refreshed through `$ --status` before they
|
|
330
|
+
* block a duplicate URL, so completed screen-isolated runs no longer require
|
|
331
|
+
* waiting for the background polling interval.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} url - The GitHub URL to check
|
|
334
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
335
|
+
* @param {Object} [options] - Test/support options
|
|
336
|
+
* @param {Function} [options.statusProvider] - Optional `$ --status` provider
|
|
337
|
+
* @returns {Promise<{isActive: boolean, sessionName: string|null, status?: string|null}>}
|
|
338
|
+
*/
|
|
339
|
+
export async function hasActiveSessionForUrlAsync(url, verbose = false, options = {}) {
|
|
340
|
+
if (!url) return { isActive: false, sessionName: null };
|
|
341
|
+
|
|
342
|
+
const normalizedUrl = normalizeSessionUrl(url);
|
|
343
|
+
|
|
344
|
+
for (const [sessionName, sessionInfo] of activeSessions.entries()) {
|
|
345
|
+
if (!sessionInfo.url || normalizeSessionUrl(sessionInfo.url) !== normalizedUrl) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!sessionInfo.isolationBackend) {
|
|
350
|
+
if (isNonIsolationSessionActive(sessionName, sessionInfo, verbose)) {
|
|
351
|
+
return { isActive: true, sessionName, status: null };
|
|
352
|
+
}
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const state = await getIsolationSessionState(sessionName, sessionInfo, {
|
|
357
|
+
verbose,
|
|
358
|
+
statusProvider: options.statusProvider,
|
|
359
|
+
});
|
|
360
|
+
if (state.running) {
|
|
361
|
+
if (verbose) {
|
|
362
|
+
console.log(`[VERBOSE] Found executing isolated session for URL ${url}: ${sessionName} (status: ${state.status || 'unknown'})`);
|
|
363
|
+
}
|
|
364
|
+
return { isActive: true, sessionName, status: state.status || null };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (verbose) {
|
|
368
|
+
console.log(`[VERBOSE] Isolated session ${sessionName} for URL ${url} is no longer running (status: ${state.status || 'unknown'}), allowing retry while monitor sends completion`);
|
|
369
|
+
}
|
|
370
|
+
sessionInfo.lastKnownStatus = state.status || null;
|
|
371
|
+
sessionInfo.lastKnownExitCode = state.exitCode ?? null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (verbose) {
|
|
375
|
+
console.log(`[VERBOSE] No active session found for URL ${url}`);
|
|
376
|
+
}
|
|
377
|
+
return { isActive: false, sessionName: null };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Refresh tracked isolation sessions and count only those that are executing.
|
|
382
|
+
*
|
|
383
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
384
|
+
* @param {Object} [options] - Test/support options
|
|
385
|
+
* @param {Function} [options.statusProvider] - Optional `$ --status` provider
|
|
386
|
+
* @returns {Promise<{count: number, sessions: string[], byTool: Object}>}
|
|
387
|
+
*/
|
|
388
|
+
export async function getRunningTrackedIsolationSessions(verbose = false, options = {}) {
|
|
389
|
+
const sessions = [];
|
|
390
|
+
const byTool = {};
|
|
391
|
+
|
|
392
|
+
for (const [sessionName, sessionInfo] of activeSessions.entries()) {
|
|
393
|
+
if (!sessionInfo.isolationBackend) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const state = await getIsolationSessionState(sessionName, sessionInfo, {
|
|
398
|
+
verbose,
|
|
399
|
+
statusProvider: options.statusProvider,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (!state.running) {
|
|
403
|
+
sessionInfo.lastKnownStatus = state.status || null;
|
|
404
|
+
sessionInfo.lastKnownExitCode = state.exitCode ?? null;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const tool = sessionInfo.tool || 'claude';
|
|
409
|
+
sessions.push(sessionName);
|
|
410
|
+
byTool[tool] = (byTool[tool] || 0) + 1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { count: sessions.length, sessions, byTool };
|
|
414
|
+
}
|
|
415
|
+
|
|
332
416
|
/**
|
|
333
417
|
* Get statistics about session tracking
|
|
334
418
|
* @param {boolean} verbose - Whether to log verbose output
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -49,7 +49,7 @@ const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFro
|
|
|
49
49
|
const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
|
|
50
50
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
|
|
51
51
|
const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
|
|
52
|
-
const { trackSession, startSessionMonitoring,
|
|
52
|
+
const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
|
|
53
53
|
|
|
54
54
|
const config = yargs(hideBin(process.argv))
|
|
55
55
|
.usage('Usage: hive-telegram-bot [options]')
|
|
@@ -549,7 +549,7 @@ async function safeReply(ctx, text, options = {}) {
|
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
-
async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null) {
|
|
552
|
+
async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null, tool = 'claude') {
|
|
553
553
|
const { chat, message_id: msgId } = startingMessage;
|
|
554
554
|
const safeEdit = async text => {
|
|
555
555
|
try {
|
|
@@ -567,19 +567,19 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
|
|
|
567
567
|
VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
|
|
568
568
|
result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
|
|
569
569
|
extraInfo = `\n🔒 Isolation: \`${iso.backend}\``;
|
|
570
|
-
if (result.success) trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session }, VERBOSE);
|
|
570
|
+
if (result.success) trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session, tool }, VERBOSE);
|
|
571
571
|
} else {
|
|
572
572
|
result = await executeStartScreen(commandName, args);
|
|
573
573
|
const match = result.success && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
|
|
574
574
|
session = match ? match[1] : 'unknown';
|
|
575
575
|
// Issue #1586: Track non-isolation sessions with timeout-based expiry.
|
|
576
576
|
// These sessions cannot reliably detect completion (screen stays alive via
|
|
577
|
-
// `exec bash`), so
|
|
577
|
+
// `exec bash`), so active URL checks auto-expire them after 10 min.
|
|
578
578
|
// This prevents accidental duplicate commands within the timeout window.
|
|
579
|
-
if (result.success && session !== 'unknown') trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName }, VERBOSE);
|
|
579
|
+
if (result.success && session !== 'unknown') trackSession(session, { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool }, VERBOSE);
|
|
580
580
|
}
|
|
581
581
|
if (result.warning) return safeEdit(`⚠️ ${result.warning}`);
|
|
582
|
-
if (result.success) await safeEdit(
|
|
582
|
+
if (result.success) await safeEdit(`🔄 ${commandName.charAt(0).toUpperCase() + commandName.slice(1)} command executing...\n\nStatus: \`Executing...\`\n📊 Session: \`${session}\`${extraInfo}\n\n${infoBlock}\n\n🔔 This message will update when the session finishes.`);
|
|
583
583
|
else await safeEdit(`❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``);
|
|
584
584
|
}
|
|
585
585
|
|
|
@@ -990,7 +990,7 @@ async function handleSolveCommand(ctx) {
|
|
|
990
990
|
return;
|
|
991
991
|
}
|
|
992
992
|
// Issue #1567: Prevent concurrent sessions on the same PR/issue
|
|
993
|
-
const activeSession =
|
|
993
|
+
const activeSession = await hasActiveSessionForUrlAsync(normalizedUrl, VERBOSE);
|
|
994
994
|
if (activeSession.isActive) {
|
|
995
995
|
await safeReply(ctx, `❌ A working session is already running for this URL.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nSession: \`${activeSession.sessionName}\`\n\n💡 Wait for the current session to complete, or use /solve\\_stop to cancel it.`, { reply_to_message_id: ctx.message.message_id });
|
|
996
996
|
return;
|
|
@@ -1006,7 +1006,7 @@ async function handleSolveCommand(ctx) {
|
|
|
1006
1006
|
const toolQueuedCount = queueStats.queuedByTool[solveTool] || 0; // tool-specific queue count (#1551)
|
|
1007
1007
|
if (check.canStart && toolQueuedCount === 0) {
|
|
1008
1008
|
const startingMessage = await safeReply(ctx, `🚀 Starting solve command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
|
|
1009
|
-
await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock, effectiveSolveIsolation);
|
|
1009
|
+
await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock, effectiveSolveIsolation, solveTool);
|
|
1010
1010
|
} else {
|
|
1011
1011
|
const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation });
|
|
1012
1012
|
let queueMessage = `📋 Solve command queued (${solveTool} queue position #${toolQueuedCount + 1})\n\n${infoBlock}`; // tool-specific position (#1551)
|
|
@@ -1171,7 +1171,7 @@ async function handleHiveCommand(ctx) {
|
|
|
1171
1171
|
}
|
|
1172
1172
|
|
|
1173
1173
|
const startingMessage = await safeReply(ctx, `🚀 Starting hive command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
|
|
1174
|
-
await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock, effectiveHiveIsolation);
|
|
1174
|
+
await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock, effectiveHiveIsolation, hiveTool);
|
|
1175
1175
|
}
|
|
1176
1176
|
|
|
1177
1177
|
bot.command(/^hive$/i, handleHiveCommand);
|
|
@@ -80,8 +80,8 @@ export function createIsolationAwareQueueCallback(botIsolationBackend, botIsolat
|
|
|
80
80
|
if (iso) {
|
|
81
81
|
const sid = iso.runner.generateSessionId();
|
|
82
82
|
const r = await iso.runner.executeWithIsolation(item.command || 'solve', item.args, { backend: iso.backend, sessionId: sid, verbose });
|
|
83
|
-
if (r.success) trackSession(sid, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: item.command || 'solve', isolationBackend: iso.backend, sessionId: sid }, verbose);
|
|
84
|
-
return { ...r, output: r.output || `session: ${sid}` };
|
|
83
|
+
if (r.success) trackSession(sid, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: item.command || 'solve', isolationBackend: iso.backend, sessionId: sid, tool: item.tool || 'claude' }, verbose);
|
|
84
|
+
return { ...r, sessionId: sid, isolationBackend: iso.backend, output: r.output || `session: ${sid}` };
|
|
85
85
|
}
|
|
86
86
|
return fallbackCallback(item);
|
|
87
87
|
};
|
|
@@ -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, getRunningProcesses } from './telegram-solve-queue.helpers.lib.mjs';
|
|
20
|
-
import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses,
|
|
20
|
+
import { formatDuration, formatWaitingReason, getRunningAgentProcesses, getRunningClaudeProcesses, getRunningProcesses } 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
|
|
|
@@ -133,6 +133,8 @@ export class SolveQueue {
|
|
|
133
133
|
this.verbose = options.verbose || false;
|
|
134
134
|
this.executeCallback = options.executeCallback || null;
|
|
135
135
|
this.messageUpdateCallback = options.messageUpdateCallback || null;
|
|
136
|
+
this.getRunningProcessesFn = options.getRunningProcesses || getRunningProcesses;
|
|
137
|
+
this.getRunningIsolatedSessionsFn = options.getRunningIsolatedSessions || getRunningIsolatedSessions;
|
|
136
138
|
|
|
137
139
|
// Separate queues per tool type - claude tasks never block agent tasks
|
|
138
140
|
// See: https://github.com/link-assistant/hive-mind/issues/1159
|
|
@@ -462,6 +464,46 @@ export class SolveQueue {
|
|
|
462
464
|
};
|
|
463
465
|
}
|
|
464
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Get external processing counts from both process scanning and tracked
|
|
469
|
+
* isolated sessions. The displayed/accounted value is the maximum of the two
|
|
470
|
+
* sources so screen-isolated sessions remain visible even when the AI CLI
|
|
471
|
+
* process is not directly observable, while regular non-isolated runs still
|
|
472
|
+
* use pgrep as before.
|
|
473
|
+
*
|
|
474
|
+
* @param {string[]} tools - Tool queues to count
|
|
475
|
+
* @returns {Promise<{byTool: Object, processByTool: Object, isolatedByTool: Object, total: number, isolatedTotal: number, processTotal: number}>}
|
|
476
|
+
*/
|
|
477
|
+
async getExternalProcessingSnapshot(tools = Object.keys(this.queues)) {
|
|
478
|
+
const uniqueTools = [...new Set(tools)];
|
|
479
|
+
const isolated = await this.getRunningIsolatedSessionsFn(this.verbose);
|
|
480
|
+
const isolatedByTool = isolated.byTool || {};
|
|
481
|
+
const processByTool = {};
|
|
482
|
+
const byTool = {};
|
|
483
|
+
|
|
484
|
+
await Promise.all(
|
|
485
|
+
uniqueTools.map(async tool => {
|
|
486
|
+
const result = await this.getRunningProcessesFn(tool, this.verbose);
|
|
487
|
+
const processCount = result?.count || 0;
|
|
488
|
+
const isolatedCount = isolatedByTool[tool] || 0;
|
|
489
|
+
processByTool[tool] = processCount;
|
|
490
|
+
byTool[tool] = Math.max(processCount, isolatedCount);
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const processTotal = Object.values(processByTool).reduce((sum, count) => sum + count, 0);
|
|
495
|
+
const isolatedTotal = isolated.count || Object.values(isolatedByTool).reduce((sum, count) => sum + count, 0);
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
byTool,
|
|
499
|
+
processByTool,
|
|
500
|
+
isolatedByTool,
|
|
501
|
+
total: Math.max(processTotal, isolatedTotal),
|
|
502
|
+
isolatedTotal,
|
|
503
|
+
processTotal,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
465
507
|
/**
|
|
466
508
|
* Check if a new command can start
|
|
467
509
|
*
|
|
@@ -505,16 +547,19 @@ export class SolveQueue {
|
|
|
505
547
|
}
|
|
506
548
|
}
|
|
507
549
|
|
|
508
|
-
// Check running
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
const
|
|
550
|
+
// Check running tool processes (this is a metric, not a blocking reason by itself).
|
|
551
|
+
// For screen-isolated sessions, use the maximum of `$ --status` executing
|
|
552
|
+
// counts and pgrep counts so detached sessions remain visible.
|
|
553
|
+
const externalProcessing = await this.getExternalProcessingSnapshot([...Object.keys(this.queues), tool]);
|
|
554
|
+
const claudeProcessCount = externalProcessing.byTool.claude || 0;
|
|
555
|
+
const codexProcessCount = externalProcessing.byTool.codex || 0;
|
|
556
|
+
const agentProcessCount = externalProcessing.byTool.agent || 0;
|
|
557
|
+
const hasRunningClaude = claudeProcessCount > 0;
|
|
558
|
+
const hasRunningCodex = codexProcessCount > 0;
|
|
514
559
|
|
|
515
560
|
// Calculate total processing count for system resources (all tools)
|
|
516
561
|
// System resources (RAM, CPU, disk) apply to all tools
|
|
517
|
-
const totalProcessing = this.processing.size +
|
|
562
|
+
const totalProcessing = this.processing.size + externalProcessing.total;
|
|
518
563
|
|
|
519
564
|
// Calculate Claude-specific processing count for Claude API limits
|
|
520
565
|
// Only counts Claude items in queue + external claude processes
|
|
@@ -572,10 +617,10 @@ export class SolveQueue {
|
|
|
572
617
|
// Add claude_running info at the END (not beginning) of reasons
|
|
573
618
|
// Since it's supplementary info, not the primary blocking reason
|
|
574
619
|
// See: https://github.com/link-assistant/hive-mind/issues/1078
|
|
575
|
-
reasons.push(formatWaitingReason('claude_running',
|
|
620
|
+
reasons.push(formatWaitingReason('claude_running', claudeProcessCount, 0) + ` (${claudeProcessCount} processes)`);
|
|
576
621
|
}
|
|
577
622
|
if (tool === 'codex' && hasRunningCodex && reasons.length > 0) {
|
|
578
|
-
reasons.push(formatWaitingReason('codex_running',
|
|
623
|
+
reasons.push(formatWaitingReason('codex_running', codexProcessCount, 0) + ` (${codexProcessCount} processes)`);
|
|
579
624
|
}
|
|
580
625
|
|
|
581
626
|
const canStart = reasons.length === 0 && !rejected;
|
|
@@ -595,8 +640,10 @@ export class SolveQueue {
|
|
|
595
640
|
reason: reasons.length > 0 ? reasons.join('\n') : undefined,
|
|
596
641
|
reasons,
|
|
597
642
|
oneAtATime,
|
|
598
|
-
claudeProcesses:
|
|
599
|
-
codexProcesses:
|
|
643
|
+
claudeProcesses: claudeProcessCount,
|
|
644
|
+
codexProcesses: codexProcessCount,
|
|
645
|
+
agentProcesses: agentProcessCount,
|
|
646
|
+
isolatedProcesses: externalProcessing.isolatedTotal,
|
|
600
647
|
totalProcessing,
|
|
601
648
|
claudeProcessingCount,
|
|
602
649
|
codexProcessingCount,
|
|
@@ -1075,7 +1122,7 @@ export class SolveQueue {
|
|
|
1075
1122
|
const result = await this.executeCallback(item);
|
|
1076
1123
|
|
|
1077
1124
|
// Extract session name from result
|
|
1078
|
-
let sessionName = 'unknown';
|
|
1125
|
+
let sessionName = result?.sessionId || 'unknown';
|
|
1079
1126
|
if (result && result.output) {
|
|
1080
1127
|
const sessionMatch = result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/);
|
|
1081
1128
|
if (sessionMatch) sessionName = sessionMatch[1];
|
|
@@ -1098,7 +1145,8 @@ export class SolveQueue {
|
|
|
1098
1145
|
if (result.warning) {
|
|
1099
1146
|
await item.ctx.telegram.editMessageText(chatId, messageId, undefined, `⚠️ ${result.warning}`, { parse_mode: 'Markdown' });
|
|
1100
1147
|
} else if (result.success) {
|
|
1101
|
-
const
|
|
1148
|
+
const isolationInfo = result.isolationBackend ? `\n🔒 Isolation: \`${result.isolationBackend}\`` : '';
|
|
1149
|
+
const response = `🔄 Solve command executing...\n\nStatus: \`Executing...\`\n📊 Session: \`${sessionName}\`${isolationInfo}\n\n${item.infoBlock}\n\n🔔 This message will update when the session finishes.`;
|
|
1102
1150
|
await item.ctx.telegram.editMessageText(chatId, messageId, undefined, response, { parse_mode: 'Markdown' });
|
|
1103
1151
|
} else {
|
|
1104
1152
|
const response = `❌ Error executing solve command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\``;
|
|
@@ -1177,9 +1225,8 @@ export class SolveQueue {
|
|
|
1177
1225
|
* Format queue status for display in /limits command
|
|
1178
1226
|
* Shows per-tool queue breakdown with processing counts.
|
|
1179
1227
|
*
|
|
1180
|
-
* Processing count = actual
|
|
1181
|
-
*
|
|
1182
|
-
* work happens in the spawned system process (claude, agent, etc.).
|
|
1228
|
+
* Processing count = max(actual AI CLI processes via pgrep, tracked
|
|
1229
|
+
* `$ --status` executing screen-isolated sessions), not queue state.
|
|
1183
1230
|
*
|
|
1184
1231
|
* Output format:
|
|
1185
1232
|
* ```
|
|
@@ -1194,10 +1241,11 @@ export class SolveQueue {
|
|
|
1194
1241
|
*/
|
|
1195
1242
|
async formatStatus() {
|
|
1196
1243
|
// Always show per-tool breakdown for all known queues
|
|
1244
|
+
const externalProcessing = await this.getExternalProcessingSnapshot(Object.keys(this.queues));
|
|
1197
1245
|
let message = 'Queues\n';
|
|
1198
1246
|
for (const [tool, toolQueue] of Object.entries(this.queues)) {
|
|
1199
1247
|
const pending = toolQueue.length;
|
|
1200
|
-
const processing =
|
|
1248
|
+
const processing = externalProcessing.byTool[tool] || 0;
|
|
1201
1249
|
message += `${tool} (pending: ${pending}, processing: ${processing})\n`;
|
|
1202
1250
|
}
|
|
1203
1251
|
|
|
@@ -1208,9 +1256,8 @@ export class SolveQueue {
|
|
|
1208
1256
|
* Format detailed queue status for Telegram message
|
|
1209
1257
|
* Groups output by tool queue, shows first 5 items per queue, and uses human-readable time.
|
|
1210
1258
|
*
|
|
1211
|
-
* Processing count = actual
|
|
1212
|
-
*
|
|
1213
|
-
* work happens in the spawned system process (claude, agent, etc.).
|
|
1259
|
+
* Processing count = max(actual AI CLI processes via pgrep, tracked
|
|
1260
|
+
* `$ --status` executing screen-isolated sessions), not queue state.
|
|
1214
1261
|
*
|
|
1215
1262
|
* Output format:
|
|
1216
1263
|
* ```
|
|
@@ -1231,16 +1278,17 @@ export class SolveQueue {
|
|
|
1231
1278
|
*/
|
|
1232
1279
|
async formatDetailedStatus() {
|
|
1233
1280
|
const stats = this.getStats();
|
|
1281
|
+
const externalProcessing = await this.getExternalProcessingSnapshot(Object.keys(this.queues));
|
|
1234
1282
|
|
|
1235
|
-
// Get actual
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1283
|
+
// Get actual processing counts for each tool queue.
|
|
1284
|
+
// This combines pgrep with tracked isolation status so users see detached
|
|
1285
|
+
// screen-isolated work even when the direct AI CLI process count is lower.
|
|
1238
1286
|
let message = '📋 *Solve Queue Status*\n\n';
|
|
1239
1287
|
|
|
1240
1288
|
// Show per-tool queue breakdown with items grouped by queue
|
|
1241
1289
|
for (const [tool, toolQueue] of Object.entries(this.queues)) {
|
|
1242
1290
|
const pending = toolQueue.length;
|
|
1243
|
-
const processing =
|
|
1291
|
+
const processing = externalProcessing.byTool[tool] || 0;
|
|
1244
1292
|
message += `*${tool}* (pending: ${pending}, processing: ${processing})\n`;
|
|
1245
1293
|
|
|
1246
1294
|
// Show first 5 queued items for this tool
|
|
@@ -1308,7 +1356,7 @@ export function createQueueExecuteCallback(executeStartScreen, trackSessionFn) {
|
|
|
1308
1356
|
const match = result.output && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
|
|
1309
1357
|
const session = match ? match[1] : null;
|
|
1310
1358
|
if (session) {
|
|
1311
|
-
trackSessionFn(session, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: 'solve' });
|
|
1359
|
+
trackSessionFn(session, { chatId: item.ctx?.chat?.id, messageId: item.messageInfo?.messageId, startTime: new Date(), url: item.url, command: 'solve', tool: item.tool || 'claude' });
|
|
1312
1360
|
}
|
|
1313
1361
|
}
|
|
1314
1362
|
return result;
|
|
@@ -1316,23 +1364,21 @@ export function createQueueExecuteCallback(executeStartScreen, trackSessionFn) {
|
|
|
1316
1364
|
}
|
|
1317
1365
|
|
|
1318
1366
|
/**
|
|
1319
|
-
* Get count of
|
|
1320
|
-
*
|
|
1321
|
-
* for more reliable task counting.
|
|
1367
|
+
* Get count of tracked isolated sessions that are still executing according
|
|
1368
|
+
* to `$ --status`. Queue display combines this with pgrep counts using max().
|
|
1322
1369
|
*
|
|
1323
1370
|
* @param {boolean} verbose - Whether to log verbose output
|
|
1324
|
-
* @returns {Promise<{count: number, sessions: string[]}>}
|
|
1371
|
+
* @returns {Promise<{count: number, sessions: string[], byTool: Object}>}
|
|
1325
1372
|
*/
|
|
1326
1373
|
export async function getRunningIsolatedSessions(verbose = false) {
|
|
1327
1374
|
try {
|
|
1328
|
-
const {
|
|
1329
|
-
|
|
1330
|
-
return { count, sessions: [] };
|
|
1375
|
+
const { getRunningTrackedIsolationSessions } = await import('./session-monitor.lib.mjs');
|
|
1376
|
+
return await getRunningTrackedIsolationSessions(verbose);
|
|
1331
1377
|
} catch (error) {
|
|
1332
1378
|
if (verbose) {
|
|
1333
1379
|
console.error(`[VERBOSE] /solve_queue error getting isolated sessions:`, error.message);
|
|
1334
1380
|
}
|
|
1335
|
-
return { count: 0, sessions: [] };
|
|
1381
|
+
return { count: 0, sessions: [], byTool: {} };
|
|
1336
1382
|
}
|
|
1337
1383
|
}
|
|
1338
1384
|
|