@link-assistant/hive-mind 1.50.2 → 1.50.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.50.2",
3
+ "version": "1.50.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -29,7 +29,7 @@ const exec = promisify(execCallback);
29
29
  * @returns {Promise<{success: boolean, status: string, runs: Array, failedRuns: Array, error: string|null}>}
30
30
  */
31
31
  export async function waitForCommitCI(owner, repo, sha, options = {}, verbose = false) {
32
- const { timeout = 60 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null } = options;
32
+ const { timeout = 60 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null, isCancelled = null } = options;
33
33
 
34
34
  const startTime = Date.now();
35
35
  let noRunsIterations = 0;
@@ -40,6 +40,9 @@ export async function waitForCommitCI(owner, repo, sha, options = {}, verbose =
40
40
  }
41
41
 
42
42
  while (Date.now() - startTime < timeout) {
43
+ // Issue #1588: Check for cancellation before each poll to allow early exit
44
+ if (isCancelled?.()) return { success: false, status: 'cancelled', runs: [], failedRuns: [], error: 'Operation was cancelled' };
45
+
43
46
  let runs;
44
47
  try {
45
48
  runs = await getWorkflowRunsForSha(owner, repo, sha, verbose);
@@ -16,7 +16,6 @@ import { exec as execCallback } from 'child_process';
16
16
 
17
17
  const exec = promisify(execCallback);
18
18
 
19
- // Import GitHub URL parser
20
19
  import { parseGitHubUrl } from './github.lib.mjs';
21
20
 
22
21
  // Issue #1413: Import ready tag sync, timeline, and label constant from separate module
@@ -728,7 +727,7 @@ export async function getActiveBranchRuns(owner, repo, branch = 'main', verbose
728
727
  * @returns {Promise<{success: boolean, waitedForRuns: boolean, completedRuns: number, error: string|null}>}
729
728
  */
730
729
  export async function waitForBranchCI(owner, repo, branch = 'main', options = {}, verbose = false) {
731
- const { timeout = 45 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null } = options;
730
+ const { timeout = 45 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null, isCancelled = null } = options;
732
731
 
733
732
  const startTime = Date.now();
734
733
  let totalWaitedRuns = 0;
@@ -738,6 +737,7 @@ export async function waitForBranchCI(owner, repo, branch = 'main', options = {}
738
737
  }
739
738
 
740
739
  while (Date.now() - startTime < timeout) {
740
+ if (isCancelled?.()) return { success: false, waitedForRuns: totalWaitedRuns > 0, completedRuns: totalWaitedRuns, error: 'Operation was cancelled' };
741
741
  let activeRuns;
742
742
  try {
743
743
  activeRuns = await getActiveBranchRuns(owner, repo, branch, verbose);
@@ -37,6 +37,19 @@ async function getQuerySessionStatus() {
37
37
  // In-memory session store
38
38
  const activeSessions = new Map();
39
39
 
40
+ /**
41
+ * Issue #1586: Timeout for non-isolation sessions.
42
+ * Non-isolation (plain start-screen) sessions cannot reliably detect completion
43
+ * because the screen stays alive via `exec bash`. To prevent false positives
44
+ * that permanently block users, non-isolation sessions are auto-expired after
45
+ * this timeout. This still prevents accidental duplicate commands within the
46
+ * timeout window (5-10 minutes).
47
+ *
48
+ * Once --isolation is fully tested and becomes the default, this timeout
49
+ * mechanism will no longer be needed.
50
+ */
51
+ export const NON_ISOLATION_SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
52
+
40
53
  /**
41
54
  * Check if a screen session exists
42
55
  * @param {string} sessionName - Name of the screen session to check
@@ -190,8 +203,23 @@ export async function monitorSessions(bot, verbose = false) {
190
203
  exitCode = await getIsolatedSessionExitCode(sessionInfo.sessionId, verbose);
191
204
  }
192
205
  } else {
193
- // Screen mode: use screen -ls for detection
194
- stillRunning = await checkScreenSessionExists(sessionName);
206
+ // Issue #1586: Non-isolation screen sessions cannot reliably detect
207
+ // completion because start-screen keeps the screen alive via `exec bash`.
208
+ // Auto-expire after timeout; within timeout, use screen -ls as best-effort.
209
+ const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
210
+ const elapsed = Date.now() - startTime.getTime();
211
+ if (elapsed >= NON_ISOLATION_SESSION_TIMEOUT_MS) {
212
+ stillRunning = false;
213
+ if (verbose) {
214
+ console.log(`[VERBOSE] Non-isolation session ${sessionName} expired after ${Math.round(elapsed / 1000)}s (timeout: ${NON_ISOLATION_SESSION_TIMEOUT_MS / 1000}s)`);
215
+ }
216
+ } else {
217
+ stillRunning = await checkScreenSessionExists(sessionName);
218
+ if (verbose) {
219
+ const remainingSec = Math.round((NON_ISOLATION_SESSION_TIMEOUT_MS - elapsed) / 1000);
220
+ console.log(`[VERBOSE] Non-isolation session ${sessionName}: screen -ls says ${stillRunning ? 'running' : 'not found'} (timeout in ${remainingSec}s)`);
221
+ }
222
+ }
195
223
  }
196
224
 
197
225
  if (!stillRunning) {
@@ -250,6 +278,14 @@ export function startSessionMonitoring(bot, verbose = false, intervalMs = 30000)
250
278
  * inconsistencies when two auto-restart-until-mergeable processes run
251
279
  * simultaneously.
252
280
  *
281
+ * Issue #1586: Non-isolation sessions (plain start-screen) cannot reliably
282
+ * detect completion because the screen stays alive via `exec bash`. To avoid
283
+ * permanent false positives, non-isolation sessions are auto-expired after
284
+ * NON_ISOLATION_SESSION_TIMEOUT_MS (10 minutes). Within that window they
285
+ * still block duplicate commands for the same URL, which prevents accidental
286
+ * re-runs. Isolation-backed sessions have no timeout since their completion
287
+ * is reliably detected by monitorSessions().
288
+ *
253
289
  * @param {string} url - The GitHub URL to check (issue or PR URL)
254
290
  * @param {boolean} verbose - Whether to log verbose output
255
291
  * @returns {{isActive: boolean, sessionName: string|null}} Whether an active session exists for this URL
@@ -262,9 +298,26 @@ export function hasActiveSessionForUrl(url, verbose = false) {
262
298
  const normalizedUrl = normalizeUrl(url);
263
299
 
264
300
  for (const [sessionName, sessionInfo] of activeSessions.entries()) {
301
+ // Issue #1586: Auto-expire non-isolation sessions after timeout
302
+ if (!sessionInfo.isolationBackend) {
303
+ const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
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
+ }
316
+ }
265
317
  if (sessionInfo.url && normalizeUrl(sessionInfo.url) === normalizedUrl) {
266
318
  if (verbose) {
267
- console.log(`[VERBOSE] Found active session for URL ${url}: ${sessionName}`);
319
+ const mode = sessionInfo.isolationBackend ? `isolation:${sessionInfo.isolationBackend}` : 'non-isolation (timeout-based)';
320
+ console.log(`[VERBOSE] Found active session for URL ${url}: ${sessionName} (${mode})`);
268
321
  }
269
322
  return { isActive: true, sessionName };
270
323
  }