@link-assistant/hive-mind 2.0.14 ā 2.0.16
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 +12 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +8 -0
- package/src/claude.lib.mjs +45 -50
- package/src/claude.model-utils.lib.mjs +32 -0
- package/src/claude.resume-output.lib.mjs +10 -0
- package/src/claude.stream-events.lib.mjs +61 -0
- package/src/codex.lib.mjs +13 -0
- package/src/opencode.lib.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.0.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a448b3d: Detect incomplete Claude stream-json runs that exit without a terminal result event, capture nested Claude tool/error events, and preserve compaction summaries for diagnostics.
|
|
8
|
+
|
|
9
|
+
## 2.0.15
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 0d2f2bb: Fix "Cannot read properties of null (reading 'type')" crash that aborted Codex (and other agent) runs when the tool echoed a stream line that parsed to a bare `null` or non-object JSON primitive. All NDJSON stream parsers (Codex, Claude, Agent, OpenCode) now ignore non-object lines instead of dereferencing them.
|
|
14
|
+
|
|
3
15
|
## 2.0.14
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -542,6 +542,9 @@ export const executeAgentCommand = async params => {
|
|
|
542
542
|
if (!line.trim()) continue;
|
|
543
543
|
try {
|
|
544
544
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
545
|
+
// Issue #1968: a bare `null`/primitive NDJSON line must not abort
|
|
546
|
+
// event processing (any data.X access would throw on null).
|
|
547
|
+
if (data === null || typeof data !== 'object') continue;
|
|
545
548
|
// Output formatted JSON
|
|
546
549
|
await log(JSON.stringify(data, null, 2));
|
|
547
550
|
// Capture session ID from the first message
|
|
@@ -616,6 +619,8 @@ export const executeAgentCommand = async params => {
|
|
|
616
619
|
if (!stderrLine.trim()) continue;
|
|
617
620
|
try {
|
|
618
621
|
const stderrData = sanitizeObjectStrings(JSON.parse(stderrLine));
|
|
622
|
+
// Issue #1968: skip bare `null`/primitive lines (see stdout handler above).
|
|
623
|
+
if (stderrData === null || typeof stderrData !== 'object') continue;
|
|
619
624
|
// Output formatted JSON (same formatting as stdout)
|
|
620
625
|
await log(JSON.stringify(stderrData, null, 2));
|
|
621
626
|
// Capture session ID from stderr too (agent sends it via stderr)
|
|
@@ -695,6 +700,9 @@ export const executeAgentCommand = async params => {
|
|
|
695
700
|
try {
|
|
696
701
|
const msg = sanitizeObjectStrings(JSON.parse(line));
|
|
697
702
|
|
|
703
|
+
// Issue #1968: ignore bare `null`/primitive lines (msg.type would throw on null).
|
|
704
|
+
if (msg === null || typeof msg !== 'object') continue;
|
|
705
|
+
|
|
698
706
|
// Check for explicit error message types from agent
|
|
699
707
|
if (msg.type === 'error' || msg.type === 'step_error') {
|
|
700
708
|
return { detected: true, type: 'AgentError', match: msg.message || msg.error || line.substring(0, 100) };
|
package/src/claude.lib.mjs
CHANGED
|
@@ -31,37 +31,11 @@ import { resolveSubSessionSize } from './sub-session-size.lib.mjs'; // Issue #17
|
|
|
31
31
|
import { withAgentsMdAsClaudeMd } from './agents-md-claude-support.lib.mjs';
|
|
32
32
|
import { deployHandoffSkill } from './handoff-skill.lib.mjs'; // Issue #1877
|
|
33
33
|
import { createThinkingBlockRecovery } from './claude.thinking-block-recovery.lib.mjs'; // Issue #1834 (PR #1835 feedback)
|
|
34
|
+
import { buildMissingClaudeResultMessage, collectClaudeStreamEventFacts, getClaudeMessageContent, shouldFailClaudeStreamWithoutResult } from './claude.stream-events.lib.mjs';
|
|
35
|
+
import { formatNumber, mapModelToId, checkModelVisionCapability } from './claude.model-utils.lib.mjs';
|
|
36
|
+
import { showResumeCommand } from './claude.resume-output.lib.mjs';
|
|
34
37
|
export { availableModels, fetchModelInfo }; // Re-export for backward compatibility
|
|
35
|
-
|
|
36
|
-
if (!sessionId || !tempDir) return;
|
|
37
|
-
await log(`\nš” To continue this session:\n`);
|
|
38
|
-
await log(` Interactive mode: ${buildClaudeResumeCommand({ tempDir, sessionId, claudePath, model })}\n`);
|
|
39
|
-
await log(` Autonomous mode: ${buildClaudeAutonomousResumeCommand({ tempDir, sessionId, claudePath, model })}\n`);
|
|
40
|
-
// Issue #942: 3rd option - restart the entire /solve flow, not just the claude session.
|
|
41
|
-
if (argv && argv.url) await log(` Solve resume mode: ${buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool: argv.tool || 'claude', model: argv.model, fallbackModel: argv.fallbackModel, tempDir })}\n`);
|
|
42
|
-
};
|
|
43
|
-
/** Format numbers with spaces as thousands separator (no commas) */
|
|
44
|
-
export const formatNumber = num => {
|
|
45
|
-
if (num === null || num === undefined) return 'N/A';
|
|
46
|
-
const parts = num.toString().split('.');
|
|
47
|
-
const integerPart = parts[0];
|
|
48
|
-
const decimalPart = parts[1];
|
|
49
|
-
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
|
50
|
-
return decimalPart !== undefined ? `${formattedInteger}.${decimalPart}` : formattedInteger;
|
|
51
|
-
};
|
|
52
|
-
// Model mapping to translate aliases to full model IDs
|
|
53
|
-
// Supports [1m] suffix for 1 million token context (Issue #1221)
|
|
54
|
-
export const mapModelToId = model => {
|
|
55
|
-
if (!model || typeof model !== 'string') return model;
|
|
56
|
-
// Check for [1m] suffix (case-insensitive)
|
|
57
|
-
const match = model.match(/^(.+?)\[1m\]$/i);
|
|
58
|
-
if (match) {
|
|
59
|
-
const baseModel = match[1];
|
|
60
|
-
const mappedBase = availableModels[baseModel] || baseModel;
|
|
61
|
-
return `${mappedBase}[1m]`;
|
|
62
|
-
}
|
|
63
|
-
return availableModels[model] || model;
|
|
64
|
-
};
|
|
38
|
+
export { formatNumber, mapModelToId, checkModelVisionCapability };
|
|
65
39
|
// Function to validate Claude CLI connection with retry logic
|
|
66
40
|
export const validateClaudeConnection = async (model = 'haiku') => {
|
|
67
41
|
// Map model alias to full ID
|
|
@@ -377,17 +351,6 @@ export const executeClaude = async params => {
|
|
|
377
351
|
})
|
|
378
352
|
);
|
|
379
353
|
};
|
|
380
|
-
/** Check if a model supports vision (image input) using models.dev API @returns {Promise<boolean>} */
|
|
381
|
-
export const checkModelVisionCapability = async modelId => {
|
|
382
|
-
try {
|
|
383
|
-
const modelInfo = await fetchModelInfo(modelId);
|
|
384
|
-
if (!modelInfo) return false;
|
|
385
|
-
const inputModalities = modelInfo.modalities?.input || [];
|
|
386
|
-
return inputModalities.includes('image');
|
|
387
|
-
} catch {
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
354
|
// Issue #1710: calculateModelCost extracted to ./claude.cost.lib.mjs to keep
|
|
392
355
|
// this file under the 1500-line repo cap (see check-file-line-limits CI job).
|
|
393
356
|
import { calculateModelCost } from './claude.cost.lib.mjs';
|
|
@@ -652,6 +615,7 @@ export const executeClaudeCommand = async params => {
|
|
|
652
615
|
let errorDuringExecution = false;
|
|
653
616
|
let resultSummary = null;
|
|
654
617
|
let resultModelUsage = null;
|
|
618
|
+
let lastToolResultError = null;
|
|
655
619
|
// Issue #1590: Track sub-agent calls (Agent tool invocations) for per-call stats
|
|
656
620
|
const subAgentCalls = [];
|
|
657
621
|
// Issue #1590: Map tool_use_id -> subAgentCalls index for accumulating per-call usage from parent_tool_use_id events
|
|
@@ -872,6 +836,7 @@ export const executeClaudeCommand = async params => {
|
|
|
872
836
|
if (!line.trim()) continue;
|
|
873
837
|
try {
|
|
874
838
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
839
|
+
if (data === null || typeof data !== 'object') continue; // Issue #1968: skip bare null/primitive NDJSON lines
|
|
875
840
|
// Issue #1510: Track last event time for all modes (not just interactive)
|
|
876
841
|
// so activity timeout can report accurate idle duration
|
|
877
842
|
lastEventTime = Date.now();
|
|
@@ -902,8 +867,19 @@ export const executeClaudeCommand = async params => {
|
|
|
902
867
|
await log(`ā ļø Could not rename log file: ${renameError.message}`, { verbose: true });
|
|
903
868
|
}
|
|
904
869
|
}
|
|
905
|
-
|
|
906
|
-
|
|
870
|
+
const eventFacts = collectClaudeStreamEventFacts(data);
|
|
871
|
+
messageCount += eventFacts.messageCountDelta;
|
|
872
|
+
toolUseCount += eventFacts.toolUseCountDelta;
|
|
873
|
+
if (eventFacts.lastText) lastMessage = eventFacts.lastText;
|
|
874
|
+
if (!resultSummary && eventFacts.compactionSummary) {
|
|
875
|
+
resultSummary = eventFacts.compactionSummary;
|
|
876
|
+
await log('š Captured fallback summary from Claude compaction context', { verbose: true });
|
|
877
|
+
}
|
|
878
|
+
if (eventFacts.toolResultError) {
|
|
879
|
+
lastToolResultError = eventFacts.toolResultError;
|
|
880
|
+
lastMessage = eventFacts.toolResultError;
|
|
881
|
+
await log(`ā ļø Tool result error detected: ${eventFacts.toolResultError.substring(0, 200)}`, { verbose: true });
|
|
882
|
+
}
|
|
907
883
|
// Issue #1708: signal busy/idle to the bidirectional handler so
|
|
908
884
|
// queue-comments-to-input mode can hold frames until the AI is
|
|
909
885
|
// idle. Any assistant/tool_use/system event means the AI is
|
|
@@ -1022,7 +998,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1022
998
|
}
|
|
1023
999
|
}
|
|
1024
1000
|
if (data.type === 'assistant' && data.message && data.message.content) {
|
|
1025
|
-
const content =
|
|
1001
|
+
const content = getClaudeMessageContent(data);
|
|
1026
1002
|
for (const item of content) {
|
|
1027
1003
|
if (item.type === 'text' && item.text) {
|
|
1028
1004
|
// Check for the specific 500/529 overload error pattern (Issue #1439: 529 is also an overload)
|
|
@@ -1110,12 +1086,26 @@ export const executeClaudeCommand = async params => {
|
|
|
1110
1086
|
try {
|
|
1111
1087
|
const data = sanitizeObjectStrings(JSON.parse(stdoutLineBuffer));
|
|
1112
1088
|
await log(JSON.stringify(data, null, 2));
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1089
|
+
const eventFacts = collectClaudeStreamEventFacts(data);
|
|
1090
|
+
messageCount += eventFacts.messageCountDelta;
|
|
1091
|
+
toolUseCount += eventFacts.toolUseCountDelta;
|
|
1092
|
+
if (eventFacts.lastText) lastMessage = eventFacts.lastText;
|
|
1093
|
+
if (!resultSummary && eventFacts.compactionSummary) resultSummary = eventFacts.compactionSummary;
|
|
1094
|
+
if (eventFacts.toolResultError) {
|
|
1095
|
+
lastToolResultError = eventFacts.toolResultError;
|
|
1096
|
+
lastMessage = eventFacts.toolResultError;
|
|
1097
|
+
}
|
|
1098
|
+
if (data?.type === 'result') {
|
|
1099
|
+
resultEventReceived = true;
|
|
1100
|
+
if (data.subtype === 'success') {
|
|
1101
|
+
resultSuccessReceived = true;
|
|
1102
|
+
if (data.result && typeof data.result === 'string') resultSummary = data.result;
|
|
1103
|
+
if (data.modelUsage) resultModelUsage = data.modelUsage;
|
|
1104
|
+
}
|
|
1105
|
+
if (data.total_cost_usd != null) {
|
|
1106
|
+
if (data.subtype === 'success') anthropicTotalCostUSD = data.total_cost_usd;
|
|
1107
|
+
else anthropicCostFromAnyResult = data.total_cost_usd;
|
|
1108
|
+
}
|
|
1119
1109
|
}
|
|
1120
1110
|
// Issue #1472: Forward remaining buffer event to interactive handler (was previously missed)
|
|
1121
1111
|
if (interactiveHandler) {
|
|
@@ -1303,6 +1293,11 @@ export const executeClaudeCommand = async params => {
|
|
|
1303
1293
|
.join('\n');
|
|
1304
1294
|
await log(`\n\nā Command failed: No messages processed and errors detected in stderr\nStderr errors:\n${errorsPreview}`, { level: 'error' });
|
|
1305
1295
|
}
|
|
1296
|
+
if (shouldFailClaudeStreamWithoutResult({ commandFailed, streamingInput, resultEventReceived })) {
|
|
1297
|
+
commandFailed = true;
|
|
1298
|
+
lastMessage = buildMissingClaudeResultMessage({ lastToolResultError, lastMessage });
|
|
1299
|
+
await log(`\n\nā Command failed: ${lastMessage}`, { level: 'error' });
|
|
1300
|
+
}
|
|
1306
1301
|
if (commandFailed) {
|
|
1307
1302
|
// Take resource snapshot after failure
|
|
1308
1303
|
const resourcesAfter = await getResourceSnapshot();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CLAUDE_MODELS as availableModels } from './models/index.mjs';
|
|
2
|
+
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
3
|
+
|
|
4
|
+
export const formatNumber = num => {
|
|
5
|
+
if (num === null || num === undefined) return 'N/A';
|
|
6
|
+
const parts = num.toString().split('.');
|
|
7
|
+
const integerPart = parts[0];
|
|
8
|
+
const decimalPart = parts[1];
|
|
9
|
+
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
|
10
|
+
return decimalPart !== undefined ? `${formattedInteger}.${decimalPart}` : formattedInteger;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const mapModelToId = model => {
|
|
14
|
+
if (!model || typeof model !== 'string') return model;
|
|
15
|
+
const match = model.match(/^(.+?)\[1m\]$/i);
|
|
16
|
+
if (match) {
|
|
17
|
+
const baseModel = match[1];
|
|
18
|
+
const mappedBase = availableModels[baseModel] || baseModel;
|
|
19
|
+
return `${mappedBase}[1m]`;
|
|
20
|
+
}
|
|
21
|
+
return availableModels[model] || model;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const checkModelVisionCapability = async modelId => {
|
|
25
|
+
try {
|
|
26
|
+
const modelInfo = await fetchModelInfo(modelId);
|
|
27
|
+
const inputModalities = modelInfo?.modalities?.input || [];
|
|
28
|
+
return inputModalities.includes('image');
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { buildClaudeResumeCommand, buildClaudeAutonomousResumeCommand } from './claude.command-builder.lib.mjs';
|
|
2
|
+
import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs';
|
|
3
|
+
|
|
4
|
+
export const showResumeCommand = async (sessionId, tempDir, claudePath, model, log, argv = null) => {
|
|
5
|
+
if (!sessionId || !tempDir) return;
|
|
6
|
+
await log(`\nš” To continue this session:\n`);
|
|
7
|
+
await log(` Interactive mode: ${buildClaudeResumeCommand({ tempDir, sessionId, claudePath, model })}\n`);
|
|
8
|
+
await log(` Autonomous mode: ${buildClaudeAutonomousResumeCommand({ tempDir, sessionId, claudePath, model })}\n`);
|
|
9
|
+
if (argv && argv.url) await log(` Solve resume mode: ${buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool: argv.tool || 'claude', model: argv.model, fallbackModel: argv.fallbackModel, tempDir })}\n`);
|
|
10
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const asArray = value => (Array.isArray(value) ? value : value ? [value] : []);
|
|
2
|
+
|
|
3
|
+
export const getClaudeMessageContent = data => {
|
|
4
|
+
if (!data || typeof data !== 'object') return [];
|
|
5
|
+
return asArray(data.message?.content).filter(item => item && typeof item === 'object');
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const normalizeToolResultError = value => {
|
|
9
|
+
if (typeof value === 'string')
|
|
10
|
+
return value
|
|
11
|
+
.replace(/^<tool_use_error>/, '')
|
|
12
|
+
.replace(/<\/tool_use_error>$/, '')
|
|
13
|
+
.trim();
|
|
14
|
+
if (value === null || value === undefined) return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
} catch {
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const collectClaudeStreamEventFacts = data => {
|
|
23
|
+
const facts = {
|
|
24
|
+
messageCountDelta: 0,
|
|
25
|
+
toolUseCountDelta: 0,
|
|
26
|
+
lastText: null,
|
|
27
|
+
toolResultError: null,
|
|
28
|
+
compactionSummary: null,
|
|
29
|
+
};
|
|
30
|
+
if (!data || typeof data !== 'object') return facts;
|
|
31
|
+
|
|
32
|
+
if (data.type === 'message' || data.type === 'assistant' || data.type === 'user') facts.messageCountDelta = 1;
|
|
33
|
+
if (data.type === 'tool_use') facts.toolUseCountDelta = 1;
|
|
34
|
+
|
|
35
|
+
for (const item of getClaudeMessageContent(data)) {
|
|
36
|
+
if (item.type === 'tool_use') facts.toolUseCountDelta++;
|
|
37
|
+
if (item.type === 'text' && typeof item.text === 'string' && item.text.trim()) {
|
|
38
|
+
facts.lastText = item.text;
|
|
39
|
+
if (data.type === 'user' && data.isSynthetic === true && item.text.includes('This session is being continued from a previous conversation') && item.text.includes('Summary:')) {
|
|
40
|
+
facts.compactionSummary = item.text;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (item.type === 'tool_result' && item.is_error === true) facts.toolResultError = normalizeToolResultError(item.content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!facts.toolResultError && typeof data.tool_use_result === 'string' && data.tool_use_result.trim().startsWith('Error:')) {
|
|
47
|
+
facts.toolResultError = data.tool_use_result.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return facts;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const shouldFailClaudeStreamWithoutResult = ({ commandFailed, streamingInput, resultEventReceived }) => {
|
|
54
|
+
return !commandFailed && !streamingInput && !resultEventReceived;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const buildMissingClaudeResultMessage = ({ lastToolResultError, lastMessage }) => {
|
|
58
|
+
const detail = lastToolResultError || lastMessage;
|
|
59
|
+
if (!detail) return 'Claude stream ended without a terminal result event';
|
|
60
|
+
return `Claude stream ended without a terminal result event after: ${String(detail).slice(0, 500)}`;
|
|
61
|
+
};
|
package/src/codex.lib.mjs
CHANGED
|
@@ -470,6 +470,16 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
|
|
|
470
470
|
continue;
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
// Issue #1968: a stream line that parses to a bare `null` (or any non-object
|
|
474
|
+
// JSON primitive such as a number/string/boolean) must not crash the parser.
|
|
475
|
+
// Codex echoes the stdout of every command it runs back into its own NDJSON
|
|
476
|
+
// stream (see issue #1955), so a target repo that prints a standalone `null`
|
|
477
|
+
// line surfaces here as `JSON.parse('null') === null`. Accessing `data.type`
|
|
478
|
+
// on that null threw "Cannot read properties of null (reading 'type')" and
|
|
479
|
+
// aborted the entire solve. Real Codex events are always JSON objects, so any
|
|
480
|
+
// non-object line is safely ignored.
|
|
481
|
+
if (data === null || typeof data !== 'object') continue;
|
|
482
|
+
|
|
473
483
|
const eventType = typeof data.type === 'string' ? data.type : 'unknown';
|
|
474
484
|
nextState.eventCounts[eventType] = (nextState.eventCounts[eventType] || 0) + 1;
|
|
475
485
|
|
|
@@ -1044,6 +1054,9 @@ export const executeCodexCommand = async params => {
|
|
|
1044
1054
|
if (!line) continue;
|
|
1045
1055
|
try {
|
|
1046
1056
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
1057
|
+
// Issue #1968: skip bare `null`/primitive lines so the handlers
|
|
1058
|
+
// below never receive a non-object event (see parseCodexExecJsonOutput).
|
|
1059
|
+
if (data === null || typeof data !== 'object') continue;
|
|
1047
1060
|
if (interactiveHandler) await interactiveHandler.processEvent(data);
|
|
1048
1061
|
if (progressMonitor) await progressMonitor.processStreamEvent(data);
|
|
1049
1062
|
} catch {
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -342,6 +342,9 @@ export const executeOpenCodeCommand = async params => {
|
|
|
342
342
|
for (const line of lines) {
|
|
343
343
|
if (!line.trim()) continue;
|
|
344
344
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
345
|
+
// Issue #1968: a bare `null`/primitive NDJSON line must not abort the
|
|
346
|
+
// rest of the chunk (data.type access would throw on null).
|
|
347
|
+
if (data === null || typeof data !== 'object') continue;
|
|
345
348
|
accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
346
349
|
// Track text content for result summary
|
|
347
350
|
// OpenCode outputs text via 'text', 'assistant', 'message', or 'result' type events
|
|
@@ -385,6 +388,8 @@ export const executeOpenCodeCommand = async params => {
|
|
|
385
388
|
for (const line of lines) {
|
|
386
389
|
if (!line.trim()) continue;
|
|
387
390
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
391
|
+
// Issue #1968: skip bare `null`/primitive lines (see stdout handler above).
|
|
392
|
+
if (data === null || typeof data !== 'object') continue;
|
|
388
393
|
accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
389
394
|
if (data.type === 'text' && data.text) {
|
|
390
395
|
lastTextContent = data.text;
|