@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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.40.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 9df62ed: fix: increase activity timeout to 1hr, fix idle tracking, improve graceful kill (#1510)
8
+
3
9
  ## 1.40.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.40.0",
3
+ "version": "1.40.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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 — forcing exit (Issue #1280)`, { verbose: true });
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 2s if still alive
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) execCommand.kill('SIGKILL');
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
- }, 2000);
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}s) — force-killing (Issue #1472)`, { level: 'warning' });
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
- if (forceExitTriggered) break;
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);
@@ -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
- // Default: 300000ms (5 minutes). Set to 0 to disable. Configurable via environment variable.
67
- streamActivityMs: parseIntWithDefault('HIVE_MIND_STREAM_ACTIVITY_MS', 300000),
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