@link-assistant/hive-mind 1.14.0 → 1.14.2

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,35 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 69a34a6: fix: NDJSON stream buffering for Claude CLI output (Issue #1183)
8
+
9
+ Fixed issue where `total_cost_usd` and other critical fields were not being captured from Claude CLI sessions when the output JSON was split across multiple stdout chunks.
10
+
11
+ **Root Cause**: Claude CLI outputs NDJSON (newline-delimited JSON) format, but long JSON messages (like the `result` type containing `total_cost_usd`) can be split across multiple stdout buffer chunks. The code was splitting each chunk by newlines and parsing independently, causing partial JSON fragments to fail parsing.
12
+
13
+ **Solution**:
14
+ - Implemented line buffering to accumulate incomplete lines across chunks
15
+ - Lines are only parsed when they're complete (have a trailing newline)
16
+ - Added processing of any remaining buffer content after the stream ends
17
+
18
+ This ensures that even very long JSON output (e.g., result messages with extensive usage data) is properly parsed and cost tracking works correctly.
19
+
20
+ **Evidence from logs**: The broken session showed JSON truncated mid-word at `ephemeral_5m_input_tok` continuing on the next line with `ens":97252}}` - making both lines unparseable.
21
+
22
+ ## 1.14.1
23
+
24
+ ### Patch Changes
25
+
26
+ - b139b00: fix: detect agent tool errors during streaming for reliable failure detection (Issue #1201)
27
+
28
+ Previously, agent tool errors (`"type": "error"`) could be missed when the post-hoc
29
+ detection function failed to parse NDJSON lines that were concatenated without newline
30
+ delimiters. Now errors are detected inline during stream processing, ensuring
31
+ `"type": "error"` events always trigger a failure exit regardless of output buffering.
32
+
3
33
  ## 1.14.0
4
34
 
5
35
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/agent.lib.mjs CHANGED
@@ -418,6 +418,10 @@ export const executeAgentCommand = async params => {
418
418
  let limitResetTime = null;
419
419
  let lastMessage = '';
420
420
  let fullOutput = ''; // Collect all output for pricing calculation and error detection
421
+ // Issue #1201: Track error events detected during streaming for reliable error detection
422
+ // Post-hoc detection on fullOutput can miss errors if NDJSON lines get concatenated without newlines
423
+ let streamingErrorDetected = false;
424
+ let streamingErrorMessage = null;
421
425
 
422
426
  for await (const chunk of execCommand.stream()) {
423
427
  if (chunk.type === 'stdout') {
@@ -437,6 +441,12 @@ export const executeAgentCommand = async params => {
437
441
  sessionId = data.sessionID;
438
442
  await log(`šŸ“Œ Session ID: ${sessionId}`);
439
443
  }
444
+ // Issue #1201: Detect error events during streaming for reliable detection
445
+ if (data.type === 'error' || data.type === 'step_error') {
446
+ streamingErrorDetected = true;
447
+ streamingErrorMessage = data.message || data.error || line.substring(0, 100);
448
+ await log(`āš ļø Error event detected in stream: ${streamingErrorMessage}`, { level: 'warning' });
449
+ }
440
450
  } catch {
441
451
  // Not JSON - log as plain text
442
452
  await log(line);
@@ -463,6 +473,12 @@ export const executeAgentCommand = async params => {
463
473
  sessionId = stderrData.sessionID;
464
474
  await log(`šŸ“Œ Session ID: ${sessionId}`);
465
475
  }
476
+ // Issue #1201: Detect error events during streaming (stderr) for reliable detection
477
+ if (stderrData.type === 'error' || stderrData.type === 'step_error') {
478
+ streamingErrorDetected = true;
479
+ streamingErrorMessage = stderrData.message || stderrData.error || stderrLine.substring(0, 100);
480
+ await log(`āš ļø Error event detected in stream: ${streamingErrorMessage}`, { level: 'warning' });
481
+ }
466
482
  } catch {
467
483
  // Not JSON - log as plain text
468
484
  await log(stderrLine);
@@ -496,7 +512,7 @@ export const executeAgentCommand = async params => {
496
512
 
497
513
  // Check for explicit error message types from agent
498
514
  if (msg.type === 'error' || msg.type === 'step_error') {
499
- return { detected: true, type: 'AgentError', match: msg.message || line.substring(0, 100) };
515
+ return { detected: true, type: 'AgentError', match: msg.message || msg.error || line.substring(0, 100) };
500
516
  }
501
517
  } catch {
502
518
  // Not JSON - ignore for error detection
@@ -510,6 +526,15 @@ export const executeAgentCommand = async params => {
510
526
  // Only check for JSON error messages, not pattern matching in output
511
527
  const outputError = detectAgentErrors(fullOutput);
512
528
 
529
+ // Issue #1201: Use streaming detection as primary, post-hoc as fallback
530
+ // Streaming detection is more reliable because it parses each JSON line as it arrives,
531
+ // avoiding issues where NDJSON lines get concatenated without newline delimiters in fullOutput
532
+ if (!outputError.detected && streamingErrorDetected) {
533
+ outputError.detected = true;
534
+ outputError.type = 'AgentError';
535
+ outputError.match = streamingErrorMessage;
536
+ }
537
+
513
538
  if (exitCode !== 0 || outputError.detected) {
514
539
  // Build JSON error structure for consistent error reporting
515
540
  const errorInfo = {
@@ -545,7 +570,7 @@ export const executeAgentCommand = async params => {
545
570
  await log(line, { level: 'warning' });
546
571
  }
547
572
  } else if (outputError.detected) {
548
- // Explicit JSON error message from agent
573
+ // Explicit JSON error message from agent (Issue #1201: includes streaming-detected errors)
549
574
  errorInfo.message = `Agent reported error: ${outputError.match}`;
550
575
  await log(`\n\nāŒ ${errorInfo.message}`, { level: 'error' });
551
576
  } else {
@@ -944,57 +944,46 @@ export const executeClaudeCommand = async params => {
944
944
  await log(`\n${formatAligned('ā–¶ļø', 'Streaming output:', '')}\n`);
945
945
  // Use command-stream's async iteration for real-time streaming
946
946
  let exitCode = 0;
947
+ // Issue #1183: Line buffer for NDJSON stream parsing - accumulate incomplete lines across chunks
948
+ // Long JSON messages (e.g., result with total_cost_usd) may be split across multiple stdout chunks
949
+ let stdoutLineBuffer = '';
947
950
  for await (const chunk of execCommand.stream()) {
948
951
  if (chunk.type === 'stdout') {
949
952
  const output = chunk.data.toString();
950
- // Split output into individual lines for NDJSON parsing
951
- // Claude CLI outputs NDJSON (newline-delimited JSON) format where each line is a separate JSON object
952
- // This allows us to parse each event independently and extract structured data like session IDs,
953
- // message counts, and error patterns. Attempting to parse the entire chunk as single JSON would fail
954
- // since multiple JSON objects aren't valid JSON together.
955
- const lines = output.split('\n');
953
+ // Append to buffer and split; keep last element (may be incomplete) for next chunk
954
+ stdoutLineBuffer += output;
955
+ const lines = stdoutLineBuffer.split('\n');
956
+ stdoutLineBuffer = lines.pop() || '';
957
+ // Parse each complete NDJSON line
956
958
  for (const line of lines) {
957
959
  if (!line.trim()) continue;
958
960
  try {
959
961
  const data = JSON.parse(line);
960
- // Process event in interactive mode (posts PR comments in real-time)
962
+ // Process event in interactive mode
961
963
  if (interactiveHandler) {
962
964
  try {
963
965
  await interactiveHandler.processEvent(data);
964
966
  } catch (interactiveError) {
965
- // Don't let interactive mode errors stop the main execution
966
967
  await log(`āš ļø Interactive mode error: ${interactiveError.message}`, { verbose: true });
967
968
  }
968
969
  }
969
- // Output formatted JSON as in v0.3.2
970
970
  await log(JSON.stringify(data, null, 2));
971
- // Capture session ID from the first message
971
+ // Capture session ID and rename log file
972
972
  if (!sessionId && data.session_id) {
973
973
  sessionId = data.session_id;
974
974
  await log(`šŸ“Œ Session ID: ${sessionId}`);
975
- // Try to rename log file to include session ID
976
975
  let sessionLogFile;
977
976
  try {
978
977
  const currentLogFile = getLogFile();
979
- const logDir = path.dirname(currentLogFile);
980
- sessionLogFile = path.join(logDir, `${sessionId}.log`);
981
- // Use fs.promises to rename the file
978
+ sessionLogFile = path.join(path.dirname(currentLogFile), `${sessionId}.log`);
982
979
  await fs.rename(currentLogFile, sessionLogFile);
983
- // Update the global log file reference
984
980
  setLogFile(sessionLogFile);
985
981
  await log(`šŸ“ Log renamed to: ${sessionLogFile}`);
986
982
  } catch (renameError) {
987
- reportError(renameError, {
988
- context: 'rename_session_log',
989
- sessionId,
990
- sessionLogFile,
991
- operation: 'rename_log_file',
992
- });
993
- // If rename fails, keep original filename
983
+ reportError(renameError, { context: 'rename_session_log', sessionId, sessionLogFile, operation: 'rename_log_file' });
994
984
  await log(`āš ļø Could not rename log file: ${renameError.message}`, { verbose: true });
995
985
  }
996
986
  }
997
- // Track message and tool use counts
998
987
  if (data.type === 'message') {
999
988
  messageCount++;
1000
989
  } else if (data.type === 'tool_use') {
@@ -1110,6 +1099,19 @@ export const executeClaudeCommand = async params => {
1110
1099
  }
1111
1100
  }
1112
1101
 
1102
+ // Issue #1183: Process remaining buffer content - extract cost from result type if present
1103
+ if (stdoutLineBuffer.trim()) {
1104
+ try {
1105
+ const data = JSON.parse(stdoutLineBuffer);
1106
+ await log(JSON.stringify(data, null, 2));
1107
+ if (data.type === 'result' && data.subtype === 'success' && data.total_cost_usd != null) {
1108
+ anthropicTotalCostUSD = data.total_cost_usd;
1109
+ }
1110
+ } catch {
1111
+ if (!stdoutLineBuffer.includes('node:internal')) await log(stdoutLineBuffer, { stream: 'raw' });
1112
+ }
1113
+ }
1114
+
1113
1115
  // Issue #1165: Check actual exit code from command result for more reliable detection
1114
1116
  // The .stream() method may not emit 'exit' chunks, but the command object still tracks the exit code
1115
1117
  // Exit code 127 is the standard Unix convention for "command not found"