@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/CHANGELOG.md +27 -0
- package/README.hi.md +876 -0
- package/README.md +1 -1
- package/README.ru.md +876 -0
- package/README.zh.md +876 -0
- package/package.json +1 -1
- package/src/github-merge-ci.lib.mjs +4 -1
- package/src/github-merge.lib.mjs +2 -2
- package/src/session-monitor.lib.mjs +56 -3
- package/src/solve.auto-merge-helpers.lib.mjs +552 -0
- package/src/solve.auto-merge.lib.mjs +4 -452
- package/src/telegram-bot.mjs +4 -0
- package/src/telegram-merge-command.lib.mjs +5 -3
- package/src/telegram-merge-queue.lib.mjs +4 -0
package/package.json
CHANGED
|
@@ -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);
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -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
|
-
//
|
|
194
|
-
|
|
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
|
-
|
|
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
|
}
|