@link-assistant/hive-mind 1.49.2 → 1.50.0

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,30 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.50.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4aed1c1: fix: interactive mode GitHub comments display improvements (#1576)
8
+ - Fix agent task comments stuck at "⏳ Running..." by propagating taskId through comment queue
9
+ - Fix misleading token counts by preferring modelUsage (cumulative per-model) over usage (last-iteration)
10
+ - Change truncation format from "[N lines truncated]" to "[X-Y lines are omitted]" showing actual line range
11
+ - Rename "Session Complete" to "Interactive session completed"
12
+ - Rename Write tool "Content" to "Change", expand by default, add line numbers to diffs
13
+ - Show checked/total count in TodoWrite: "Todos (2/9 items)" instead of "Todos (9 items)"
14
+ - Make Task prompt and Edit Change sections expanded by default
15
+ - Add ToolSearch-specific display with Query/Max Results fields
16
+ - Mark sub-agent tasks with 🤖🔀 emoji and Agent ID field
17
+ - Add queue flushing before waiting for comment IDs in task progress/notification handlers
18
+
19
+ ## 1.49.3
20
+
21
+ ### Patch Changes
22
+
23
+ - b15a494: fix: make usage limit footer message consistent with auto-resume mode (#1569)
24
+ - Fix footer message in "Usage Limit Reached" GitHub comments to reflect auto-resume/auto-restart mode
25
+ - Previously the footer always showed "You can resume once the limit resets." even when auto-resume was enabled
26
+ - Now shows mode-specific messages: "The session will automatically resume when the limit resets." or "The session will automatically restart when the limit resets."
27
+
3
28
  ## 1.49.2
4
29
 
5
30
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.49.2",
3
+ "version": "1.50.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -491,11 +491,7 @@ The automated solution draft was interrupted because the ${toolName} usage limit
491
491
  const modeName = autoResumeMode === 'restart' ? 'restart' : 'resume';
492
492
  const modeDescription = autoResumeMode === 'restart' ? 'The session will automatically restart (fresh start) when the limit resets.' : 'The session will automatically resume (with context preserved) when the limit resets.';
493
493
 
494
- if (limitResetTime) {
495
- logComment += `**Auto-${modeName} is enabled.** ${modeDescription}`;
496
- } else {
497
- logComment += `**Auto-${modeName} is enabled.** ${modeDescription}`;
498
- }
494
+ logComment += `**Auto-${modeName} is enabled.** ${modeDescription}`;
499
495
  } else {
500
496
  // Manual resume mode - show CLI commands
501
497
  if (limitResetTime) {
@@ -516,6 +512,8 @@ ${resumeCommand}
516
512
  }
517
513
  }
518
514
 
515
+ const footerNote = isAutoResumeEnabled ? (autoResumeMode === 'restart' ? '*This session was interrupted due to usage limits. The session will automatically restart when the limit resets.*' : '*This session was interrupted due to usage limits. The session will automatically resume when the limit resets.*') : '*This session was interrupted due to usage limits. You can resume once the limit resets.*';
516
+
519
517
  logComment += `${modelInfoString}
520
518
 
521
519
  <details>
@@ -528,7 +526,7 @@ ${logContent}
528
526
  </details>
529
527
 
530
528
  ---
531
- *This session was interrupted due to usage limits. You can resume once the limit resets.*`;
529
+ ${footerNote}`;
532
530
  } else if (errorMessage) {
533
531
  // Failure log format (non-usage-limit errors)
534
532
  logComment = `## 🚨 Solution Draft Failed
@@ -707,13 +705,15 @@ ${resumeCommand}
707
705
  }
708
706
  }
709
707
 
708
+ const uploadFooterNote = isAutoResumeEnabled ? (autoResumeMode === 'restart' ? '*This session was interrupted due to usage limits. The session will automatically restart when the limit resets.*' : '*This session was interrupted due to usage limits. The session will automatically resume when the limit resets.*') : '*This session was interrupted due to usage limits. You can resume once the limit resets.*';
709
+
710
710
  logUploadComment += `${modelInfoString}
711
711
 
712
712
  ### 📎 **Execution log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
713
713
  - [View complete execution log](${logUrl})
714
714
 
715
715
  ---
716
- *This session was interrupted due to usage limits. You can resume once the limit resets.*`;
716
+ ${uploadFooterNote}`;
717
717
  } else if (errorMessage) {
718
718
  // Failure log format (non-usage-limit errors)
719
719
  logUploadComment = `## 🚨 Solution Draft Failed
@@ -143,9 +143,11 @@ const truncateMiddle = (content, options = {}) => {
143
143
 
144
144
  const startLines = lines.slice(0, keepStart);
145
145
  const endLines = lines.slice(-keepEnd);
146
- const removedCount = lines.length - keepStart - keepEnd;
146
+ // Show the actual line number range that was omitted (1-based)
147
+ const omitStart = keepStart + 1;
148
+ const omitEnd = lines.length - keepEnd;
147
149
 
148
- return sanitizeUnicode([...startLines, '', `... [${removedCount} lines truncated] ...`, '', ...endLines].join('\n'));
150
+ return sanitizeUnicode([...startLines, '', `... [${omitStart}-${omitEnd} lines are omitted] ...`, '', ...endLines].join('\n'));
149
151
  };
150
152
 
151
153
  /**
@@ -278,6 +280,7 @@ const getToolIcon = toolName => {
278
280
  WebFetch: '🌐',
279
281
  WebSearch: '🔍',
280
282
  TodoWrite: '📋',
283
+ ToolSearch: '🔍',
281
284
  Task: '🎯',
282
285
  Agent: '🤖',
283
286
  NotebookEdit: '📓',
@@ -339,10 +342,11 @@ export const createInteractiveHandler = options => {
339
342
  * Post a comment to the PR (with rate limiting)
340
343
  * @param {string} body - Comment body
341
344
  * @param {string} [toolId] - Optional tool ID for tracking pending tool calls
345
+ * @param {string} [taskId] - Optional task ID for tracking pending agent tasks
342
346
  * @returns {Promise<string|null>} Comment ID if successful, null if queued or failed
343
347
  * @private
344
348
  */
345
- const postComment = async (body, toolId = null) => {
349
+ const postComment = async (body, toolId = null, taskId = null) => {
346
350
  if (!prNumber || !owner || !repo) {
347
351
  if (verbose) {
348
352
  await log('⚠️ Interactive mode: Cannot post comment - missing PR info', { verbose: true });
@@ -354,10 +358,10 @@ export const createInteractiveHandler = options => {
354
358
  const timeSinceLastComment = now - state.lastCommentTime;
355
359
 
356
360
  if (timeSinceLastComment < CONFIG.MIN_COMMENT_INTERVAL) {
357
- // Queue the comment for later with toolId for tracking
358
- state.commentQueue.push({ body, toolId });
361
+ // Queue the comment for later with toolId/taskId for tracking
362
+ state.commentQueue.push({ body, toolId, taskId });
359
363
  if (verbose) {
360
- await log(`📝 Interactive mode: Comment queued (${state.commentQueue.length} in queue)${toolId ? ` [tool: ${toolId}]` : ''}`, { verbose: true });
364
+ await log(`📝 Interactive mode: Comment queued (${state.commentQueue.length} in queue)${toolId ? ` [tool: ${toolId}]` : ''}${taskId ? ` [task: ${taskId}]` : ''}`, { verbose: true });
361
365
  }
362
366
  return null;
363
367
  }
@@ -461,8 +465,8 @@ export const createInteractiveHandler = options => {
461
465
 
462
466
  const queueItem = state.commentQueue.shift();
463
467
  if (queueItem) {
464
- const { body, toolId } = queueItem;
465
- // Post the comment (don't pass toolId to avoid re-queueing)
468
+ const { body, toolId, taskId } = queueItem;
469
+ // Post the comment (don't pass toolId/taskId to avoid re-queueing)
466
470
  const commentId = await postComment(body);
467
471
 
468
472
  // If this was a tool use comment, update the pending call with the comment ID
@@ -481,6 +485,25 @@ export const createInteractiveHandler = options => {
481
485
  }
482
486
  }
483
487
  }
488
+
489
+ // If this was a task comment, update the pending task with the comment ID
490
+ // Fix: task comments previously lost their commentId when queued, causing
491
+ // task_notification edits to fail and leaving tasks stuck at "⏳ Running..."
492
+ // See: https://github.com/link-assistant/hive-mind/issues/1576
493
+ if (taskId && commentId) {
494
+ const pendingTask = state.pendingTasks.get(taskId);
495
+ if (pendingTask) {
496
+ pendingTask.commentId = commentId;
497
+ if (pendingTask.resolveCommentId) {
498
+ pendingTask.resolveCommentId(commentId);
499
+ }
500
+ if (verbose) {
501
+ await log(`📋 Interactive mode: Updated pending task ${taskId} with comment ID ${commentId}`, {
502
+ verbose: true,
503
+ });
504
+ }
505
+ }
506
+ }
484
507
  }
485
508
  }
486
509
 
@@ -613,26 +636,26 @@ ${createRawJsonSection(data)}`;
613
636
  keepStart: 12,
614
637
  keepEnd: 12,
615
638
  });
616
- // Format content as diff with + prefix for added lines
639
+ // Format content as diff with + prefix and line numbers for added lines
617
640
  const diffContent = truncatedContent
618
641
  .split('\n')
619
- .map(line => `+ ${line}`)
642
+ .map((line, i) => `+${String(i + 1).padStart(4)} | ${line}`)
620
643
  .join('\n');
621
- inputDisplay += '\n\n' + createCollapsible('📄 Content', '```diff\n' + escapeMarkdown(diffContent) + '\n```');
644
+ inputDisplay += '\n\n' + createCollapsible('📄 Change', '```diff\n' + escapeMarkdown(diffContent) + '\n```', true);
622
645
  }
623
646
  } else if (toolName === 'Edit' && input.file_path) {
624
647
  inputDisplay = `**File:** \`${input.file_path}\``;
625
648
  if (input.old_string && input.new_string) {
626
649
  const truncatedOld = truncateMiddle(input.old_string, { maxLines: 15, keepStart: 6, keepEnd: 6 });
627
650
  const truncatedNew = truncateMiddle(input.new_string, { maxLines: 15, keepStart: 6, keepEnd: 6 });
628
- // Format as unified diff with - for removed lines and + for added lines
651
+ // Format as unified diff with - for removed lines and + for added lines, with line numbers
629
652
  const diffOld = truncatedOld
630
653
  .split('\n')
631
- .map(line => `- ${line}`)
654
+ .map((line, i) => `-${String(i + 1).padStart(4)} | ${line}`)
632
655
  .join('\n');
633
656
  const diffNew = truncatedNew
634
657
  .split('\n')
635
- .map(line => `+ ${line}`)
658
+ .map((line, i) => `+${String(i + 1).padStart(4)} | ${line}`)
636
659
  .join('\n');
637
660
  inputDisplay += '\n\n' + createCollapsible('🔄 Change', '```diff\n' + escapeMarkdown(diffOld + '\n' + diffNew) + '\n```', true);
638
661
  }
@@ -665,13 +688,17 @@ ${createRawJsonSection(data)}`;
665
688
  todosPreview = [...startTodos, `- _...and ${skipped} more_`, ...endTodos].join('\n');
666
689
  }
667
690
 
668
- inputDisplay = createCollapsible(`📋 Todos (${todos.length} items)`, todosPreview, true);
691
+ const completedCount = todos.filter(t => t.status === 'completed').length;
692
+ inputDisplay = createCollapsible(`📋 Todos (${completedCount}/${todos.length} items)`, todosPreview, true);
669
693
  } else if (toolName === 'Task') {
670
694
  inputDisplay = `**Description:** ${input.description || 'N/A'}`;
671
695
  if (input.prompt) {
672
696
  const truncatedPrompt = truncateMiddle(input.prompt, { maxLines: 20, keepStart: 8, keepEnd: 8 });
673
- inputDisplay += '\n\n' + createCollapsible('📝 Prompt', truncatedPrompt);
697
+ inputDisplay += '\n\n' + createCollapsible('📝 Prompt', truncatedPrompt, true);
674
698
  }
699
+ } else if (toolName === 'ToolSearch') {
700
+ inputDisplay = `**Query:** \`${input.query || 'N/A'}\``;
701
+ if (input.max_results) inputDisplay += `\n**Max Results:** ${input.max_results}`;
675
702
  } else {
676
703
  // Generic input display
677
704
  const inputJson = truncateMiddle(safeJsonStringify(input, 2), {
@@ -885,7 +912,7 @@ ${createRawJsonSection(data)}`;
885
912
  const handleResult = async data => {
886
913
  const isError = data.is_error || false;
887
914
  const statusIcon = isError ? '❌' : '✅';
888
- const statusText = isError ? 'Session Failed' : 'Session Complete';
915
+ const statusText = isError ? 'Interactive session failed' : 'Interactive session completed';
889
916
 
890
917
  // Format result text
891
918
  const resultText = data.result || '_No result message_';
@@ -913,9 +940,22 @@ ${createRawJsonSection(data)}`;
913
940
  statsTable += `| **Cost** | ${formatCost(data.total_cost_usd)} |\n`;
914
941
  }
915
942
 
916
- // Usage breakdown if available
943
+ // Usage breakdown prefer modelUsage (cumulative per-model totals including sub-agents)
944
+ // over usage (which only contains last-iteration tokens and is misleading).
945
+ // See: https://github.com/link-assistant/hive-mind/issues/1576
917
946
  let usageSection = '';
918
- if (data.usage) {
947
+ if (data.modelUsage && Object.keys(data.modelUsage).length > 0) {
948
+ usageSection = '\n### 📊 Token Usage (by model)\n\n';
949
+ for (const [model, mu] of Object.entries(data.modelUsage)) {
950
+ usageSection += `**${model}:**\n\n| Type | Count |\n|------|-------|\n`;
951
+ if (mu.inputTokens) usageSection += `| Input | ${mu.inputTokens.toLocaleString()} |\n`;
952
+ if (mu.outputTokens) usageSection += `| Output | ${mu.outputTokens.toLocaleString()} |\n`;
953
+ if (mu.cacheCreationInputTokens) usageSection += `| Cache Creation | ${mu.cacheCreationInputTokens.toLocaleString()} |\n`;
954
+ if (mu.cacheReadInputTokens) usageSection += `| Cache Read | ${mu.cacheReadInputTokens.toLocaleString()} |\n`;
955
+ if (typeof mu.costUSD === 'number') usageSection += `| Cost | ${formatCost(mu.costUSD)} |\n`;
956
+ usageSection += '\n';
957
+ }
958
+ } else if (data.usage) {
919
959
  const u = data.usage;
920
960
  usageSection = '\n### 📊 Token Usage\n\n| Type | Count |\n|------|-------|\n';
921
961
  if (u.input_tokens) usageSection += `| Input | ${u.input_tokens.toLocaleString()} |\n`;
@@ -954,6 +994,7 @@ ${createRawJsonSection(data)}`;
954
994
  const toolUseId = data.tool_use_id || '';
955
995
  const description = data.description || 'Agent task';
956
996
  const taskType = data.task_type || 'unknown';
997
+ const agentId = data.agent_id || taskId;
957
998
 
958
999
  // Create a promise for the comment ID (handles queued comments)
959
1000
  let resolveCommentId;
@@ -965,13 +1006,14 @@ ${createRawJsonSection(data)}`;
965
1006
  let promptSection = '';
966
1007
  if (data.prompt) {
967
1008
  const truncatedPrompt = truncateMiddle(data.prompt, { maxLines: 15, keepStart: 6, keepEnd: 6 });
968
- promptSection = '\n\n' + createCollapsible('📝 Task prompt', truncatedPrompt);
1009
+ promptSection = '\n\n' + createCollapsible('📝 Task prompt', truncatedPrompt, true);
969
1010
  }
970
1011
 
971
- const comment = `## 🤖 Agent task: ${escapeMarkdown(description)}
1012
+ const comment = `## 🤖🔀 Agent task: ${escapeMarkdown(description)}
972
1013
 
973
1014
  | Property | Value |
974
1015
  |----------|-------|
1016
+ | **Agent ID** | \`${agentId}\` |
975
1017
  | **Task ID** | \`${taskId || 'unknown'}\` |
976
1018
  | **Type** | \`${taskType}\` |
977
1019
  | **Status** | ⏳ Running... |
@@ -988,12 +1030,13 @@ ${createRawJsonSection(data)}`;
988
1030
  resolveCommentId,
989
1031
  toolUseId,
990
1032
  description,
1033
+ agentId,
991
1034
  lastProgressDescription: description,
992
1035
  progressCount: 0,
993
1036
  allEvents: [data],
994
1037
  });
995
1038
 
996
- const commentId = await postComment(comment, null);
1039
+ const commentId = await postComment(comment, null, taskId);
997
1040
 
998
1041
  if (commentId) {
999
1042
  const pendingTask = state.pendingTasks.get(taskId);
@@ -1028,19 +1071,29 @@ ${createRawJsonSection(data)}`;
1028
1071
 
1029
1072
  let commentId = pendingTask.commentId;
1030
1073
 
1031
- // Wait for comment ID if not yet available
1074
+ // Wait for comment ID if not yet available — flush queue first to avoid timeout
1032
1075
  if (!commentId && pendingTask.commentIdPromise) {
1033
- const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1034
- commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1076
+ if (state.commentQueue.length > 0) {
1077
+ const wasProcessing = state.isProcessing;
1078
+ state.isProcessing = false;
1079
+ await processQueue();
1080
+ state.isProcessing = wasProcessing;
1081
+ }
1082
+ commentId = pendingTask.commentId;
1083
+ if (!commentId) {
1084
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1085
+ commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1086
+ }
1035
1087
  }
1036
1088
 
1037
1089
  if (commentId) {
1038
- // Build progress steps list from accumulated events
1090
+ // Build progress steps list from accumulated events, marking with agent ID
1091
+ const agentTag = pendingTask.agentId ? `\`[${pendingTask.agentId}]\`` : '';
1039
1092
  const progressSteps = pendingTask.allEvents
1040
1093
  .filter(e => e.subtype === 'task_progress')
1041
1094
  .map(e => {
1042
1095
  const toolIcon = e.last_tool_name ? getToolIcon(e.last_tool_name) : '🔄';
1043
- return `- ${toolIcon} ${e.description || 'Working...'}`;
1096
+ return `- 🔀 ${agentTag} ${toolIcon} ${e.description || 'Working...'}`;
1044
1097
  })
1045
1098
  .join('\n');
1046
1099
 
@@ -1048,10 +1101,11 @@ ${createRawJsonSection(data)}`;
1048
1101
  const toolUsesText = usage.tool_uses ? `${usage.tool_uses} tool calls` : '';
1049
1102
  const statsText = [durationText, toolUsesText].filter(Boolean).join(' | ');
1050
1103
 
1051
- const updatedComment = `## 🤖 Agent task: ${escapeMarkdown(pendingTask.description)}
1104
+ const updatedComment = `## 🤖🔀 Agent task: ${escapeMarkdown(pendingTask.description)}
1052
1105
 
1053
1106
  | Property | Value |
1054
1107
  |----------|-------|
1108
+ | **Agent ID** | \`${pendingTask.agentId || taskId}\` |
1055
1109
  | **Task ID** | \`${taskId}\` |
1056
1110
  | **Status** | ⏳ Running... |
1057
1111
  | **Progress** | ${pendingTask.progressCount} updates |
@@ -1098,19 +1152,29 @@ ${createRawJsonSection(pendingTask.allEvents.slice(-3))}`;
1098
1152
 
1099
1153
  let commentId = pendingTask.commentId;
1100
1154
 
1101
- // Wait for comment ID if not yet available
1155
+ // Wait for comment ID if not yet available — flush queue first to avoid timeout
1102
1156
  if (!commentId && pendingTask.commentIdPromise) {
1103
- const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1104
- commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1157
+ if (state.commentQueue.length > 0) {
1158
+ const wasProcessing = state.isProcessing;
1159
+ state.isProcessing = false;
1160
+ await processQueue();
1161
+ state.isProcessing = wasProcessing;
1162
+ }
1163
+ commentId = pendingTask.commentId;
1164
+ if (!commentId) {
1165
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1166
+ commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1167
+ }
1105
1168
  }
1106
1169
 
1107
1170
  if (commentId) {
1108
- // Build final progress steps list
1171
+ // Build final progress steps list, marking with agent ID
1172
+ const agentTag = pendingTask.agentId ? `\`[${pendingTask.agentId}]\`` : '';
1109
1173
  const progressSteps = pendingTask.allEvents
1110
1174
  .filter(e => e.subtype === 'task_progress')
1111
1175
  .map(e => {
1112
1176
  const toolIcon = e.last_tool_name ? getToolIcon(e.last_tool_name) : '🔄';
1113
- return `- ${toolIcon} ${e.description || 'Working...'}`;
1177
+ return `- 🔀 ${agentTag} ${toolIcon} ${e.description || 'Working...'}`;
1114
1178
  })
1115
1179
  .join('\n');
1116
1180
 
@@ -1119,10 +1183,11 @@ ${createRawJsonSection(pendingTask.allEvents.slice(-3))}`;
1119
1183
  const tokensText = usage.total_tokens ? `${usage.total_tokens.toLocaleString()} tokens` : '';
1120
1184
  const statsText = [durationText, toolUsesText, tokensText].filter(Boolean).join(' | ');
1121
1185
 
1122
- const updatedComment = `## 🤖 Agent task: ${escapeMarkdown(pendingTask.description)}
1186
+ const updatedComment = `## 🤖🔀 Agent task: ${escapeMarkdown(pendingTask.description)}
1123
1187
 
1124
1188
  | Property | Value |
1125
1189
  |----------|-------|
1190
+ | **Agent ID** | \`${pendingTask.agentId || taskId}\` |
1126
1191
  | **Task ID** | \`${taskId}\` |
1127
1192
  | **Status** | ${statusIcon} ${statusText} |
1128
1193
  | **Summary** | ${escapeMarkdown(summary)} |
@@ -1140,8 +1205,11 @@ ${createRawJsonSection([pendingTask.allEvents[0], data])}`;
1140
1205
  state.pendingTasks.delete(taskId);
1141
1206
  } else {
1142
1207
  // Post as standalone if no pending task
1143
- const comment = `## 🤖 Agent task ${statusIcon} ${statusText}
1208
+ const agentId = data.agent_id || taskId;
1209
+ const comment = `## 🤖🔀 Agent task ${statusIcon} ${statusText}
1144
1210
 
1211
+ | **Agent ID** | \`${agentId}\` |
1212
+ |---|---|
1145
1213
  **Summary:** ${escapeMarkdown(summary)}
1146
1214
 
1147
1215
  ---