@link-assistant/hive-mind 2.0.15 ā 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 +6 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +44 -49
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 2.0.15
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
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
|
|
@@ -903,8 +867,19 @@ export const executeClaudeCommand = async params => {
|
|
|
903
867
|
await log(`ā ļø Could not rename log file: ${renameError.message}`, { verbose: true });
|
|
904
868
|
}
|
|
905
869
|
}
|
|
906
|
-
|
|
907
|
-
|
|
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
|
+
}
|
|
908
883
|
// Issue #1708: signal busy/idle to the bidirectional handler so
|
|
909
884
|
// queue-comments-to-input mode can hold frames until the AI is
|
|
910
885
|
// idle. Any assistant/tool_use/system event means the AI is
|
|
@@ -1023,7 +998,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1023
998
|
}
|
|
1024
999
|
}
|
|
1025
1000
|
if (data.type === 'assistant' && data.message && data.message.content) {
|
|
1026
|
-
const content =
|
|
1001
|
+
const content = getClaudeMessageContent(data);
|
|
1027
1002
|
for (const item of content) {
|
|
1028
1003
|
if (item.type === 'text' && item.text) {
|
|
1029
1004
|
// Check for the specific 500/529 overload error pattern (Issue #1439: 529 is also an overload)
|
|
@@ -1111,11 +1086,26 @@ export const executeClaudeCommand = async params => {
|
|
|
1111
1086
|
try {
|
|
1112
1087
|
const data = sanitizeObjectStrings(JSON.parse(stdoutLineBuffer));
|
|
1113
1088
|
await log(JSON.stringify(data, null, 2));
|
|
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
|
+
};
|