@link-assistant/hive-mind 1.40.0 → 1.40.1
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/claude.lib.mjs +30 -8
- package/src/config.lib.mjs +5 -2
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -853,21 +853,27 @@ export const executeClaudeCommand = async params => {
|
|
|
853
853
|
let lastEventTime = null;
|
|
854
854
|
let activityTimeoutId = null;
|
|
855
855
|
let isActivityTimeout = false;
|
|
856
|
+
// Issue #1510: Separate SIGTERM (graceful) and SIGKILL (force) phases to allow
|
|
857
|
+
// capturing final output from the process during graceful shutdown
|
|
856
858
|
const forceExitOnTimeout = async () => {
|
|
857
859
|
if (forceExitTriggered) return;
|
|
858
860
|
forceExitTriggered = true;
|
|
859
|
-
await log(`⚠️ Stream timeout —
|
|
861
|
+
await log(`⚠️ Stream timeout — sending SIGTERM for graceful shutdown (Issue #1280, #1510)`, { verbose: true });
|
|
860
862
|
try {
|
|
861
863
|
if (execCommand.kill) {
|
|
862
864
|
execCommand.kill('SIGTERM');
|
|
863
|
-
// Issue #1346: Follow up with SIGKILL after
|
|
865
|
+
// Issue #1346/#1510: Follow up with SIGKILL after 5s if still alive
|
|
866
|
+
// Increased from 2s to 5s to give more time for final output capture
|
|
864
867
|
const t = setTimeout(() => {
|
|
865
868
|
try {
|
|
866
|
-
if (!execCommand.result?.code)
|
|
869
|
+
if (!execCommand.result?.code) {
|
|
870
|
+
log(`⚠️ Process did not exit after SIGTERM, sending SIGKILL`, { verbose: true });
|
|
871
|
+
execCommand.kill('SIGKILL');
|
|
872
|
+
}
|
|
867
873
|
} catch {
|
|
868
874
|
/* exited */
|
|
869
875
|
}
|
|
870
|
-
},
|
|
876
|
+
}, 5000);
|
|
871
877
|
t.unref();
|
|
872
878
|
}
|
|
873
879
|
} catch (e) {
|
|
@@ -892,8 +898,8 @@ export const executeClaudeCommand = async params => {
|
|
|
892
898
|
activityTimeoutId = setTimeout(async () => {
|
|
893
899
|
if (!forceExitTriggered && !resultEventReceived) {
|
|
894
900
|
isActivityTimeout = true;
|
|
895
|
-
const idleSeconds = lastEventTime ? Math.round((Date.now() - lastEventTime) / 1000) : 'unknown';
|
|
896
|
-
await log(`\n⚠️ No stream output for ${timeouts.streamActivityMs / 1000}s after previous activity (idle: ${idleSeconds}
|
|
901
|
+
const idleSeconds = lastEventTime ? `${Math.round((Date.now() - lastEventTime) / 1000)}s` : 'unknown';
|
|
902
|
+
await log(`\n⚠️ No stream output for ${timeouts.streamActivityMs / 1000}s after previous activity (idle: ${idleSeconds}) — force-killing (Issue #1472)`, { level: 'warning' });
|
|
897
903
|
await forceExitOnTimeout();
|
|
898
904
|
}
|
|
899
905
|
}, timeouts.streamActivityMs);
|
|
@@ -901,7 +907,8 @@ export const executeClaudeCommand = async params => {
|
|
|
901
907
|
}
|
|
902
908
|
};
|
|
903
909
|
for await (const chunk of execCommand.stream()) {
|
|
904
|
-
|
|
910
|
+
// Issue #1510: Continue processing stream after SIGTERM to capture final output
|
|
911
|
+
// The stream will naturally end when the process exits (SIGTERM) or is force-killed (SIGKILL after 5s)
|
|
905
912
|
if (!firstChunkReceived) {
|
|
906
913
|
// Issue #1472/#1475: Clear startup timeout on first output
|
|
907
914
|
firstChunkReceived = true;
|
|
@@ -922,12 +929,14 @@ export const executeClaudeCommand = async params => {
|
|
|
922
929
|
if (!line.trim()) continue;
|
|
923
930
|
try {
|
|
924
931
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
932
|
+
// Issue #1510: Track last event time for all modes (not just interactive)
|
|
933
|
+
// so activity timeout can report accurate idle duration
|
|
934
|
+
lastEventTime = Date.now();
|
|
925
935
|
if (interactiveHandler) {
|
|
926
936
|
if (!interactiveHandler._firstEventLogged) {
|
|
927
937
|
interactiveHandler._firstEventLogged = true;
|
|
928
938
|
await log(`🔌 Interactive mode: First event received (type: ${data.type || 'unknown'}) — stream is active`, { verbose: true });
|
|
929
939
|
}
|
|
930
|
-
lastEventTime = Date.now();
|
|
931
940
|
try {
|
|
932
941
|
await interactiveHandler.processEvent(data);
|
|
933
942
|
} catch (interactiveError) {
|
|
@@ -1193,6 +1202,19 @@ export const executeClaudeCommand = async params => {
|
|
|
1193
1202
|
const retryMode = isStartupTimeout ? ' (fresh start)' : ' (session preserved)';
|
|
1194
1203
|
await log(`\n⚠️ ${errorLabel} detected. Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}${retryMode}${notRetryableHint}...`, { level: 'warning' });
|
|
1195
1204
|
await log(` Error: ${isStartupTimeout ? `No output from Claude CLI within ${timeouts.streamStartupMs / 1000}s` : isActivityTimeout ? `No output for ${timeouts.streamActivityMs / 1000}s after previous activity` : lastMessage.substring(0, 200)}`, { verbose: true });
|
|
1205
|
+
// Issue #1510: Post PR comment when force-killing and auto-resuming so reviewers can follow the session lifecycle
|
|
1206
|
+
if ((isActivityTimeout || isStartupTimeout) && owner && repo && prNumber && $) {
|
|
1207
|
+
try {
|
|
1208
|
+
const timeoutType = isActivityTimeout ? 'activity' : 'startup';
|
|
1209
|
+
const sessionInfo = sessionId ? `\nSession ID: \`${sessionId}\`` : '';
|
|
1210
|
+
const resumeInfo = isStartupTimeout ? 'Session will be restarted (fresh start).' : `Session will be resumed with \`--resume\` (context preserved).`;
|
|
1211
|
+
const commentBody = `## :warning: Session Force-Killed (${timeoutType} timeout)\n\nThe working session was force-killed due to ${timeoutType} timeout (no stream output for ${isActivityTimeout ? timeouts.streamActivityMs / 1000 : timeouts.streamStartupMs / 1000}s).\n\n**Auto-resuming**: Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}. ${resumeInfo}${sessionInfo}\n\n*This is an automated notification — the session will continue automatically.*`;
|
|
1212
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
1213
|
+
await log(` Posted force-kill notification to PR #${prNumber}`, { verbose: true });
|
|
1214
|
+
} catch (commentError) {
|
|
1215
|
+
await log(` Warning: Could not post force-kill comment to PR: ${commentError.message}`, { verbose: true });
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1196
1218
|
// Activity timeout preserves session (work was started), startup timeout does not (no session created)
|
|
1197
1219
|
if (!isStartupTimeout && sessionId && !argv.resume) argv.resume = sessionId;
|
|
1198
1220
|
await waitWithCountdown(delay, log);
|
package/src/config.lib.mjs
CHANGED
|
@@ -63,8 +63,11 @@ export const timeouts = {
|
|
|
63
63
|
// after at least one event was received, the process is considered hung mid-session.
|
|
64
64
|
// This catches the case where Claude CLI starts producing output but then stops (e.g., the
|
|
65
65
|
// original Issue #1472 where CLI was stuck for 4.5h with all output arriving only at CTRL+C).
|
|
66
|
-
//
|
|
67
|
-
|
|
66
|
+
// Issue #1510: Increased from 300000ms (5 min) to 3600000ms (1 hour) because Claude Code can
|
|
67
|
+
// legitimately wait for long-running operations (docker builds, CI polls, large compilations).
|
|
68
|
+
// The 5-minute timeout was force-killing sessions during `sleep 300 && gh run view ...` commands.
|
|
69
|
+
// Default: 3600000ms (1 hour). Set to 0 to disable. Configurable via environment variable.
|
|
70
|
+
streamActivityMs: parseIntWithDefault('HIVE_MIND_STREAM_ACTIVITY_MS', 3600000),
|
|
68
71
|
};
|
|
69
72
|
|
|
70
73
|
// Auto-continue configurations
|