@link-assistant/hive-mind 1.34.8 → 1.35.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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.35.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f3de781: Add handlers for agent task lifecycle events (task_started, task_progress, task_notification) and rate_limit_event in interactive mode, reducing PR comment noise by ~30%
8
+
3
9
  ## 1.34.8
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.34.8",
3
+ "version": "1.35.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",
@@ -7,10 +7,14 @@
7
7
  *
8
8
  * Supported JSON event types:
9
9
  * - system.init: Session initialization
10
+ * - system.task_started: Agent subtask started (Issue #1450)
11
+ * - system.task_progress: Agent subtask progress update (Issue #1450)
12
+ * - system.task_notification: Agent subtask completed/failed (Issue #1450)
10
13
  * - assistant (text): AI text responses
11
14
  * - assistant (tool_use): Tool invocations
12
15
  * - user (tool_result): Tool execution results
13
16
  * - result: Session completion
17
+ * - rate_limit_event: Rate limit info (silently logged, Issue #1450)
14
18
  * - unrecognized: Any unknown event types
15
19
  *
16
20
  * Features:
@@ -213,6 +217,7 @@ const getToolIcon = toolName => {
213
217
  WebSearch: '🔍',
214
218
  TodoWrite: '📋',
215
219
  Task: '🎯',
220
+ Agent: '🤖',
216
221
  NotebookEdit: '📓',
217
222
  default: '🔧',
218
223
  };
@@ -253,6 +258,9 @@ export const createInteractiveHandler = options => {
253
258
  // Simple map of tool_use_id -> { toolName, toolIcon } for standalone tool results
254
259
  // This is preserved even after pendingToolCalls entry is deleted
255
260
  toolUseRegistry: new Map(),
261
+ // Track active agent tasks for progress update deduplication
262
+ // Map of task_id -> { commentId, toolUseId, description, commentIdPromise, resolveCommentId }
263
+ pendingTasks: new Map(),
256
264
  };
257
265
 
258
266
  /**
@@ -809,6 +817,232 @@ ${createRawJsonSection(data)}`;
809
817
  }
810
818
  };
811
819
 
820
+ /**
821
+ * Handle system.task_started event (Agent subtask started)
822
+ * Creates a progress comment that will be updated by task_progress events
823
+ * @param {Object} data - Event data
824
+ */
825
+ const handleTaskStarted = async data => {
826
+ const taskId = data.task_id;
827
+ const toolUseId = data.tool_use_id || '';
828
+ const description = data.description || 'Agent task';
829
+ const taskType = data.task_type || 'unknown';
830
+
831
+ // Create a promise for the comment ID (handles queued comments)
832
+ let resolveCommentId;
833
+ const commentIdPromise = new Promise(resolve => {
834
+ resolveCommentId = resolve;
835
+ });
836
+
837
+ // Build prompt preview if available
838
+ let promptSection = '';
839
+ if (data.prompt) {
840
+ const truncatedPrompt = truncateMiddle(data.prompt, { maxLines: 15, keepStart: 6, keepEnd: 6 });
841
+ promptSection = '\n\n' + createCollapsible('📝 Task prompt', truncatedPrompt);
842
+ }
843
+
844
+ const comment = `## 🤖 Agent task: ${escapeMarkdown(description)}
845
+
846
+ | Property | Value |
847
+ |----------|-------|
848
+ | **Task ID** | \`${taskId || 'unknown'}\` |
849
+ | **Type** | \`${taskType}\` |
850
+ | **Status** | ⏳ Running... |
851
+ ${promptSection}
852
+
853
+ ---
854
+
855
+ ${createRawJsonSection(data)}`;
856
+
857
+ // Track this task BEFORE posting
858
+ state.pendingTasks.set(taskId, {
859
+ commentId: null,
860
+ commentIdPromise,
861
+ resolveCommentId,
862
+ toolUseId,
863
+ description,
864
+ lastProgressDescription: description,
865
+ progressCount: 0,
866
+ allEvents: [data],
867
+ });
868
+
869
+ const commentId = await postComment(comment, null);
870
+
871
+ if (commentId) {
872
+ const pendingTask = state.pendingTasks.get(taskId);
873
+ if (pendingTask) {
874
+ pendingTask.commentId = commentId;
875
+ resolveCommentId(commentId);
876
+ }
877
+ }
878
+
879
+ if (verbose) {
880
+ await log(`🤖 Interactive mode: Agent task started - ${description} (task: ${taskId})`, { verbose: true });
881
+ }
882
+ };
883
+
884
+ /**
885
+ * Handle system.task_progress event (Agent subtask progress update)
886
+ * Updates the existing task comment instead of creating a new one
887
+ * @param {Object} data - Event data
888
+ */
889
+ const handleTaskProgress = async data => {
890
+ const taskId = data.task_id;
891
+ const description = data.description || 'Working...';
892
+ const lastToolName = data.last_tool_name || '';
893
+ const usage = data.usage || {};
894
+
895
+ const pendingTask = state.pendingTasks.get(taskId);
896
+
897
+ if (pendingTask) {
898
+ pendingTask.progressCount++;
899
+ pendingTask.lastProgressDescription = description;
900
+ pendingTask.allEvents.push(data);
901
+
902
+ let commentId = pendingTask.commentId;
903
+
904
+ // Wait for comment ID if not yet available
905
+ if (!commentId && pendingTask.commentIdPromise) {
906
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
907
+ commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
908
+ }
909
+
910
+ if (commentId) {
911
+ // Build progress steps list from accumulated events
912
+ const progressSteps = pendingTask.allEvents
913
+ .filter(e => e.subtype === 'task_progress')
914
+ .map(e => {
915
+ const toolIcon = e.last_tool_name ? getToolIcon(e.last_tool_name) : '🔄';
916
+ return `- ${toolIcon} ${e.description || 'Working...'}`;
917
+ })
918
+ .join('\n');
919
+
920
+ const durationText = usage.duration_ms ? formatDuration(usage.duration_ms) : '';
921
+ const toolUsesText = usage.tool_uses ? `${usage.tool_uses} tool calls` : '';
922
+ const statsText = [durationText, toolUsesText].filter(Boolean).join(' | ');
923
+
924
+ const updatedComment = `## 🤖 Agent task: ${escapeMarkdown(pendingTask.description)}
925
+
926
+ | Property | Value |
927
+ |----------|-------|
928
+ | **Task ID** | \`${taskId}\` |
929
+ | **Status** | ⏳ Running... |
930
+ | **Progress** | ${pendingTask.progressCount} updates |
931
+ ${statsText ? `| **Stats** | ${statsText} |\n` : ''}
932
+ ${createCollapsible(`📋 Progress steps (${pendingTask.progressCount})`, progressSteps, true)}
933
+
934
+ ---
935
+
936
+ ${createRawJsonSection(pendingTask.allEvents.slice(-3))}`;
937
+
938
+ await editComment(commentId, updatedComment);
939
+ }
940
+ } else {
941
+ // No pending task found - this can happen if task_started was missed
942
+ // Just log it silently rather than creating an unrecognized comment
943
+ if (verbose) {
944
+ await log(`🤖 Interactive mode: Task progress for unknown task ${taskId}: ${description}`, { verbose: true });
945
+ }
946
+ }
947
+
948
+ if (verbose) {
949
+ await log(`🤖 Interactive mode: Task progress - ${description} (task: ${taskId}, tool: ${lastToolName})`, { verbose: true });
950
+ }
951
+ };
952
+
953
+ /**
954
+ * Handle system.task_notification event (Agent subtask completed/failed)
955
+ * Updates the existing task comment with final status
956
+ * @param {Object} data - Event data
957
+ */
958
+ const handleTaskNotification = async data => {
959
+ const taskId = data.task_id;
960
+ const status = data.status || 'unknown';
961
+ const summary = data.summary || data.description || 'Task finished';
962
+ const usage = data.usage || {};
963
+ const isCompleted = status === 'completed';
964
+ const statusIcon = isCompleted ? '✅' : '❌';
965
+ const statusText = isCompleted ? 'Completed' : status.charAt(0).toUpperCase() + status.slice(1);
966
+
967
+ const pendingTask = state.pendingTasks.get(taskId);
968
+
969
+ if (pendingTask) {
970
+ pendingTask.allEvents.push(data);
971
+
972
+ let commentId = pendingTask.commentId;
973
+
974
+ // Wait for comment ID if not yet available
975
+ if (!commentId && pendingTask.commentIdPromise) {
976
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
977
+ commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
978
+ }
979
+
980
+ if (commentId) {
981
+ // Build final progress steps list
982
+ const progressSteps = pendingTask.allEvents
983
+ .filter(e => e.subtype === 'task_progress')
984
+ .map(e => {
985
+ const toolIcon = e.last_tool_name ? getToolIcon(e.last_tool_name) : '🔄';
986
+ return `- ${toolIcon} ${e.description || 'Working...'}`;
987
+ })
988
+ .join('\n');
989
+
990
+ const durationText = usage.duration_ms ? formatDuration(usage.duration_ms) : '';
991
+ const toolUsesText = usage.tool_uses ? `${usage.tool_uses} tool calls` : '';
992
+ const tokensText = usage.total_tokens ? `${usage.total_tokens.toLocaleString()} tokens` : '';
993
+ const statsText = [durationText, toolUsesText, tokensText].filter(Boolean).join(' | ');
994
+
995
+ const updatedComment = `## 🤖 Agent task: ${escapeMarkdown(pendingTask.description)}
996
+
997
+ | Property | Value |
998
+ |----------|-------|
999
+ | **Task ID** | \`${taskId}\` |
1000
+ | **Status** | ${statusIcon} ${statusText} |
1001
+ | **Summary** | ${escapeMarkdown(summary)} |
1002
+ ${statsText ? `| **Stats** | ${statsText} |\n` : ''}
1003
+ ${progressSteps ? createCollapsible(`📋 Progress steps (${pendingTask.progressCount})`, progressSteps) : ''}
1004
+
1005
+ ---
1006
+
1007
+ ${createRawJsonSection([pendingTask.allEvents[0], data])}`;
1008
+
1009
+ await editComment(commentId, updatedComment);
1010
+ }
1011
+
1012
+ // Clean up
1013
+ state.pendingTasks.delete(taskId);
1014
+ } else {
1015
+ // Post as standalone if no pending task
1016
+ const comment = `## 🤖 Agent task ${statusIcon} ${statusText}
1017
+
1018
+ **Summary:** ${escapeMarkdown(summary)}
1019
+
1020
+ ---
1021
+
1022
+ ${createRawJsonSection(data)}`;
1023
+
1024
+ await postComment(comment);
1025
+ }
1026
+
1027
+ if (verbose) {
1028
+ await log(`🤖 Interactive mode: Task ${statusText.toLowerCase()} - ${summary} (task: ${taskId})`, {
1029
+ verbose: true,
1030
+ });
1031
+ }
1032
+ };
1033
+
1034
+ /**
1035
+ * Handle rate_limit_event (silently logged, no comment created)
1036
+ * @param {Object} data - Event data
1037
+ */
1038
+ const handleRateLimitEvent = async data => {
1039
+ // Rate limit events are internal/informational - log but don't create a PR comment
1040
+ if (verbose) {
1041
+ const info = data.rate_limit_info || {};
1042
+ await log(`⏱️ Interactive mode: Rate limit event - status: ${info.status || 'unknown'}, type: ${info.rateLimitType || 'unknown'}`, { verbose: true });
1043
+ }
1044
+ };
1045
+
812
1046
  /**
813
1047
  * Handle unrecognized event types
814
1048
  * @param {Object} data - Event data
@@ -851,12 +1085,22 @@ ${createRawJsonSection(data)}`;
851
1085
  case 'system':
852
1086
  if (data.subtype === 'init') {
853
1087
  await handleSystemInit(data);
1088
+ } else if (data.subtype === 'task_started') {
1089
+ await handleTaskStarted(data);
1090
+ } else if (data.subtype === 'task_progress') {
1091
+ await handleTaskProgress(data);
1092
+ } else if (data.subtype === 'task_notification') {
1093
+ await handleTaskNotification(data);
854
1094
  } else {
855
1095
  // Unknown system subtype
856
1096
  await handleUnrecognized(data);
857
1097
  }
858
1098
  break;
859
1099
 
1100
+ case 'rate_limit_event':
1101
+ await handleRateLimitEvent(data);
1102
+ break;
1103
+
860
1104
  case 'assistant':
861
1105
  if (data.message && data.message.content) {
862
1106
  const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
@@ -923,6 +1167,10 @@ ${createRawJsonSection(data)}`;
923
1167
  handleToolUse,
924
1168
  handleToolResult,
925
1169
  handleResult,
1170
+ handleTaskStarted,
1171
+ handleTaskProgress,
1172
+ handleTaskNotification,
1173
+ handleRateLimitEvent,
926
1174
  handleUnrecognized,
927
1175
  },
928
1176
  };