@link-assistant/hive-mind 1.14.1 → 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,24 @@
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
+
3
22
  ## 1.14.1
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.14.1",
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",
@@ -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"