@link-assistant/hive-mind 2.0.15 → 2.0.17

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,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.0.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 1cec7b4: Allow bare indented LINO option/value links in .lenv configuration, matching parenthesized links.
8
+
9
+ ## 2.0.16
10
+
11
+ ### Patch Changes
12
+
13
+ - 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.
14
+
3
15
  ## 2.0.15
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
- const showResumeCommand = async (sessionId, tempDir, claudePath, model, log, argv = null) => {
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
- if (data.type === 'message') messageCount++;
907
- else if (data.type === 'tool_use') toolUseCount++;
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 = Array.isArray(data.message.content) ? data.message.content : [data.message.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
- if (data?.type === 'result' && data.subtype === 'success' && data.total_cost_usd != null) {
1115
- anthropicTotalCostUSD = data.total_cost_usd;
1116
- } else if (data?.type === 'result' && data.total_cost_usd != null) {
1117
- // Issue #1886: keep a non-success terminal result's cost as a fallback (see streaming branch above).
1118
- anthropicCostFromAnyResult = data.total_cost_usd;
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
+ };
@@ -39,10 +39,6 @@ const LinoParser = linoModule.Parser || linoModule.default?.Parser;
39
39
 
40
40
  const fs = await import('fs');
41
41
 
42
- function isCliOptionToken(value) {
43
- return /^--[a-zA-Z0-9][a-zA-Z0-9=_.-]*$/.test(value) || /^-[a-zA-Z]$/.test(value);
44
- }
45
-
46
42
  function collectStringValues(value, result = []) {
47
43
  if (value && typeof value === 'object' && Array.isArray(value.values)) {
48
44
  if (value.id !== null && value.id !== undefined) {
@@ -57,25 +53,6 @@ function collectStringValues(value, result = []) {
57
53
  return result;
58
54
  }
59
55
 
60
- function validateNoBareSameLineOptions(content) {
61
- let currentVar = 'configuration';
62
-
63
- for (const line of content.split(/\r?\n/)) {
64
- const trimmed = line.trim();
65
- if (!trimmed || trimmed === '(' || trimmed === ')') continue;
66
-
67
- const topLevelMatch = !/^\s/.test(line) ? trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*):(?:\s*(.*))?$/) : null;
68
- const valueText = topLevelMatch ? (topLevelMatch[2] || '').trim() : trimmed;
69
- if (topLevelMatch) currentVar = topLevelMatch[1];
70
- if (!valueText || valueText === '(' || valueText === ')' || valueText.startsWith('(')) continue;
71
-
72
- const parts = valueText.split(/\s+/).filter(Boolean);
73
- if (parts.length > 1 && isCliOptionToken(parts[0])) {
74
- throw new Error(`Invalid LINO format in "${currentVar}": Multiple values on the same line are not supported.\n` + `Found: "${parts.join(' ')}"\n` + `Each value must be on its own line with proper indentation, or grouped explicitly as a parenthesized link.`);
75
- }
76
- }
77
- }
78
-
79
56
  /**
80
57
  * LenvReader - Reads and parses .lenv files using LINO notation
81
58
  */
@@ -97,8 +74,6 @@ export class LenvReader {
97
74
  const result = {};
98
75
 
99
76
  try {
100
- validateNoBareSameLineOptions(content);
101
-
102
77
  // Parse the entire content as LINO
103
78
  const parsed = this.parser.parse(content);
104
79