@link-assistant/hive-mind 1.22.5 ā 1.22.6
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 +21 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +69 -101
- package/src/config.lib.mjs +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.22.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ed87517: Fix: Add workaround for process stream hanging after completion (Issue #1280)
|
|
8
|
+
|
|
9
|
+
After the Claude CLI sends the final result event, the `for await` loop over
|
|
10
|
+
`command-stream`'s `stream()` can hang indefinitely. Root cause: `command-stream` v0.9.4's
|
|
11
|
+
`stream()` async iterator waits for both process exit AND stdout/stderr pipe close before
|
|
12
|
+
ending. If the CLI process keeps stdout open after sending the result, `pumpReadable()` hangs,
|
|
13
|
+
`finish()` never fires, and the stream iterator never terminates.
|
|
14
|
+
|
|
15
|
+
Additionally, `command-stream` v0.9.4 `stream()` does NOT yield `{type:'exit'}` chunks,
|
|
16
|
+
making the exit code detection via `chunk.type === 'exit'` dead code (exit code is obtained
|
|
17
|
+
from `execCommand.result.code` after the loop instead).
|
|
18
|
+
|
|
19
|
+
Workaround: after receiving the result event, start a configurable timeout (default 30s,
|
|
20
|
+
`HIVE_MIND_RESULT_STREAM_CLOSE_MS`) to force-kill the process with SIGTERM/SIGKILL.
|
|
21
|
+
|
|
22
|
+
Related: https://github.com/link-foundation/command-stream/issues/155
|
|
23
|
+
|
|
3
24
|
## 1.22.5
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -17,7 +17,6 @@ import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
|
|
|
17
17
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
18
18
|
import { CLAUDE_MODELS as availableModels } from './model-validation.lib.mjs'; // Issue #1221
|
|
19
19
|
export { availableModels }; // Re-export for backward compatibility
|
|
20
|
-
|
|
21
20
|
// Helper to display resume command at end of session
|
|
22
21
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
23
22
|
if (!sessionId || !tempDir) return;
|
|
@@ -25,7 +24,6 @@ const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) =>
|
|
|
25
24
|
await log('\nš” To continue this session in Claude Code interactive mode:\n');
|
|
26
25
|
await log(` ${cmd}\n`);
|
|
27
26
|
};
|
|
28
|
-
|
|
29
27
|
/** Format numbers with spaces as thousands separator (no commas) */
|
|
30
28
|
export const formatNumber = num => {
|
|
31
29
|
if (num === null || num === undefined) return 'N/A';
|
|
@@ -38,10 +36,7 @@ export const formatNumber = num => {
|
|
|
38
36
|
// Model mapping to translate aliases to full model IDs
|
|
39
37
|
// Supports [1m] suffix for 1 million token context (Issue #1221)
|
|
40
38
|
export const mapModelToId = model => {
|
|
41
|
-
if (!model || typeof model !== 'string')
|
|
42
|
-
return model;
|
|
43
|
-
}
|
|
44
|
-
|
|
39
|
+
if (!model || typeof model !== 'string') return model;
|
|
45
40
|
// Check for [1m] suffix (case-insensitive)
|
|
46
41
|
const match = model.match(/^(.+?)\[1m\]$/i);
|
|
47
42
|
if (match) {
|
|
@@ -49,7 +44,6 @@ export const mapModelToId = model => {
|
|
|
49
44
|
const mappedBase = availableModels[baseModel] || baseModel;
|
|
50
45
|
return `${mappedBase}[1m]`;
|
|
51
46
|
}
|
|
52
|
-
|
|
53
47
|
return availableModels[model] || model;
|
|
54
48
|
};
|
|
55
49
|
// Function to validate Claude CLI connection with retry logic
|
|
@@ -108,7 +102,6 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
108
102
|
throw timeoutError;
|
|
109
103
|
}
|
|
110
104
|
}
|
|
111
|
-
|
|
112
105
|
// Check for common error patterns
|
|
113
106
|
const stdout = result.stdout?.toString() || '';
|
|
114
107
|
const stderr = result.stderr?.toString() || '';
|
|
@@ -137,7 +130,6 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
137
130
|
const jsonError = checkForJsonError(stdout) || checkForJsonError(stderr);
|
|
138
131
|
// Check for API overload error pattern
|
|
139
132
|
const isOverloadError = (stdout.includes('API Error: 500') && stdout.includes('Overloaded')) || (stderr.includes('API Error: 500') && stderr.includes('Overloaded')) || (jsonError && jsonError.type === 'api_error' && jsonError.message === 'Overloaded');
|
|
140
|
-
|
|
141
133
|
// Handle overload errors with retry
|
|
142
134
|
if (isOverloadError) {
|
|
143
135
|
if (retryCount < maxRetries) {
|
|
@@ -223,47 +215,27 @@ export const validateClaudeConnection = async (model = 'haiku-3') => {
|
|
|
223
215
|
return await attemptValidation();
|
|
224
216
|
};
|
|
225
217
|
export { handleClaudeRuntimeSwitch }; // Re-export from ./claude.runtime-switch.lib.mjs
|
|
226
|
-
|
|
227
218
|
// Store Claude Code version globally (set during validation)
|
|
228
219
|
let detectedClaudeVersion = null;
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Get the detected Claude Code version
|
|
232
|
-
* @returns {string|null} The detected version or null if not yet detected
|
|
233
|
-
*/
|
|
220
|
+
/** Get the detected Claude Code version @returns {string|null} */
|
|
234
221
|
export const getClaudeVersion = () => detectedClaudeVersion;
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Set the detected Claude Code version (called during validation)
|
|
238
|
-
* @param {string} version - The detected version string
|
|
239
|
-
*/
|
|
222
|
+
/** Set the detected Claude Code version (called during validation) @param {string} version */
|
|
240
223
|
export const setClaudeVersion = version => {
|
|
241
224
|
detectedClaudeVersion = version;
|
|
242
225
|
};
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Resolve thinking settings based on --think and --thinking-budget options
|
|
246
|
-
* Handles translation between thinking levels and token budgets based on Claude Code version
|
|
247
|
-
* @param {Object} argv - Command line arguments
|
|
248
|
-
* @param {Function} log - Logging function
|
|
249
|
-
* @returns {Object} { thinkingBudget, thinkLevel, translation, maxBudget } - Resolved settings
|
|
250
|
-
*/
|
|
226
|
+
/** Resolve thinking settings based on --think and --thinking-budget options */
|
|
251
227
|
export const resolveThinkingSettings = async (argv, log) => {
|
|
252
228
|
const minVersion = argv.thinkingBudgetClaudeMinimumVersion || '2.1.12';
|
|
253
229
|
const version = detectedClaudeVersion || '0.0.0'; // Assume old version if not detected
|
|
254
230
|
const isNewVersion = supportsThinkingBudget(version, minVersion);
|
|
255
|
-
|
|
256
231
|
// Get max thinking budget from argv or use default (see issue #1146)
|
|
257
232
|
const maxBudget = argv.maxThinkingBudget ?? DEFAULT_MAX_THINKING_BUDGET;
|
|
258
|
-
|
|
259
233
|
// Get thinking level mappings calculated from maxBudget
|
|
260
234
|
const thinkingLevelToTokens = getThinkingLevelToTokens(maxBudget);
|
|
261
235
|
const tokensToThinkingLevel = getTokensToThinkingLevel(maxBudget);
|
|
262
|
-
|
|
263
236
|
let thinkingBudget = argv.thinkingBudget;
|
|
264
237
|
let thinkLevel = argv.think;
|
|
265
238
|
let translation = null;
|
|
266
|
-
|
|
267
239
|
if (isNewVersion) {
|
|
268
240
|
// Claude Code >= 2.1.12: translate --think to --thinking-budget
|
|
269
241
|
if (thinkLevel !== undefined && thinkingBudget === undefined) {
|
|
@@ -290,45 +262,23 @@ export const resolveThinkingSettings = async (argv, log) => {
|
|
|
290
262
|
thinkingBudget = undefined;
|
|
291
263
|
}
|
|
292
264
|
}
|
|
293
|
-
|
|
294
265
|
return { thinkingBudget, thinkLevel, translation, isNewVersion, maxBudget };
|
|
295
266
|
};
|
|
296
|
-
/**
|
|
297
|
-
* Check if Playwright MCP is available and connected to Claude
|
|
298
|
-
* @returns {Promise<boolean>} True if Playwright MCP is available, false otherwise
|
|
299
|
-
*/
|
|
267
|
+
/** Check if Playwright MCP is available and connected to Claude @returns {Promise<boolean>} */
|
|
300
268
|
export const checkPlaywrightMcpAvailability = async () => {
|
|
301
269
|
try {
|
|
302
|
-
// Try to run a simple claude command that would list MCP servers if available
|
|
303
|
-
// Use a timeout to avoid hanging if Claude is not installed
|
|
304
270
|
const result = await $`timeout 5 claude mcp list 2>&1`.catch(() => null);
|
|
305
|
-
|
|
306
|
-
if (!result || result.code !== 0) {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
|
|
271
|
+
if (!result || result.code !== 0) return false;
|
|
310
272
|
const output = result.stdout?.toString() || '';
|
|
311
|
-
|
|
312
|
-
// Check if playwright is in the list of MCP servers
|
|
313
|
-
if (output.toLowerCase().includes('playwright')) {
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
|
|
273
|
+
if (output.toLowerCase().includes('playwright')) return true;
|
|
317
274
|
return false;
|
|
318
275
|
} catch {
|
|
319
|
-
// If any error occurs, assume Playwright MCP is not available
|
|
320
276
|
return false;
|
|
321
277
|
}
|
|
322
278
|
};
|
|
323
|
-
/**
|
|
324
|
-
* Execute Claude with all prompts and settings
|
|
325
|
-
* This is the main entry point that handles all prompt building and execution
|
|
326
|
-
* @param {Object} params - Parameters for Claude execution
|
|
327
|
-
* @returns {Object} Result of the execution including success status and session info
|
|
328
|
-
*/
|
|
279
|
+
/** Execute Claude with all prompts and settings - main entry point */
|
|
329
280
|
export const executeClaude = async params => {
|
|
330
281
|
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params;
|
|
331
|
-
|
|
332
282
|
// Check if agent-commander is installed when the option is enabled
|
|
333
283
|
if (argv.promptSubagentsViaAgentCommander) {
|
|
334
284
|
try {
|
|
@@ -339,17 +289,14 @@ export const executeClaude = async params => {
|
|
|
339
289
|
await log('ā ļø agent-commander not installed; prompt guidance will be skipped (npm i -g @link-assistant/agent-commander)');
|
|
340
290
|
}
|
|
341
291
|
}
|
|
342
|
-
|
|
343
292
|
// Import prompt building functions from claude.prompts.lib.mjs
|
|
344
293
|
const { buildUserPrompt, buildSystemPrompt } = await import('./claude.prompts.lib.mjs');
|
|
345
|
-
|
|
346
294
|
// Check if the model supports vision using models.dev API
|
|
347
295
|
const mappedModel = mapModelToId(argv.model);
|
|
348
296
|
const modelSupportsVision = await checkModelVisionCapability(mappedModel);
|
|
349
297
|
if (argv.verbose) {
|
|
350
298
|
await log(`šļø Model vision capability: ${modelSupportsVision ? 'supported' : 'not supported'}`, { verbose: true });
|
|
351
299
|
}
|
|
352
|
-
|
|
353
300
|
// Build the user prompt
|
|
354
301
|
const prompt = buildUserPrompt({
|
|
355
302
|
issueUrl,
|
|
@@ -489,34 +436,18 @@ export const fetchModelInfo = async modelId => {
|
|
|
489
436
|
return null;
|
|
490
437
|
}
|
|
491
438
|
};
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Check if a model supports vision (image input) using models.dev API
|
|
495
|
-
* @param {string} modelId - The model ID (e.g., "claude-sonnet-4-5-20250929")
|
|
496
|
-
* @returns {Promise<boolean>} True if the model supports vision, false otherwise
|
|
497
|
-
*/
|
|
439
|
+
/** Check if a model supports vision (image input) using models.dev API @returns {Promise<boolean>} */
|
|
498
440
|
export const checkModelVisionCapability = async modelId => {
|
|
499
441
|
try {
|
|
500
442
|
const modelInfo = await fetchModelInfo(modelId);
|
|
501
|
-
if (!modelInfo)
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
504
|
-
// Check if 'image' is in the input modalities
|
|
443
|
+
if (!modelInfo) return false;
|
|
505
444
|
const inputModalities = modelInfo.modalities?.input || [];
|
|
506
445
|
return inputModalities.includes('image');
|
|
507
446
|
} catch {
|
|
508
|
-
// If we can't determine vision capability, default to false
|
|
509
447
|
return false;
|
|
510
448
|
}
|
|
511
449
|
};
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Calculate USD cost for a model's usage with detailed breakdown
|
|
515
|
-
* @param {Object} usage - Token usage object
|
|
516
|
-
* @param {Object} modelInfo - Model information from pricing API
|
|
517
|
-
* @param {boolean} includeBreakdown - Whether to include detailed calculation breakdown
|
|
518
|
-
* @returns {Object} Cost data with optional breakdown
|
|
519
|
-
*/
|
|
450
|
+
/** Calculate USD cost for a model's usage with detailed breakdown */
|
|
520
451
|
export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) => {
|
|
521
452
|
if (!modelInfo || !modelInfo.cost) {
|
|
522
453
|
return includeBreakdown ? { total: 0, breakdown: null } : 0;
|
|
@@ -862,26 +793,16 @@ export const executeClaudeCommand = async params => {
|
|
|
862
793
|
let anthropicTotalCostUSD = null; // Capture Anthropic's official total_cost_usd from result
|
|
863
794
|
let errorDuringExecution = false; // Issue #1088: Track if error_during_execution subtype occurred
|
|
864
795
|
let resultSummary = null; // Issue #1263: Capture AI result summary for --attach-solution-summary
|
|
865
|
-
|
|
866
796
|
// Create interactive mode handler if enabled
|
|
867
797
|
let interactiveHandler = null;
|
|
868
798
|
if (argv.interactiveMode && owner && repo && prNumber) {
|
|
869
799
|
await log('š Interactive mode: Creating handler for real-time PR comments', { verbose: true });
|
|
870
|
-
interactiveHandler = createInteractiveHandler({
|
|
871
|
-
owner,
|
|
872
|
-
repo,
|
|
873
|
-
prNumber,
|
|
874
|
-
$,
|
|
875
|
-
log,
|
|
876
|
-
verbose: argv.verbose,
|
|
877
|
-
});
|
|
800
|
+
interactiveHandler = createInteractiveHandler({ owner, repo, prNumber, $, log, verbose: argv.verbose });
|
|
878
801
|
} else if (argv.interactiveMode) {
|
|
879
802
|
await log('ā ļø Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
|
|
880
803
|
}
|
|
881
|
-
|
|
882
804
|
// Build claude command with optional resume flag
|
|
883
805
|
let execCommand;
|
|
884
|
-
// Map model alias to full ID
|
|
885
806
|
const mappedModel = mapModelToId(argv.model);
|
|
886
807
|
// Build claude command arguments
|
|
887
808
|
let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel}`;
|
|
@@ -890,13 +811,10 @@ export const executeClaudeCommand = async params => {
|
|
|
890
811
|
claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
|
|
891
812
|
}
|
|
892
813
|
claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
|
|
893
|
-
// Build the full command for display (with jq for formatting as in v0.3.2)
|
|
894
814
|
const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
|
|
895
|
-
// Print the actual raw command being executed
|
|
896
815
|
await log(`\n${formatAligned('š', 'Raw command:', '')}`);
|
|
897
816
|
await log(`${fullCommand}`);
|
|
898
817
|
await log('');
|
|
899
|
-
// Output prompts in verbose mode for debugging
|
|
900
818
|
if (argv.verbose) {
|
|
901
819
|
await log('š User prompt:', { verbose: true });
|
|
902
820
|
await log('---BEGIN USER PROMPT---', { verbose: true });
|
|
@@ -908,10 +826,8 @@ export const executeClaudeCommand = async params => {
|
|
|
908
826
|
await log('---END SYSTEM PROMPT---', { verbose: true });
|
|
909
827
|
}
|
|
910
828
|
try {
|
|
911
|
-
// Resolve thinking settings (
|
|
912
|
-
// See issue #1146 for details on thinking budget translation
|
|
829
|
+
// Resolve thinking settings (see issue #1146)
|
|
913
830
|
const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion, maxBudget } = await resolveThinkingSettings(argv, log);
|
|
914
|
-
|
|
915
831
|
// Set CLAUDE_CODE_MAX_OUTPUT_TOKENS (see issue #1076), MAX_THINKING_TOKENS (see issue #1146),
|
|
916
832
|
// MCP timeout configurations (see issue #1066), and CLAUDE_CODE_EFFORT_LEVEL for Opus 4.6 (Issue #1238)
|
|
917
833
|
// Pass model for model-specific max output tokens (Issue #1221)
|
|
@@ -947,7 +863,45 @@ export const executeClaudeCommand = async params => {
|
|
|
947
863
|
// Issue #1183: Line buffer for NDJSON stream parsing - accumulate incomplete lines across chunks
|
|
948
864
|
// Long JSON messages (e.g., result with total_cost_usd) may be split across multiple stdout chunks
|
|
949
865
|
let stdoutLineBuffer = '';
|
|
866
|
+
// Issue #1280: Track result event and timeout for hung processes
|
|
867
|
+
// Root cause: command-stream's stream() async iterator waits for BOTH process exit AND
|
|
868
|
+
// stdout/stderr pipe close before emitting 'end'. If the CLI process keeps stdout open after
|
|
869
|
+
// sending the result event, pumpReadable() hangs ā finish() never fires ā stream never ends.
|
|
870
|
+
// Additionally, command-stream v0.9.4 does NOT yield {type:'exit'} chunks from stream(),
|
|
871
|
+
// so the exit code detection via chunk.type==='exit' below is dead code.
|
|
872
|
+
// Workaround: after receiving the result event, start a timeout to force-kill the process.
|
|
873
|
+
// See: https://github.com/link-foundation/command-stream/issues/155
|
|
874
|
+
let resultEventReceived = false;
|
|
875
|
+
let resultTimeoutId = null;
|
|
876
|
+
let forceExitTriggered = false;
|
|
877
|
+
const streamCloseTimeoutMs = timeouts.resultStreamCloseMs;
|
|
878
|
+
const forceExitOnTimeout = async () => {
|
|
879
|
+
if (forceExitTriggered) return;
|
|
880
|
+
forceExitTriggered = true;
|
|
881
|
+
const elapsed = `${streamCloseTimeoutMs / 1000}s`;
|
|
882
|
+
await log(`ā ļø Stream didn't close ${elapsed} after result event, forcing exit (Issue #1280)`, { verbose: true });
|
|
883
|
+
await log(` command-stream stream() is likely stuck waiting for pipe close`, { verbose: true });
|
|
884
|
+
try {
|
|
885
|
+
if (execCommand.kill) {
|
|
886
|
+
await log(` Sending SIGTERM to process...`, { verbose: true });
|
|
887
|
+
execCommand.kill('SIGTERM');
|
|
888
|
+
setTimeout(() => {
|
|
889
|
+
try {
|
|
890
|
+
if (!execCommand.result?.code) {
|
|
891
|
+
log(` Process still alive after 2s, sending SIGKILL`, { verbose: true });
|
|
892
|
+
execCommand.kill('SIGKILL');
|
|
893
|
+
}
|
|
894
|
+
} catch {
|
|
895
|
+
/* process may have exited */
|
|
896
|
+
}
|
|
897
|
+
}, 2000);
|
|
898
|
+
}
|
|
899
|
+
} catch (e) {
|
|
900
|
+
await log(` Warning: Could not kill process: ${e.message}`, { verbose: true });
|
|
901
|
+
}
|
|
902
|
+
};
|
|
950
903
|
for await (const chunk of execCommand.stream()) {
|
|
904
|
+
if (forceExitTriggered) break;
|
|
951
905
|
if (chunk.type === 'stdout') {
|
|
952
906
|
const output = chunk.data.toString();
|
|
953
907
|
// Append to buffer and split; keep last element (may be incomplete) for next chunk
|
|
@@ -990,10 +944,14 @@ export const executeClaudeCommand = async params => {
|
|
|
990
944
|
toolUseCount++;
|
|
991
945
|
}
|
|
992
946
|
// Handle session result type from Claude CLI (emitted when session completes)
|
|
993
|
-
// Subtypes: "success", "error_during_execution" (work may have been done), etc.
|
|
994
947
|
if (data.type === 'result') {
|
|
948
|
+
// Issue #1280: Start 30s timeout for stream close after result event
|
|
949
|
+
if (!resultEventReceived) {
|
|
950
|
+
resultEventReceived = true;
|
|
951
|
+
await log(`š Result event received, starting ${streamCloseTimeoutMs / 1000}s stream close timeout (Issue #1280)`, { verbose: true });
|
|
952
|
+
resultTimeoutId = setTimeout(forceExitOnTimeout, streamCloseTimeoutMs);
|
|
953
|
+
}
|
|
995
954
|
// Issue #1104: Only extract cost from subtype 'success' results
|
|
996
|
-
// This is explicit and reliable - error_during_execution results have zero cost
|
|
997
955
|
if (data.subtype === 'success' && data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
|
|
998
956
|
anthropicTotalCostUSD = data.total_cost_usd;
|
|
999
957
|
await log(`š° Anthropic official cost captured from success result: $${anthropicTotalCostUSD.toFixed(6)}`, { verbose: true });
|
|
@@ -1096,11 +1054,13 @@ export const executeClaudeCommand = async params => {
|
|
|
1096
1054
|
}
|
|
1097
1055
|
}
|
|
1098
1056
|
} else if (chunk.type === 'exit') {
|
|
1057
|
+
// Note: command-stream v0.9.4 stream() does NOT yield exit chunks (Issue #1280).
|
|
1058
|
+
// Exit code is obtained from execCommand.result.code after the loop.
|
|
1059
|
+
// This branch is kept for forward-compatibility if command-stream adds exit chunks.
|
|
1099
1060
|
exitCode = chunk.code;
|
|
1100
1061
|
if (chunk.code !== 0) {
|
|
1101
1062
|
commandFailed = true;
|
|
1102
1063
|
}
|
|
1103
|
-
// Don't break here - let the loop finish naturally to process all output
|
|
1104
1064
|
}
|
|
1105
1065
|
}
|
|
1106
1066
|
|
|
@@ -1116,7 +1076,15 @@ export const executeClaudeCommand = async params => {
|
|
|
1116
1076
|
if (!stdoutLineBuffer.includes('node:internal')) await log(stdoutLineBuffer, { stream: 'raw' });
|
|
1117
1077
|
}
|
|
1118
1078
|
}
|
|
1119
|
-
|
|
1079
|
+
// Issue #1280: Clear the stream close timeout since we exited the loop
|
|
1080
|
+
if (resultTimeoutId) {
|
|
1081
|
+
clearTimeout(resultTimeoutId);
|
|
1082
|
+
if (forceExitTriggered) {
|
|
1083
|
+
await log('ā ļø Stream exited via force-kill timeout (Issue #1280)', { verbose: true });
|
|
1084
|
+
} else {
|
|
1085
|
+
await log('ā
Stream closed normally after result event', { verbose: true });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1120
1088
|
// Issue #1165: Check actual exit code from command result for more reliable detection
|
|
1121
1089
|
// The .stream() method may not emit 'exit' chunks, but the command object still tracks the exit code
|
|
1122
1090
|
// Exit code 127 is the standard Unix convention for "command not found"
|
package/src/config.lib.mjs
CHANGED
|
@@ -49,6 +49,9 @@ export const timeouts = {
|
|
|
49
49
|
githubRepoDelay: parseIntWithDefault('HIVE_MIND_GITHUB_REPO_DELAY_MS', 2000),
|
|
50
50
|
retryBaseDelay: parseIntWithDefault('HIVE_MIND_RETRY_BASE_DELAY_MS', 5000),
|
|
51
51
|
retryBackoffDelay: parseIntWithDefault('HIVE_MIND_RETRY_BACKOFF_DELAY_MS', 1000),
|
|
52
|
+
// Issue #1280: Timeout (ms) to wait for stream close after result event before force-killing
|
|
53
|
+
// command-stream's stream() waits for process exit + pipe close; if stdout stays open, it hangs
|
|
54
|
+
resultStreamCloseMs: parseIntWithDefault('HIVE_MIND_RESULT_STREAM_CLOSE_MS', 30000),
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
// Auto-continue configurations
|