@link-assistant/hive-mind 2.0.19 → 2.0.20
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 +6 -0
- package/package.json +1 -1
- package/src/isolation-runner.lib.mjs +41 -0
- package/src/session-monitor.lib.mjs +97 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.0.20
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 078f346: Reap successful Docker-isolated task containers at session completion, keep failed containers with cleanup instructions by default, and update Docker images to start-command 0.30.1.
|
|
8
|
+
|
|
3
9
|
## 2.0.19
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -831,6 +831,47 @@ export async function checkDockerContainerRunning(containerName, verbose = false
|
|
|
831
831
|
}
|
|
832
832
|
}
|
|
833
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Best-effort removal for a Docker container backing a native
|
|
836
|
+
* `$ --isolated docker` session.
|
|
837
|
+
*
|
|
838
|
+
* start-command names the container after the `--session` value. The monitor
|
|
839
|
+
* calls this only after the session is terminal and after the host-side log has
|
|
840
|
+
* been inspected, so removing the container reclaims its writable layer without
|
|
841
|
+
* losing the captured task log. Never throws: completion notification must not
|
|
842
|
+
* fail just because Docker already removed the container.
|
|
843
|
+
*
|
|
844
|
+
* @param {string} containerName - Container name (the session UUID)
|
|
845
|
+
* @param {boolean} [verbose] - Enable verbose logging
|
|
846
|
+
* @returns {Promise<{success: boolean, output: string, error: string|null}>}
|
|
847
|
+
*/
|
|
848
|
+
export async function removeDockerContainer(containerName, verbose = false) {
|
|
849
|
+
if (!containerName) {
|
|
850
|
+
return { success: false, output: '', error: 'missing container name' };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
const result = await $({ mirror: false })`docker rm -f ${containerName}`;
|
|
855
|
+
const stdout = result.stdout?.toString() || '';
|
|
856
|
+
const stderr = result.stderr?.toString() || '';
|
|
857
|
+
if (verbose) {
|
|
858
|
+
console.log(`[VERBOSE] isolation-runner: docker rm -f '${containerName}' succeeded`);
|
|
859
|
+
}
|
|
860
|
+
return { success: true, output: stdout || stderr, error: null };
|
|
861
|
+
} catch (error) {
|
|
862
|
+
const stderr = error?.stderr?.toString?.() || '';
|
|
863
|
+
const stdout = error?.stdout?.toString?.() || '';
|
|
864
|
+
if (verbose) {
|
|
865
|
+
console.log(`[VERBOSE] isolation-runner: docker rm -f '${containerName}' failed: ${stderr.trim() || error?.message || error}`);
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
output: stdout,
|
|
870
|
+
error: stderr.trim() || error?.message || String(error),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
834
875
|
/**
|
|
835
876
|
* Check whether a tmux session with the given name still exists.
|
|
836
877
|
* `tmux has-session -t <name>` exits 0 when it exists and non-zero otherwise,
|
|
@@ -110,6 +110,20 @@ export function getIsolationSessionStateForTests(sessionName, sessionInfo, optio
|
|
|
110
110
|
* mechanism will no longer be needed.
|
|
111
111
|
*/
|
|
112
112
|
export const NON_ISOLATION_SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
113
|
+
export const DEFAULT_DOCKER_TASK_CONTAINER_KEEP_POLICY = 'on-failure';
|
|
114
|
+
export const DOCKER_TASK_CONTAINER_KEEP_POLICIES = ['always', 'on-failure', 'never'];
|
|
115
|
+
|
|
116
|
+
export function resolveDockerTaskContainerKeepPolicy({ env = process.env, verbose = false } = {}) {
|
|
117
|
+
const raw = String(env?.HIVE_MIND_KEEP_TASK_CONTAINER || '')
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
if (!raw) return DEFAULT_DOCKER_TASK_CONTAINER_KEEP_POLICY;
|
|
121
|
+
if (DOCKER_TASK_CONTAINER_KEEP_POLICIES.includes(raw)) return raw;
|
|
122
|
+
if (verbose) {
|
|
123
|
+
console.log(`[VERBOSE] Invalid HIVE_MIND_KEEP_TASK_CONTAINER='${raw}', using '${DEFAULT_DOCKER_TASK_CONTAINER_KEEP_POLICY}'`);
|
|
124
|
+
}
|
|
125
|
+
return DEFAULT_DOCKER_TASK_CONTAINER_KEEP_POLICY;
|
|
126
|
+
}
|
|
113
127
|
|
|
114
128
|
/**
|
|
115
129
|
* Check if a screen session exists
|
|
@@ -345,6 +359,70 @@ async function buildDiskDiagnosticsExtraSection(logPath, { verbose = false, read
|
|
|
345
359
|
}
|
|
346
360
|
}
|
|
347
361
|
|
|
362
|
+
function isSuccessfulTaskCompletion({ exitCode = null, status = null } = {}) {
|
|
363
|
+
const outcome = classifySessionOutcome({ exitCode, status });
|
|
364
|
+
if (outcome.failed) return false;
|
|
365
|
+
if (exitCode === 0) return true;
|
|
366
|
+
|
|
367
|
+
const normalizedStatus = String(status || '')
|
|
368
|
+
.trim()
|
|
369
|
+
.toLowerCase();
|
|
370
|
+
return exitCode === null && (normalizedStatus === 'executed' || normalizedStatus === 'completed');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function formatDockerTaskContainerKeptSection({ containerName, keepPolicy }) {
|
|
374
|
+
return ['*Docker container kept*', `Container: \`${containerName}\``, `Policy: \`HIVE_MIND_KEEP_TASK_CONTAINER=${keepPolicy}\``, `Inspect: \`docker start -ai ${containerName}\``, `Shell: \`docker exec -it ${containerName} sh\``, `Remove when done: \`docker rm -f ${containerName}\``].join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function buildDockerTaskContainerCompletionAction({ sessionName, sessionInfo, exitCode = null, status = null, env = process.env, verbose = false } = {}) {
|
|
378
|
+
if (sessionInfo?.isolationBackend !== 'docker') {
|
|
379
|
+
return { applies: false, containerName: null, keepPolicy: null, shouldRemove: false, extraSection: '' };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const containerName = sessionInfo.sessionId || sessionName || null;
|
|
383
|
+
if (!containerName) {
|
|
384
|
+
return { applies: false, containerName: null, keepPolicy: null, shouldRemove: false, extraSection: '' };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const keepPolicy = resolveDockerTaskContainerKeepPolicy({ env, verbose });
|
|
388
|
+
const successful = isSuccessfulTaskCompletion({ exitCode, status });
|
|
389
|
+
const shouldKeep = keepPolicy === 'always' || (keepPolicy === 'on-failure' && !successful);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
applies: true,
|
|
393
|
+
containerName,
|
|
394
|
+
keepPolicy,
|
|
395
|
+
successful,
|
|
396
|
+
shouldRemove: !shouldKeep,
|
|
397
|
+
extraSection: shouldKeep ? formatDockerTaskContainerKeptSection({ containerName, keepPolicy }) : '',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function applyDockerTaskContainerCompletionAction(action, { verbose = false, removeDockerContainer = null } = {}) {
|
|
402
|
+
if (!action?.applies || !action.shouldRemove || !action.containerName) return;
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const removeFn =
|
|
406
|
+
removeDockerContainer ||
|
|
407
|
+
(async (containerName, removeVerbose) => {
|
|
408
|
+
const runner = await getIsolationRunner();
|
|
409
|
+
return runner.removeDockerContainer(containerName, removeVerbose);
|
|
410
|
+
});
|
|
411
|
+
const result = await removeFn(action.containerName, verbose);
|
|
412
|
+
if (verbose) {
|
|
413
|
+
if (result?.success) {
|
|
414
|
+
console.log(`[VERBOSE] Removed docker task container '${action.containerName}' after terminal session completion`);
|
|
415
|
+
} else {
|
|
416
|
+
console.log(`[VERBOSE] Could not remove docker task container '${action.containerName}': ${result?.error || 'unknown error'}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (verbose) {
|
|
421
|
+
console.log(`[VERBOSE] Could not remove docker task container '${action.containerName}': ${error?.message || error}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
348
426
|
function isNonIsolationSessionActive(sessionName, sessionInfo, verbose = false) {
|
|
349
427
|
const startTime = sessionInfo.startTime instanceof Date ? sessionInfo.startTime : new Date(sessionInfo.startTime);
|
|
350
428
|
const elapsed = Date.now() - startTime.getTime();
|
|
@@ -602,8 +680,17 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
602
680
|
if (!stillRunning) {
|
|
603
681
|
console.log(`Session ${sessionName} has finished. Sending notification to chat ${sessionInfo.chatId}`);
|
|
604
682
|
|
|
683
|
+
let dockerTaskContainerAction = null;
|
|
605
684
|
try {
|
|
606
685
|
const finalExitCode = getSessionCompletionExitCode({ exitCode, statusResult });
|
|
686
|
+
dockerTaskContainerAction = buildDockerTaskContainerCompletionAction({
|
|
687
|
+
sessionName,
|
|
688
|
+
sessionInfo,
|
|
689
|
+
exitCode: finalExitCode,
|
|
690
|
+
status: resolvedStatus,
|
|
691
|
+
env: options.env || process.env,
|
|
692
|
+
verbose,
|
|
693
|
+
});
|
|
607
694
|
|
|
608
695
|
// Issue #1688/#1905: When the original /solve URL was an issue, look up
|
|
609
696
|
// the created PR so the completion message can include both an
|
|
@@ -709,6 +796,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
709
796
|
console.log(`[VERBOSE] Could not build disk diagnostics section for ${sessionName}: ${diskError?.message || diskError}`);
|
|
710
797
|
}
|
|
711
798
|
}
|
|
799
|
+
const dockerTaskContainerExtraSections = dockerTaskContainerAction?.extraSection ? [dockerTaskContainerAction.extraSection] : [];
|
|
712
800
|
|
|
713
801
|
const message = formatSessionCompletionMessage({
|
|
714
802
|
sessionName,
|
|
@@ -718,7 +806,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
718
806
|
exitCode: finalExitCode,
|
|
719
807
|
infoBlock: sessionInfo?.infoBlock || '',
|
|
720
808
|
pullRequestUrl,
|
|
721
|
-
extraSections: [...limitsExtraSections, ...resumeExtraSections, ...diskExtraSections],
|
|
809
|
+
extraSections: [...limitsExtraSections, ...resumeExtraSections, ...diskExtraSections, ...dockerTaskContainerExtraSections],
|
|
722
810
|
});
|
|
723
811
|
|
|
724
812
|
// Update the original reply message if messageId is available, otherwise send new message
|
|
@@ -758,10 +846,18 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
758
846
|
}
|
|
759
847
|
}
|
|
760
848
|
|
|
849
|
+
await applyDockerTaskContainerCompletionAction(dockerTaskContainerAction, {
|
|
850
|
+
verbose,
|
|
851
|
+
removeDockerContainer: options.removeDockerContainer,
|
|
852
|
+
});
|
|
761
853
|
completeSession(sessionName, finalExitCode || 0, verbose, resolvedStatus);
|
|
762
854
|
} catch (error) {
|
|
763
855
|
console.error(`Failed to send completion notification for ${sessionName}:`, error);
|
|
764
856
|
if (isMessageAlreadyUpdatedError(error)) {
|
|
857
|
+
await applyDockerTaskContainerCompletionAction(dockerTaskContainerAction, {
|
|
858
|
+
verbose,
|
|
859
|
+
removeDockerContainer: options.removeDockerContainer,
|
|
860
|
+
});
|
|
765
861
|
completeSession(sessionName, exitCode || 0, verbose, resolvedStatus);
|
|
766
862
|
} else {
|
|
767
863
|
sessionInfo.lastNotificationError = error.message;
|