@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 +30 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +27 -2
- package/src/claude.lib.mjs +25 -23
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
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 {
|
package/src/claude.lib.mjs
CHANGED
|
@@ -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
|
-
//
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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"
|